畸变矫正是上一篇博文的遗留问题,当畸变系数和内外参数矩阵标定完成后,就应该进行畸变的矫正,以达到消除畸变的目的,此其一。
在该系列第一部分的博文中介绍的立体成像原理中提到,要通过两幅图像估计物点的深度信息,就必须在两幅图像中准确的匹配到同一物点,这样才能根据该物点在两幅图像中的位置关系,计算物体深度。为了降低匹配的计算量,两个摄像头的成像平面应处于同一平面。但是,单单依靠严格的摆放摄像头来达到这个目的显然有些困难。立体校正就是利用几何图形变换(Geometric Image Transformation)关系,使得原先不满足上述位置关系的两幅图像满足该条件,此其二。
数学原理
- 畸变矫正(compensate lens distortion)
畸变矫正的方法就是用上一篇博文给出的公式对像素位置进行重新映射。这里重新写出重新映射的公式。
先矫正径向畸变,
再矫正切向畸变,
- 立体矫正(stereo rectify)
立体矫正能够有效降低立体匹配的计算量,立体矫正的具体作用见下图,
立体矫正后,
立体矫正的算法原理没有详细了解,此处从略。
OpenCV相关函数说明
- 畸变矫正函数 undistort()
undistort() 是独立的一个畸变矫正函数,一次性可以完成映射矩阵的求解和重新映射。下面我们还会看到把这两步分开来做的函数。
调用方法,
- src-输入未经过矫正的图像
- dst-经过矫正后输出的图像
- cameraMatrix-标定而得到的摄像机矩阵
- distCoeffs-标定得到的摄像机畸变矩阵
- newCameraMatrix-输入矫正后的摄像机矩阵(可以省略)
- 立体标定函数 stereoCalibrate()
stereoCalibrate() 是用来标定一个立体摄像头的,也就是同时标定两个摄像头。标定的结果除了能够求出两个摄像头的内外参数矩阵,跟能够得出两个摄像头的位置关系R,T。
调用方法,
double stereoCalibrate(InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints1,
InputArrayOfArrays imagePoints2, InputOutputArray cameraMatrix1,InputOutputArray distCoeffs1,
InputOutputArray cameraMatrix2, InputOutputArray distCoeffs2, Size imageSize, OutputArray R, OutputArray T, OutputArray E, OutputArray F, TermCriteria criteria= TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 1e-6), int flags=CALIB_FIX_INTRINSIC )
- objectPoints- vector
型的数据结构,存储标定角点在世界坐标系中的位置 - imagePoints1- vector
> 型的数据结构,存储标定角点在第一个摄像机下的投影后的亚像素坐标 - imagePoints2- vector
> 型的数据结构,存储标定角点在第二个摄像机下的投影后的亚像素坐标 - cameraMatrix1-输入/输出型的第一个摄像机的相机矩阵。如果
CV_CALIB_USE_INTRINSIC_GUESS , CV_CALIB_FIX_ASPECT_RATIO ,CV_CALIB_FIX_INTRINSIC , or CV_CALIB_FIX_FOCAL_LENGTH
其中的一个或多个标志被设置,该摄像机矩阵的一些或全部参数需要被初始化 - distCoeffs1-第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定
- cameraMatrix2-输入/输出型的第二个摄像机的相机矩阵。参数意义同第一个相机矩阵相似
- distCoeffs2-第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定
- imageSize-图像的大小
- R-输出型,第一和第二个摄像机之间的旋转矩阵
- T-输出型,第一和第二个摄像机之间的平移矩阵
- E-输出型,基本矩阵
- F-输出型,基础矩阵
- term_crit-迭代优化的终止条件
-
flag-
- CV_CALIB_FIX_INTRINSIC 如果该标志被设置,那么就会固定输入的cameraMatrix和distCoeffs不变,只求解$$$R,T,E,F$$$.
- CV_CALIB_USE_INTRINSIC_GUESS 根据用户提供的cameraMatrix和distCoeffs为初始值开始迭代
- CV_CALIB_FIX_PRINCIPAL_POINT 迭代过程中不会改变主点的位置
- CV_CALIB_FIX_FOCAL_LENGTH 迭代过程中不会改变焦距
- CV_CALIB_SAME_FOCAL_LENGTH 强制保持两个摄像机的焦距相同
- CV_CALIB_ZERO_TANGENT_DIST 切向畸变保持为零
- CV_CALIB_FIX_K1,...,CV_CALIB_FIX_K6 迭代过程中不改变相应的值。如果设置了 CV_CALIB_USE_INTRINSIC_GUESS 将会使用用户提供的初始值,否则设置为零
- CV_CALIB_RATIONAL_MODEL 畸变模型的选择,如果设置了该参数,将会使用更精确的畸变模型,distCoeffs的长度就会变成8
- 立体校正函数 stereoRectify()
stereoRectify() 的作用是为每个摄像头计算立体校正的映射矩阵。所以其运行结果并不是直接将图片进行立体矫正,而是得出进行立体矫正所需要的映射矩阵。
调用方法,
void stereoRectify(InputArray cameraMatrix1, InputArray distCoeffs1, InputArray cameraMatrix2,InputArray distCoeffs2, Size imageSize, InputArray R, InputArray T,OutputArray R1, OutputArray R2, OutputArray P1, OutputArray P2, OutputArray Q, int flags=CALIB_ZERO_DISPARITY, double alpha=-1,
Size newImageSize=Size(), Rect* validPixROI1=0, Rect* validPixROI2=0 )
- cameraMatrix1-第一个摄像机的摄像机矩阵
- distCoeffs1-第一个摄像机的畸变向量
- cameraMatrix2-第二个摄像机的摄像机矩阵
- distCoeffs1-第二个摄像机的畸变向量
- imageSize-图像大小
- R- stereoCalibrate() 求得的R矩阵
- T- stereoCalibrate() 求得的T矩阵
- R1-输出矩阵,第一个摄像机的校正变换矩阵(旋转变换)
- R2-输出矩阵,第二个摄像机的校正变换矩阵(旋转矩阵)
- P1-输出矩阵,第一个摄像机在新坐标系下的投影矩阵
- P2-输出矩阵,第二个摄像机在想坐标系下的投影矩阵
- Q-4*4的深度差异映射矩阵
- flags-可选的标志有两种零或者 CV_CALIB_ZERO_DISPARITY ,如果设置 CV_CALIB_ZERO_DISPARITY 的话,该函数会让两幅校正后的图像的主点有相同的像素坐标。否则该函数会水平或垂直的移动图像,以使得其有用的范围最大
- alpha-拉伸参数。如果设置为负或忽略,将不进行拉伸。如果设置为0,那么校正后图像只有有效的部分会被显示(没有黑色的部分),如果设置为1,那么就会显示整个图像。设置为0~1之间的某个值,其效果也居于两者之间。
- newImageSize-校正后的图像分辨率,默认为原分辨率大小。
- validPixROI1-可选的输出参数,
Rect
型数据。其内部的所有像素都有效 - validPixROI2-可选的输出参数,
Rect
型数据。其内部的所有像素都有效
- 映射变换计算函数 initUndistortRectifyMap()
该函数功能是计算畸变矫正和立体校正的映射变换。
调用方法,
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray distCoeffs, InputArray R,InputArray newCameraMatrix, Size size, int m1type, OutputArray map1, OutputArray map2)
- cameraMatrix-摄像机参数矩阵
- distCoeffs-畸变参数矩阵
- R- stereoCalibrate() 求得的R矩阵
- newCameraMatrix-矫正后的摄像机矩阵(可省略)
- Size-没有矫正图像的分辨率
- m1type-第一个输出映射的数据类型,可以为 CV_32FC1 或 CV_16SC2
- map1-输出的第一个映射变换
- map2-输出的第二个映射变换
- 几何变换函数 remap()
调用方法,
void remap(InputArray src, OutputArray dst, InputArray map1, InputArray map2, int interpolation,int borderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar())
- src-原图像
- dst-几何变换后的图像
- map1-第一个映射,无论是点(x,y)或者单纯x的值都需要是
CV_16SC2
,CV_32FC1
, 或CV_32FC2
类型 - map2-第二个映射,y需要是
CV_16UC1
,CV_32FC1
类型。或者当map1是点(x,y)时,map2为空。 - interpolation-插值方法,但是不支持最近邻插值
- 剩下两个我也没看懂,但是一般示例程序中不会设置
基于OpenCV的仿真
- 仿真程序
1 int main() 2 { 3 //initialize some parameters 4 bool okcalib = false; 5 Mat intrMatFirst, intrMatSec, distCoeffsFirst, distCoffesSec; 6 Mat R, T, E, F, RFirst, RSec, PFirst, PSec, Q; 7 vector> imagePointsFirst, imagePointsSec; 8 vector > ObjectPoints(1); 9 Rect validRoi[2]; 10 Size imageSize; 11 int cameraIdFirst = 0, cameraIdSec = 1; 12 double rms = 0; 13 14 //get pictures and calibrate 15 vector<string> imageList; 16 string filename = "stereo_calib.xml"; 17 bool okread = readStringList(filename, imageList); 18 if (!okread || imageList.empty()) 19 { 20 cout << "can not open " << filename << " or the string list is empty" << endl; 21 return false; 22 } 23 if (imageList.size() % 2 != 0) 24 { 25 cout << "Error: the image list contains odd (non-even) number of elements\n"; 26 return false; 27 } 28 29 //calibrate 30 cout << "calibrate left camera..." << endl; 31 okcalib = calibrate(intrMatFirst, distCoeffsFirst, imagePointsFirst, ObjectPoints, 32 imageSize, cameraIdFirst, imageList); 33 34 if (!okcalib) 35 { 36 cout << "fail to calibrate left camera" << endl; 37 return -1; 38 } 39 else 40 { 41 cout << "calibrate the right camera..." << endl; 42 } 43 44 okcalib = calibrate(intrMatSec, distCoffesSec, imagePointsSec, ObjectPoints, 45 imageSize, cameraIdSec, imageList); 46 47 if (!okcalib) 48 { 49 cout << "fail to calibrate the right camera" << endl; 50 return -1; 51 } 52 destroyAllWindows(); 53 54 //estimate position and orientation 55 cout << "estimate position and orientation of the second camera" << endl 56 << "relative to the first camera..." << endl; 57 rms = stereoCalibrate(ObjectPoints, imagePointsFirst, imagePointsSec, 58 intrMatFirst, distCoeffsFirst, intrMatSec, distCoffesSec, 59 imageSize, R, T, E, F, CV_CALIB_FIX_INTRINSIC, 60 TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, 1e-6)); 61 cout << "done with RMS error=" << rms << endl; 62 63 //stereo rectify 64 cout << "stereo rectify..." << endl; 65 stereoRectify(intrMatFirst, distCoeffsFirst, intrMatSec, distCoffesSec, imageSize, R, T, RFirst, 66 RSec, PFirst, PSec, Q, 0, 1, imageSize, &validRoi[0], &validRoi[1]); 67 68 //read pictures for 3d-reconstruction 69 namedWindow("canvas", 1); 70 cout << "read the picture for 3d-reconstruction..."; 71 Mat canvas(imageSize.height, imageSize.width * 2, CV_8UC3), viewLeft, viewRight; 72 Mat canLeft = canvas(Rect(0, 0, imageSize.width, imageSize.height)); 73 Mat canRight = canvas(Rect(imageSize.width, 0, imageSize.width, imageSize.height)); 74 viewLeft = imread(imageList[cameraIdFirst], 1); 75 viewRight = imread(imageList[cameraIdSec], 1); 76 viewLeft.copyTo(canLeft); 77 viewRight.copyTo(canRight); 78 cout << "done" << endl; 79 imshow("canvas", canvas); 80 waitKey(50); 81 82 //stereoRectify 83 Mat rmapFirst[2], rmapSec[2], rviewFirst, rviewSec; 84 initUndistortRectifyMap(intrMatFirst, distCoeffsFirst, RFirst, PFirst, 85 imageSize, CV_16SC2, rmapFirst[0], rmapFirst[1]); 86 initUndistortRectifyMap(intrMatSec, distCoffesSec, RSec, PSec, 87 imageSize, CV_16SC2, rmapSec[0], rmapSec[1]); 88 remap(viewLeft, rviewFirst, rmapFirst[0], rmapFirst[1], INTER_LINEAR); 89 remap(viewRight, rviewSec, rmapSec[0], rmapSec[1], INTER_LINEAR); 90 rviewFirst.copyTo(canLeft); 91 rviewSec.copyTo(canRight); 92 93 rectangle(canLeft, validRoi[0], Scalar(255, 0, 0), 3, 8); 94 rectangle(canRight, validRoi[1], Scalar(255, 0, 0), 3, 8); 95 for (int j = 0; j <= canvas.rows; j += 16) 96 line(canvas, Point(0, j), Point(canvas.cols, j), Scalar(0, 255, 0), 1, 8); 97 cout << "stereo rectify done" << endl; 98 imshow("canvas", canvas); 99 waitKey(50);
子函数calibrate()
和calcChessboardCorners()
分别是用来表达相机和计算objectPoints的。函数体如下,
1 bool calibrate(Mat& intrMat, Mat& distCoeffs, vector>& imagePoints, 2 vector >& ObjectPoints, Size& imageSize,const int cameraId , 3 vector<string> imageList) 4 { 5 int w = 6; 6 int h = 9; 7 double rms = 0; 8 9 Size boardSize; 10 boardSize.width = w; 11 boardSize.height = h; 12 vector pointBuf; 13 float squareSize = 1.f; 14 vector rvecs, tvecs; 15 bool ok = false; 16 17 int nImages = (int)imageList.size() / 2; 18 namedWindow("View", 1); 19 for (int i = 0; i ) 20 { 21 Mat view, viewGray; 22 view = imread(imageList[i*2+cameraId], 1); 23 imageSize = view.size(); 24 cvtColor(view, viewGray, COLOR_BGR2GRAY); 25 26 bool found = findChessboardCorners(view, boardSize, pointBuf, 27 CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_FAST_CHECK | CV_CALIB_CB_NORMALIZE_IMAGE); 28 29 if (found) 30 { 31 cornerSubPix(viewGray, pointBuf, Size(11, 11), 32 Size(-1, -1), TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1)); 33 drawChessboardCorners(view, boardSize, Mat(pointBuf), found); 34 bitwise_not(view, view); 35 imagePoints.push_back(pointBuf); 36 cout << '.'; 37 } 38 imshow("View", view); 39 waitKey(50); 40 } 41 //calculate chessboardCorners 42 calcChessboardCorners(boardSize, squareSize, ObjectPoints[0]); 43 ObjectPoints.resize(imagePoints.size(), ObjectPoints[0]); 44 45 rms = calibrateCamera(ObjectPoints, imagePoints, imageSize, intrMat, distCoeffs, 46 rvecs, tvecs); 47 ok = checkRange(intrMat) && checkRange(distCoeffs); 48 49 if (ok) 50 { 51 cout << "done with RMS error=" << rms << endl; 52 return true; 53 } 54 else 55 return false; 56 }
1 static void calcChessboardCorners(Size boardSize, float squareSize, vector& corners) 2 { 3 corners.resize(0); 4 for (int i = 0; i < boardSize.height; i++) //height和width位置不能颠倒 5 for (int j = 0; j < boardSize.width; j++) 6 { 7 corners.push_back(Point3f(j*squareSize, i*squareSize, 0)); 8 } 9 }
- 仿真结果