# 3D Rehabilation System Instruction

[理論](https://hackmd.io/@yQgYhimORGmkQrmqS-yKJg/HywPNkS4L)
## 安裝OpenPose Package
可參考[Github Repository](https://github.com/CMU-Perceptual-Computing-Lab/openpose)
1. 解壓縮程式碼
2. `cd openpose`,移動到`openpose`資料夾下。
3. 確認系統環境符合以下條件:
* Cuda 8.0
* Cudnn 5.1.0
* Gcc 5.4
* Protoc 3.2.0
* OpenCV 3.2.0
* cmake-gui with cmake version==3.5.1
4. `cmake-gui`打開圖形化介面後,依照下面圖片操作。




5. 關閉`cmake-gui`,`cd build`進入`build`資料夾。
6. 編譯程式
```bash
make -j8 `nproc`
```
## 使用`OpenPose`校正相機內在參數
### 實驗設備
1. Logitech C310 x 3
2. Type A USB 2.0 延長線 x 3
3. 腳架 x 3
4. 校正板
* Aruco Marker
* 棋盤格
5. OpenPose Package
### 確認相機編號
相機USB口連接主機USB孔後,用`python3 camIdx.py --cam_idx <cam_idx>`確認可以正常連接。
* 參考範例:
0. `cd ../../Aruco`,移動到`Aruco`資料夾下。
1. 連接相機A
2. `python3 camIdx.py --cam_idx 0` → 確認相機A可正常使用後,相機A的編號則是`0`,之後的程式都以這個編號讀取相機A。
3. 連接相機B
4. `python3 camIdx.py --cam_idx 1` → 確認相機B可正常使用後,相機B的編號則是`1`,之後的程式都以這個編號讀取相機B。
5. 連接相機C
6. `python3 camIdx.py --cam_idx 2` → 確認相機C可正常使用後,相機C的編號則是`2`,之後的程式都以這個編號讀取相機C。
### 步驟
0. `cd ../openpose`,移動到`openpose`資料夾下。
1. 產生校正板
* 參考[Camera-Calibration-Pattern-Generator](https://github.com/ProximaB/Camera-Calibration-Pattern-Generator),`python3 CheckerboardCreator.py -r 8 -c 7`產生校正版
* 實驗用校正板大小:8x7校正板,50.0mm。
* 將校正板黏在平面上

2. 拍攝~450張內含校正板的照片。
* ```python3 RecordInt.py --cam_idx_in <idx_in> --cam_idx_out <idx_out>```
用途:使用OpenCV讀取編號為`<idx_in>`的相機,每秒拍攝一張照片,並且將照片存到`IntrinsicsImgs/<idx_out>`資料夾裡面。
* --cam_idx_in : OpenCV讀相機所需要的相機編號。
* --cam_idx_out : 照片儲存位置的名稱,建議設定成跟`--cam_idx_in`相同。
* 參考範例:```python3 RecordInt.py --cam_idx_in 0 --cam_idx_out 0```
* 每秒讀取編號為`0`的相機,並將照片儲存到`IntrinsicsImgs/0`
* 注意事項
* 校正板不能旋轉超過30°。
* 拍攝時將校正版由近處移動到遠處。
* 拍攝時校正板要涵蓋整個照片。
* 參考: [OpenPose Calibration Package](https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/modules/calibration_module.md)
3. 使用`OpenPose Calibration Package`校正相機內在參數
* 修改`mScripts/calibrate_intrinsics.sh`
```./build/examples/calibration/calibration.bin --mode 1 --grid_square_size_mm <grid_size> --grid_number_inner_corners <hxw> --camera_serial_number <idx_out> --calibration_image_dir ./IntrinsicsImgs/<idx_out>```
* --grid_square_size_mm : 棋盤格大小,單位為mm。
* --grid_number_inner_corners : 棋盤格內層點的數量,算法如下圖。 
* --camera_serial_number : 將校正得到的相機內在參數存到`models/cameraParameters/flir/<idx_out>.xml`,`<idx_out>`需要和拍攝校正板時的`<idx_out>`參數相同。
* --calibration_image_dir : 從`./IntrinsicsImgs/<idx_out>`讀取上一步拍攝的照片,`<idx_out>`需要和拍攝校正板時的`<idx_out>`參數相同。
* 參考範例```./build/examples/calibration/calibration.bin --mode 1 --grid_square_size_mm 50.0 --grid_number_inner_corners 7x6 --camera_serial_number 0 --calibration_image_dir ./IntrinsicsImgs/0```
* 棋盤格子大小:`50.0mm`
* 棋盤內圈點數量(直,橫):`(7,6)`
* 輸出檔案名稱:`models/cameraParameters/flir/0.xml`
* 棋盤格照片:`./IntrinsicsImgs/0`
4. `bash mScripts/calibrate_intrinsics.sh`,使用修改過的Script校正內在參數。生成檔案放在`models/cameraParameters/flir/<idx_out>.xml`
* 注意事項
* 450張相片大概需要花9小時做校正。
* 每一台相機都要校正一次,也就是每一台相機都要跑一次步驟2(拍照)+步驟3(改script)+步驟4(執行script)。
* 每台相機校正一次後就不需要再做內在參數校正。
## 架設相機
實驗環境,下圖為俯視圖:

* 三台攝影機高度最高到兩公尺。
* 三台攝影機夾角最大到45°。
* 注意事項:
* 攝影機必須能夠看到的`Aruco Marker`,下圖為校正成功的最大夾角之下`Aruco Marker`在相機之中的樣子。

* 攝影機必須完整照到受測者。
* 受測者面向至中的攝影機。
## 使用`Aruco`校正外在參數
1. `cd ../Aruco`,移動到`../Aruco`資料夾下。
2. 安裝`python 3.5.2`, `opencv-python`, `opencv-contrib-python`, `numpy`
3. 使用`Aruco Marker`
* 檔名:`Aruco_DICT_6x6_250.png`
* 實驗用Marker大小:26.8x26.8 cm。
* 將Marker黏在平面上。
* 
4. 每台攝影機拍攝15秒內含`Aruco Marker`的影片。
* ```python3 Record.py --cam_num <cam_num> --dur 15.0```
用途:同時讀取編號為`0`~`cam_num-1`的攝影機畫面,並錄製長度為15.0秒的影片(Aruco Marker不需要移動)並且存到`Videos/camera_*.avi`裡面。
* --cam_num : 同時讀取編號為`0`~`cam_num-1`的攝影機。
* 範例影片
{%youtube WteZrxS3lDc%}
{%youtube aaGWR_HKYoc%}
{%youtube et1DHLObgRs%}
5. 拍攝計算誤差用的照片。
* `python3 RecordImg.py --cam_num <cam_num>`
用途:同時讀取編號為`0`~`cam_num-1`的攝影機畫面,每秒各拍攝一張照片(要移動Aruco Marker),並將儲存到`ReprojError/`裡面。
* --cam_num : 同時讀取編號為`0`~`cam_num-1`的攝影機。
* 範例照片

6. 新增一個資料夾:`Params`,並且將`../OpenPose/models/cameraParameters/flir/*.xml`移到`Params/`裡面。
7. 產生校正外在參數所需要到`Map Configuration`
* `python3 Config.py`
用途:指定`Root Camera`,並且指定其他相機和Root Camera連結之間的關係。將關係輸出到`config.json`
使用方法(互動式Script):
1. `#Camera` : 輸入相機數量。注意相機編號應為`0`~`cam_num-1`。
2. `Root index` : 輸入指定成`Root Camera`的相機的編號。
3. `Config` : 輸入其他相機跟`Root Camera`之間的連結關係。
> 格式: \<A\>-\<B\> \<A\>-\<C\>
> 意思: 編號B相機是編號A相機的Child; 編號C相機是編號A相機的Child
>
* 參考範例

使用三台相機,`Root Camera`是相機編號0,編號1和編號2連結到編號0。
8. 校正相機外在參數
* `python3 Map.py --length <marker_length meter>`
用途:使用`Step 4`拍攝的15秒影片,`Step 6`移動的內在參數檔案,`Step 7`產生的`config.json`計算出外在參數。產生出內含相機參數的檔案生成到`Outputs/*.xml`
* `*.xml`範例

9. 計算投影誤差
* `python3 Error.py --length <marker_length meter>`
用途:使用`Step 5`拍攝的照片,`Step 8`得到的相機參數檔案,`Step 7`產生的`config.json`計算出外在參數。計算外在參數的投影誤差,投影誤差會顯示在`Terminal`上面。
* 參考輸出結果:
```bash
camera id: Average Reprojection Error
{0: 0.586689, 1: 0.366055, 2: 2.751398}
# Camera 0 → Reprojection Error : 0.586689
# Camera 1 → Reprojection Error : 0.366055
# Camera 2 → Reprojection Error : 2.751398
```
* 實驗成功之三台相機的最大投影誤差: `0.5869; 0.3661; 2.7514`
10. 修改`ROI`以限制骨架偵測範圍。

* `0 640` : 最小有效範圍寬度/最大有效範圍寬度
* `0 380` : 最小有效範圍高度/最大有效範圍高度
## 使用OpenPose進行骨架分析
1. 將相機參數從`Outputs/*.xml`移動到`../openpose/models/cameraParameters/flir/`裡面取代原本的檔案。
2. 移動到`../openpose/`
3. 執行程式。
`bash mScripts/skeleton.sh`
成功執行後會出現兩個視窗:
1. 使用OpenGL繪製3D骨架產生的視窗,在這個視窗可以用滑鼠滾輪縮放3D骨架大小。

2. 使用OpenCV產生的視窗,這個視窗整合每個相機預測的2D骨架,由OpenGL繪製的3D骨架,以及計算出給使用者使用的資訊。使用者主要使用這個視窗。

## OpenPose細節
針對3D骨架預測的修改主要分成四個部分:
1. 3D : 3D骨架重建。
2. Kalman : 加入Kalman Filter。
3. Angle : 計算角度。
4. GUI : 繪製GUI。
5. ROI : 限制每台相機中可以偵測2D骨架的區域。
這四個部分可以用`grep -rnw './<include/src>' -e '\[<3D/Kalman/Angle/GUI>\]'`找的對應的程式碼。
### 3D
* `examples/tutorial_api_cpp/18_3d_reconstruction.cpp`
* `Line 98, 103, 170, 171` : `// [3D] Read from videos/webcams` 從攝影機或是影片讀取資料。(Realtime or not)
* `src/openpose/3d/poseTriangulation.cpp`
* `Line 525` : `// [3D] Collect detected keypoints and camera parameters` 將每台相機偵測到的2D關節點以及相機參數放到`xyPointsElement(vector), cameraMatricesElement(vector)`裡面。
* `Line 541` : `// [3D] Collect keypoints that are visible from at least two cameras. ...` 從上一部得到的`Vector`裡面找出至少在兩台相機中被預測的到的關節點。將該關節點在相機中預測位置,關節點的編號,以及預測相機的相機參數放到`xyPoints(vector), indexesUsed(vector), cameraMatricesPerPoint(vector)`裡面。
* 等同於移除無法重建的關節點
* `Line 553` : `// [3D] Do 3D reconstruction` 使用上一步得到的`Vector`重建3D座標並放到`xyzPoints(vector)`。
* `Line 301` : `// [3D] RanSac for >= 3 cameras` 使用`RanSac`對投影誤差超過threshold的點做最佳化。
* `Line 588` : `// [3D] Remove outliers` 移除 (1) 重建失敗 (2)投影誤差過大的重建3D座標。
* `Line 596` : `// [3D] Save reconstructed 3D keypoints` 將最佳會後的3D關節點存入指標中。
```
keypoints3D[baseIndex] = xyzPoints[index].x // x position
keypoints3D[baseIndex+1] = xyzPoints[index].y // y position
keypoints3D[baseIndex+2] = xyzPoints[index].z // z position
keypoints3D[baseIndex+3] = 1.0 // Score of this keypoint
```
### Kalman
* `include/openpose/pose/wPoseExtractor.hpp`
* `Line 100` : `// [Kalman] Implement Kalman filter` 初始化`Kalman filter`
* `Line 144` : `// [Kalman] Apply Kalman filter` 使用`Kalman filter`對關節點做最佳化。
### Angle
* `src/openpose/3d/poseTriangulation.cpp`
* `Line 613, 43` : `// [Angle] Calculate Angle` 計算角度。先用關節點計算出代表肢體的向量,再計算向量之間的夾角。關節對照圖可參考[OpenPose 的文件](https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/output.md#pose-output-format-body_25)。

* `Line 625, 753` : `// [Angle] Return Angle` 以`pair`回傳計算角度。計算角度放在`second mamber variable`。
### GUI
* `src/openpose/3d/poseTriangulation.cpp`
* `Line 116` : `// [GUI] Pair labels and results` 用`std::map`儲存GUI上顯示名稱和計算出的角度。
* `Line 582` : `// [GUI] Log the missing frame rate in the terminal` 再terminal上顯示`missing frame rate`
* `include/openpose/pose/wPoseExtractor.hpp`
* `Line 193` : `// [GUI] Remove unwanter keypoints` 移除五官/左腳掌/右腳掌的3D關節點。
* `src/openpose/gui/gui3D.cpp`
* `Line 58` : `// [GUI] Default skeleton size` 3D 骨架初始大小。數字小->骨架大。
* `include/openpose/gui/wGui3D.hpp`
* `Line 88` : `// [GUI] Concat image, Display angle(Entry)` 將名稱和角度顯示在畫面上的函式之進入點。
* `src/openpose/gui/frameDisplayer.cpp`
* `Line 145` : `// [GUI] Concat image, Display angle` 用`cv::putText`將名稱和角度顯示在畫面上。
### ROI
* `include/openpose/3d/wPoseTriangulation.hpp`
* `Line 89` : `// [ROI] Save ROI` 檔案裡讀取每台相機的`ROI`
* `src/openpose/3d/poseTriangulation.hpp`
* `Line 467` : `// [ROI] Restrict ROI` 使用ROI限制偵測所得關節點,若關節點在ROI之外則是`invalid keypoint`