萬聖節快樂!
1. 相機模型
針孔相機模型:過空間某特定點的光線才能通過針孔(針孔光圈),這些光束被投影 到圖像平面形成圖像。
將圖像平面在針孔前方,重新把針孔相機模型整理成另一種等價形式,
實際上,芯片的中心通常不在光軸上,我們因此引入兩個新的參數cx和cy,對投影屏幕(圖像平面)坐標中心可能的偏移(對光軸而言)進行建模。這樣一來物理世界中的點Q,其坐標為(X, Y, Z),根據下式投射到成像裝置上某個像素位置(xscreen,yscreen):
xscreen = fx*X/Z+cx, and yscreen = fy*Y/Z+cy
我們引入了兩個不同的焦距,原因是單個像素在低價成像裝置上是矩形而不是正方形。例如,焦距fx實際上是透鏡的物理焦距長度與成像裝置每個單元尺寸sx的乘積(這樣做的意義在於sx的單位是像素/每亳米,而f的單位是毫米,這意味着fx的單位是像素。
2. 射影幾何基礎
將物理世界中坐標為(Xi, Yi , Zi)的一系列物理點Q映射到投影平面上坐標為(xi, yi)的點的過程叫“射影變換”。公式為:
其中,M為相機的內參矩陣,Q為物理世界的點。將q的x和y坐標都除以w,可以得到實際的像素坐標值。
函數cv::convertPointsToHomogeneous()和cv::convertPointsFromHomogeneous()允許我們在齊次坐標和非齊次坐標之間轉換。
采用理想針孔,由於只有很少量的光線通過針孔, 在實際中, 無論使用何種圖像采集器, 都需要等待積累足夠的光線, 因此成像速度非常慢。 對於快速生成圖像的相機而言,必須利用更大面積的光線,甚至讓光線彎曲,從而讓足夠多的光線能夠聚焦到投影點上。我們使用透鏡來實現這個目的。 透鏡可以聚焦足夠多的光線到一個點上,使得圖像生成更加迅速,但代價是引入了畸變。
3.Rodrigues變換
在三維空間操作時,通常使用3x3矩陣來表示空間中的旋轉。這種表示通常最方便, 因為將向量乘以該矩陣相當於以某種方式旋轉向量。缺點是很難理解3x3矩陣表示什么樣的旋轉。除此之外,可以通過一個向量表示沿着某一角度進行旋轉,向量的方向表示旋轉軸的方向,向量的長度表示沿逆時針方向的旋轉量。這個很容易做到,因為方向可以用任意幅度的向量表示。因此,我們可以選擇我們的向量幅度等於旋轉的角度。旋轉向量與旋轉矩陣可以通過Rodrigues變換進行轉換。三維空間的旋轉包含三個自由度, 從數值優化的角度而言, 處理只有三個部分的Rodrigues表示比處理有幾個部分的3x3旋轉矩陣方便得多。
void Rodrigues( InputArray src, //輸入旋轉向量or矩陣
OutputArray dst, //輸出旋轉矩陣or向量
OutputArray jacobian = noArray() );
最后一個參數是可選的, 如果jacobian不是cv::noArray(),那么它應該是一個指向3x9或9x3矩陣的指針, 該矩陣將填充輸出矩陣元素相對於輸入矩陣元素的偏導數。輸出的jacobian主要用千CV: : solvePnP( )和cv::calibrateCamera()函數的內部優化,使用cv::Rodrigues()函數主要用於將cv:: solvePnP()和cv::calibrateCamera()的輸出從Rodrigues格式的1x3或3x1軸—角度向量轉化為旋轉矩陣。在此時,可以將jacobian設置為cv::noArray( )。
示例:
#include <opencv.hpp> using namespace cv; using namespace std; int main() { Mat r = (cv::Mat_<float>(3, 1) << -2.100418, -2.167796, 0.273330); Mat R; Rodrigues(r, R, noArray()); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%f ", R.at<float>(i,j)); } printf("\n"); } return 0; }
結果:
-0.036254 0.978364 -0.203692 0.998304 0.026168 -0.051995 -0.045539 -0.205232 -0.977653
4. 透鏡畸變
4.1 徑向畸變
徑向畸變是由於透鏡的形狀造成的,遠離透鏡中心的光線比靠近透鏡中心的光線更加彎曲,在成像裝置邊緣附近的像素位置產生顯著的畸變。也稱為筒形畸變。
對於徑向畸變, 成像裝置的(光學)中心處的畸變為0,隨着向邊緣移動,畸變越來越大。實際上,這種畸變很小,可以用 r = 0 附近的泰勒級數展開的前幾項來描述。對於廉價的網絡相機,我們通常使用前兩項;其中通常將第一項稱作 k1,第二項稱作 k2。對於畸變很大的相機, 比如魚眼透鏡,我們可以使用第三個徑向畸變項 k3。通常,成像裝置上某點的徑向位置可以根據以下等式進行調整:
xcorrected = x • (1 + k1r2 + k2r4 + k3r6)
ycorrected = y • (1 + k1r2 + k2r4+k3r6)
這里(x, y)是(成像裝置上)畸變點的原始位置,(xcorrected,ycorrected)是矯正后的新位置。 下圖顯示了矩形由於徑向畸變產生的偏移。 隨着距光學中心的徑向距離的增加, 矩形上的外部點越來越向內偏移。
4.2 切向畸變
這種畸變是由於制造上的缺陷使透鏡不與成像平面平行而產生的。在廉價的相機中,這種現象發生在成像裝置被粘在相機背面的時候。切向畸變可以用兩個額外參數P1 和P2來表示:
xcorrected = x + [ 2p1xy + p2(r2 + 2x2)]
ycorrected = y + [ p1(r2 + 2y2) + 2p2xy ]
因此我們總共需要五個畸變參數。因為在大多數使用它們的OpenCV程序中,這五個參數都是必需的,因此它們被放到一個畸變向量中,這是一個5x1的矩陣包括k1,k2,p1,p2和k3(按順序)。在成像系統中還有很多其他類型的畸變,但是它們比徑向和切向畸變的影響小。因此,我們和OpenCV都不會進一步處理它們。
5. 標定
OpenCV提供了一些算法來幫我們計算這些內部參數。實際標定過程是通過cv::calibrateCamera()來完成的。標定的方法是把相機對准一個具有很多獨立可標識點的已知結構。通過從多個視角觀察這個結構,我們可以計算拍攝每個圖像時相機的(相對)位置和方向以及相機的內部參數。為了提供多個視角, 我們需要旋轉和平移物體。
5.1 旋轉矩陣和平移變量
我們已經知道可以用三個角度來表示三維旋轉,可以用三個參數(x,y, z)來表示三維平 移,因此我們目前有六個參數。OpenCV相機內參矩陣有四個參數(fx、fy、cx和cy),所以每個視圖都需要求解10個參數(注意,相機內在參數在不同視圖保持不變)。使用一個平面物體,我們很快可以看到每個視圖固定八個參數。因為在不同視圖下旋轉和平移的 六個參數會變化,對於每個視圖,我們對兩個額外參數進行約束,隨后使用它們來求解相機內參矩陣。因此,我們需要(至少)兩個視圖來求解所有的幾何參數。
5.2 標定板
OpenCV選擇使用平面物體的多個視圖,而不是特別構造的三維物體的一個視圖。目前我們將集中討論棋盤模式。使用交替的黑色和白色方格的圖案確保在測量中沒有偏向一側或另一側。
5.3 相機標定
首先要注意的是外參數包括3個旋轉參數和3個平移參數,每個棋盤視圖共有6個外參數。 由相機內參矩陣的4個參數和6個外參數共同構成10個需要求解的參數, 在單個視圖的情況下, 每個額外的視圖就會增加6個參數。
假設有N個角點和K個棋盤圖像(不同位置)。 我們需要看到多少視圖和角點才能有足夠的約束條件來求解所有這些參數?
• K個棋盤圖像提供2 · N · K個約束(出現因子2是因為圖像上的每個點都具有x和y兩個坐標值)
• 忽略每次的畸變參數, 我們有4個內在參數和6 · K個外參數(因為我們需要在K個視圖中找到棋盤位置的6個參數)。
• 求解的前提是2·N·K≥6·K+4。
所以看起來如果N = 5, 那么我們只需要K = 1的圖像, 但要注意!無論我們在平面上發現多少角點, 我們只得到四個有用的角點信息,因此至少需要兩個視圖才能求解我們的標定問題。 在實踐中, 為了獲得高質批的效果, 需要至少10張7X8或更大的棋盤圖像(只有在圖像之間移動足夠次數的棋盤才能獲得 “豐富的“ 視圖)。
5.4 用cv::findChessboardCorners()找到棋盤角點★★★
給定一個棋盤圖像(或一個人手持棋盤,或任何具有棋盤的場景和合適的無干擾背景),可以使用函數cv::findChessboardCorners()來定位棋盤的角點。如果可以找到並排序所有的角點,返回值被設為true,否則為false。
bool findChessboardCorners( InputArray image, //必須是8bit圖像
Size patternSize, //棋盤圖,8UC1 OR 8UC3
OutputArray corners, //棋盤每行每列有多少角點 Size(cols,rows) int flags = CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE );
//用於實現一個或多個附加濾波步驟,以幫助找到棋盤上的角點
棋盤上的亞像素角點和cv::cornerSubPix()
findChessboardCorners()使用的內部算法僅提供角點的近似位置。因此,cornerSubPix()由indChessboardCorners()自動調用,以獲得更准確的結果。這在實際中意味着這些位置是相對准確的。但是,如果你希望將它們定位到非常高的精度,則需要在輸出上自己調用cornerSubPix()(有效地再次調用它), 但是需要更嚴格的終止條件。
5.5 使用cv::drawChessboardCorners()繪制棋盤角點★★★
函數cv::drawChessboardCorners()將cv::findChessboardCorners()找到的角點繪制到你提供的圖像上。如果沒有找到所有的角點, 則可用的角點將被表示為小的紅色圓圈。如果整個圖案上的角點都找到, 那么角點將被繪制成不同的顏色(每一行都將有自己的顏色), 並將角點以一定順序用線連接起來。
void drawChessboardCorners( InputOutputArray image, //由於角點是用有顏色的圓圈表示的,因此必須為8位的彩色圖像。
Size patternSize, InputArray corners,
bool patternWasFound );//是否整個棋盤圖案上的角點都被成功找到
5.6 cv::calibrateCamera()得到相機內參和物體外參★★★
double calibrateCamera( InputArrayOfArrays objectPoints,//在x和y維中是整數,在z維中為零 InputArrayOfArrays imagePoints, //圖像中每個點的位置
Size imageSize,//圖像的大小(以像素為單位) InputOutputArray cameraMatrix, //包含線性內在參數, 應為3x3矩陣
InputOutputArray distCoeffs,//畸變參數,可以是4,5或8個元素 OutputArrayOfArrays rvecs, //旋轉矩陣(以Rodrigues形式表示)
OutputArrayOfArrays tvecs,//平移矩陣 int flags = 0,
TermCriteria criteria = TermCriteria(//終止標准 TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON) );
flags參數允許對標定過程進行更精確的控制。 以下值可以根據需要與布爾OR運算組合在一起。
- CALIB_USE_INTRINSIC_GUESS
在計算內參矩陣時不需要其他信息。具體來說,參數cx和cy(圖像中心)的初始值直接從imageSize參數中得到。如果設置此參數,則假定cameraMatrix中包含有效值,該值將作為初始猜測值被進一步優化。在許多實際應用中, 我們知道相機的焦距, 因為我們可以從鏡頭的側面讀取它們。在這種情況下, 將這些信息放入相機矩陣中並使用 cv::CALIB_USE_INTRINSIC_GUESS是一個好主意。
- CALIB_FIX_PRINCIPAL_POINT
該標志可以與CALIB_USE_INTRINSIC_GUESS結合使用,也可以單獨使用。如果單獨使用,則主點固定在圖像中心;如果共同使用,則主點固定在cameraMatrix中提供的初始值。
- CALIB FIX ASPECT RATIO
如果設置這個標志,那么在調用標定程序時,優化過程將一起改變fx和fy,並且它們的比值保持在cameraMatrix中設置的值。如果沒有設置cv::CALIB_USE_INTRINSIC_ GUESS標志, 那么cameraMatrix中的fx和fy的值可以是任意值,只是它們的比值是相關的。
- CALIB FIX FOCAL LENGTH
該標志在優化時直接使用cameraMatrix中傳遞的fx和fy
- CALIB_FIX_K1, cv::CAlIB_FIX_K2, ... cv::CALIB_FIX_K6
修正徑向畸變參數k 1 , k2到k6。 可以通過組合這些標志來設置徑向參數。
- CALIB RATIONAL MODEL
該標志告訴OpenCV計算k4,k5和k6和從三個畸變系數。 這是因為向后兼容性問題, 如果不添加此標志, 則只計算前三個K參數(即使你為distCoeffs提供了一個八元素矩陣)。
5.7 已知內參計算外參數
-
僅使用cv::solvePnP()計算外參數
在某些情況下,我們已經知道了相機的內在參數,因此只需要計算正在觀察的對象的位置。這種情況與一般的相機標定明顯不同,但它仍然是有用的工作。
bool solvePnP( InputArray objectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArray distCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess = false, int flags = SOLVEPNP_ITERATIVE );
solvePnP()的參數與calibrateCamera()的對應參數類似,但是有兩個不同的地方:
1) objectPoints和imagePoints參數是來自物體的單個視圖的參數(即,它們的類型為 cv::InputArray, 而不是cv::InputArrayOfArrays) 。
2) 內參矩陣和畸變系數是直接提供的而不必計算(即, 它們是輸入而不是輸出) 。
所輸出的旋轉向量以 Rodrigues形式表示:由三個部分組成的旋轉向量表示棋盤或點旋轉的三維坐標軸, 向量的幅度或長度代表逆時針旋轉角度。這個旋轉向量可以通過cv::Rodrigues()函數轉換成我們之前討論過的3x3旋轉矩陣。平移向量是相機坐標中棋盤原點的偏移量。
useExtrinsicGuess參數可以設置為true,以表示rvec和tvec參數中的當前值應被視為求解的初始猜測值。默認值為false。
參數flags可以設置為三個值之一,即cv::IT ERATIVE, cv::P3P或cv:: EPNP , 以 表明應該使用哪種方法來求解整個系統。 當使用cv::ITERATIVE時, 會使用Levenberg-
-
只用cv::solvePnPRansac()計算外參數
cv: :solvePnP的一個缺點是對異常值不夠魯棒。 在相機標定時, 這不是一個問題, 主要 是因為棋盤本身為我們提供一種可靠的方法來找到我們關心的各個特征, 並通過它們的 相對幾何位置來驗證我們正在看的實物和我們認為的一致。 然而 , 當我們用相機定位的不是棋盤上的點而是真實世界中的點(例如, 使用稀疏關鍵點特征)時, 可能發生錯配並導致嚴重的問題。 回想一下我們在前面討論單應性時講過RANSAC方法可以成為處理這種離群值的有效方法:
bool solvePnPRansac( InputArray objectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArray distCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess = false, int iterationsCount = 100, float reprojectionError = 8.0, double confidence = 0.99, OutputArray inliers = noArray(), int flags = SOLVEPNP_ITERATIVE );
RANSAC算法還由一些新參數控制。具體來說,iterationsCount參數設置RANSAC迭代的次數,
而reprojectionError參數表示將某點設置為內部點的最大重投影誤差注39參數mininliersCount的命名有一定的誤導性,如果在運行RANSAC時,內部點數最超過mininliersCount,則該過程將被終止,並且該組將被認為是內部點組。這樣做可以顯著提高性能,但如果設置得太低也會出現很多問題。最后,inliers參數是一個輸出,如果提供的話,將用內部點的索引值(從objectPoints到imagePoints)來填充該參數。
6. 矯正
通過輸入原始圖像和由函數calibrateCamera()得到的畸變系數,生成矯正后的圖像。我們既可以只用函數undistort()使用該算法一次性完成所需的任務,也可以用一對函數initUndistortRectifyMap()和remap()來更有效地處理此事,這通常適用於視頻或者同一相機中獲取多個圖像的應用中。
6.1 矯正映射
當進行圖像矯正時,我們必須指定輸入圖像中的每個像素在輸出圖像中移動到的位置,稱為矯正映射(或有時為畸變映射)。這樣的映射有以下幾種表示:雙通道浮點表示,雙矩陣浮點數表示。定點表示。
6.2 使用 cv::convertMaps()在不同表示方式之間轉換矯正映射
因為在矯正映射中有多個表示方式, 所以人們很自然地希望在這些表示方式之間能夠進 行相互轉換。我們用cv::convertMaps()函數便能夠做到這一點。 這個功能允許你提供
6.3 使用cv::initUndistortRectifyMap()計算矯正映射★★★
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray distCoeffs, InputArray R, //補償相機相對千相機所處的全局坐標系的旋轉
InputArray newCameraMatrix,// 對於單目圖像,設為noArray() Size size, //輸出映射的尺寸
int m1type, //最終的映射類型,可能值為CV_32FC1或CV_16SC2
OutputArray map1, OutputArray map2);
6.4 使用cv::remap()矯正圖像★★★
一且計算了矯正映射,就可以使用cv:: remap()將它們應用於傳入的圖像。如前所述,cv::remap()函數有兩個對應於矯正映射的映射參數,例如由 cv::initUndistortRectifyMap()計算得到的映射參數。cv:: remap()接受我們討論的任何矯正映射格式:雙通道浮點型、雙矩陣浮點型或定點格式(帶或不帶插值表索引矩陣)。
void remap( InputArray src, OutputArray dst, InputArray map1, InputArray map2,//initUndistortRectifyMap()計算得到的映射參數 int interpolation, int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar());
6.5 使用cv::undistort()進行矯正★★★
在某些情況下,只需要校正一個圖像,或者對每一個圖像重新計算矯正映射。在這種情況下,可以使用更加簡潔的undistort(),它可以有效地計算映射並將其應用於單個圖像。
void undistort( InputArray src, OutputArray dst, InputArray cameraMatrix, InputArray distCoeffs, InputArray newCameraMatrix = noArray() );
6.6 使用cv::undistortPoints()進行稀疏矯正
只對你關心的點矯正。
7. 示例
#include<iostream> #include<opencv2\opencv.hpp> using namespace std; using namespace cv; int main(int argc, char* argv[]) { int n_boards = 10; float image_sf = 0.5f; float delay = 1.f; int board_w = 3; int board_h = 3; int board_n = board_w * board_h; Size board_sz = Size(board_w, board_h); //開啟攝像頭 VideoCapture capture(0); if (!capture.isOpened()) { cout << "\nCouldn't open the camera\n"; return -1; } //分配存儲空間 vector<vector<Point2f>> image_points; vector<vector<Point3f>> object_points; double last_captured_timestamp = 0; Size image_size; //不斷取圖,直到取夠n_boards張 while (image_points.size() < (size_t)n_boards) { Mat image0, image; capture >> image0; image_size = image0.size(); resize(image0, image, Size(), image_sf, INTER_LINEAR); //Find the board vector<Point2f> corners; bool found = findChessboardCorners(image, board_sz, corners); //Draw it drawChessboardCorners(image, board_sz, corners, found); double timestamp = (double)clock() / CLOCKS_PER_SEC; if (found && timestamp - last_captured_timestamp > 1) { last_captured_timestamp = timestamp; image ^= Scalar::all(255); Mat mcorners(corners); mcorners *= (1. / image_sf); image_points.push_back(corners); object_points.push_back(vector<Point3f>()); vector<Point3f>& opts = object_points.back(); opts.resize(board_n); for (int j = 0; j < board_n; j++) { opts[j] = Point3f((float)(j / board_w), (float)(j % board_w), 0.f); } cout << "Collected our " << (int)image_points.size() << "of" << n_boards << "needed chessboard images\n" << endl; } imshow("calibration", image); if ((waitKey(30) & 255) == 27) return -1; } destroyWindow("calibration"); cout << "\n\n*** CALIBRATIING THE CAMERA... \n" << endl; //Calibrate the camera Mat intrinsic_matrix, distortion_coeffs; double err = calibrateCamera( object_points, image_points, image_size, intrinsic_matrix, distortion_coeffs, noArray(), noArray(), CALIB_ZERO_TANGENT_DIST | CALIB_FIX_PRINCIPAL_POINT); //保存相機內參和畸變 cout << "***DONE!\n\n Reprojection error is" << err << "\nStoring Intrinsics.xml and Distortions.xml files\n\n"; FileStorage fs("intrinsics.xml", FileStorage::WRITE); fs << "image_width" << image_size.width << "image_height" << image_size.height << "camera_matrix" << intrinsic_matrix << "distortion_coeffs" << distortion_coeffs; fs.release(); //加載這些參數 fs.open("intrinsics.xml", FileStorage::READ); cout << "\nimage width:" << (int)fs["image_width"]; cout << "\nimage height:" << (int)fs["image_height"]; Mat intrinsic_matrix_loaded, distortion_coeffs_loaded; fs["camera_matrix"] >> intrinsic_matrix_loaded; fs["distortion_coeffs"] >> distortion_coeffs_loaded; cout << "\nintrinsic matrix:" << intrinsic_matrix_loaded; cout << "\ndistortion coefficients:" << distortion_coeffs_loaded << endl; //矯正映射 Mat map1, map2; initUndistortRectifyMap( intrinsic_matrix_loaded, distortion_coeffs_loaded, Mat(), intrinsic_matrix_loaded, image_size, CV_16SC2, map1, map2 ); //傳入圖像,顯示的是沒有畸變的圖像 for (;;) { Mat image, image0; capture >> image0; if (image0.empty()) break; remap( image0, image, map1, map2, INTER_LINEAR, BORDER_CONSTANT, Scalar() ); imshow("Undistorted", image); if ((waitKey(30) & 255) == 27) break; } return 0; }
結果:
<?xml version="1.0"?> -<opencv_storage> <image_width>640</image_width> <image_height>480</image_height> -<camera_matrix type_id="opencv-matrix"> <rows>3</rows> <cols>3</cols> <dt>d</dt> <data>5.7907667726308171e+02 0. 3.1950000000000000e+02 0.1.1801417596095703e+03 2.3950000000000000e+02 0. 0. 1.</data> </camera_matrix> -<distortion_coeffs type_id="opencv-matrix"> <rows>1</rows> <cols>5</cols> <dt>d</dt> <data>1.3156136239488735e-02 -2.0824275792988209e-01 0. 0. -3.1422421138402745e-01</data> </distortion_coeffs> </opencv_storage>
補充:
1. 關於為什么
(waitKey(30) & 255) == 27
文章https://blog.csdn.net/hao5119266/article/details/104173400詳細講解了。
2. image_points和object_points的區別
image_points是圖像上點的像素坐標,object_points是(0,0,0)(0,1,0)這樣的位置
object_points的size是標定圖片的個數,打印一個object_points,如下所示
[0, 0, 0;
0, 1, 0; 0, 2, 0; 0, 3, 0; 1, 0, 0; 1, 1, 0; 1, 2, 0; 1, 3, 0; 2, 0, 0; 2, 1, 0; 2, 2, 0; 2, 3, 0; 3, 0, 0; 3, 1, 0; 3, 2, 0; 3, 3, 0]
3. 為什么opts的z維為0,也就是object_points的z維為0
opts[j] = Point3f((float)(j / board_w), (float)(j % board_w), 0.f);
我們關注的點不是所有空間的坐標,只是在觀察平面上的坐標,因此做一些簡化,選擇定義物體平面使得Z=0。(我猜的,我也不李姐)
4.opencv 圖像畸變矯正加速、透視變換加速方法
https://blog.csdn.net/lcydhr/article/details/72726396
來自蝴蝶書18章P553-P597