# 影像投影
* 影像投影是轉換式是三維重建中最重要的一個轉換式。基本上所有重要三維重建算法都離不開投影的原理。因此實作投影轉換式的過程,可以加強三維重建的觀念。
* 介紹投影轉換式之前必須先了解 1.[內外部參數](https://)、2.[座標系統](https://) (詳細介紹請參考連結)
* 投影轉換式是一連串座標轉換的過程。這個轉換過程由世界座標開始依序被轉為相機座標,然後又轉成正規化座標,接著將正規化座標扭曲成扭曲正規化座標,最後轉換成影像座標。
<br />
| | 自由度 | 單位 |
|---------------|--------|--------|
|世界座標系統 | 3 |m、cm、mm、inch|
|相機座標系統 | 3 |m、cm、mm、inch|
|正規化座標系統 | 2 |無因次|
|扭曲正規化座標系統 | 2 |無因次|
|影像座標系統 | 2 |pixel|
各個座標系統的自由度以及單位如上表所示
<br />
---
## 1. 座標轉換參數
* 轉換參數主要用於座標系之間的轉換,是電腦視覺中非常重要的觀念。
* 座標轉換參數主要可以分為內部參數以及外部參數。**內部參數**負責影像上的處理,例如:<u>影像去扭曲化</u>﹑ <u>影像的視野變換</u>等功能可以透過內部參數完成;**外部參數**負責空間上的轉換,例如:<u>以人造衛星為準的座標系統轉換成以地球為準的座標系統</u>就需要透過外部參數完成。
---
### 1.1 內部參數
內部參數主要可以分成 1. 相機參數 2. 扭曲參數。內部參數有一個重要的特性,通常在不改變焦距的情況下可以相機參數恆定、而扭曲參數則是相機出廠後恆定。
#### 1.1.1 相機參數
相機參數分為f<sub>x</sub>、f<sub>y</sub>主要用來描述相機的焦距;c<sub>x</sub>、c<sub>y</sub>主要用來描述相機的影像中心距。OpenCV中通常會將相機參數合成為$\eqref{eq11a}$一個[ 3 x 3 ]的矩陣
<br />
$$
\begin{bmatrix}
f_x & 0 & c_x \\
0 & f_y & c_y \\
0 & 0 & 1
\end{bmatrix}\tag{1-1a} \label{eq11a}
$$
上述為相機參數在OpenCV中的矩陣格式。
#### 1.1.2 扭曲參數
扭曲參數中分為k<sub>1</sub>、k<sub>2</sub>、k<sub>3</sub>主要描述了相機鏡頭中不同次項的扭曲;$\rho$<sub>1</sub>、$\rho$<sub>2</sub>主要描述感光元件傾斜造成的透視率。OpenCV中最常見的格式如下式$\eqref{eq11b}$使用5個參數來描述扭曲參數。
<br />
$$
\begin{bmatrix}
k_1 & k_2 & \rho_1 & \rho_2 & k_3
\end{bmatrix}\tag{1-1b} \label{eq11b}
$$
上述為扭曲參數在OpenCV中的矩陣格式(注意順序),需要注意的是扭曲參數常常有著不同的規格。常規影像通常有 5 個扭曲參數;魚眼影像通常有 6 個扭曲參數。有時也可能會為了描數不同等級的鏡頭使用更多或更少的參數數量。
<br />
### 1.2 外部參數
外部參數主要可以分成 1. 旋轉向量 2. 平移向量。旋轉向量以尤拉角的形式來描述兩座標系間相差的旋轉量;平移向量描述兩座標系之間旋轉完成後相差的平移量。
#### 1.2.1 旋轉向量
有兩種格式可以描述旋轉量。分別為[3 x 1]的旋轉向量格式如$\eqref{eq12a}$;[3 x 3]的旋轉矩陣格式如$\eqref{eq12b}$,兩格式間的轉換可逆。通常旋轉向量格式用來寫資料;旋轉矩陣格式用來做計算。
<br />
$$\tag{1-2a} \label{eq12a}
\begin{bmatrix}
R_x \\
R_y \\
R_z
\end{bmatrix}
$$
<br />
$$ \tag{1-2b}\label{eq12b}
\begin{bmatrix}
r_{xx} & r_{xy} & r_{xz} \\
r_{yx} & r_{yy} & r_{yz} \\
r_{zx} & r_{zy} & r_{zz}
\end{bmatrix}
$$
<br />
#### 1.2.2 平移向量
平移向量為[3 x 1]的格式如$\eqref{eq12c}$所式
$$ \tag{1-2c} \label{eq12c}
\begin{bmatrix}
t_x \\
t_y \\
t_z
\end{bmatrix}
$$
<br />
---
## 2. 座標系統
實現手寫影像投影之前,我們必須先搞懂各個參數座標系統之間的關係
<br />
- - -
### 2.1 世界座標系統
世界座標是自定義的座標系統,如下式$\eqref{eq21}$。
<br />
$$ \tag{2-1} \label{eq21}
\begin{bmatrix}
x_w \\
y_w \\
z_w
\end{bmatrix}
$$
<br />
### 2.2 相機座標系統
相機座標以相機為原點的座標系統,是將世界座標經過外部參數轉換而成,如式
$\eqref{eq22}$所示。
<br />
$$
\begin{bmatrix}
x_c \\
y_c \\
z_c
\end{bmatrix} \tag{2-2} \label{eq22}
$$
<br />
### 2.3 正規化座標系統
正規化座標是將相機座標上的點,轉換為寬高等比例失去深度的狀況,如式$\eqref{eq23}$所示。
<br />
$$
\begin{bmatrix}
x_n \\
y_n \\
\end{bmatrix} \tag{2-3} \label{eq23}
$$
<br />
### 2.4 扭曲正規化座標系統
扭曲正規化座標是考慮透鏡問題後產生的座標系統,如式$\eqref{eq24}$所示。
<br />
$$
\begin{bmatrix}
x_d \\
y_d \\
\end{bmatrix} \tag{2-4} \label{eq24}
$$
<br />
### 2.5 影像座標系統
影像座標是將扭曲正規化座標經過內部參數轉換而成,如式$\eqref{eq25}$所示。
<br />
$$
\begin{bmatrix}
x_i \\
y_i \\
\end{bmatrix} \tag{2-5} \label{eq25}
$$
<br />
---
## 3. 座標轉換
下面來進行座標轉換吧!
轉換的順序分別是 世界座標 <sup>3</sup> → 相機座標 <sup>3</sup> → 正規化座標 <sup>2</sup> → 扭曲正規化 <sup>2</sup> → 影像座標 <sup>2</sup>
<br />
- - -
### 3.1 首先將 **==世界座標==** 轉成 **==相機座標==** 吧~
```
世界座標系統 → 相機座標系統
```
<br />
$$\tag{3-1}
\begin{bmatrix}
x_c \\
y_c \\
z_c \\
1
\end{bmatrix} =
\begin{bmatrix}
r_{xx} & r_{xy} & r_{xz} & T_x\\
r_{yx} & r_{yy} & r_{yz} & T_y \\
r_{zx} & r_{zy} & r_{zz} & T_z \\
0 & 0 & 0 & 1
\end{bmatrix} ×
\begin{bmatrix}
x_w \\
y_w \\
z_w \\
1
\end{bmatrix}
$$
<br />
### 3.2 接著我們要將 **==相機座標==** 轉成 **==正規化座標==** ~
```
相機座標系統 → 正規化座標系統
```
<br />
$$\tag{3-2}
\begin{bmatrix}
x_n \\
y_n \\
1
\end{bmatrix} =
{1\over z_c}
\begin{bmatrix}
x_c \\
y_c \\
z_c
\end{bmatrix}
$$
<br />
### 3.3 接著 **==正規化座標系統==** 轉成 **==扭曲正規化座標系統==** ~
```
正規化座標系統 → 扭曲正規化座標系統
```
<br />
$$
k = k_1 × r^2 + k_2 × r^4 + k_3 × r^6 \tag{3-3a}
$$
<br />
$$ \begin{bmatrix} x_d \\ y_d \end{bmatrix} = \begin{bmatrix} x_n × k + 3{x_n}^2\rho_2 + {y_n}^2\rho_2 + 2x_{n}y{_n}\rho_1 \\ y_n × k + {x_n}^2\rho_1 + 3{y_n}^2\rho_1 + 2x_{n}y{_n}\rho_2 \end{bmatrix} \tag{3-3b}
$$
<br />
### 3.4 最後我們將 **==扭曲正規化座標==** 轉成 **==影像座標==** !
```
扭曲正規化座標系統 → 影像座標系統
```
<br />
$$\tag{3-4}
\begin{bmatrix}
x_i \\
y_i \\
1
\end{bmatrix} = \begin{bmatrix}
f_x & 0 & c_x \\
0 & f_y & c_y \\
0 & 0 & 1 \end{bmatrix} ×
\begin{bmatrix}
x_d \\
y_d \\
1
\end{bmatrix}
$$
- - -
## 4. C++實作
下文將示範一支手寫影像投影的code。大致分為下列幾個步驟
* **Step0.** 宣告變數 & 影像讀取
* **Step1.** 透過棋盤格影像計算內外參
<center><img src="https://i.imgur.com/gcHoxo4.jpg" width="100%" height="100%" />Figure 4.1. source影像</center>
<br />
原始影像,可以直接下載該圖片。
<br />
* **Step2.** 手寫影像投影運算
* **Step3.** 計算重投影誤差 & 繪製Marker檢查
<center><img src="https://i.imgur.com/189lfG1.jpg" width="100%" height="100%" />Figure 4.2. marker標註後影像</center>
<br />
從結果看來手寫投影法的過程正確,都可以成功將3D點投影回角點。(綠色為影像點、藍色為重投影點)
<br />
<font size=5> ***↓↓↓↓ Main Code ↓↓↓↓*** </font>
```cpp
#include <iostream>
#include <opencv2/opencv.hpp>
#include <math.h>
using namespace std;
using namespace cv;
/**
單點投影
*/
cv::Point2d projectsinglePoint(cv::Point3d Singleobject, cv::Mat rvec, cv::Mat tvec, cv::Mat CameraMatrix, cv::Mat dvec);
int main()
{
//Step 0. 宣告變數 & 影像讀取
Mat cmat;
Mat dvec;
vector<Mat> rvecs(1);
vector<Mat> tvecs(1);
cv::Mat R33(3, 3, CV_64F);
cv::Mat R44(4, 4, CV_64F);
vector<Point2f> imgPoint;
vector<Point3f> objPoint;
string filename = "D:\\ToDirk_20201228\\DirkTest\\calibright_g(L)\\img8.jpg"; //這是一張棋盤格影像
Mat img = imread(filename,IMREAD_GRAYSCALE);
Size sizeofchess(11, 7);
double space = 30.0;
//Step 1. 透過棋盤格影像計算內外參
bool check = findChessboardCorners(img, sizeofchess, imgPoint);
if (check) cout << "true" << endl;
for (int irow = 0; irow < sizeofchess.height; irow++)
{
for (int icol = 0; icol < sizeofchess.width; icol++)
{
double x = 0.0;
double y = 0.0;
double z = 0.0;
x += icol * space;
y += irow * space;
objPoint.push_back(Point3d(x, y, z));
}
}
vector<vector<Point2f>> Pointsin2d;
vector<vector<Point3f>> Pointson3d;
Pointsin2d.push_back(imgPoint);
Pointson3d.push_back(objPoint);
dvec = Mat::zeros(5, 1, CV_32F);
cmat = Mat::eye(3, 3, CV_32F);
cmat.at<float>(0, 0) = img.cols;
cmat.at<float>(1, 1) = cmat.at<float>(0, 0);
cmat.at<float>(0, 2) = img.cols * 0.5f;
cmat.at<float>(1, 2) = img.rows * 0.5f;
calibrateCamera(Pointson3d, Pointsin2d,Size(img.cols,img.rows), cmat, dvec, rvecs, tvecs
,CALIB_USE_INTRINSIC_GUESS );
vector<Point2f> reprjPtsbyCV;
vector<Point2d> reprjPtsbySelf;
//Step 2. 影像投影 - 1.func: projectsinglePoint 為手寫投影算法
// 2.func: projectPoints 為opencv的投影算法
cv::projectPoints(Pointson3d[0], rvecs[0], tvecs[0], cmat, dvec, reprjPtsbyCV);
for (int ipt = 0; ipt < Pointson3d[0].size(); ipt++)
{
Point3d pt3d = Point3d(Pointson3d[0][ipt].x, Pointson3d[0][ipt].y, Pointson3d[0][ipt].z);
Point2d pt2d = projectsinglePoint(pt3d, rvecs[0], tvecs[0], cmat, dvec);
reprjPtsbySelf.push_back(pt2d);
}
Mat imgdraw = img.clone();
cvtColor(imgdraw, imgdraw, COLOR_GRAY2BGR);
//Step 3. 計算重投影誤差 & 繪製Marker檢查
double Re_PrjErr_avg_Self = 0;
double Re_PrjErr_avg_cv = 0;
for (int ipt = 0; ipt < reprjPtsbySelf.size(); ipt++)
{
double xp_self, yp_self, xp_cv, yp_cv, xi, yi;
xp_self = reprjPtsbySelf[ipt].x;
yp_self = reprjPtsbySelf[ipt].y;
xp_cv = reprjPtsbyCV[ipt].x;
yp_cv = reprjPtsbyCV[ipt].y;
xi = imgPoint[ipt].x;
yi = imgPoint[ipt].y;
Re_PrjErr_avg_Self += sqrt((xp_self - xi)*(xp_self - xi) + (yp_self - yi)*(yp_self - yi));
Re_PrjErr_avg_cv += sqrt((xp_cv - xi)*(xp_cv - xi) + (yp_cv - yi)*(yp_cv - yi));
drawMarker(imgdraw, imgPoint[ipt], Vec3b(50, 200, 50), 1,5); //原始影像點繪製的marker為綠色
drawMarker(imgdraw, reprjPtsbyCV[ipt], Vec3b(50, 50, 200), 1,10); //opencv投影算法繪製的marker為紅色
drawMarker(imgdraw, reprjPtsbySelf[ipt], Vec3b(200, 50, 50), 1,10); //手寫投影算法繪製的marker為藍色
//(可自由標註測試感覺)
}
Re_PrjErr_avg_Self /= reprjPtsbySelf.size();
Re_PrjErr_avg_cv /= reprjPtsbyCV.size();
cout << "re-prjection Error(Self) :" << Re_PrjErr_avg_Self << endl;
cout << "re-prjection Error( CV ) :" << Re_PrjErr_avg_cv << endl;
cout << "最終結果 有些微差距 推測為捨位誤差" << endl;
imshow("draw", imgdraw);
waitKey(0);
system("pause");
return 0;
};
```
<font size=5> ***↑↑↑↑ Main code ↑↑↑↑*** </font>
<br />
<font size=5> ***↓↓↓↓ 手寫影像投影 ↓↓↓↓*** </font>
``` cpp
cv::Point2d projectsinglePoint(cv::Point3d Singleobject, cv::Mat rvec, cv::Mat tvec, cv::Mat CameraMatrix, cv::Mat dvec)
{
/**Step0 宣告&外參型態轉換**/
cv::Mat r33;
cv::Rodrigues(rvec, r33);
r33.convertTo(r33, CV_64F);
tvec.convertTo(tvec, CV_64F);
cv::Mat r44 = Mat::eye(4,4,CV_64F);
r33.copyTo(r44(cv::Rect(0, 0, 3, 3)));
tvec.copyTo(r44(Rect(3, 0, 1, 3)));
/**Step1 世界座標系統 → 相機座標系統**/
cv::Mat Obj3dPoint(4, 1, CV_64F);
cv::Mat Camera3dPoint(4, 1, CV_64F);
Obj3dPoint.at<double>(0, 0) = Singleobject.x;
Obj3dPoint.at<double>(1, 0) = Singleobject.y;
Obj3dPoint.at<double>(2, 0) = Singleobject.z;
Obj3dPoint.at<double>(3, 0) = 1.;
Camera3dPoint = r44 * Obj3dPoint; //公式1
cv::Mat normalizationPoint(2, 1, CV_64F);
/**Step2 相機座標系統 → 正規化座標系統**/
normalizationPoint.at<double>(0, 0) = Camera3dPoint.at<double>(0, 0) / //公式2
Camera3dPoint.at<double>(2, 0);
normalizationPoint.at<double>(1, 0) = Camera3dPoint.at<double>(1, 0) /
Camera3dPoint.at<double>(2, 0);
/**Step3 正規化座標系統 → 扭曲正規化座標系統**/
double xn, yn;
double xd, yd;
double k1, k2, k3, p1, p2;
double r;
double k;
k1 = dvec.at<double>(0, 0);
k2 = dvec.at<double>(1, 0);
p1 = dvec.at<double>(2, 0);
p2 = dvec.at<double>(3, 0);
k3 = dvec.at<double>(4, 0);
xn = normalizationPoint.at<double>(0, 0);
yn = normalizationPoint.at<double>(1, 0);
r = sqrt(xn*xn + yn * yn);
k = (1 + k1 * r*r + k2 * r*r*r*r + k3 * r*r*r*r*r*r); //公式3.1
xd = xn * k + 2 * p1 * xn * yn + 3 * p2 * xn * xn + p2 * yn * yn; //公式3.2
yd = yn * k + p1 * xn * xn + 2 * p2 * xn * yn + 3 * p1 * yn * yn;
cv::Mat distortionPoint(3, 1, CV_64F);
distortionPoint.at<double>(0, 0) = xd;
distortionPoint.at<double>(1, 0) = yd;
distortionPoint.at<double>(2, 0) = 1.;
cv::Mat SingleImgPoint(3, 1, CV_64F);
SingleImgPoint = CameraMatrix * distortionPoint; //公式4
return cv::Point2d(
SingleImgPoint.at<double>(0, 0),
SingleImgPoint.at<double>(1, 0)
);
}
```
<font size=5> ***↑↑↑↑ 手寫影像投影 ↑↑↑↑*** </font>
---
## 5. 成果
<br />
* 手寫影像投影算法與OpenCV投影算法的平均重投影誤差僅相差0.000001。足以認定手寫投影算法無誤。
* 手寫投影與OpenCV投影算法基本上都畫在相同的覘標交點。
---
## 6. 參考文獻
<br />
<font face="Time New Roman">**Yang, Y. S. (2019). Measurement of Dynamic Responses from Large Structural Tests by Analyzing Non-Synchronized Videos.** *Sensors, 19(16).* </font>