简介:这份资源面向计算机视觉初学者与需要完成课程设计、毕业设计的开发者,提供一套基于OpenCV、VS2019与MATLAB的双目视觉测距完整工程。代码覆盖从相机标定、立体校正到BM局部匹配算法输出视差图的全流程,可帮助读者理解双目测距的标定参数求解与视差计算原理,并直接编译运行验证效果。压缩包共42个文件,约1.16MB,以30张左右相机标定图像为主,另含CameraCalibration.cpp核心源码、sln解决方案与vcxproj工程文件、若干txt标定结果与参数文件,以及项目说明md文档,目录结构清晰,便于按标定、校正、匹配三个阶段分步学习。目前已有661人学习下载,适合希望快速搭建双目测距实验环境、对照源码理解BM算法实现细节的读者参考。
1. 双目测距这套组合拳:OpenCV 算视差、VS2019 管工程、MATLAB 做标定
双目视觉测距这件事,说穿了就一句话:用两个相机拍同一场景,找出左右图里同一个物理点的像素位置差,也就是视差,再用Z = f·B / d把视差换算成真实距离。公式简单到一行,但真正落地时你会发现,难的不是公式,而是标定参数准不准、极线校正做没做、匹配点找得对不对。这套「OpenCV + VS2019 + MATLAB」的组合,恰好把这三件事拆给了三个最顺手的工具:MATLAB 负责相机标定和参数验证,OpenCV 负责 StereoBM/SGBM 立体匹配和测距主流程,VS2019 负责把 C++ 工程组织成能编译、能调试、能出 Release 的 sln 解决方案。
它适合谁?适合已经会写一点 C++、装过 OpenCV、但一直卡在「标定完不知道怎么用、视差图一团糊、测出来的距离飘」的在校生和初级工程师。你不需要深度学习背景,也不需要昂贵的双目相机模组,两个同型号 USB 摄像头加上一块棋盘格就能起步。下面我按「先立住原理、再动手复现、最后讲坑」的顺序,把这条链路完整走一遍。
2. 从标定到测距:双目系统的参数到底怎么串起来
2.1 内参、外参与视差公式的对应关系
双目测距的输入是左右两张图,输出是深度。中间要经过四组参数:左相机内参矩阵M1、右相机内参矩阵M2、畸变系数D1/D2、以及左右相机之间的旋转R和平移T。MATLAB 的 Camera Calibrator 或 Stereo Camera Calibrator 就是用来求这些的。
内参矩阵长这样:
M = [fx 0 cx 0 fy cy 0 0 1]fx/fy是焦距(像素单位),cx/cy是主点。外参里的T向量,它的第一个分量Tx就是基线B——两个相机光心之间的水平距离。测距公式Z = fx · B / d里的f用的就是fx,d是视差(左图像素 x 减去右图对应点像素 x)。
这里有个新手最容易忽略的点:fx和B的单位必须自洽。fx是像素,B是毫米或米,算出来的Z就是对应单位。如果你标定时棋盘格方格尺寸填的是毫米,那B也得用毫米量。
2.2 用 MATLAB 做标定的完整步骤
标定质量直接决定测距精度,这一步不能省。我一般用 MATLAB 的 Stereo Camera Calibrator App,流程如下:
第一步,打印棋盘格。常见做法是 9×6 或 10×7 的内角点,方格边长 25mm 或 30mm,打印后贴在硬板上保证平整。
第二步,采集图像。左右相机同时拍,至少 15 对,覆盖画面各个区域和不同倾斜角度。图像命名要有规律,比如left01.jpg、right01.jpg。
第三步,在 MATLAB 里加载。命令行输入stereoCameraCalibrator打开 App,Add Images 分别指定左右文件夹,填入方格实际尺寸。
第四步,标定并看误差。App 会给出每张图的重投影误差,单位是像素。平均误差控制在 0.5 像素以内算合格,超过 1 像素就要重拍。误差大的图直接删掉重来,别心疼。
第五步,导出参数。点 Export Camera Parameters,得到stereoParams对象。用下面几行把关键参数打印出来:
% 导出标定结果中的关键参数 M1 = stereoParams.CameraParameters1.IntrinsicMatrix'; % 左内参,转置成常规形式 M2 = stereoParams.CameraParameters2.IntrinsicMatrix'; % 右内参 D1 = stereoParams.CameraParameters1.RadialDistortion; % 左径向畸变 D2 = stereoParams.CameraParameters2.RadialDistortion; % 右径向畸变 R = stereoParams.RotationOfCamera2; % 右相对左的旋转 T = stereoParams.TranslationOfCamera2; % 右相对左的平移,T(1) 即基线 fprintf('基线 B = %.4f\n', norm(T));逻辑说明:IntrinsicMatrix在 MATLAB 里是转置存储的,导出给 OpenCV 用时要转回来。TranslationOfCamera2是右相机相对左相机的平移向量,它的模长就是基线。参数说明:RadialDistortion只取了径向畸变,如果镜头畸变明显,还要把TangentialDistortion一起导出,OpenCV 的stereoRectify需要完整的 5 参数畸变向量。
2.3 把 MATLAB 参数喂给 OpenCV 的映射表
MATLAB 和 OpenCV 的参数顺序不一样,直接复制会翻车。对照关系如下:
| 参数 | MATLAB 字段 | OpenCV 变量 | 注意事项 |
|---|---|---|---|
| 左内参 | IntrinsicMatrix' | M1 (3×3) | 必须转置 |
| 右内参 | IntrinsicMatrix' | M2 (3×3) | 必须转置 |
| 畸变 | RadialDistortion + TangentialDistortion | D1 (1×5) | 顺序 k1,k2,p1,p2,k3 |
| 旋转 | RotationOfCamera2 | R (3×3) | 直接用 |
| 平移 | TranslationOfCamera2 | T (3×1) | 直接用,T(1) 为基线 |
这张表建议存成注释贴在代码里,省得每次都要翻。参数对齐之后,才能进入 OpenCV 的校正和匹配环节。
3. VS2019 里搭 sln:OpenCV 工程配置与立体匹配主流程
3.1 新建 sln 并配置 OpenCV 的四个关键项
VS2019 建工程这一步,坑基本都在属性配置上。新建一个空项目(Empty Project),然后右键项目 → 属性,切到「所有配置」:
第一项,C/C++ → 常规 → 附加包含目录,加上你的opencv\build\include和你的opencv\build\include\opencv2。
第二项,链接器 → 常规 → 附加库目录,加上你的opencv\build\x64\vc15\lib。注意 VS2019 对应的是 vc15 或 vc16,取决于你下的 OpenCV 版本,选错了会报 LNK2019。
第三项,链接器 → 输入 → 附加依赖项。Debug 配置加opencv_world4xxd.lib,Release 配置加opencv_world4xx.lib,带 d 的是 Debug 版。两个配置要分别设,别只设一个。
第四项,把解决方案平台从 Win32 改成 x64。OpenCV 官方预编译包基本都是 64 位的,用 Win32 编译必然链接失败。
配置完可以写个最小测试:
#include <opencv2/opencv.hpp> #include <iostream> int main() { cv::Mat img = cv::imread("left01.jpg"); if (img.empty()) { std::cout << "读取失败,检查路径" << std::endl; return -1; } std::cout << "宽 " << img.cols << " 高 " << img.rows << std::endl; return 0; }逻辑说明:这段只验证环境通不通。能打印出图像尺寸,说明包含目录、库目录、依赖项、平台四项都对了。参数说明:imread的路径用绝对路径最稳,相对路径是相对 exe 所在目录,不是源码目录,这点经常让人懵。
3.2 立体校正:stereoRectify 与 remap 的配合
拿到标定参数后,第一步是校正,让左右图的极线水平对齐。核心是stereoRectify算出校正变换,再用initUndistortRectifyMap生成映射表,最后remap应用。
cv::Mat M1 = (cv::Mat_<double>(3,3) << fx, 0, cx, 0, fy, cy, 0, 0, 1); cv::Mat M2 = (cv::Mat_<double>(3,3) << fx2, 0, cx2, 0, fy2, cy2, 0, 0, 1); cv::Mat D1 = (cv::Mat_<double>(1,5) << k1, k2, p1, p2, k3); cv::Mat D2 = (cv::Mat_<double>(1,5) << k1b, k2b, p1b, p2b, k3b); cv::Mat R, T; // 从标定结果填入 cv::Size imgSize(640, 480); cv::Mat R1, R2, P1, P2, Q; cv::stereoRectify(M1, D1, M2, D2, imgSize, R, T, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, 0, imgSize); cv::Mat map1x, map1y, map2x, map2y; cv::initUndistortRectifyMap(M1, D1, R1, P1, imgSize, CV_32FC1, map1x, map1y); cv::initUndistortRectifyMap(M2, D2, R2, P2, imgSize, CV_32FC1, map2x, map2y); cv::Mat leftRect, rightRect; cv::remap(leftImg, leftRect, map1x, map1y, cv::INTER_LINEAR); cv::remap(rightImg, rightRect, map2x, map2y, cv::INTER_LINEAR);逻辑说明:stereoRectify输出Q矩阵,这个矩阵后面reprojectImageTo3D要用。CALIB_ZERO_DISPARITY让主点在两图中保持一致。参数说明:initUndistortRectifyMap的CV_32FC1是映射表类型,别改成CV_8U,否则精度掉得厉害。remap的插值方式用INTER_LINEAR就够,追求速度可以换INTER_NEAREST,但视差图会更毛糙。
3.3 StereoBM 与 StereoSGBM 的参数怎么调
校正完就进入匹配。OpenCV 提供两个现成的:StereoBM快但糙,StereoSGBM慢但细。我一般先用 SGBM 出效果,再考虑要不要换 BM 提速。
int numDisparities = 16 * 6; // 必须是 16 的整数倍 int blockSize = 5; // 奇数,3~11 之间 cv::Ptr<cv::StereoSGBM> sgbm = cv::StereoSGBM::create( 0, numDisparities, blockSize, 8 * blockSize * blockSize, // P1 32 * blockSize * blockSize, // P2 1, 0, 10, 100, 32, cv::StereoSGBM::MODE_SGBM); cv::Mat disp, dispNorm; sgbm->compute(leftRect, rightRect, disp); disp.convertTo(dispNorm, CV_8U, 255.0 / (numDisparities * 16.0));逻辑说明:numDisparities决定能测多近,值越大能覆盖的视差范围越广,但计算量也涨。blockSize是匹配窗口,越大越平滑但边缘越糊。P1/P2是平滑惩罚项,经验公式就是上面写的8·blockSize²和32·blockSize²。参数说明:compute出来的disp是CV_16S,真实视差要除以 16,别直接当像素用。dispNorm只是为了显示,测距要用原始disp。
3.4 从视差图到真实距离:reprojectImageTo3D 与手算对照
拿到视差后,两条路都能出距离。一条是用Q矩阵:
cv::Mat dispFloat; disp.convertTo(dispFloat, CV_32F, 1.0 / 16.0); // 还原真实视差 cv::Mat points3D; cv::reprojectImageTo3D(dispFloat, points3D, Q); // 取图像中心点的三维坐标 int cx = leftRect.cols / 2, cy = leftRect.rows / 2; cv::Vec3f p = points3D.at<cv::Vec3f>(cy, cx); std::cout << "中心点距离 " << p[2] << " mm" << std::endl;另一条是手算,用来交叉验证:
double fx_rect = P1.at<double>(0, 0); // 校正后的焦距 double baseline = -T.at<double>(0, 0); // 基线,注意符号 double d = dispFloat.at<float>(cy, cx); double Z = fx_rect * baseline / d; std::cout << "手算距离 " << Z << " mm" << std::endl;逻辑说明:reprojectImageTo3D内部就是用Q做矩阵运算,结果和手算应该一致。如果两者差很多,八成是Q用错了或者视差没除以 16。参数说明:baseline取-T(0,0)是因为 MATLAB 导出的T方向约定和 OpenCV 的Q矩阵约定可能差一个符号,实测时两个方向都试一下,哪个结果合理用哪个。这一步的交叉验证很关键,能帮你快速定位是标定问题还是代码问题。
4. 避坑与排查:双目测距最常见的 5 个翻车现场
4.1 视差图全黑或全白
现象:compute出来的视差图要么一片黑,要么一片白,看不到任何场景轮廓。
原因:全黑通常是numDisparities太小,真实视差超出了搜索范围;全白往往是左右图没校正对齐,匹配器找不到对应点,或者图像被转成了灰度但通道数不对。
解决:先把numDisparities调到16*10试,确认有轮廓后再往回收。检查leftRect和rightRect的type()是不是CV_8UC1,SGBM 只吃单通道灰度图。如果还是全白,把校正后的左右图并排显示,画几条水平线,看同一物体在两图里是不是在同一行——不在同一行说明校正没做对。
4.2 测距结果随距离线性漂移
现象:近处测得挺准,越远误差越大,而且误差方向一致。
原因:基线B或焦距fx填错了。这两个参数是线性放大的,错一点,远距离就被放大成大误差。
解决:拿一个已知距离的物体(比如卷尺量好 1 米)做基准,反推fx·B的实际值,和你填的值对比。如果差得离谱,回去检查 MATLAB 导出的T是不是被转置了,以及P1里的fx是不是校正后的值而不是原始内参的fx。
4.3 VS2019 报 LNK2019 无法解析的外部符号
现象:编译通过,链接时报一堆LNK2019,符号名里带cv::。
原因:附加依赖项里的 lib 名字和实际 OpenCV 版本对不上,或者 Debug/Release 配置混用了。
解决:去opencv\build\x64\vc15\lib目录下看实际文件名,照着抄。Debug 配置只加带d的,Release 只加不带d的。改完记得「所有配置」和「当前配置」都检查一遍,VS 经常只改了一个。
4.4 MATLAB 标定误差合格但实测就是不准
现象:MATLAB 里重投影误差 0.3 像素,看着很漂亮,但 OpenCV 里测距就是飘。
原因:标定用的图像分辨率和实际测距用的分辨率不一致。MATLAB 标定时如果用了 1280×720,测距时却把图缩到 640×480,内参全部失效。
解决:标定和测距必须用同一分辨率。如果非要缩放,内参的fx/fy/cx/cy都要按比例缩放,fx乘缩放系数,cx也乘。最稳的做法是标定和测距都锁定同一个分辨率,别中途改。
4.5 视差图边缘有大量空洞
现象:物体边缘和纹理少的区域,视差图出现大片无效值(SGBM 里是 -16 或负值)。
原因:这是立体匹配的固有缺陷,纹理缺失区域没有足够特征做匹配,遮挡区域左右图看不到同一点。
解决:对无效值做后处理,用filterSpeckles去掉小块噪点,或者用左右一致性检查(disp12MaxDiff参数)过滤掉不可靠的视差。如果对空洞特别敏感,可以考虑加一个结构光投射器给场景补纹理,这是工业双目常用的手段。别指望纯算法能把所有空洞填满,那不现实。
5. 让测距更稳的几个进阶技巧
5.1 用 ROI 限制匹配范围换精度
全图匹配既慢又容易在无关区域出错。如果你的目标物体大致在画面中央,可以只对 ROI 做匹配,numDisparities也能相应调小,精度和速度都受益。做法是先leftRect(roiRect)裁剪,匹配完再把视差图贴回全图坐标。注意 ROI 的 x 起点会影响视差值的绝对坐标,贴回时要补偿。
5.2 多帧中值滤波压抖动
单帧测距的抖动可能有好几毫米,尤其是纹理弱的表面。连续采 5 到 10 帧,对同一像素的深度值取中值,比均值更抗离群点。我一般用一个std::vector<double>存最近 N 帧的中心点距离,排序后取中间那个。这个技巧在静态场景下几乎零成本,效果立竿见影。
5.3 用已知尺寸物体做在线校验
系统跑起来之后,别急着信它。放一个已知尺寸的物体(比如 A4 纸,297mm×210mm)在几个固定距离上,测它的宽度,和真实值对比。如果误差在 1% 以内,说明标定和参数都对;超过 3%,回去查基线和焦距。这个习惯能帮你在换镜头、换相机、重新标定后快速判断系统是否还健康。
5.4 参数速查表
| 参数 | 典型值 | 调整方向 |
|---|---|---|
| numDisparities | 16×6 ~ 16×10 | 越大覆盖越近,越慢 |
| blockSize | 5 ~ 11 奇数 | 越大越平滑,边缘越糊 |
| P1 | 8·blockSize² | 太小噪点多,太大过度平滑 |
| P2 | 32·blockSize² | 同上,影响更强 |
| disp12MaxDiff | 1 ~ 2 | 越大越宽松,无效值越少 |
| uniquenessRatio | 10 ~ 15 | 越大越严格,空洞越多 |
这张表我贴在显示器边上,调参时直接对着改,比每次翻文档快得多。
5.5 我踩过的最深的一个坑
最后说个血泪经验。我最早做这套系统时,MATLAB 标定完直接把参数抄进代码,测出来距离总是差 10% 左右,查了两天没找到原因。后来发现是 MATLAB 的TranslationOfCamera2单位跟着棋盘格走,我棋盘格填的是毫米,但代码里baseline当米用了,量纲差了 1000 倍。从那以后我养成了一个习惯:所有长度参数在代码开头统一定义成double常量,注释里写清单位,fx用像素、B用毫米、Z输出毫米,绝不混用。这个习惯比任何调参技巧都值钱。
希望帮到你。
本文还有配套的精品资源,点击获取