寫在題前:這篇文章磨磨蹭蹭了很久,曾經兩次接近完稿而丟失。我想任何事情在起步時都會有相似的囧境,還好我還有恆心繼續下去。 ios
攝像頭標定的目的有兩個。第一,要還原攝像頭成像的物體在真實世界的位置就須要知道世界中的物體到計算機圖像平面是如何變換的,攝像頭標定的目的之一就是爲了搞清楚這種變換關係,求解內外參數矩陣。第二,針孔攝像頭的發明使得攝像頭變成了親民物品,大行於世,可是針孔攝像頭有個很大的問題——畸變。攝像頭標定的另外一個目的就是求解畸變係數,而後用於計算求解正確的成像。 數組
前面所說的真實世界到成像平面的變換過程牽扯到四個座標系,變換矩陣能夠大體分爲兩組參數——內部參數和外部參數。下面依次根據四種座標系的關係來推導內外參數的形式。ide
計算機座標系是指數字圖像在計算機中的保存形式-二維數組的座標形式。成像平面座標系是攝像機鏡頭的成像平面上創建的座標系,通常是位於感光器件上(如CCD)所創建的以鏡頭光軸與成像平面交點爲原點的二維座標系。因爲二維數組以離散的形式保存,因此計算機座標系的長度單位爲1,通常感光器件大小約爲指甲蓋大小,上面又密集的分別不少感光單元,每一個感光單元採集的圖像都會計算成計算機座標系中的像素值,因此通常長度單位爲微米。具體兩者關係見下圖,函數
圖中座標系O-uv爲計算機座標系,其座標軸方向從右上到左下遞增。座標系Q-xy爲成像平面座標系,其座標原點Q在計算機座標系的座標爲(u0,v0)。假設有一點P,其在計算機座標系中的座標爲(u,v),在成像平面座標系中的座標爲(x,y),並設像素單元在x,y方向的尺寸分別爲a,b。則有以下等式成立,ui
整理成齊次變換矩陣的形式以下,spa
攝像機座標系是一個以攝像機鏡頭的光心爲原點而創建的空間三維座標系。成像平面座標系能夠當作攝像機座標系在其Zc軸上投影而成的(其x和y軸與成像平面座標系方向一致)。其與成像平面座標系的關係以下圖,命令行
這裏須要詳細解釋一下上圖怎樣理解。圖中Oc-XcYcZc爲攝像機座標系,M點爲物點,而Q-xy爲成像平面座標系。實際狀況下成像平面與物體應該分居攝像頭兩側,可是在這裏咱們將成像平面以攝像頭座標系原點爲中心對稱點對稱過來。這樣的好處是原本倒立的像變成正立的,且大小不變。如上一篇博客寫到的那樣,中心對稱的後的像點和物點連線一定過光心。咱們要記住小孔成像的性質是,成像平面到鏡頭的距離始終等於焦距f。由類似三角形,有以下等式成立,3d
整理成齊次變換矩陣的形式以下,code
世界座標系是區別於攝像機座標系的一個空間座標系,其依附於咱們標定時要使用的物點而存在。既咱們觀察的物點相對於世界座標系的位置是固定的,如張正友標定法中就是選的世界座標系爲以chessboard平面爲XOY平面的相對於chessboard不動的空間座標系。下圖是我費了老大勁繪製的攝像機座標系和世界座標系之間的位置關係,xml
咱們知道空間中的兩個座標系能夠經過平移旋轉變換而重合,那麼如今咱們來簡單粗暴解釋一下物點M在兩個座標系下座標的代數關係是什麼樣的。假設攝像機座標系能夠經過旋轉平移變換與世界座標系重合,其齊次變換矩陣形式以下,
也就是說攝像機座標系上的每一個點(在世界座標系中的座標)經過上述旋轉平移變換後與世界座標系中的相應點重合。咱們能夠理解的是,對攝像機座標系上的每一個點和物點M(在世界座標系下的座標)作相同的旋轉平移變換後其相對位置不會改變。也就是說物點M在攝像機座標系中的座標不會改變。咱們考察一下變換完後是什麼狀況。這個時候世界座標系和攝像機座標系重合,且物點M相對於攝像機座標系的座標沒有改變,那麼這個時候物點M在世界座標系中的座標應該變成了變換前其在攝像機座標系下的座標。因此有以下關係成立,
綜合上面的三組關係,咱們能夠推導出世界座標系和計算機座標系的直接代數關係,
進一步化簡,
咱們記,
A矩陣式刻畫攝像機的內部參數,包括焦距f、成像中心的位置、及成像單元尺寸,所以稱爲內參矩陣。M描述的攝像頭的運動關係,因此稱爲外參矩陣。其中R有三個相關參數,分別是繞x,y,z軸的旋轉角度。T也有三個相關參數,分別是在三個座標軸上的平移量Tx、Ty、Tz。
攝像頭的畸變是因爲成像模型的不精確形成的。人們爲了提升光通量用透鏡代替小孔來成像,因爲這種代替不能徹底符合小孔成像的性質,所以畸變就產生了。另外這裏再插句額外的話,如今大量使用的透鏡爲球面鏡,緣由是其廉價易得。可是真正的徹底符合理想光學系統的透鏡實際是個四次曲面(很好證實,依據光程不變),制形成本很大哦。
畸變能夠分爲兩大類,徑向畸變和切向畸變。詳細的畸變介紹能夠參考工程光學的相關課程,下面簡單介紹相關畸變及其修正。
徑向畸變的效應有兩種,一種是枕形效應,另外一種是桶形效應,具體見下圖(圖片來自互聯網),
徑向畸變可用下面公式修正,
徑向畸變是因爲透鏡與成像平面不嚴格的平行,其能夠用以下公式修正,
這樣又引入了五個畸變參數,
咱們記fx=f/a,fy=f/b。經過上面的介紹,咱們瞭解到,攝像機標定共有4個內參,6個外參和五個畸變參數要求。下面就介紹怎麼基於OpenCV函數庫標定求得這三組參數。其求解原理放在之後的博客中敘述。
OpenCV中有標定實例哦,寫的很好,功能很完善。一個是基於命令行標定參數讀入的標定程序,另外一個是基於xml文件參數讀入的標定程序。它們的位置分別爲,
...\opencv\sources\samples\cpp\calibration.cpp
...\opencv\sources\samples\cpp\tutorial_code\calib3d\camera_calibration\ camera_calibration.cpp
可是爲了更深刻的理解OpenCV的標定方法庫,我本身寫了一個簡單粗暴易讀的標定程序。下面簡單介紹一下標定的過程。
標定過程以下,
我寫的標定程序以下,
1 /* 2 Writer: Wang Xianshun 3 Email: german_iris@outlook.com 4 */ 5 #include <iostream> 6 #include <stdio.h> 7 #include <time.h> 8 #include <string.h> 9 10 #include <cv.hpp> 11 #include <highgui\highgui.hpp> 12 #include <calib3d\calib3d.hpp> 13 #include <imgproc\imgproc.hpp> 14 #include <core\core.hpp> 15 16 using namespace std; 17 using namespace cv; 18 19 static void calcChessboardCorners(Size boardSize, float squareSize, vector<Point3f>& corners) 20 { 21 corners.resize(0); 22 for (int i = 0; i < boardSize.height; i++) //height和width位置不能顛倒 23 for (int j = 0; j < boardSize.width; j++) 24 { 25 corners.push_back(Point3f(j*squareSize, i*squareSize, 0)); 26 } 27 } 28 29 int main(int argc, char** argv) 30 { 31 int success = 0; 32 int cameraId = 0; 33 int nFrames = 10; 34 int w = 6; 35 int h = 9; 36 clock_t prevTimestamp = 0; 37 int delay = 1000; 38 39 //相關參數初始化 40 Size boardSize, imageSize; 41 boardSize.width = w; 42 boardSize.height = h; 43 vector<vector<Point2f>> imagePoints; 44 float squareSize = 1.f; 45 Mat intrMatrix, distCoeffs; 46 vector<Mat> rvecs, tvecs; 47 48 //標定參數讀取 49 if (argc < 5) 50 { 51 cout << "參數不足" << endl; 52 return 0; 53 } 54 55 for (int i = 1; i < argc; i++) 56 { 57 if (!strcmp(argv[i], "-w")) 58 { 59 if (!sscanf(argv[++i], "%u", &boardSize.width)) 60 { 61 return fprintf(stderr, "無效的標定角點寬度\n"), -1; 62 } 63 } 64 else if (!strcmp(argv[i], "-h")) 65 { 66 if (!sscanf(argv[++i], "%u", &boardSize.height)) 67 { 68 return fprintf(stderr, "無效的標定角點高度\n"), -1; 69 } 70 } 71 else if (!strcmp(argv[i], "-s")) 72 { 73 if (!sscanf(argv[++i], "%f", &squareSize) != 1 || squareSize <= 0) 74 { 75 return fprintf(stderr, "無效的方格尺寸\n"), -1; 76 } 77 } 78 else 79 return fprintf(stderr, "未知參數\n"), -1; 80 } 81 82 //圖像採集 83 VideoCapture capture; 84 capture.open(cameraId); 85 namedWindow("Image View", 1); 86 87 if (!capture.isOpened()) 88 { 89 cout << "沒法打開攝像頭,(づ ̄3 ̄)づ╭❤~……" << endl; 90 return -1; 91 } 92 93 for (int i = 0; success < nFrames; i++) 94 { 95 string msg = "CAPTURING"; 96 Mat viewGray, view; 97 capture >> view; 98 imageSize = view.size(); 99 vector<Point2f> pointBuf; 100 cvtColor(view, viewGray, COLOR_BGR2GRAY); 101 102 //尋找角點 103 bool found = findChessboardCorners(view, boardSize, pointBuf, 104 CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_FAST_CHECK | CV_CALIB_CB_NORMALIZE_IMAGE); 105 106 if (found) 107 { 108 //亞像素檢測以提升精度 109 cornerSubPix(viewGray, pointBuf, Size(11, 11), 110 Size(-1, -1), TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1)); 111 //標記出檢測到的角點 112 drawChessboardCorners(view, boardSize, Mat(pointBuf), found); 113 } 114 115 //等待用戶改變姿態 116 if (found && clock() - prevTimestamp > delay*1e-3*CLOCKS_PER_SEC) 117 { 118 imagePoints.push_back(pointBuf); 119 prevTimestamp = clock(); 120 success = success + 1; 121 bitwise_not(view, view); 122 } 123 124 imshow("Image View", view); 125 waitKey(20); 126 } 127 128 cout << "圖像採集完成,開始標定……" << endl; 129 130 //標定 131 vector<vector<Point3f>> ObjectPoints(1); 132 calcChessboardCorners(boardSize, squareSize, ObjectPoints[0]); 133 ObjectPoints.resize(imagePoints.size(), ObjectPoints[0]); 134 calibrateCamera(ObjectPoints, imagePoints, imageSize, intrMatrix, 135 distCoeffs, rvecs, tvecs); 136 bool ok = checkRange(intrMatrix) && checkRange(distCoeffs); 137 138 if (!ok) 139 { 140 cout << "標定失敗,再來一次" << endl; 141 return -3; 142 } 143 144 //標定結果保存 145 FileStorage fs("caliResult.xml", FileStorage::WRITE); 146 fs << "cameraId" << cameraId; 147 fs << "intrinsic_parameters" << intrMatrix; 148 fs << "distortion_parametes" << distCoeffs; 149 fs.release(); 150 151 return 0; 152 }
該程序有三個參數輸入,基於命令行讀入參數
-w #標定板一個方向上的角點數
-h #標定板另外一個方向上的角點數
-s #標定板上正方形的邊長,默認爲1
另,發表下-s參數設置的觀點。之因此該參數在不少標定實例程序中設置爲默認1,是由於該參數的改變確實是會影響到標定結果,可是不會影響到攝像頭的矯正。由於標定和矯正相似一個逆運算過程,單位定義對其沒有影響。
標定過程,
標定結果,
<?xml version="1.0"?> <opencv_storage> <cameraId>0</cameraId> <intrinsic_parameters type_id="opencv-matrix"> <rows>3</rows> <cols>3</cols> <dt>d</dt> <data> 7.7881772950073355e+002 0. 3.1562441595543476e+002 0. 7.8624564811643825e+002 2.5630331974129393e+002 0. 0. 1.</data></intrinsic_parameters> <distortion_parametes type_id="opencv-matrix"> <rows>1</rows> <cols>5</cols> <dt>d</dt> <data> -7.2660835182078581e-002 2.0765291395491934e+000 5.9477659924542790e-004 -8.2981148319346263e-004 -7.0307616798578119e+000</data></distortion_parametes> </opencv_storage>