本程序的執行界面如下圖所示,只需要輸入標定板影像路徑,即可全自動完成相機的標定,得出相機的焦距和畸變係數。
圖 1. 相機標定程序運行界面
下面首先簡略介紹相機標定的原理,然後簡略介紹OpenCV的實現思路,最後詳細介紹帶UI的相機標定程序的開發方法。
一,相機標定的原理
(參考《學習OpenCV(中文版)》(清華大學出版社))
要通過拍攝的二維相片信息回覆相片中物體的三維信息,就必須明確物方點三維座標(X,Y,Z)與其在相片上的座標(x,y)之間的幾何關係。物方座標和像方座標的幾何關係由相機的成像模型確定。普通相機的成像模型都是小孔成像,其數學模型爲中心投影,表達式爲:
相機標定的目標是求出內參數fx,fy,cx,cy,即焦距(主距)和像主點偏移量,下面我們來分析如何求得這四個內參數。
由以上公式可以看出,每拍攝一張圖像,就有6個外參數未知量(三個角度和三個偏移量,注意W矩陣中的R0-R8的獨立參數只有三個角度),以及4個內參數未知量,同時每拍攝一張圖像,對於圖像上每個像點就會產生2個方程(展開以上公式很容易得到)。
以棋盤作爲標定板進行試驗,假設棋盤有K個角點,一共拍攝N幅圖像,那麼將會有2NK個方程,6N+4個未知量,想要求解4個內參數,則只需2NK>=6N+4,於是,當N=1且K=5時即可完成標定。然而因爲棋盤上無論有多少個點,真正獨立的只有4個,因爲其他點可以通過線性變換得出。因此將K固定爲4,那麼可得出能完成標定的要求是N>=2,即至少拍攝兩幅棋盤。考慮到誤差問題,一般需要10幅以上7*8個角點的棋盤圖像才能完成高質量的標定。
此外,實際的相機的透鏡成像並非完全的小孔成像,而是存在畸變的,主要是徑向畸變和切向畸變,在數學上通過二階多項式來模擬。在原來的方程q=sMWQ的基礎上,增加一個畸變改正係數矩陣即可得到帶畸變糾正的標定模型。這樣做會帶來另外4個內參數(2個徑向畸變係數和2個切向畸變係數),但是可以通過增加拍攝圖像數目來增加方程進而完成解算。
二,OpenCV對相機標定的實現思路
OpenCV提供了cvCalibrateCamera2函數用以完成相機標定,以下是函數說明(來自百度百科)
void cvCalibrateCamera2( const CvMat* object_points, const CvMat* image_points, const CvMat* point_counts, CvSize image_size, CvMat* intrinsic_matrix, CvMat* distortion_coeffs, CvMat* rotation_vectors=NULL, CvMat* translation_vectors=NULL, int flags=0 );
-
CV_CALIB_USE_INTRINSIC_GUESS - 內參數矩陣包含fx,fy,cx和cy的初始值。否則,(cx, cy)被初始化到圖像中心(這兒用到圖像大小),焦距用最小平方差方式計算得到。注意,如果內部參數已知,沒有必要使用這個函數,使用cvFindExtrinsicCameraParams2則可。
-
CV_CALIB_FIX_PRINCIPAL_POINT - 主點在全局優化過程中不變,一直在中心位置或者在其他指定的位置(當CV_CALIB_USE_INTRINSIC_GUESS設置的時候)。
-
CV_CALIB_FIX_ASPECT_RATIO - 優化過程中認爲fx和fy中只有一個獨立變量,保持比例fx/fy不變,fx/fy的值跟內參數矩陣初始化時的值一樣。在這種情況下, (fx, fy)的實際初始值或者從輸入內存矩陣中讀取(當CV_CALIB_USE_INTRINSIC_GUESS被指定時),或者採用估計值(後者情況中fx和fy可能被設置爲任意值,只有比值被使用)。
-
CV_CALIB_ZERO_TANGENT_DIST – 切向形變參數(p1, p2)被設置爲0,其值在優化過程中保持爲0。
三,帶UI的相機標定程序的開發方法
按圖索驥,我們要做的就是兩件事:其一,做出一個交互界面接收標定板影像,其二,按照OpenCV提供的函數cvCalibrateCamera2函數的參數列表爲其提供數據,完成標定。
3.1 用於接收標定板影像的交互界面設計--批量輸入影像
我們實現這樣的功能,第一,給定標定板影像文件夾路徑就可以把該文件夾下所有的標定板影像全部讀入程序,第二,程序要能顯示輸入輸出信息。
解決方案代碼如下:
void CCameraCalibraterDlg::OnBtnImg()
{
// TODO: Add your control notification handler code here
TCHAR pszPath[MAX_PATH];
BROWSEINFO bi;
bi.hwndOwner = this->GetSafeHwnd();
bi.pidlRoot = NULL;
bi.pszDisplayName = NULL;
bi.lpszTitle = TEXT("請選擇文件夾");
bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT;
bi.lpfn = NULL;
bi.lParam = 0;
bi.iImage = 0;
LPITEMIDLIST pidl = SHBrowseForFolder(&bi);
if (pidl == NULL)
{
return;
}
if (SHGetPathFromIDList(pidl, pszPath))
{
m_path_img = pszPath;
}
m_info = "輸出信息:\r\n\r\n";
m_num_img = 0;
m_paths.clear();
CString str;
CFileFind finder;
BOOL bf;
str = pszPath;
bf = finder.FindFile(str+"/*.*");
while (bf)
{
bf = finder.FindNextFile();
if (!finder.IsDirectory() && !finder.IsDots())
{
str = finder.GetFilePath();
if(
strcmpi( str.Right(4),".jpg" )==0 ||
strcmpi( str.Right(4),".JPG" )==0 ||
strcmpi( str.Right(5),".JPEG")==0 ||
strcmpi( str.Right(4),".bmp" )==0 ||
strcmpi( str.Right(4),".BMP" )==0 )
{
m_paths.push_back(str);
m_info += str+"\r\n";
m_num_img++;
}
}
}
UpdateData(FALSE);
}
以上函數OnBtnImg()是瀏覽按鈕的響應函數,它把用戶輸入的文件夾路徑中的圖片路徑存儲在了vector<CString>型變量m_paths中,在之後的處理中只需從m_paths取出路徑就可以對圖像進行處理。實現後的功能界面如下圖
圖 2 標定板影像輸入界面
對於輸入輸出信息的顯示則很簡單,利用一個Edit控件來實現,具體方法爲首先把Edit控件的屬性標記爲Read-Only,然後爲其關聯一個CString型變量m_info,以後凡是需要顯示的信息都加入m_info,然後UpdateData(FALSE)即可。上文中的代碼已經體現了這一點,實現後的界面如圖1所示。3.2 相機標定程序的內核設計--爲cvCalibrateCamera2函數準備參數
在3.1中我們已經把標定板影像路徑存儲在m_paths中,下面只需從中依次取出影像,提取角點,就可以得到cvCalibrateCamera2函數的參數,從而完成標定,代碼如下:
BOOL CCameraCalibraterDlg::CameraCalibrate()
{
int i,j;
//定義基本數據
int n_boards = m_num_img;
int board_w = BOARDW;
int board_h = BOARDH;
int board_n = board_w*board_h;
CvSize board_sz = cvSize(board_w,board_h);
CvMat* image_points = cvCreateMat(n_boards*board_n,2,CV_32FC1);
CvMat* object_points = cvCreateMat(n_boards*board_n,3,CV_32FC1);
CvMat* point_counts = cvCreateMat(n_boards,1,CV_32SC1);
CvMat* intrinsic_matrix = cvCreateMat(3,3,CV_32FC1);
CvMat* distortion_coeffs = cvCreateMat(4,1,CV_32FC1);
CvPoint2D32f* corners = new CvPoint2D32f[board_n];
int corner_count=0;
int successes = 0;
int step;
IplImage* image = cvLoadImage(m_paths.at(0));
IplImage* gray_image = cvCreateImage(cvGetSize(image),8,1);
//逐個讀取影像,提取角點,存儲角點
for (int t=0;t<m_num_img;t++)
{
image = cvLoadImage(m_paths.at(t));
int found = cvFindChessboardCorners(image,board_sz,corners,&corner_count,
CV_CALIB_CB_ADAPTIVE_THRESH|CV_CALIB_CB_FILTER_QUADS);
cvCvtColor(image,gray_image,CV_BGR2GRAY);
cvFindCornerSubPix(gray_image,corners,corner_count,cvSize(11,11),cvSize(-1,-1),
cvTermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER,
30,0.1));
cvDrawChessboardCorners(image,board_sz,corners,corner_count,found);
//如果提取角點正確,則加入數據
if (corner_count == board_n)
{
step = successes*board_n;
for (i=step,j=0;j<board_n;++i,++j)
{
CV_MAT_ELEM(*image_points,float,i,0) = corners[j].x;
CV_MAT_ELEM(*image_points,float,i,1) = corners[j].y;
CV_MAT_ELEM(*object_points,float,i,0) = j/board_w;
CV_MAT_ELEM(*object_points,float,i,1) = j%board_w;
CV_MAT_ELEM(*object_points,float,i,2) = 0.0f;
}
CV_MAT_ELEM(*point_counts,int,successes,0) = board_n;
successes++;
}
}
//保存正確提取的點數據
CvMat* object_points2 = cvCreateMat(successes*board_n,3,CV_32FC1);
CvMat* image_points2 = cvCreateMat(successes*board_n,2,CV_32FC1);
CvMat* point_counts2 = cvCreateMat(successes,1,CV_32SC1);
for (i=0;i<successes*board_n;i++)
{
CV_MAT_ELEM(*image_points2,float,i,0) = CV_MAT_ELEM(*image_points,float,i,0);
CV_MAT_ELEM(*image_points2,float,i,1) = CV_MAT_ELEM(*image_points,float,i,1);
CV_MAT_ELEM(*object_points2,float,i,0) = CV_MAT_ELEM(*object_points,float,i,0);
CV_MAT_ELEM(*object_points2,float,i,1) = CV_MAT_ELEM(*object_points,float,i,1);
CV_MAT_ELEM(*object_points2,float,i,2) = CV_MAT_ELEM(*object_points,float,i,2);
}
for (i=0;i<successes;i++)
{
CV_MAT_ELEM(*point_counts2,int,i,0) = CV_MAT_ELEM(*point_counts,int,i,0);
}
cvReleaseMat(&object_points);
cvReleaseMat(&image_points);
cvReleaseMat(&point_counts);
//進行標定
CV_MAT_ELEM(*intrinsic_matrix,float,0,0) = 1.0f;
CV_MAT_ELEM(*intrinsic_matrix,float,1,1) = 1.0f;
cvCalibrateCamera2(object_points2,image_points2,point_counts2,cvGetSize(image),
intrinsic_matrix,distortion_coeffs,NULL,NULL,0);
//保存標定結果
FILE *fp;
CString str;
str = m_path_img + "/camera.cmr";
fp = fopen(str,"w");
fprintf(fp,"%f\t%f\n%f\t%f",CV_MAT_ELEM(*intrinsic_matrix,float,0,0),CV_MAT_ELEM(*intrinsic_matrix,float,1,1),
CV_MAT_ELEM(*intrinsic_matrix,float,0,2),CV_MAT_ELEM(*intrinsic_matrix,float,1,2));
fclose(fp);
str = m_path_img + "/distortioncoef.dat";
fp = fopen(str,"w");
fprintf(fp,"%f\t%f\t%f\t%f\n",CV_MAT_ELEM(*distortion_coeffs,float,0,0),CV_MAT_ELEM(*distortion_coeffs,float,1,0),
CV_MAT_ELEM(*distortion_coeffs,float,2,0),CV_MAT_ELEM(*distortion_coeffs,float,3,0));
fclose(fp);
cvSave("intrinsics.xml",intrinsic_matrix);
cvSave("distortion.xml",distortion_coeffs);
//矯正影像
IplImage* mapx = cvCreateImage(cvGetSize(image),IPL_DEPTH_32F,1);
IplImage* mapy = cvCreateImage(cvGetSize(image),IPL_DEPTH_32F,1);
cvInitUndistortMap(intrinsic_matrix,distortion_coeffs,mapx,mapy);
for (i=0;i<m_num_img;i++)
{
str = m_paths.at(i);
image = cvLoadImage(str);
IplImage* t = cvCloneImage(image);
cvRemap(t,image,mapx,mapy);
cvSaveImage(str+"res.tif",image);
cvReleaseImage(&t);
}
return TRUE;
}
以上函數不僅解算出4個內參數和4個畸變參數,將其存儲在camera.cmr和distortioncoef.dat中,同時還將原來的標定板影像進行了糾正。以下是其中一幅標定板被糾正前後的對比圖: