張正友相機標定Opencv實現以及標定流程&&標定結果評價&&圖像矯正流程解析(附標定程序和棋盤圖)
使用Opencv實現張正友法相機標定之前,有幾個問題事先要確認一下,那就是相機為什么需要標定,標定需要的輸入和輸出分別是哪些?
相機標定的目的:獲取攝像機的內參和外參矩陣(同時也會得到每一幅標定圖像的選擇和平移矩陣),內參和外參系數可以對之后相機拍攝的圖像就進行矯正,得到畸變相對很小的圖像。
相機標定的輸入:標定圖像上所有內角點的圖像坐標,標定板圖像上所有內角點的空間三維坐標(一般情況下假定圖像位于Z=0平面上)。
相機標定的輸出:攝像機的內參、外參系數。
這三個基礎的問題就決定了使用Opencv實現張正友法標定相機的標定流程、標定結果評價以及使用標定結果矯正原始圖像的完整流程:
1. 準備標定圖片
2. 對每一張標定圖片,提取角點信息
3. 對每一張標定圖片,進一步提取亞像素角點信息
4. 在棋盤標定圖上繪制找到的內角點(非必須,僅為了顯示)
5. 相機標定
6. 對標定結果進行評價
7. 查看標定效果——利用標定結果對棋盤圖進行矯正
1. 準備標定圖片
標定圖片需要使用標定板在不同位置、不同角度、不同姿態下拍攝,最少需要3張,以10~20張為宜。標定板需要是黑白相間的矩形構成的棋盤圖,制作精度要求較高,如下圖所示:
2.對每一張標定圖片,提取角點信息
需要使用findChessboardCorners函數提取角點,這里的角點專指的是標定板上的內角點,這些角點與標定板的邊緣不接觸。
?findChessboardCorners函數原型:
[cpp] view plaincopy
??CV_EXPORTS_W?bool?findChessboardCorners(?InputArray?image,?Size?patternSize,???????????????????????????????????????????OutputArray?corners,???????????????????????????????????????????int?flags=CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE?);??
第一個參數Image,傳入拍攝的棋盤圖Mat圖像,必須是8位的灰度或者彩色圖像;
第二個參數patternSize,每個棋盤圖上內角點的行列數,一般情況下,行列數不要相同,便于后續標定程序識別標定板的方向;
第三個參數corners,用于存儲檢測到的內角點圖像坐標位置,一般用元素是Point2f的向量來表示:vector<Point2f> image_points_buf;
第四個參數flage:用于定義棋盤圖上內角點查找的不同處理方式,有默認值。
3. 對每一張標定圖片,進一步提取亞像素角點信息
為了提高標定精度,需要在初步提取的角點信息上進一步提取亞像素信息,降低相機標定偏差,常用的方法是cornerSubPix,另一個方法是使用find4QuadCornerSubpix函數,這個方法是專門用來獲取棋盤圖上內角點的精確位置的,或許在相機標定的這個特殊場合下它的檢測精度會比cornerSubPix更高?
cornerSubPix函數原型:
[cpp] view plaincopy
??CV_EXPORTS_W?void?cornerSubPix(?InputArray?image,?InputOutputArray?corners,??????????????????????????????????Size?winSize,?Size?zeroZone,??????????????????????????????????TermCriteria?criteria?);??
第一個參數image,輸入的Mat矩陣,最好是8位灰度圖像,檢測效率更高;
第二個參數corners,初始的角點坐標向量,同時作為亞像素坐標位置的輸出,所以需要是浮點型數據,一般用元素是Pointf2f/Point2d的向量來表示:vector<Point2f/Point2d> iamgePointsBuf;
第三個參數winSize,大小為搜索窗口的一半;
第四個參數zeroZone,死區的一半尺寸,死區為不對搜索區的中央位置做求和運算的區域。它是用來避免自相關矩陣出現某些可能的奇異性。當值為(-1,-1)時表示沒有死區;
第五個參數criteria,定義求角點的迭代過程的終止條件,可以為迭代次數和角點精度兩者的組合;
find4QuadCornerSubpix函數原型:
[cpp] view plaincopy
??CV_EXPORTS?bool?find4QuadCornerSubpix(InputArray?img,?InputOutputArray?corners,?Size?region_size);??
第一個參數img,輸入的Mat矩陣,最好是8位灰度圖像,檢測效率更高;
第二個參數corners,初始的角點坐標向量,同時作為亞像素坐標位置的輸出,所以需要是浮點型數據,一般用元素是Pointf2f/Point2d的向量來表示:vector<Point2f/Point2d> iamgePointsBuf;
第三個參數region_size,角點搜索窗口的尺寸;
在其中一個標定的棋盤圖上分別運行cornerSubPix和find4QuadCornerSubpix尋找亞像素角點,兩者定位到的亞像素角點坐標分別為:
? ?cornerSubPix: ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??find4QuadCornerSubpix:
? ? ? ? ? ? ? ? ? ? ? ?
雖然有一定差距,但偏差基本都控制在0.5個像素之內。
4. 在棋盤標定圖上繪制找到的內角點(非必須,僅為了顯示)
drawChessboardCorners函數用于繪制被成功標定的角點,函數原型:
[cpp] view plaincopy
??CV_EXPORTS_W?void?drawChessboardCorners(?InputOutputArray?image,?Size?patternSize,???????????????????????????????????????????InputArray?corners,?bool?patternWasFound?);??
第一個參數image,8位灰度或者彩色圖像;
第二個參數patternSize,每張標定棋盤上內角點的行列數;
第三個參數corners,初始的角點坐標向量,同時作為亞像素坐標位置的輸出,所以需要是浮點型數據,一般用元素是Pointf2f/Point2d的向量來表示:vector<Point2f/Point2d> iamgePointsBuf;
第四個參數patternWasFound,標志位,用來指示定義的棋盤內角點是否被完整的探測到,true表示別完整的探測到,函數會用直線依次連接所有的內角點,作為一個整體,false表示有未被探測到的內角點,這時候函數會以(紅色)圓圈標記處檢測到的內角點;
以下是drawChessboardCorners函數中第四個參數patternWasFound設置為true和false時內角點的繪制效果:
patternWasFound=ture時,依次連接各個內角點:
patternWasFound=false時,以(紅色)圓圈標記處角點位置:
5. 相機標定
獲取到棋盤標定圖的內角點圖像坐標之后,就可以使用calibrateCamera函數進行標定,計算相機內參和外參系數,
calibrateCamera函數原型:
[cpp] view plaincopy
??CV_EXPORTS_W?double?calibrateCamera(?InputArrayOfArrays?objectPoints,???????????????????????????????????????InputArrayOfArrays?imagePoints,???????????????????????????????????????Size?imageSize,???????????????????????????????????????CV_OUT?InputOutputArray?cameraMatrix,???????????????????????????????????????CV_OUT?InputOutputArray?distCoeffs,???????????????????????????????????????OutputArrayOfArrays?rvecs,?OutputArrayOfArrays?tvecs,???????????????????????????????????????int?flags=0,?TermCriteria?criteria?=?TermCriteria(???????????????????????????????????????????TermCriteria::COUNT+TermCriteria::EPS,?30,?DBL_EPSILON)?);??
第一個參數objectPoints,為世界坐標系中的三維點。在使用時,應該輸入一個三維坐標點的向量的向量,即vector<vector<Point3f>> object_points。需要依據棋盤上單個黑白矩陣的大小,計算出(初始化)每一個內角點的世界坐標。
第二個參數imagePoints,為每一個內角點對應的圖像坐標點。和objectPoints一樣,應該輸入vector<vector<Point2f>> image_points_seq形式的變量;
第三個參數imageSize,為圖像的像素尺寸大小,在計算相機的內參和畸變矩陣時需要使用到該參數;
第四個參數cameraMatrix為相機的內參矩陣。輸入一個Mat cameraMatrix即可,如Mat cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0));
第五個參數distCoeffs為畸變矩陣。輸入一個Mat distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0))即可;
第六個參數rvecs為旋轉向量;應該輸入一個Mat類型的vector,即vector<Mat>rvecs;
第七個參數tvecs為位移向量,和rvecs一樣,應該為vector<Mat> tvecs;
第八個參數flags為標定時所采用的算法。有如下幾個參數:
CV_CALIB_USE_INTRINSIC_GUESS:使用該參數時,在cameraMatrix矩陣中應該有fx,fy,u0,v0的估計值。否則的話,將初始化(u0,v0)圖像的中心點,使用最小二乘估算出fx,fy。?
CV_CALIB_FIX_PRINCIPAL_POINT:在進行優化時會固定光軸點。當CV_CALIB_USE_INTRINSIC_GUESS參數被設置,光軸點將保持在中心或者某個輸入的值。?
CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只將fy作為可變量,進行優化計算。當CV_CALIB_USE_INTRINSIC_GUESS沒有被設置,fx和fy將會被忽略。只有fx/fy的比值在計算中會被用到。?
CV_CALIB_ZERO_TANGENT_DIST:設定切向畸變參數(p1,p2)為零。?
CV_CALIB_FIX_K1,…,CV_CALIB_FIX_K6:對應的徑向畸變在優化中保持不變。?
CV_CALIB_RATIONAL_MODEL:計算k4,k5,k6三個畸變參數。如果沒有設置,則只計算其它5個畸變參數。
第九個參數criteria是最優迭代終止條件設定。
在使用該函數進行標定運算之前,需要對棋盤上每一個內角點的空間坐標系的位置坐標進行初始化,標定的結果是生成相機的內參矩陣cameraMatrix、相機的5個畸變系數distCoeffs,另外每張圖像都會生成屬于自己的平移向量和旋轉向量。
6. 對標定結果進行評價
對標定結果進行評價的方法是通過得到的攝像機內外參數,對空間的三維點進行重新投影計算,得到空間三維點在圖像上新的投影點的坐標,計算投影坐標和亞像素角點坐標之間的偏差,偏差越小,標定結果越好。
對空間三維坐標點進行反向投影的函數是projectPoints,函數原型是:
[cpp] view plaincopy
??CV_EXPORTS_W?void?projectPoints(?InputArray?objectPoints,???????????????????????????????????InputArray?rvec,?InputArray?tvec,???????????????????????????????????InputArray?cameraMatrix,?InputArray?distCoeffs,???????????????????????????????????OutputArray?imagePoints,???????????????????????????????????OutputArray?jacobian=noArray(),???????????????????????????????????double?aspectRatio=0?);??
第一個參數objectPoints,為相機坐標系中的三維點坐標;
第二個參數rvec為旋轉向量,每一張圖像都有自己的選擇向量;
第三個參數tvec為位移向量,每一張圖像都有自己的平移向量;
第四個參數cameraMatrix為求得的相機的內參數矩陣;
第五個參數distCoeffs為相機的畸變矩陣;
第六個參數iamgePoints為每一個內角點對應的圖像上的坐標點;
第七個參數jacobian是雅可比行列式;
第八個參數aspectRatio是跟相機傳感器的感光單元有關的可選參數,如果設置為非0,則函數默認感光單元的dx/dy是固定的,會依此對雅可比矩陣進行調整;
下邊顯示了某一張標定圖片上的亞像素角點坐標和根據標定結果把空間三維坐標點映射回圖像坐標點的對比:
find4QuadCornerSubpix查找到的亞像素點坐標: ? ? ? ? ? ? ? ? ? ? ? ? ??projectPoints映射的坐標:
? ? ? ? ? ? ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????
以下是每一幅圖像上24個內角點的平均誤差統計數據:
7. 查看標定效果——利用標定結果對棋盤圖進行矯正
利用求得的相機的內參和外參數據,可以對圖像進行畸變的矯正,這里有兩種方法可以達到矯正的目的,分別說明一下。
方法一:使用initUndistortRectifyMap和remap兩個函數配合實現。
initUndistortRectifyMap用來計算畸變映射,remap把求得的映射應用到圖像上。
initUndistortRectifyMap的函數原型:
[cpp] view plaincopy
??CV_EXPORTS_W?void?initUndistortRectifyMap(?InputArray?cameraMatrix,?InputArray?distCoeffs,?????????????????????????????InputArray?R,?InputArray?newCameraMatrix,?????????????????????????????Size?size,?int?m1type,?OutputArray?map1,?OutputArray?map2?);??
第一個參數cameraMatrix為之前求得的相機的內參矩陣;
第二個參數distCoeffs為之前求得的相機畸變矩陣;
第三個參數R,可選的輸入,是第一和第二相機坐標之間的旋轉矩陣;
第四個參數newCameraMatrix,輸入的校正后的3X3攝像機矩陣;
第五個參數size,攝像機采集的無失真的圖像尺寸;
第六個參數m1type,定義map1的數據類型,可以是CV_32FC1或者CV_16SC2;
第七個參數map1和第八個參數map2,輸出的X/Y坐標重映射參數;
remap函數原型:
[cpp] view plaincopy
??CV_EXPORTS_W?void?remap(?InputArray?src,?OutputArray?dst,???????????????????????????InputArray?map1,?InputArray?map2,???????????????????????????int?interpolation,?int?borderMode=BORDER_CONSTANT,???????????????????????????const?Scalar&?borderValue=Scalar());??
第一個參數src,輸入參數,代表畸變的原始圖像;
第二個參數dst,矯正后的輸出圖像,跟輸入圖像具有相同的類型和大小;
第三個參數map1和第四個參數map2,X坐標和Y坐標的映射;
第五個參數interpolation,定義圖像的插值方式;
第六個參數borderMode,定義邊界填充方式;
方法二:使用undistort函數實現
undistort函數原型:
[cpp] view plaincopy
??CV_EXPORTS_W?void?undistort(?InputArray?src,?OutputArray?dst,???????????????????????????????InputArray?cameraMatrix,???????????????????????????????InputArray?distCoeffs,???????????????????????????????InputArray?newCameraMatrix=noArray()?);??
第一個參數src,輸入參數,代表畸變的原始圖像;
第二個參數dst,矯正后的輸出圖像,跟輸入圖像具有相同的類型和大小;
第三個參數cameraMatrix為之前求得的相機的內參矩陣;
第四個參數distCoeffs為之前求得的相機畸變矩陣;
第五個參數newCameraMatrix,默認跟cameraMatrix保持一致;
方法一相比方法二執行效率更高一些,推薦使用。
以下是使用某一張標定圖使用方法一和方法二進行矯正的效果圖對比。
原始標定圖像:
方法一,使用initUndistortRectifyMap和remap實現矯正效果:
方法二,使用undistort函數實現矯正效果:
兩個方法從矯正效果上看,結果是一致的。
以下是完整的工程代碼:
[cpp] view plaincopy
#include?"opencv2/core/core.hpp"??#include?"opencv2/imgproc/imgproc.hpp"??#include?"opencv2/calib3d/calib3d.hpp"??#include?"opencv2/highgui/highgui.hpp"??#include?<iostream>??#include?<fstream>????using?namespace?cv;??using?namespace?std;????void?main()???{??????ifstream?fin("calibdata.txt");???????ofstream?fout("caliberation_result.txt");????????????????cout<<"開始提取角點………………";??????int?image_count=0;????????Size?image_size;????????Size?board_size?=?Size(4,6);??????????vector<Point2f>?image_points_buf;????????vector<vector<Point2f>>?image_points_seq;???????string?filename;??????int?count=?-1?;??????while?(getline(fin,filename))??????{??????????image_count++;??????????????????????????cout<<"image_count?=?"<<image_count<<endl;????????????????????????????cout<<"-->count?=?"<<count;????????????????Mat?imageInput=imread(filename);??????????if?(image_count?==?1)????????????{??????????????image_size.width?=?imageInput.cols;??????????????image_size.height?=imageInput.rows;???????????????????????cout<<"image_size.width?=?"<<image_size.width<<endl;??????????????cout<<"image_size.height?=?"<<image_size.height<<endl;??????????}??????????????????????if?(0?==?findChessboardCorners(imageInput,board_size,image_points_buf))??????????{?????????????????????????cout<<"can?not?find?chessboard?corners!\n";???????????????exit(1);??????????}???????????else???????????{??????????????Mat?view_gray;??????????????cvtColor(imageInput,view_gray,CV_RGB2GRAY);????????????????????????????find4QuadCornerSubpix(view_gray,image_points_buf,Size(5,5));?????????????????????????????image_points_seq.push_back(image_points_buf);??????????????????????????????drawChessboardCorners(view_gray,board_size,image_points_buf,false);???????????????imshow("Camera?Calibration",view_gray);??????????????waitKey(500);??????????}??????}??????int?total?=?image_points_seq.size();??????cout<<"total?=?"<<total<<endl;??????int?CornerNum=board_size.width*board_size.height;????????for?(int?ii=0?;?ii<total?;ii++)??????{??????????if?(0?==?ii%CornerNum)??????????{?????????????????int?i?=?-1;??????????????i?=?ii/CornerNum;??????????????int?j=i+1;??????????????cout<<"-->?第?"<<j?<<"圖片的數據?-->?:?"<<endl;??????????}??????????if?(0?==?ii%3)????????????{??????????????cout<<endl;??????????}??????????else??????????{??????????????cout.width(10);??????????}????????????????????cout<<"?-->"<<image_points_seq[ii][0].x;??????????cout<<"?-->"<<image_points_seq[ii][0].y;??????}?????????cout<<"角點提取完成!\n";??????????????cout<<"開始標定………………";????????????Size?square_size?=?Size(10,10);????????vector<vector<Point3f>>?object_points;?????????????Mat?cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0));???????vector<int>?point_counts;????????Mat?distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0));???????vector<Mat>?tvecsMat;????????vector<Mat>?rvecsMat;?????????????int?i,j,t;??????for?(t=0;t<image_count;t++)???????{??????????vector<Point3f>?tempPointSet;??????????for?(i=0;i<board_size.height;i++)???????????{??????????????for?(j=0;j<board_size.width;j++)???????????????{??????????????????Point3f?realPoint;????????????????????????????????????realPoint.x?=?i*square_size.width;??????????????????realPoint.y?=?j*square_size.height;??????????????????realPoint.z?=?0;??????????????????tempPointSet.push_back(realPoint);??????????????}??????????}??????????object_points.push_back(tempPointSet);??????}????????????for?(i=0;i<image_count;i++)??????{??????????point_counts.push_back(board_size.width*board_size.height);??????}???????????????calibrateCamera(object_points,image_points_seq,image_size,cameraMatrix,distCoeffs,rvecsMat,tvecsMat,0);??????cout<<"標定完成!\n";????????????cout<<"開始評價標定結果………………\n";??????double?total_err?=?0.0;???????double?err?=?0.0;???????vector<Point2f>?image_points2;???????cout<<"\t每幅圖像的標定誤差:\n";??????fout<<"每幅圖像的標定誤差:\n";??????for?(i=0;i<image_count;i++)??????{??????????vector<Point3f>?tempPointSet=object_points[i];????????????????????projectPoints(tempPointSet,rvecsMat[i],tvecsMat[i],cameraMatrix,distCoeffs,image_points2);????????????????????vector<Point2f>?tempImagePoint?=?image_points_seq[i];??????????Mat?tempImagePointMat?=?Mat(1,tempImagePoint.size(),CV_32FC2);??????????Mat?image_points2Mat?=?Mat(1,image_points2.size(),?CV_32FC2);??????????for?(int?j?=?0?;?j?<?tempImagePoint.size();?j++)??????????{??????????????image_points2Mat.at<Vec2f>(0,j)?=?Vec2f(image_points2[j].x,?image_points2[j].y);??????????????tempImagePointMat.at<Vec2f>(0,j)?=?Vec2f(tempImagePoint[j].x,?tempImagePoint[j].y);??????????}??????????err?=?norm(image_points2Mat,?tempImagePointMat,?NORM_L2);??????????total_err?+=?err/=??point_counts[i];?????????????std::cout<<"第"<<i+1<<"幅圖像的平均誤差:"<<err<<"像素"<<endl;?????????????fout<<"第"<<i+1<<"幅圖像的平均誤差:"<<err<<"像素"<<endl;?????????}?????????std::cout<<"總體平均誤差:"<<total_err/image_count<<"像素"<<endl;?????????fout<<"總體平均誤差:"<<total_err/image_count<<"像素"<<endl<<endl;?????????std::cout<<"評價完成!"<<endl;??????????????std::cout<<"開始保存定標結果………………"<<endl;?????????????Mat?rotation_matrix?=?Mat(3,3,CV_32FC1,?Scalar::all(0));???????fout<<"相機內參數矩陣:"<<endl;?????????fout<<cameraMatrix<<endl<<endl;?????????fout<<"畸變系數:\n";?????????fout<<distCoeffs<<endl<<endl<<endl;?????????for?(int?i=0;?i<image_count;?i++)???????{???????????fout<<"第"<<i+1<<"幅圖像的旋轉向量:"<<endl;?????????????fout<<tvecsMat[i]<<endl;???????????????????????????Rodrigues(tvecsMat[i],rotation_matrix);?????????????fout<<"第"<<i+1<<"幅圖像的旋轉矩陣:"<<endl;?????????????fout<<rotation_matrix<<endl;?????????????fout<<"第"<<i+1<<"幅圖像的平移向量:"<<endl;?????????????fout<<rvecsMat[i]<<endl<<endl;?????????}?????????std::cout<<"完成保存"<<endl;???????fout<<endl;??????????????Mat?mapx?=?Mat(image_size,CV_32FC1);??????Mat?mapy?=?Mat(image_size,CV_32FC1);??????Mat?R?=?Mat::eye(3,3,CV_32F);??????std::cout<<"保存矯正圖像"<<endl;??????string?imageFileName;??????std::stringstream?StrStm;??????for?(int?i?=?0?;?i?!=?image_count?;?i++)??????{??????????std::cout<<"Frame?#"<<i+1<<"..."<<endl;??????????initUndistortRectifyMap(cameraMatrix,distCoeffs,R,cameraMatrix,image_size,CV_32FC1,mapx,mapy);????????????????StrStm.clear();??????????imageFileName.clear();??????????string?filePath="chess";??????????StrStm<<i+1;??????????StrStm>>imageFileName;??????????filePath+=imageFileName;??????????filePath+=".bmp";??????????Mat?imageSource?=?imread(filePath);??????????Mat?newimage?=?imageSource.clone();??????????????????????????????remap(imageSource,newimage,mapx,?mapy,?INTER_LINEAR);?????????????????StrStm.clear();??????????filePath.clear();??????????StrStm<<i+1;??????????StrStm>>imageFileName;??????????imageFileName?+=?"_d.jpg";??????????imwrite(imageFileName,newimage);??????}??????std::cout<<"保存結束"<<endl;??????????return?;??}??
標定圖例1:
標定圖例2:
標定結果1:
標定結果2:
矯正效果1:
矯正效果2:
以上程序已經是完整程序,需要棋盤標定圖或者整個項目包的可以到這里下載:張正友相機標定Opencv實現(完整程序+棋盤圖)。
總結
以上是生活随笔為你收集整理的转载:https://blog.csdn.net/dcrmg/article/details/52939318的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。