OpenCV 張正友標定法的實現
阿新 • • 發佈:2019-01-25
關於張正友標定法的原理,網上的資料很多,本人雖然看了一些,但覺得還沒有到能講的非常清楚的程度,因此不在這裡做太多原理描述。有興趣瞭解細節的可以看張大神的原文,或者這篇文章。
需要大概知道的是,相機標定中內參、外參和畸變引數的概念。
內參有五個,分別是:
攝像頭拍攝到的物體和實際物體在x,y軸上的對映關係(兩個引數)。
攝像頭中心和影象中心的偏移關係(兩個引數)。
攝像頭和鏡頭安裝非完全垂直,存在一個角度的偏差。(一個引數)
外參有六個,分別是x,y,z方向上的平移和旋轉。
有了上面兩種引數,我們基本上知道攝像頭拍攝到的影象和現實事物的對應關係了,但“畸變”亦不能忽略。它是由於鏡頭質量等原因導致的2D點的偏移。舉個簡單的例子就是用攝像頭拍攝一個正方形,影象上會變成一個桶形或者其他的形狀。在張氏標定法中張大神用“極大似然法”去計算出畸變的各項引數(如果想加深理解,也可以參考本人之前寫過的一篇 相關的文章)。
到此,就介紹完了相機標定三個最為重要的概念。一般我們要處理攝像頭的畸變,只要求內參和畸變引數就可以了,而要做雙目標定則需要把外引數也求出來。
本篇文章主要介紹用OpenCV自帶的張正友標定法相關的函式來對攝像頭進行標定(求取攝像頭內外引數和畸變引數)並對單張影象進行校正的方法。主要參考的是這篇文章(如同雷同,是我抄的)。
需要自行準備標定板,長相如下(因為快遞比較慢,自己先做了一個,但因為這個的精度直接影響到後面標定的精確度,建議還是買一塊給力一點的):
標定的流程是:角點提取->相機標定
一.角點提取
會用到比較重要的函式是:
//用於提取標定板的內角點,也就是提取上圖中每四個黑白格中間的那些角點
bool findChessboardCorners( InputArray image, Size patternSize, OutputArray corners, int flags=CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE );
有四個引數:
第一個“Image”,是拍攝到的棋盤影象,也就是上圖那樣的影象;
第二個“patternSize”,即每個棋盤圖上內角點的行列數,一般情況下,行列數不要相同,便於後續標定程式識別標定板的方向,像上面那樣的板子就是Size(7, 5),也就是每行7個角點,每列5個角點;
第三個“corners”,用於儲存檢測到的內角點影象座標位置,一般用元素是Point2f的向量來表示;
第四個“flage”:用於定義棋盤圖上內角點查詢的不同處理方式,有預設值。
另外返回值很重要,它會告訴你是不是真的從圖中找到了角點。如果後面想做成自動標定的程式,這個非常有用;
例如如果輸入的影象如上圖所示,而我們的第二個引數是Size(10, 5),則會返回錯誤;
//用於在初步提取的角點資訊上進一步提取亞畫素資訊,降低相機標定偏差,該方法專門用來獲取棋盤圖上內角點的精確位置。而比較普遍的提取亞畫素角點的方法是cornerSubPix,這裡不做贅述
bool find4QuadCornerSubpix(InputArray img, InputOutputArray corners, Size region_size);
有五個引數:
第一個“mage”,輸入的Mat矩陣,最好是8位灰度影象,檢測效率更高;
第二個“corners”,初始的角點座標向量,同時作為亞畫素座標位置的輸出,所以需要是浮點型資料,一般用元素是Pointf2f/Point2d的向量來表示。也即輸入上面findChessboardCorners函式的第三個引數。
第三個“winSize”,大小為搜尋視窗的一半;
第四個“zeroZone”,死區的一半尺寸,死區為不對搜尋區的中央位置做求和運算的區域。它是用來避免自相關矩陣出現某些可能的奇異性。當值為(-1,-1)時表示沒有死區;
第五個“criteria”,定義求角點的迭代過程的終止條件,可以為迭代次數和角點精度兩者的組合;
//用於畫出求得的角點,以便檢視是否標定正確
void drawChessboardCorners( InputOutputArray image, Size patternSize, InputArray corners, bool patternWasFound );
有四個引數:
第一個“image”,8位灰度或者彩色影象;
第二個“patternSize”,每張標定棋盤上內角點的行列數,即findChessboardCorners的第二個引數;
第三個“corners”,角點座標向量,可用find4QuadCornerSubpix函式的第二個引數輸出做輸入;
第四個“patternWasFound”,標誌位,用來指示定義的棋盤內角點是否被完整的探測到,true表示被完整的探測到,函式會用直線依次連線所有的內角點,作為一個整體,false表示有未被探測到的內角點,這時候函式會以(紅色)圓圈標記處檢測到的內角點;
總的查詢角點的示例程式碼如下:
Mat imageInput = imread("1.bmp");
Size board_size = Size(7, 5);//標定板上每行、列的角點數
vector<Point2f> image_points_buf;//快取每幅影象上檢測到的角點
/*提取角點*/
if (!findChessboardCorners(imageInput, board_size, image_points_buf))
{
cout << "can not find chessboard corners!\n"; //找不到角點
return;
}
else
{
Mat view_gray;
cvtColor(imageInput, view_gray, CV_RGB2GRAY);
/*亞畫素精確化*/
find4QuadCornerSubpix(view_gray, image_points_buf, Size(5, 5)); //對粗提取的角點進行精確化
drawChessboardCorners(view_gray, board_size, image_points_buf, true); //用於在圖片中標記角點
imshow("Camera Calibration", view_gray);//顯示圖片
waitKey(0);
}
二.相機標定
利用上面獲取到的影象角點(理論上需要三張影象,即三組資料,事實上以10~20張為宜,因為這樣誤差會比較小),便可以用calibrateCamera函式做攝像頭標定,計算出攝像頭的內參、外參和畸變引數了。當然前面程式碼在本人只做了一張影象的角點提取,可以改成求多張的,程式碼如下:
Size board_size = Size(7, 5);//標定板上每行、列的角點數
vector<Point2f> image_points_buf;//快取每幅影象上檢測到的角點
vector<vector<Point2f>> image_points_seq; //儲存檢測到的所有角點
/* 提取角點 */
char filename[10];
for (size_t image_num = 1; image_num <= 14; image_num++)
{
sprintf_s(filename, "%d.bmp", image_num);
Mat imageInput = imread(filename);
if (!findChessboardCorners(imageInput, board_size, image_points_buf))
{
cout << "can not find chessboard corners!\n"; //找不到角點
return;
}
else
{
Mat view_gray;
cvtColor(imageInput, view_gray, CV_RGB2GRAY);
/*亞畫素精確化*/
find4QuadCornerSubpix(view_gray, image_points_buf, Size(5, 5)); //對粗提取的角點進行精確化
drawChessboardCorners(view_gray, board_size, image_points_buf, true); //用於在圖片中標記角點
image_points_seq.push_back(image_points_buf);//儲存亞畫素角點
imshow("Camera Calibration", view_gray);//顯示圖片
waitKey(500);//停半秒
}
imageInput.release();
}
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”,為世界座標系中的三維點。在使用時,應該輸入一個三維座標點的向量集合。一般我們假定標定板放在z=0的平面上,然後依據棋盤上單個黑白方塊的大小(也可以直接都取10,如果不需要很準確的對映到現實事物的話)可以計算出每個內角點的世界座標。
第二個“imagePoints”,為每一個內角點對應的影象座標點。也即是上面求得的各張影象的角點集合;
第三個“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,另外每張影象都會生成屬於自己的平移向量和旋轉向量。
具體的實現程式碼如下:
Size image_size;//影象的尺寸
Size board_size = Size(7, 5); //標定板上每行、列的角點數
vector<Point2f> image_points_buf; //快取每幅影象上檢測到的角點
vector<vector<Point2f>> image_points_seq; //儲存檢測到的所有角點
/*提取角點*/
char filename[10];
for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
{
sprintf_s(filename, "%d.bmp", image_num);
Mat imageInput = imread(filename);
if (!findChessboardCorners(imageInput, board_size, image_points_buf))
{
cout << "can not find chessboard corners!\n";//找不到角點
return;
}
else
{
Mat view_gray;
cvtColor(imageInput, view_gray, CV_RGB2GRAY);
/*亞畫素精確化*/
find4QuadCornerSubpix(view_gray, image_points_buf, Size(5, 5));//對粗提取的角點進行精確化
drawChessboardCorners(view_gray, board_size, image_points_buf, true);//用於在圖片中標記角點
image_points_seq.push_back(image_points_buf);//儲存亞畫素角點
imshow("Camera Calibration", view_gray);//顯示圖片
//waitKey(500);//停半秒
}
image_size.width = imageInput.cols;
image_size.height = imageInput.rows;
imageInput.release();
}
/*相機標定*/
vector<vector<Point3f>> object_points; //儲存標定板上角點的三維座標,為標定函式的第一個引數
Size square_size = Size(10, 10);//實際測量得到的標定板上每個棋盤格的大小,這裡其實沒測,就假定了一個值,感覺影響不是太大,後面再研究下
for (int t = 0; t<IMGCOUNT; t++)
{
vector<Point3f> tempPointSet;
for (int i = 0; i<board_size.height; i++)
{
for (int j = 0; j<board_size.width; j++)
{
Point3f realPoint;
//假設標定板放在世界座標系中z=0的平面上
realPoint.x = i*square_size.width;
realPoint.y = j*square_size.height;
realPoint.z = 0;
tempPointSet.push_back(realPoint);
}
}
object_points.push_back(tempPointSet);
}
//內外引數物件
Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));//攝像機內參數矩陣
vector<int> point_counts;// 每幅影象中角點的數量
Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));//攝像機的5個畸變係數:k1,k2,p1,p2,k3
vector<Mat> tvecsMat;//每幅影象的旋轉向量
vector<Mat> rvecsMat;//每幅影象的平移向量
calibrateCamera(object_points, image_points_seq, image_size, cameraMatrix, distCoeffs, rvecsMat, tvecsMat, 0);//攝像頭標定
到此我們已經完成了標定的過程,得到了攝像頭的各個引數,後面就可以用這些得到的引數來做攝像頭的矯正了。
矯正可以使用下面的函式:
void undistort( InputArray src, OutputArray dst,InputArray cameraMatrix,InputArray distCoeffs,InputArray newCameraMatrix=noArray() );
有五個引數:
第一個“src”,輸入引數,代表畸變的原始影象;
第二個“dst”,矯正後的輸出影象,跟輸入影象具有相同的型別和大小;
第三個“cameraMatrix”為之前求得的相機的內參矩陣;
第四個“distCoeffs”為之前求得的相機畸變矩陣;
第五個“newCameraMatrix”,預設跟cameraMatrix保持一致;
具體程式碼如下:
/*用標定的結果矯正影象*/
for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
{
sprintf_s(filename, "%d.bmp", image_num);
Mat imageSource = imread(filename);
Mat newimage = imageSource.clone();
undistort(imageSource, newimage, cameraMatrix, distCoeffs);
imshow("source", imageSource);//顯示圖片
imshow("drc", newimage);//顯示圖片
waitKey(500);//停半秒
imageSource.release();
newimage.release();
}
到此就完成了攝像頭的標定和影象的矯正的整個流程,如果想要知道標定的效果如何評定,可以參考上文提到過的參考文章,本文的很多函式說明基本是照搬的,只是做了一些程式碼上的拆分。
如果想要把標定的結果儲存下載後面直接用,則可以如下程式碼儲存:
/*儲存內參和畸變係數,以便後面直接矯正*/
ofstream fout("caliberation_result.txt");//儲存標定結果的檔案
fout << "相機內參數矩陣:" << endl;
fout << cameraMatrix << endl << endl;
fout << "畸變係數:\n";
fout << distCoeffs << endl << endl << endl;
fout.close();
讀取標定檔案檔案程式碼如下:
char read[100];
double getdata;
Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));//攝像機內參數矩陣
Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));//攝像機的5個畸變係數:k1,k2,p1,p2,k3
ifstream fin("caliberation_result.txt");//讀取儲存標定結果的檔案,以供矯正
fin >> read;
fin.seekg(3, ios::cur);
for (size_t j = 0; j < 3; j++)
for (size_t i = 0; i < 3; i++)
{
fin >> getdata;
cameraMatrix.at<float>(j, i) = getdata;
fin >> read;
}
fin >> read;
fin.seekg(3, ios::cur);
for (size_t i = 0; i < 5; i++)
{
fin >> getdata;
distCoeffs.at<float>(i) = getdata;
fin >> read;
}
fin.close();
本文的全部程式碼如下:
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/calib3d/calib3d.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <fstream>
#define IMGCOUNT 20
using namespace cv;
using namespace std;
void main()
{
Size image_size;//影象的尺寸
Size board_size = Size(9, 6); //標定板上每行、列的角點數
vector<Point2f> image_points_buf; //快取每幅影象上檢測到的角點
vector<vector<Point2f>> image_points_seq; //儲存檢測到的所有角點
/*提取角點*/
char filename[10];
for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
{
sprintf_s(filename, "%d.bmp", image_num);
Mat imageInput = imread(filename);
if (!findChessboardCorners(imageInput, board_size, image_points_buf))
{
cout << "can not find chessboard corners!\n";//找不到角點
return;
}
else
{
Mat view_gray;
cvtColor(imageInput, view_gray, CV_RGB2GRAY);
/*亞畫素精確化*/
find4QuadCornerSubpix(view_gray, image_points_buf, Size(5, 5));//對粗提取的角點進行精確化
drawChessboardCorners(view_gray, board_size, image_points_buf, true);//用於在圖片中標記角點
image_points_seq.push_back(image_points_buf);//儲存亞畫素角點
imshow("Camera Calibration", view_gray);//顯示圖片
waitKey(500);//停半秒
}
image_size.width = imageInput.cols;
image_size.height = imageInput.rows;
imageInput.release();
}
/*相機標定*/
vector<vector<Point3f>> object_points; //儲存標定板上角點的三維座標,為標定函式的第一個引數
Size square_size = Size(10, 10);//實際測量得到的標定板上每個棋盤格的大小,這裡其實沒測,就假定了一個值,感覺影響不是太大,後面再研究下
for (int t = 0; t<IMGCOUNT; t++)
{
vector<Point3f> tempPointSet;
for (int i = 0; i<board_size.height; i++)
{
for (int j = 0; j<board_size.width; j++)
{
Point3f realPoint;
//假設標定板放在世界座標系中z=0的平面上
realPoint.x = i*square_size.width;
realPoint.y = j*square_size.height;
realPoint.z = 0;
tempPointSet.push_back(realPoint);
}
}
object_points.push_back(tempPointSet);
}
//內外引數物件
Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));//攝像機內參數矩陣
vector<int> point_counts;// 每幅影象中角點的數量
Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));//攝像機的5個畸變係數:k1,k2,p1,p2,k3
vector<Mat> tvecsMat;//每幅影象的旋轉向量
vector<Mat> rvecsMat;//每幅影象的平移向量
calibrateCamera(object_points, image_points_seq, image_size, cameraMatrix, distCoeffs, rvecsMat, tvecsMat, 0);//相機標定
/*用標定的結果矯正影象*/
for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
{
sprintf_s(filename, "%d.bmp", image_num);
Mat imageSource = imread(filename);
Mat newimage = imageSource.clone();
undistort(imageSource, newimage, cameraMatrix, distCoeffs);
imshow("source", imageSource);//顯示圖片
imshow("drc", newimage);//顯示圖片
sprintf_s(filename, "%d_d.bmp", image_num);
imwrite(filename, newimage);//顯示圖片
waitKey(500);//停半秒
imageSource.release();
newimage.release();
}
/*儲存內參和畸變係數,以便後面直接矯正*/
ofstream fout("caliberation_result.txt");//儲存標定結果的檔案
fout << "相機內參數矩陣:" << endl;
fout << cameraMatrix << endl << endl;
fout << "畸變係數:\n";
fout << distCoeffs << endl << endl << endl;
fout.close();
///*讀取之前標定好的資料直接矯正*/
//char read[100];
//double getdata;
//Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));//攝像機內參數矩陣
//Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));//攝像機的5個畸變係數:k1,k2,p1,p2,k3
//ifstream fin("caliberation_result.txt");//讀取儲存標定結果的檔案,以供矯正
//fin >> read;
//fin.seekg(3, ios::cur);
//for (size_t j = 0; j < 3; j++)
// for (size_t i = 0; i < 3; i++)
// {
// fin >> getdata;
// cameraMatrix.at<float>(j, i) = getdata;
// fin >> read;
// }
//fin >> read;
//fin.seekg(3, ios::cur);
//for (size_t i = 0; i < 5; i++)
//{
// fin >> getdata;
// distCoeffs.at<float>(i) = getdata;
// fin >> read;
//}
//fin.close();
//char filename[10];
//for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
//{
// sprintf_s(filename, "%d.bmp", image_num);
// Mat imageSource = imread(filename);
// Mat newimage = imageSource.clone();
// undistort(imageSource, newimage, cameraMatrix, distCoeffs);
// imshow("source", imageSource);//顯示圖片
// imshow("drc", newimage);//顯示圖片
// sprintf_s(filename, "%d_d.bmp", image_num);
// imwrite(filename, newimage);//顯示圖片
// waitKey(500);//停半秒
// imageSource.release();
// newimage.release();
//}
}