学习笔记:使⽤opencv做双⽬测距(相机标定+⽴体匹配+测距).
最近在做双⽬测距,觉得有必要记录点东西,所以我的第⼀篇博客就这么诞⽣啦~
双⽬测距属于⽴体视觉这⼀块,我觉得应该有很多⼈踩过这个坑了,但⽹上的资料依旧是云⾥雾⾥的,要么是理论讲⼀⼤堆,最后发现还不知道怎么做,要么就是直接代码⼀贴,让你懵逼。所以今天我想做的,是尽量给⼤家⼀个明确的阐述,并且能够上⼿做出来。
⼀、标定
⾸先我们要对摄像头做标定,具体的公式推导在learning opencv中有详细的解释,这⾥顺带提⼀句,这本书虽然确实⽼,但有些理论、算法类的东西⾥⾯还是讲的很不错的,必要的时候可以去看看。
Q1:为什么要做摄像头标定?
A:  标定的⽬的是为了消除畸变以及得到内外参数矩阵,内参数矩阵可以理解为焦距相关,它是⼀个从平⾯到像素的转换,焦距不变它就不变,所以确定以后就可以重复使⽤,⽽外参数矩阵反映的是摄像机坐标系与世界坐标系的转换,⾄于畸变参数,⼀般也包含在内参数矩阵中。从作⽤上来看,内参数矩阵是为了得到镜头的信息,并消除畸变,使得到的图像更为准确,外参数矩阵是为了得到相机相对于世界坐标的联系,是为了最终的测距。
ps1:关于畸变,⼤家可以看到⾃⼰摄像头的拍摄的画⾯,在看矩形物体的时候,边⾓处会有明显的畸变现象,⽽矫正的⽬的就是修复这个。
ps2:我们知道双⽬测距的时候两个相机需要平⾏放置,但事实上这个是很难做到的,所以就需要⽴体校正得到两个相机之间的旋转平移矩阵,也就是外参数矩阵。
Q2:如何做摄像头的标定?
A:这⾥可以直接⽤opencv⾥⾯的sample,在opencv/sources/sample/cpp⾥⾯,有个calibration.cpp的⽂件,这是单⽬的标定,是可以直接编译使⽤的,这⾥要注意⼏点:
1.棋盘
棋盘也就是标定板是要预先打印好的,你打印的棋盘的样式决定了后⾯参数的填写,具体要求也不是很严谨,清晰能⽤就⾏。之所⽤棋盘是因为他检测⾓点很⽅便,and..你没得选。。
2. 参数
⼀般设置为这个样⼦:-w 6 -h 8 -s 2 -n 10 -l -op -oe [<list_>]  ,这是⼏个重要参数的含义:
-w <board_width>        # 图⽚某⼀维⽅向上的交点个数
-h <board_height>        # 图⽚另⼀维上的交点个数
[-n <number_of_frames>]  # 标定⽤的图⽚帧数
[-s <square_size>]      # square size in some user-defined units (1 by default)
[-o <out_camera_params>] # the output filename for intrinsic [and extrinsic] parameters
[-op]                    # write detected feature points
[-oe]                    # write extrinsic parameters
可以发现 -w -h是棋盘的长和⾼,也就是有⼏个⿊⽩交点,-s是每个格⼦的长度,单位是cm 长和⾼⼀定要数对,不然程序在识别⾓点的时候会识别不出来的。
最终得到的yml⽂件,就是单⽬标定的参数矩阵,之后使⽤它就可以得到校正后的图像啦。
3. 需要对程序做⼀些修改,这是我遇到的问题,就是他的读取摄像头的代码在我这边没有⽤,所以我⾃⼰重新修改了,不知道⼤家会不会碰到
这个问题。
然后就是双⽬标定了,同样的地⽅,到stereo_calib.cpp,这个参数⽐较简单,只要确定长、宽和输⼊的⼀个xml⽂件(在之前的⽂件夹⾥⾯),这个⽂件是为了读取图⽚⽤的,你需要⾃⼰⽤固定好的双⽬摄像头拍14对棋盘图⽚,命名为 这样⼀系列的名字,另外,最简单的⽅法就是把⾃⼰拍的照⽚放到相应的⼯程下,以及stereo开头的那个xml⽂件也复制过去这个程序代码并不复杂,可以稍微研究⼀下,⼯程向的代码确实严谨,各种情况都考虑到了,⽐起⾃⼰之前做的那个⼩项⽬不知道⾼到哪⾥去了
这⾥也有⼏个注意点(坑):
1.⽼⽣常谈的问题,长宽⼀定要写对!!!这个不多说了,都是泪。
2.代码的核⼼函数static void  StereoCalib(const vector<string>& imagelist, Size boardSize, bool useCalibrated=true, bool  showRectified=true),注意搞清楚参数的意
义,因为我是⽤的单⽬标定好的摄像头拍摄的图⽚,不需要再校正了,所以第三个参数要⽤false,这样最后的结果才能看,不说了,都是泪...
3.另外注意到计算rms误差的时候,结束条件的⼏个参数是可以调整的,
double rms = stereoCalibrate(objectPoints, imagePoints[0], imagePoints[1],
cameraMatrix[0], distCoeffs[0],
cameraMatrix[1], distCoeffs[1],
imageSize, R, T, E, F,
TermCriteria(CV_TERMCRIT_ITER+CV_TERMCRIT_EPS, 100, 1e-5),
CV_CALIB_FIX_ASPECT_RATIO +CV_CALIB_ZERO_TANGENT_DIST +CV_CALIB_SAME_FOCAL_LENGTH +CV_CALIB_RATIONAL_MODEL +CV_CALIB_FIX_K3 + CV_CALIB_FIX_K4 +
CV_CALIB_FIX_K5)
下⾯这段话是某度百科上的:
这个函数计算了两个摄像头进⾏⽴体像对之间的转换关系。如果你有⼀个⽴体相机的相对位置,并且两个摄像头的⽅向是固定的,以及你计算了物体相对于第⼀照相机和第⼆照相机的姿态,(R1,T1)和(
R2,T2),各⾃(这个可以通过solvepnp()做到)通过这些姿态确定。你只需要知道第⼆相机相对于第⼀相机的位置和⽅向。
除了⽴体的相关信息,该函数也可以两个相机的每⼀个做⼀个完整的校准。然⽽,由于在输⼊数据中的⾼维的参数空间和噪声的,可能偏离正确值。如果每个单独的相机内参数可以被精确估计(例如,使⽤calibratecamera()),建议这样做,然后在本征参数计算之中使CV_CALIB_FIX_INTRINSIC的功能。否则,如果⼀旦计算出所有的参数,它将会合理的限制某些参数,例如,传CV_CALIB_SAME_FOCAL_LENGTH and CV_CALIB_ZERO_TANGENT_DIST,这通常是⼀个合理的假设。 Q3:标定之后做什么呢?
A: 写到这我发现把单⽬和双⽬的⼀起写确实有点乱...不过,开⼸没有回头箭!(不是因为懒!!)
⾸先还是单⽬,单⽬的使⽤很简单,使⽤标定得到的参数进⾏校正就⾏了,代码如下:
void loadCameraParams(Mat &cameraMatrix, Mat &distCoeffs)
{
FileStorage fs("l", FileStorage::READ);//这个名字就是你之前校正得到的yml⽂件
fs["camera_matrix"] >> cameraMatrix;
侧边值问题一定要用正则化吗
fs["distortion_coefficients"] >> distCoeffs;
}
Mat calibrator(Mat &view)//需要校正处理的图⽚
{
vector<string> imageList;
static bool bLoadCameraParams = false;
static Mat cameraMatrix, distCoeffs, map1, map2;
Mat rview;
Size imageSize, newImageSize;
if (!view.data)
return Mat();
imageSize.width = ls;
imageSize.height = ws;
newImageSize.width = imageSize.width;
newImageSize.height = imageSize.height;
if (bLoadCameraParams == false)
{
loadCameraParams(cameraMatrix, distCoeffs);
bLoadCameraParams = true;
initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(),
getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, newImageSize, 0), newImageSize, CV_16SC2, map1, map2);
}
//undistort( view, rview, cameraMatrix, distCoeffs, cameraMatrix );
remap(view, rview, map1, map2, INTER_LINEAR);
imshow("左图", rview);
//int c = 0xff & waitKey();
return view;
}
这样最后就可以得到校正后消除畸变的图⽚。
OK,接下来显然就是双⽬啦,双⽬校正之后的⼯作就⽐较多了,我准备另开⼀节来说...
⼆、⽴体匹配
这是⼀个很⼤的题⽬,⽹上的资料也很多,所以我想说的是我的⼀些理解。
这⾥最好的⽅法是从后往前说,我们⾸先需要理解测距的原理。这个很多⼈看了⼀⼤堆还不明⽩(其实只有我⾃⼰吧..),相似三⾓形测距,这种东西⼩学⽣都能搞清楚,但两摄像头到底怎么做到的,就是我们需要搞清楚的。
⾸先需要搞清楚⼀个⾮常重要的概念,视差,搞清楚视差,后⾯的就简单了,⽼⽣常谈的问题我不想多说,⽹上那些⼀⼤堆,我希望给⼤家的是⼀些明了的东西
这三幅图看明⽩了就⾏,其实视差确实很简单,但很多⼈都没去理清楚,第⼀幅图是三维世界的⼀个点在两个相机的成像,我们可以相信的是,这两个在各⾃相机的相对位置基本不可能是⼀样的,⽽这种位置的差别,也正是我们眼睛区别3D和2D的关键,将右边的摄像机投影到左边,怎么做呢?因为他的坐标和左边相机的左边相距Tx(标定测出来的外参数),所以它相当于在左边的相机对三维世界内的(x-tx,y,z)进⾏投影,所以这时候,⼀个完美的形似三⾓形就出来,这⾥视差就是d=x-x‘,
得到视差以后,再⽤相似三⾓形......也就得到了深度也就是距离啦。
结束了么??并没有....这样做确实很完美,但是问题来了:1.当我在左边相机确定⼀个点的时候,我怎么在右边到这个点? 2.我左边点所在的⾏⼀点跟右边点所在的⾏上的像素⼀定完全⼀样么?
解决第⼀个问题的⽅法就是⽴体匹配了。
Q1:⽴体匹配是什么,怎么进⾏⽴体匹配?
A:简单的回答就是:⽴体匹配就是解决上⾯问题的东西啦....其实我觉得这样就是也够了,有些成熟的算法,未必需要钻研太深,毕竟我这种实在的菜鸡,还是⼯程导向的..学术的事,⽇后再说!
opencv中提供了很多的⽴体匹配算法,类似于局部的BM,全局的SGBM等等,这些算法的⽐较⼤概是,速度越快的效果越差,如果不是很追究时效性,并且你的校正做的不是很好的话..推荐使⽤SGBM,算法的具体原理⼤家可以去百度,不难。这⾥我想提⼀下的是为什么做⽴体匹配有⽤,原因就是极线约束,这也是个很重要的概念,理解起来并不难,左摄像机上的⼀个点,对应三维空间上的⼀个点,当我们要这个点在右边的投影点时,有必要把这个图像都遍历⼀边么,当然不⽤...
如上图,显然,PL对应的P这个点⼀定在⼀条极线上,只要在这条线上就⾏了,更明显的是下⾯这个图:
最后,怎么在opencv⾥⾯实现呢..机智的我⼜到了sample..到stereo_match.cpp这个⽂件,命令⾏设置为:left01.jpg right02.jpg --algorithm=hh --blocksize=5 --max-disparity=256  --scale=1.0 --no-display -l -l -o disparity.jpg同意给⼏个建议:
1.参数的意义:
-max-disparity 是最⼤视差,可以理解为对⽐度,越⼤整个视差的range也就越⼤,这个要求是16的倍数
--blocksize ⼀般设置为5-29之间的奇数,应该是局部算法的窗⼝⼤⼩。
另,注意带上参数-l -l,毕竟咱有校正参数...
2.后⾯有两⾏代码:
reprojectImageTo3D(disp, xyz, Q, true);
saveXYZ(point_cloud_filename, xyz);
这个就是得到图⽚的三维坐标,Z也就是我们最终要求的深度啦。
第⼆个问题,⾏和⾏是对应的么?之前我们说过,双⽬校正的⽬的就是为了得到两个平⾏的摄像头,所以当程序运⾏完毕以后,它会把两幅图像显⽰出来,并作出⼀系列的平⾏线,这样你会看到线上的点⼤致是呈对应关系,左边的⾓点对应右边的交点,所以,经过匹配和校正后,是对应的。
三、总结
双⽬拖了很久,⼀直没做,最重要的原因就是...我没有两个⼀样的摄像头,所以最后也没有贴出效果图,因为两个不⼀样的摄像头,做出来的东西画⾯太美我不敢看,不过最终搞清楚了整个流程和原理,还是⽐较开⼼的。这⾥⾯像校正和匹配的算法,我只是有所理解,因为以后不⼀定⾛3D这⼀块,所以也没有过去深⼊,如果⽤到在去研究,其实也不晚..总之,第⼀篇博客,完⼯啦~

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。