简介:这份资源面向计算机视觉初学者与从事三维重建、双目测距的开发者,提供基于OpenCV的双目视觉立体标定与校正完整代码,帮助解决摄像头内参外参估计、图像去畸变与极线校正等实际问题。压缩包内共1个doc文件,约294KB,以文档形式集中呈现源码与注释,便于对照阅读和移植到自己的工程中。内容围绕cv::stereoCalibrate、cv::undistort、cv::stereoRectify等核心函数展开,涵盖棋盘格角点读取、标定flag选择、参数保存以及校正结果展示等关键环节,并附有StereoCalib函数的完整实现逻辑。目前已有2593人学习下载,适合希望快速理解双目标定流程、掌握标定与校正代码组织方式的读者参考,也可作为课程设计或项目开发的起步模板。
1. 双目立体视觉标定校正:从一堆棋盘格照片到可用的深度图
手里有一份基于 OpenCV 的双目立体标定与校正源码,第一次跑的时候大概率会卡在“为什么我的视差图全是噪点”上。这份代码做的事情很具体:读入一组左右相机拍摄的棋盘格图片,自动提取角点,调用cvStereoCalibrate算出两个相机的内参、畸变系数以及它们之间的旋转矩阵 R 和平移向量 T,再通过cvStereoRectify做极线校正,最后用块匹配算出视差图。它适合正在做双目测距、三维重建、机器人避障的开发者,尤其是需要一套能直接编译运行的 C++ 参考实现的人。代码本身来自经典教材配套示例,结构清晰,但用的是旧版 C 接口,直接照抄到新项目里会踩不少坑。
2. 标定链路拆解:从棋盘格角点到 R、T 矩阵
2.1 图像列表读取与角点提取的工程细节
代码没有用cv::Mat和 C++ 接口,而是用IplImage*和CvMat,这是 2008 年前后 OpenCV 的典型写法。入口函数StereoCalib接收三个参数:图片列表文件路径、棋盘格横向角点数 nx、纵向角点数 ny,以及一个useUncalibrated标志。图片列表文件里每行一个路径,左右相机图片交替排列,代码用lr = i % 2来区分当前读入的是左图还是右图。
// 读取图片列表,左右交替 for(i=0;;i++) { char buf[1024]; lr = i % 2; vector<CvPoint2D32f>& pts = points[lr]; if( !fgets(buf, sizeof(buf)-3, f) ) break; // 跳过注释行 if( buf[0] == '#') continue; IplImage* img = cvLoadImage(buf, 0); // 以灰度模式加载 if( !img ) break; imageSize = cvGetSize(img); imageNames[lr].push_back(buf); // 寻找棋盘格角点 result = cvFindChessboardCorners(timg, cvSize(nx, ny), &temp[0], &count, CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_NORMALIZE_IMAGE); // ... }这里有几个参数值得注意。CV_CALIB_CB_ADAPTIVE_THRESH让角点检测使用自适应阈值,对光照不均的图片更鲁棒;CV_CALIB_CB_NORMALIZE_IMAGE会先对图像做直方图归一化,减少曝光差异的影响。maxScale默认为 1,意味着不做图像金字塔缩放,如果图片分辨率很高、棋盘格占比小,可以把它改成 2 或 3,代码会自动缩放后再检测,最后把角点坐标除回原尺度。
角点检测成功后,代码立刻调用cvFindCornerSubPix做亚像素级优化,搜索窗口 11×11,迭代终止条件是 30 次迭代或精度 0.01。这一步不做的话,后续标定精度会明显下降——棋盘格角点的整数像素坐标误差在 ±0.5 像素量级,亚像素优化后能压到 0.1 像素以内。
2.2 三维物点构建与 cvStereoCalibrate 参数详解
角点提取完成后,代码构建棋盘格的三维物理坐标。squareSize默认设为 1.0,表示棋盘格方块的实际物理尺寸。如果你用的是 25mm 的方格,就把它改成 25,这样平移向量 T 的单位就是毫米。
// 构建棋盘格三维坐标,Z 恒为 0 objectPoints.resize(nframes*n); for( i = 0; i < ny; i++ ) for( j = 0; j < nx; j++ ) objectPoints[i*nx + j] = cvPoint3D32f(i*squareSize, j*squareSize, 0); // 每帧的物点相同,复制 nframes 份 for( i = 1; i < nframes; i++ ) copy(objectPoints.begin(), objectPoints.begin() + n, objectPoints.begin() + i*n);然后就是核心调用cvStereoCalibrate。这个函数的参数列表很长,代码注释里写得很清楚,我按实际使用顺序再梳理一遍:
| 参数位置 | 类型 | 含义 | 常见取值 |
|---|---|---|---|
| 1 | CvMat* | 物点矩阵 N×3 | 棋盘格物理坐标 |
| 2,3 | CvMat* | 左右图像点 N×2 | 角点检测结果 |
| 4 | CvMat* | 每幅图点数 M×1 | 全为 nx*ny |
| 5,7 | CvMat* | 左右内参 3×3 | 输出 |
| 6,8 | CvMat* | 左右畸变 1×5 | 输出 |
| 9 | CvSize | 图像尺寸 | 仅用于初始化 |
| 10,11 | CvMat* | 旋转 R、平移 T | 输出 |
| 12,13 | CvMat* | 本征矩阵 E、基础矩阵 F | 输出 |
| 14 | CvTermCriteria | 迭代终止条件 | 100 次或 1e-5 |
| 15 | int | 标定 flag | 见下方说明 |
代码里用的 flag 组合是CV_CALIB_FIX_ASPECT_RATIO + CV_CALIB_ZERO_TANGENT_DIST + CV_CALIB_SAME_FOCAL_LENGTH。FIX_ASPECT_RATIO固定焦距比 fx/fy,ZERO_TANGENT_DIST强制切向畸变系数为 0,SAME_FOCAL_LENGTH假设左右相机焦距相同。这三个 flag 在双目相机型号一致、装配良好的情况下能提高标定稳定性;但如果两个相机差异较大,强行加这些约束反而会引入系统误差。
2.3 标定质量评估:用极线约束算平均误差
标定完不能直接就用,代码里有一段质量检查逻辑,用极线约束 m2^T * F * m1 = 0 来算平均误差。先把左右图像点用cvUndistortPoints去畸变,再用cvComputeCorrespondEpilines计算每个点在另一幅图中的对应极线,最后把点代入极线方程取绝对值累加。
// 去畸变 cvUndistortPoints( &_imagePoints1, &_imagePoints1, &_M1, &_D1, 0, &_M1 ); cvUndistortPoints( &_imagePoints2, &_imagePoints2, &_M2, &_D2, 0, &_M2 ); // 计算极线 cvComputeCorrespondEpilines( &_imagePoints1, 1, &_F, &_L1 ); cvComputeCorrespondEpilines( &_imagePoints2, 2, &_F, &_L2 ); // 累加误差 double avgErr = 0; for( i = 0; i < N; i++ ) { double err = fabs(points[0][i].x*lines[1][i].x + points[0][i].y*lines[1][i].y + lines[1][i].z) + fabs(points[1][i].x*lines[0][i].x + points[1][i].y*lines[0][i].y + lines[0][i].z); avgErr += err; } printf( "avg err = %g\n", avgErr/(nframes*n) );这个avg err是判断标定好坏最直接的指标。经验值:小于 0.5 算不错,小于 0.2 算优秀,超过 1.0 基本说明角点提取有问题或者图片质量太差。如果误差偏大,先检查棋盘格是否平整、光照是否均匀,再考虑增加图片数量或调整棋盘格在画面中的位置分布。
3. 校正与视差计算:Bouguet 方法和 Hartley 方法怎么选
3.1 两种校正路径的分叉逻辑
代码用useUncalibrated参数控制走哪条路。传 0 走 Bouguet 方法,需要已知内参和 R、T;传 1 或 2 走 Hartley 方法,只用基础矩阵 F 做校正,不需要相机内参。这个设计很实用——当你拿不到相机内参,或者相机是临时拼凑的、标定数据不可靠时,Hartley 方法能救急。
Bouguet 路径的核心是cvStereoRectify,它输出左右校正旋转矩阵 R1、R2 和新的投影矩阵 P1、P2。然后cvInitUndistortRectifyMap预计算映射表,后续每帧用cvRemap做重映射。
// Bouguet 方法:标定立体校正 cvStereoRectify( &_M1, &_M2, &_D1, &_D2, imageSize, &_R, &_T, &_R1, &_R2, &_P1, &_P2, 0, 0/*CV_CALIB_ZERO_DISPARITY*/ ); // 预计算映射表 cvInitUndistortRectifyMap(&_M1,&_D1,&_R1,&_P1,mx1,my1); cvInitUndistortRectifyMap(&_M2,&_D2,&_R2,&_P2,mx2,my2);cvStereoRectify最后一个参数如果传CV_CALIB_ZERO_DISPARITY,会把主点对齐到同一位置,让视差范围更集中;传 0 则保持原始主点。代码里传的是 0,实际项目中我一般会加上CV_CALIB_ZERO_DISPARITY,这样视差图的有效区域更大。
Hartley 路径则先用cvFindFundamentalMat算 F,再调cvStereoRectifyUncalibrated得到单应矩阵 H1、H2,最后通过矩阵乘法把 H 转成旋转矩阵形式。
// Hartley 方法:非标定立体校正 if( useUncalibrated == 2 ) cvFindFundamentalMat( &_imagePoints1, &_imagePoints2, &_F); cvStereoRectifyUncalibrated( &_imagePoints1, &_imagePoints2, &_F, imageSize, &_H1, &_H2, 3); // 将 H 转换为 R 形式 cvInvert(&_M1, &_iM); cvMatMul(&_H1, &_M1, &_R1); cvMatMul(&_iM, &_R1, &_R1);注意 Hartley 方法里cvInitUndistortRectifyMap的第四个参数传的是_M1和_M2而不是 P1、P2,因为 Hartley 方法不产生新的投影矩阵,直接用原内参即可。
3.2 块匹配参数调优与视差图生成
校正完成后,代码创建CvStereoBMState并设置一组参数,然后对每一对图片做cvRemap和cvFindStereoCorrespondenceBM。
CvStereoBMState *BMState = cvCreateStereoBMState(); BMState->preFilterSize = 41; // 预滤波窗口,奇数 BMState->preFilterCap = 31; // 预滤波截断值 BMState->SADWindowSize = 41; // SAD 匹配窗口,奇数 BMState->minDisparity = -64; // 最小视差 BMState->numberOfDisparities = 128; // 视差搜索范围 BMState->textureThreshold = 10; // 纹理阈值 BMState->uniquenessRatio = 15; // 唯一性比率这组参数是代码作者针对棋盘格场景调的,实际用的时候要根据你的场景改。SADWindowSize越大,视差图越平滑但边缘越模糊;越小则细节多但噪点也多。numberOfDisparities必须能被 16 整除,它决定了最大可测视差,进而决定最近可测距离。minDisparity设成负数是为了覆盖校正后可能出现的负视差区域。
textureThreshold是个容易被忽略的参数。它过滤掉纹理太弱的区域,避免在纯色墙面上产生错误匹配。如果你的场景纹理丰富,可以降到 5 甚至 0;如果噪点多,就往上加。
3.3 校正结果可视化与极线检查
代码最后把校正后的左右图拼在一起,每隔 16 行画一条水平线,用来直观检查极线是否对齐。
if( !isVerticalStereo ) { cvGetCols( pair, &part, 0, imageSize.width ); cvCvtColor( img1r, &part, CV_GRAY2BGR ); cvGetCols( pair, &part, imageSize.width, imageSize.width*2 ); cvCvtColor( img2r, &part, CV_GRAY2BGR ); for( j = 0; j < imageSize.height; j += 16 ) cvLine( pair, cvPoint(0,j), cvPoint(imageSize.width*2,j), CV_RGB(0,255,0) ); }如果校正正确,左右图中同一个物理点的 y 坐标应该基本一致,绿色横线会穿过左右图中对应的特征点。如果偏差明显,说明标定或校正环节有问题,需要回头检查角点提取和cvStereoRectify的调用参数。
4. 避坑与排查:双目标定校正的五个血泪教训
4.1 角点检测失败但程序不报错
现象:程序跑完输出avg err很大,或者视差图完全不可用,但控制台没有明显报错。
原因:cvFindChessboardCorners返回 0 时,代码把该帧标记为active[lr].push_back((uchar)result),即 0。后续cvStereoCalibrate会跳过这些帧,但如果失败帧太多,有效数据就不够了。更隐蔽的是,如果左右图中有一侧检测失败,另一侧成功,会导致左右点数量不匹配。
解决:在角点检测后加一个判断,如果result == 0,打印当前图片路径并跳过该帧。同时统计成功帧数,低于 10 对就别往下跑了,标定结果不可信。
4.2 棋盘格方向搞反导致 R、T 异常
现象:标定输出的平移向量 T 方向明显不对,比如基线应该是水平方向,结果 T 的 y 分量比 x 分量还大。
原因:nx和ny传反了。棋盘格横向 9 个角点、纵向 6 个角点,如果写成StereoCalib("list.txt", 6, 9, 0),角点检测可能仍然成功,但物点构建时行列互换,导致 R、T 计算错误。
解决:确认棋盘格的实际角点数。注意是内角点,不是方格数。一个 10×7 的棋盘格,内角点是 9×6。用cvDrawChessboardCorners把检测结果画出来看一眼,角点顺序对不对一目了然。
4.3 视差图全黑或全白
现象:cvFindStereoCorrespondenceBM输出的视差图经过cvNormalize后要么全黑要么全白,看不到任何结构。
原因:minDisparity和numberOfDisparities的范围没有覆盖实际视差。如果实际视差是 200 像素,而minDisparity=-64、numberOfDisparities=128,搜索范围只到 64,超出范围的像素全部无效。
解决:先粗略估计视差范围。用校正后的图像,手动找几个对应点,算一下 x 坐标差。然后设minDisparity为最小视差减 16,numberOfDisparities为最大最小视差之差向上取整到 16 的倍数。
4.4 旧版 C 接口在新 OpenCV 上编译失败
现象:在 OpenCV 4.x 上编译,报大量cvLoadImage、cvCreateMat、CvMat未定义。
原因:OpenCV 4.x 默认不包含 C 接口的 legacy 模块,opencv/cv.h、opencv/cvaux.h这些头文件已经移除。
解决:两个选择。一是安装 OpenCV 3.x 或更早版本,保留 C 接口;二是把代码迁移到 C++ 接口,cvLoadImage换成cv::imread,CvMat换成cv::Mat,cvStereoCalibrate换成cv::stereoCalibrate。迁移工作量不小,但长远看值得做。
4.5 标定结果在不同次运行间波动大
现象:同样的图片,跑两次标定,得到的 R、T 矩阵差异明显,avg err也不一样。
原因:cvStereoCalibrate内部用迭代优化,初始值对结果有影响。如果图片数量少、棋盘格姿态变化不够丰富,优化容易陷入局部极小。
解决:增加图片数量到 20 对以上,确保棋盘格在画面中覆盖不同位置、不同角度、不同距离。另外,可以在cvStereoCalibrate之前先用cvCalibrateCamera2分别标定左右相机,把结果作为初始值传进去,能显著提高稳定性。
5. 二次开发改造:从示例代码到可复用标定模块
这份代码最大的问题是把标定、校正、视差计算全塞在一个函数里,而且用的是旧版 C 接口。如果你要把它集成到自己的项目里,我建议按下面的思路改造。
第一步,把StereoCalib拆成三个独立函数:calibrateStereo负责读图、找角点、调cvStereoCalibrate,返回内参、畸变、R、T;rectifyStereo负责调cvStereoRectify和cvInitUndistortRectifyMap,返回映射表;computeDisparity负责cvRemap和块匹配。这样每个环节可以单独测试和替换。
第二步,把标定结果持久化。代码里标定完直接用了,没有保存。实际项目中,标定是一次性的,校正和视差计算是每帧都要跑的。用cvSave把 M1、D1、M2、D2、R、T、R1、R2、P1、P2 存成 XML 或 YAML,下次启动直接加载,省去重复标定的时间。
// 保存标定结果示例 cvSave("calib_M1.xml", &_M1); cvSave("calib_D1.xml", &_D1); cvSave("calib_R.xml", &_R); cvSave("calib_T.xml", &_T); // 加载 CvMat* M1 = (CvMat*)cvLoad("calib_M1.xml");第三步,把块匹配参数做成可配置的。代码里硬编码了一组参数,但不同场景需要不同参数。我一般会把这些参数放到一个结构体里,从配置文件读取,运行时可以动态调整。
第四步,如果要做实时双目测距,块匹配的速度可能不够。可以考虑用cv::StereoBM的 GPU 版本,或者换用 SGBM(半全局块匹配),后者在视差图质量上明显更好,代价是计算量更大。OpenCV 的cv::StereoSGBM提供了 C++ 接口,参数和 BM 类似,但多了P1、P2两个平滑项参数,调参时需要额外注意。
验证改造是否成功的方法很简单:用同一组标定图片,分别跑原始代码和改造后的代码,对比avg err和视差图。如果误差在同一个量级、视差图结构一致,说明改造没有引入回归。
从那以后我每次做双目标定,都会先把角点检测结果可视化一遍,确认每一对图片的角点都正确提取,再往下走标定流程。这个习惯帮我省了很多来回排查的时间。希望帮到你。
本文还有配套的精品资源,点击获取