# 影像投影 * 影像投影是轉換式是三維重建中最重要的一個轉換式。基本上所有重要三維重建算法都離不開投影的原理。因此實作投影轉換式的過程,可以加強三維重建的觀念。 * 介紹投影轉換式之前必須先了解 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>