最近方向定下來是雙目立體視覺,主要是做重建這塊的研究。大致過程是圖像獲取->攝像機標定->特征提取->匹配->三維重建,當然開始可以進行圖像預處理,矯正,后期可以進行點雲的進一步處理,如渲染表面使其更接近於現實物體。
圖像獲取相對來說比較簡單,用相機拍攝目標物(大型場景或特定小型的室內物體)。但有兩點需要注意:
1、雙目重建所需的圖像一般為兩張,角度相差不應過大,否則公共部分太少以至於重建效果不佳;整個過程簡便,成本也不高,但缺陷是只有兩張圖像的點雲所表示的物體信息不會很全面;
2、標定所需的圖像又是另外拍攝的,用張正友標定法的話,把印有黑白棋盤格的圖像粘至硬紙板上,然后左右攝像機各自進行拍攝,理論上獲得角度(圖像)越多,最終標定結果越精確;標定板見下圖:
這里主要結合OpenCV對左右攝像機標定做一個簡單的介紹,望朋友們指正,一起交流、進步。
攝像機的標定問題是機器視覺領域的入門問題,可以分為傳統的攝像機定標方法和攝像機自定標方法。定標的方法有很多中常見的有:Tsai(傳統)和張正友(介於傳統和自定標)等,
攝像機成像模型和四個坐標系(通用原理)。
攝像機模型采用經典的小孔模型,如圖中Oc(光心),像面π表示的是視野平面,其到光心的距離為f(鏡頭焦距)。
四個坐標系分別為:世界坐標系(Ow),攝像機坐標系(Oc),圖像物理坐標系(O1,單位mm),圖像像素坐標系(O,位於視野平面的左上角,單位pix)。
空間某點P到其像點p的坐標轉換過程主要是通過這四套坐標系的三次轉換實現的,首先將世界坐標系進行平移和轉換得到攝像機坐標系,然后根據三角幾何變換得到圖像物理坐標系,最后根據像素和公制單位的比率得到圖像像素坐標系。(實際的應用過程是這個的逆過程,即由像素長度獲知實際的長度)。
ps:通過攝像頭的標定,可以得到視野平面上的mm/pix分辨率,對於視野平面以外的物體還是需要通過坐標轉換得到視野平面上。
轉化的過程和公式參見:攝像機標定原理(關鍵是三個坐標系).ppt
2 張正友算法的原理
zhang法通過對一定標板在不同方向多次(三次以上)完整拍照,不需要知道定標板的運動方式。直接獲得相機的內參(參考文獻上矩陣A)和畸變系數。該標定方法精度高於自定標法,且不需要高精度的定位儀器。
ZHANG的算法包含兩個模型:一.經典針孔模型,包含四個坐標系,二畸變模型(這個來源未知)
公式三項依次表示,徑向畸變,切線畸變,薄棱鏡畸變。OPENCV中函數只能給出k1,k2,p1,p2。
還存在另外一種畸變模型,見《攝像機標定算法庫的設計和試驗驗證》一文26 page。
張正友標定有matlab的工具箱TOOLBOX_CAL,以及OpenCV庫,下面是C++結合OpenCV的代碼:
1 #include "cvut.h" 2 #include <iostream> 3 #include <fstream> 4 #include <string> 5 using namespace cvut; 6 using namespace std; 7 void main() 8 { 9 ifstream fin("calibdata.txt"); 10 ofstream fout("calibration_result.txt"); 11 //****************開始提取角點***********************// 12 cout<<"開始提取角點………………"; 13 int image_count=0; 14 CvSize image_size; 15 CvSize board_size = cvSize(5,7); 16 CvPoint2D32f * image_points_buf = 17 new CvPoint2D32f[board_size.width*board_size.height]; 18 Seq<CvPoint2D32f> image_points_seq; 19 string filename; 20 while (std::getline(fin,filename)) 21 { 22 cout<<"\n 將鼠標焦點移到標定圖像所在窗口" 23 <<"並輸入回車進行下一幅圖像的角點提取 \n"; 24 image_count++; 25 int count; 26 Image<uchar> view(filename); 27 if (image_count == 1) 28 { 29 image_size.width = view.size().width; 30 image_size.height = view.size().height; 31 } 32 if (0 == cvFindChessboardCorners( view.cvimage, board_size, 33 image_points_buf, &count, CV_CALIB_CB_ADAPTIVE_THRESH )) 34 { 35 cout<<"can not find chessboard corners!\n"; 36 exit(1); 37 } 38 else 39 { 40 Image<uchar> view_gray(view.size(),8,1); 41 rgb2gray(view,view_gray); 42 cvFindCornerSubPix( view_gray.cvimage, 43 image_points_buf, count, cvSize(11,11), 44 cvSize(-1,-1), 45 cvTermCriteria( CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 30, 0.1 )); 46 image_points_seq.push_back(image_points_buf,count); 47 cvDrawChessboardCorners( view.cvimage, board_size, 48 image_points_buf, count, 1); 49 view.show("calib"); 50 cvWaitKey(); 51 view.close(); 52 } 53 }//角點提取循環 54 delete []image_points_buf; 55 cout<<"角點提取完成!\n"<<endl; 56 cout<<"開始定標………………"<<"\n"<<endl; 57 CvSize square_size = cvSize(10,10); 58 Matrix<double> object_points(1, 59 board_size.width*board_size.height*image_count,3); 60 Matrix<double> image_points(1,image_points_seq.cvseq->total,2); 61 Matrix<int> point_counts(1,image_count,1); 62 Matrix<double> intrinsic_matrix(3,3,1); 63 Matrix<double> distortion_coeffs(1,4,1); 64 Matrix<double> rotation_vectors(1,image_count,3); 65 Matrix<double> translation_vectors(1,image_count,3); 66 int i,j,t; 67 for (t=0;t<image_count;t++) { 68 for (i=0;i<board_size.height;i++) { 69 for (j=0;j<board_size.width;j++) { 70 object_points(0,t*board_size.height*board_size.width 71 + i*board_size.width + j,0) = i*square_size.width; 72 object_points(0,t*board_size.height*board_size.width 73 + i*board_size.width + j,1) = j*square_size.height; 74 object_points(0,t*board_size.height*board_size.width 75 + i*board_size.width + j,2) = 0; 76 } 77 } 78 } 79 char str[10]; 80 itoa(image_points_seq.cvseq->total,str,10); 81 cout<<str<<"\n"<<endl; 82 for (i=0;i<image_points_seq.cvseq->total;i++) 83 { 84 image_points(0,i,0) = image_points_seq[i].x; 85 image_points(0,i,1) = image_points_seq[i].y; 86 } 87 for (i=0;i<image_count;i++) 88 { 89 point_counts(0,i) = board_size.width*board_size.height; 90 } 91 cvCalibrateCamera2(object_points.cvmat, 92 image_points.cvmat, 93 point_counts.cvmat, 94 image_size, 95 intrinsic_matrix.cvmat, 96 distortion_coeffs.cvmat, 97 rotation_vectors.cvmat, 98 translation_vectors.cvmat, 99 0); 100 cout<<"定標完成!\n"; 101 cout<<"標定結果顯示\n"; 102 cout<<"***************************************\n"; 103 cout<<"相機內參intrinsic_matrix\n"; 104 for(int h=0;h<3;h++) 105 { 106 cout<<"X:"<<intrinsic_matrix(h,0,0)<<"\tY:"<< 107 intrinsic_matrix(h,1,0)<<"\tZ:"<<intrinsic_matrix(h,2,0) 108 <<"\n"; 109 } 110 cout<<"\n畸變系數:distortion_coeffs\n"; 111 for(int ndis=0;ndis<4;ndis++) 112 { 113 cout<<distortion_coeffs(0,ndis,0)<<"\\"; 114 } 115 cout<<"\n"; 116 cout<<"\nrotation_vectors\n"; 117 for(int rot=0;rot<7;rot++) 118 { 119 cout<<"X:"<<rotation_vectors(0,rot,0)<<"\tY:" 120 <<rotation_vectors(0,rot,1)<<"\tZ:"<<rotation_vectors(0,rot,2) 121 <<"\n"; 122 } 123 cout<<"\ntranslation_vectors\n"; 124 for(i=0;i<7;i++) 125 { 126 cout<<"第"<<i+1<<"張圖"<<"\tX:"<<translation_vectors(0,i,0) 127 <<"\tY:"<<translation_vectors(0,i,1) 128 <<"\tZ:"<<translation_vectors(0,i,2)<<"\n"; 129 } 130 cout<<"*********************\n"; 131 cout<<"開始評價定標結果………………\n"; 132 double total_err = 0.0; 133 double err = 0.0; 134 Matrix<double> image_points2(1,point_counts(0,0,0),2); 135 int temp_num = point_counts(0,0,0); 136 cout<<"\t每幅圖像的定標誤差:\n"; 137 fout<<"每幅圖像的定標誤差:\n"; 138 for (i=0;i<image_count;i++) 139 { 140 cvProjectPoints2(object_points.get_cols(i * 141 point_counts(0,0,0),(i+1)*point_counts(0,0,0)-1).cvmat, 142 rotation_vectors.get_col(i).cvmat, 143 translation_vectors.get_col(i).cvmat, 144 intrinsic_matrix.cvmat, 145 distortion_coeffs.cvmat, 146 image_points2.cvmat, 147 0,0,0,0); 148 err = cvNorm(image_points.get_cols(i*point_counts(0,0,0),(i+1) 149 *point_counts(0,0,0)-1).cvmat, 150 image_points2.cvmat, 151 CV_L1); 152 total_err += err/=point_counts(0,0,0); 153 cout<<"****************************\n"; 154 cout<<"\t\t第"<<i+1<<"幅圖像的平均誤差:"<<err<<"像素"<<'\n'; 155 fout<<"\t第"<<i+1<<"幅圖像的平均誤差:"<<err<<"像素"<<'\n'; 156 cout<<"顯示image_point2\n"; 157 for(int ih=0;ih<7;ih++) 158 { 159 cout<<"X:"<<image_points2(0,ih,0)<<"\tY:" 160 <<image_points2(0,ih,1)<<"\n"; 161 } 162 cout<<"顯示object_Points\n"; 163 for(int iw=0;iw<7;iw++) 164 { 165 cout<<"X:"<<image_points.get_cols(i*point_counts(0,0,0),(i+1) 166 *point_counts(0,0,0)-1)(0,iw,0) 167 <<"\tY:"<<image_points.get_cols(i*point_counts(0,0,0),(i+1) 168 *point_counts(0,0,0)-1)(0,iw,1)<<"\n"; 169 } 170 } 171 cout<<"\t總體平均誤差:"<<total_err/image_count<<"像素"<<'\n'; 172 fout<<"總體平均誤差:"<<total_err/image_count<<"像素"<<'\n'<<'\n'; 173 cout<<"評價完成!\n"; 174 cout<<"開始保存定標結果………………"; 175 Matrix<double> rotation_vector(3,1); 176 Matrix<double> rotation_matrix(3,3); 177 fout<<"相機內參數矩陣:\n"; 178 fout<<intrinsic_matrix<<'\n'; 179 fout<<"畸變系數:\n"; 180 fout<<distortion_coeffs<<'\n'; 181 for (i=0;i<image_count;i++) { 182 fout<<"第"<<i+1<<"幅圖像的旋轉向量:\n"; 183 fout<<rotation_vectors.get_col(i); 184 for (j=0;j<3;j++) 185 { 186 rotation_vector(j,0,0) = rotation_vectors(0,i,j); 187 } 188 cvRodrigues2(rotation_vector.cvmat,rotation_matrix.cvmat); 189 fout<<"第"<<i+1<<"幅圖像的旋轉矩陣:\n"; 190 fout<<rotation_matrix; 191 fout<<"第"<<i+1<<"幅圖像的平移向量:\n"; 192 fout<<translation_vectors.get_col(i)<<'\n'; 193 } 194 cout<<"完成保存\n"; 195 }
這段程序可以直接運行,本人是在vs2010+OpenCV2.4.0環境下,配置在前面文章中有介紹。標定板圖像序列名稱放在文件"calibdata.txt"中,每行一幅圖像名,路勁問題這里就不論述了。如
image/chess1.jpg
image/chess2.jpg
……
最后標定結果存儲在"calibration_result.txt"中,命令行窗口也會顯示一些信息。觀察結果會發現,攝像機內參數矩陣M1和畸變系數k1,k2,p1,p2是直接調用OpenCV中標定函數求解出的,以及每幅圖像相對於對應攝像機成像平面的旋轉矩陣R'(或旋轉向量)和平移向量t',而不是世界坐標系相對於攝像機坐標系,也不是兩部攝像機之間的空間關系。而對於外參[R t],一直都是很抽象的問題。
在重建的過程中,主要需要兩個元素:
1、攝像機內M1、外參數M2(或者說是投影矩陣M=M1*M2)
2、左右成像平面上的匹配特征點對
最后通過視差原理計算出物體上特征點對應的空間點坐標,得到最終點雲。
當然,有興趣的可以繼續學習、研究下去,外參可以通過圖像上的點和預先得到的世界坐標系下的物體上的空間點坐標,利用一系列數學關系計算。
在實際操作中,可以在重建過程進行自標定,即不用分別進行標定求得攝像機參數,而是直接通過特征點對計算投影矩陣,后面步驟同上。當然自標定得到的結果精度不是很高,目前較多的是上面所述的張正友標定法,介於傳統與自標定之間。