简介:这份资源面向计算机视觉初学者与需要完成课程设计、毕业设计的开发者,提供一套基于OpenCV、VS2019与MATLAB的双目视觉测距完整实现。代码覆盖从相机标定到利用BM局部匹配算法输出视差图的全流程,可帮助读者理解双目测距的标定、校正与匹配环节,并对照运行结果排查问题。压缩包共42个文件,约1.16MB,以30张jpg标定与校正图像、6个txt标定参数与结果文件为主,另含sln解决方案、cpp源码、vcxproj工程文件及项目说明md,目录结构清晰,便于按标定、校正、测距模块检索学习。目前已有661人学习下载,适合希望快速搭建双目测距实验环境、对照参数文件复现视差图并掌握BM算法调参思路的读者参考。
1. 从一组左右图到真实距离:这套双目测距源码到底能跑出什么
手头有一对左右相机拍的同场景照片,想知道画面里某个物体离镜头到底几米,这件事听起来简单,真做起来会卡在三个地方:相机内参从哪来、畸变怎么消、视差图怎么算成深度。这套基于 OpenCV + VS2019 + MATLAB 的双目视觉测距源码,把这三个环节串成了一条能跑通的链路——MATLAB 负责标定出内外参,VS2019 里的 C++ 工程用 OpenCV 做立体校正和 BM 局部匹配输出视差图,最后按三角测量公式换算成距离。它适合正在做课程设计、毕设或者刚接触双目视觉测距的 C++ 开发者,也适合已经会调 OpenCV 但没完整走过标定到测距全流程的人。资源包里带了 13 组左右标定图、标定结果文本、校正前后对比图和一个可直接打开的 sln 解决方案,省掉了自己凑数据集和搭工程骨架的时间。
2. 标定先行:MATLAB 工具箱怎么把左右相机参数抠出来
双目标定的本质是求两组东西:每台相机自己的内参和畸变系数,以及两台相机之间的旋转平移关系。内参决定像素坐标和相机坐标怎么换算,外参决定左相机坐标系到右相机坐标系怎么变换。没有这两组参数,后面的立体校正和深度计算全是空中楼阁。这套资源用 MATLAB 的 Camera Calibrator 工具箱做标定,原因是它可视化好、能直接导出 OpenCV 格式的参数矩阵,比手写标定代码省事得多。
2.1 标定图的采集与筛选逻辑
资源包里 left01 到 left13、right01 到 right13 是成对的棋盘格图。标定时左右相机各导入自己那一组,MATLAB 会自动检测棋盘角点。这里有个容易被忽略的点:不是所有图都能用。角点检测失败的图、棋盘格倾斜过大导致透视畸变严重的图、左右图棋盘格数量不一致的图,都要剔掉。我一般会先跑一遍检测,把 MATLAB 提示 "Failed to detect checkerboard" 的图删掉再重新导入。棋盘格的实际方格尺寸要填对,比如你打印的是 25mm 方格,就在设置里填 25,单位保持一致,这个值直接影响平移向量的物理尺度。
2.2 导出参数并转成 OpenCV 能读的格式
MATLAB 标定完成后导出的是它自己的结构体,需要转成 OpenCV 的矩阵格式。资源里已经给了 calibrationresults_L.txt、calibrationresults_R.txt、stereocalibrateresult.txt 这几个文本,里面存的就是标定结果。核心参数对应关系如下:
| 参数 | 含义 | 在 OpenCV 中的变量 |
|---|---|---|
| 内参矩阵 3x3 | 焦距和主点 | cameraMatrix |
| 畸变系数 1x5 | 径向和切向畸变 | distCoeffs |
| 旋转矩阵 3x3 | 左右相机相对姿态 | R |
| 平移向量 3x1 | 左右相机基线 | T |
如果你要自己重新标定,导出后在 MATLAB 命令窗用下面这段把矩阵按行打印出来,方便粘进 C++ 代码:
% 假设标定结果存在 stereoParams 里 disp('左相机内参:'); disp(stereoParams.CameraParameters1.IntrinsicMatrix'); % 注意转置 disp('右相机内参:'); disp(stereoParams.CameraParameters2.IntrinsicMatrix'); disp('旋转矩阵:'); disp(stereoParams.RotationOfCamera2); disp('平移向量:'); disp(stereoParams.TranslationOfCamera2);逻辑说明:MATLAB 的内参矩阵默认是行向量乘以矩阵的形式,和 OpenCV 的列向量约定相反,所以导出时要转置。平移向量的单位跟你填的棋盘格尺寸一致,如果填的是毫米,T 就是毫米,后面算出来的距离也是毫米,别忘了换算。这一步参数填错,后面视差图看着正常但距离会整体偏,属于典型的玄学翻车点。
3. VS2019 工程拆解:从 sln 到 BM 视差图的完整链路
拿到标定参数后,重头戏在 VS2019 的 C++ 工程里。资源里的 CameraCalibration.sln 可以直接双击打开,工程结构不复杂,核心逻辑集中在 CameraCalibration.cpp。整个流程分四步:读入标定参数、对左右图做立体校正、用 BM 算法算视差、把视差转成深度。下面按这四步拆。
3.1 立体校正:让左右图的极线对齐
立体校正的目的是把左右图像重投影,使得同一个物点在两幅图里处于同一行,这样匹配时只需要在同一行水平搜索,计算量大幅下降。OpenCV 提供 stereoRectify 和 initUndistortRectifyMap 两个函数配合完成。常见做法是先算校正映射表,再用 remap 把原图映射过去。
// 读入标定参数(示例,实际从 txt 或硬编码读取) Mat K1 = (Mat_<double>(3,3) << fx1, 0, cx1, 0, fy1, cy1, 0, 0, 1); Mat D1 = (Mat_<double>(1,5) << k1, k2, p1, p2, k3); Mat K2 = (Mat_<double>(3,3) << fx2, 0, cx2, 0, fy2, cy2, 0, 0, 1); Mat D2 = (Mat_<double>(1,5) << k1_, k2_, p1_, p2_, k3_); Mat R, T; // 从 stereocalibrateresult.txt 读入 Size imgSize = leftImg.size(); Mat R1, R2, P1, P2, Q; stereoRectify(K1, D1, K2, D2, imgSize, R, T, R1, R2, P1, P2, Q, CALIB_ZERO_DISPARITY, 0, imgSize); Mat map1x, map1y, map2x, map2y; initUndistortRectifyMap(K1, D1, R1, P1, imgSize, CV_32FC1, map1x, map1y); initUndistortRectifyMap(K2, D2, R2, P2, imgSize, CV_32FC1, map2x, map2y); Mat leftRect, rightRect; remap(leftImg, leftRect, map1x, map1y, INTER_LINEAR); remap(rightImg, rightRect, map2x, map2y, INTER_LINEAR);参数说明:CALIB_ZERO_DISPARITY 表示让主点在左右图中水平对齐,通常都开。Q 是后面用来做 reprojectImageTo3D 的 4x4 视差转深度矩阵,别丢。initUndistortRectifyMap 生成的映射表可以缓存,如果处理视频流,没必要每帧重算,这是性能优化的关键点。校正效果好不好,直接看资源里那张「校正后左右相机图像.jpg」,如果左右图同一行能对上,说明校正成功。
3.2 BM 局部匹配:参数怎么调视差图才不花
BM(Block Matching)是局部立体匹配里最经典的一种,原理是对左图每个像素,在右图同一行的某个范围内找最相似的块,相似度用 SAD 算。它的优点是快,缺点是参数敏感、遮挡区域和弱纹理区域容易出空洞。资源里用的就是 BM,核心参数在 StereoSGBM 或 StereoBM 的初始化里。
Ptr<StereoBM> bm = StereoBM::create(16*5, 21); bm->setPreFilterCap(31); bm->setBlockSize(21); // 匹配块大小,奇数 bm->setMinDisparity(0); bm->setNumDisparities(16*5); // 视差搜索范围,16 的倍数 bm->setTextureThreshold(10); bm->setUniquenessRatio(15); bm->setSpeckleWindowSize(100); bm->setSpeckleRange(32); bm->setDisp12MaxDiff(1); Mat disp, disp8; bm->compute(leftRect, rightRect, disp); disp.convertTo(disp8, CV_8U, 255/(16*5.0));逻辑说明:numDisparities 决定搜索范围,物体越近视差越大,如果近处物体测不出来,把这个值调大,但计算量也上去。blockSize 越大越平滑但边缘越糊,越小细节多但噪点多,一般 15 到 25 之间试。uniquenessRatio 是唯一性检验,防止误匹配,值太小会引入错误视差,太大又会让视差图变稀疏。speckleWindowSize 用来滤掉小块噪点,弱纹理场景可以适当加大。调参没有万能值,得对着你的场景反复试,这就是双目测距里最像玄学的部分。
3.3 视差转深度:三角测量公式和 Q 矩阵
拿到视差图后,深度计算就是三角测量。公式很简单:Z = f * B / d,f 是焦距,B 是基线,d 是视差。OpenCV 里用 reprojectImageTo3D 一步到位,它内部用的就是 stereoRectify 输出的 Q 矩阵。
Mat xyz; reprojectImageTo3D(disp, xyz, Q, true); // 取图像中心点看看深度 Point p(disp.cols/2, disp.rows/2); Vec3f point3D = xyz.at<Vec3f>(p); float distance = point3D[2]; // Z 值就是距离 cout << "中心点距离:" << distance << " mm" << endl;参数说明:reprojectImageTo3D 输出的 xyz 是三通道浮点图,每个像素存 (X, Y, Z)。Z 就是相机坐标系下的深度。注意 handleMissingValues 设为 true 时,视差为 0 或无效的点会被置成很大的值,取点前最好判断一下视差是否有效。距离单位跟标定时棋盘格尺寸一致,如果标定填的是毫米,这里就是毫米。资源里 stereocalibrateresult.txt 存的就是 Q 矩阵相关参数,可以直接读进来用。
4. 避坑与排查:双目测距跑不通时先看这几条
这套流程我前后搭过几次,每次翻车的地方都不太一样,但高频问题就那么几个。下面按「现象 → 原因 → 解决」列出来,遇到问题先对照排查。
现象一:视差图全黑或者大片空洞。原因通常是 numDisparities 设得太小,近处物体视差超出搜索范围,或者左右图根本没校正对齐。解决:先把 numDisparities 调到 168 甚至 1610 试,再检查校正后的左右图同一行是否对齐,没对齐就回头查标定参数和 stereoRectify 的输入顺序。
现象二:距离数值明显偏大或偏小,但视差图看着正常。原因基本是标定参数单位不一致,比如棋盘格实际是 25mm 但填了 25cm,或者平移向量 T 读入时没按正确顺序。解决:核对标定时填的方格尺寸和实际打印尺寸,检查 T 向量的三个分量是否和 stereocalibrateresult.txt 一致,单位统一后再算。
现象三:BM 匹配结果噪点极多,像雪花屏。原因可能是 preFilterCap 和 textureThreshold 没调好,弱纹理区域被强行匹配。解决:适当提高 textureThreshold 让弱纹理区域不参与匹配,同时加大 speckleWindowSize 滤掉孤立噪点。如果场景本身纹理就少,BM 效果有限,可以考虑换 SGBM,但资源里给的是 BM,先把 BM 参数吃透。
现象四:VS2019 编译报错找不到 opencv_world 或头文件。原因是 OpenCV 环境没配好,包含目录、库目录、附加依赖项三处都要设。解决:在项目属性里把 OpenCV 的 include 目录加到 C/C++ 常规,lib 目录加到链接器常规,opencv_world4xx.lib 加到附加依赖项。注意 Debug 和 Release 用的 lib 不一样,别混。
现象五:MATLAB 标定导出的参数粘进 C++ 后结果全乱。原因是 MATLAB 矩阵是列主序还是行主序没搞清,内参矩阵忘了转置。解决:按 2.2 节的方式先转置再导出,旋转矩阵 R 也要确认方向,是从左到右还是从右到左,搞反了校正图会明显错位。
5. 进阶技巧:把单帧测距改成能落地的验证流程
单帧跑通只是第一步,真正要验证这套系统靠不靠谱,得做几件事。第一是拿已知距离的物体做标定验证,比如把棋盘格放在离相机 1 米处,看算出来的 Z 值偏差多少,偏差在 2% 以内算合格。第二是换不同场景的左右图测试,资源里那 13 组图可以轮流当测试集,看参数泛化能力。第三是把 Q 矩阵和标定参数存成配置文件,别硬编码在 cpp 里,换相机时只改配置不动代码。
// 把关键参数存成 YAML,方便复用 FileStorage fs("stereo_config.yml", FileStorage::WRITE); fs << "K1" << K1 << "D1" << D1; fs << "K2" << K2 << "D2" << D2; fs << "R" << R << "T" << T << "Q" << Q; fs.release(); // 读取时 FileStorage fs2("stereo_config.yml", FileStorage::READ); fs2["K1"] >> K1;参数说明:YAML 存 Mat 时 OpenCV 会自动处理格式,读回来直接就是 Mat,比手写 txt 解析省事。注意 FileStorage 写中文路径可能出问题,路径用英文。
还有一个实用技巧是给视差图做后处理。BM 出来的视差图边缘毛刺多,可以用中值滤波先平滑,再用双边滤波保边。另外视差图里无效值(视差为 0)的区域,深度值不可信,取距离时最好在有效视差区域内取中位数,而不是随便取一个点,这样抗噪能力会好很多。
Mat dispFiltered; medianBlur(disp8, dispFiltered, 5); // 只统计有效视差区域的深度中位数 vector<float> validDepths; for (int i = 0; i < disp.rows; i++) { for (int j = 0; j < disp.cols; j++) { if (disp.at<short>(i, j) > 0) { Vec3f pt = xyz.at<Vec3f>(i, j); if (pt[2] > 0 && pt[2] < 10000) // 过滤异常值 validDepths.push_back(pt[2]); } } } sort(validDepths.begin(), validDepths.end()); float medianDepth = validDepths[validDepths.size()/2];逻辑说明:先判断视差大于 0 才认为是有效匹配,再过滤掉深度异常大或异常小的点,最后取中位数。这样比单点取值稳得多,尤其在纹理不均匀的场景里。我一般还会把有效视差的比例打印出来,低于 30% 就说明这组图匹配质量不行,得回头调 BM 参数或者换图。
从那以后我每次拿到新的双目图,都强制先跑一遍校正对齐检查,再跑视差有效比例统计,两个都过了才看距离数值。这套源码把标定、校正、匹配、测距的骨架都搭好了,剩下的就是拿你自己的图去磨参数。希望帮到你。
本文还有配套的精品资源,点击获取