简介:这是一套面向计算机视觉初学者与嵌入式视觉开发者的双目测距完整工程,基于OpenCV、VS2019与MATLAB联合实现,重点解决从相机标定到视差图输出的全流程问题。资源包共42个文件,约1.16MB,包含30张左右相机标定与校正图像、6个标定参数与图像列表文本、CameraCalibration.sln解决方案、CameraCalibration.cpp核心源码及vcxproj工程文件,另附项目说明文档,便于直接编译运行与对照理解。代码中实现了相机标定、立体校正以及基于BM局部匹配算法输出视差图的功能,并给出校正前后左右相机图像与标定结果文件,方便读者验证每一步中间结果。目前已有661人学习下载,适合需要快速搭建双目视觉测距实验环境、理解标定与立体匹配参数含义、并在此基础上继续扩展SGBM或深度计算的学习者参考。
1. 从一组左右图到真实距离:这套双目测距源码到底能跑出什么
很多人第一次接触双目视觉测距,脑子里想的是「两个摄像头拍同一场景,算一下像素差就能出深度」,真上手才发现卡在第一步——标定。棋盘格拍了一堆,角点检测要么漏检要么错位,标定出来的重投影误差大得离谱,后面视差图全是噪点,测出来的距离跟实际差出好几倍。这套基于 OpenCV + VS2019 + MATLAB 的双目视觉测距系统源码,解决的正是从标定到视差输出的完整链路问题。它用 C++ 在 VS2019 里跑 OpenCV 的标定和 BM 局部匹配算法,MATLAB 负责标定结果的可视化验证,压缩包里带了 13 组左右相机标定图、标定结果文本、校正前后对比图,还有完整的 sln 解决方案。适合正在做双目测距课程设计、毕业设计,或者想把双目测距模块集成进自己项目的 C++ 开发者。你拿到手不是一堆散代码,而是一个能直接编译、能对照中间结果排查问题的工程。
2. 标定链路拆解:从棋盘格到 stereoRectifyParams.txt
2.1 为什么标定结果要落成文本文件
这套源码有个很实在的设计:标定不是跑完就完事,而是把左右相机的内参、畸变系数、立体标定参数分别写进calibrationresults_L.txt、calibrationresults_R.txt、stereocalibrateresult.txt和stereoRectifyParams.txt。很多人做双目测距时标定和测距写在一个 main 里,标定一改就得重跑整个流程,调试起来非常痛苦。把标定结果落盘之后,测距阶段直接读文件,标定和测距解耦,改测距参数不用重新标定。
常见做法是先用calibrateCamera分别标左右相机,再用stereoCalibrate做立体标定,最后stereoRectify算校正参数。这套源码的 CameraCalibration.cpp 就是按这个顺序走的。标定结果文本里一般包含相机矩阵M、畸变向量D、旋转矩阵R、平移向量T、本质矩阵E、基础矩阵F,以及校正后的投影矩阵P1、P2和重投影矩阵Q。Q矩阵是后面reprojectImageTo3D出三维坐标的关键,很多人只存了M和D,到测距那步发现算不出真实尺度,就是漏了Q。
2.2 标定代码的关键参数与调用顺序
下面这段是标定流程的核心骨架,参数按源码里的实际调用整理:
// 左右相机图像路径列表,源码里用 caliberationpics_L.txt / caliberationpics_R.txt 存 std::vector<std::string> leftPaths = readList("caliberationpics_L.txt"); std::vector<std::string> rightPaths = readList("caliberationpics_R.txt"); // 棋盘格内角点数,源码用的 9x6,注意是内角点不是格子数 cv::Size boardSize(9, 6); std::vector<std::vector<cv::Point2f>> imgPointsL, imgPointsR; std::vector<std::vector<cv::Point3f>> objPoints; // 世界坐标系下的棋盘格角点,Z=0 std::vector<cv::Point3f> obj; for (int i = 0; i < boardSize.height; i++) for (int j = 0; j < boardSize.width; j++) obj.push_back(cv::Point3f(j * squareSize, i * squareSize, 0)); for (size_t i = 0; i < leftPaths.size(); i++) { cv::Mat imgL = cv::imread(leftPaths[i], cv::IMREAD_GRAYSCALE); cv::Mat imgR = cv::imread(rightPaths[i], cv::IMREAD_GRAYSCALE); std::vector<cv::Point2f> cornersL, cornersR; // 亚像素角点检测,winSize 和零区域参数直接影响角点精度 bool foundL = cv::findChessboardCorners(imgL, boardSize, cornersL, cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); bool foundR = cv::findChessboardCorners(imgR, boardSize, cornersR, cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); if (foundL && foundR) { cv::cornerSubPix(imgL, cornersL, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.001)); cv::cornerSubPix(imgR, cornersR, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.001)); imgPointsL.push_back(cornersL); imgPointsR.push_back(cornersR); objPoints.push_back(obj); } } // 单目标定 cv::Mat M1, D1, M2, D2; cv::calibrateCamera(objPoints, imgPointsL, imgL.size(), M1, D1, cv::noArray(), cv::noArray()); cv::calibrateCamera(objPoints, imgPointsR, imgR.size(), M2, D2, cv::noArray(), cv::noArray()); // 立体标定 cv::Mat R, T, E, F; cv::stereoCalibrate(objPoints, imgPointsL, imgPointsR, M1, D1, M2, D2, imgL.size(), R, T, E, F, cv::CALIB_FIX_INTRINSIC); // 立体校正 cv::Mat R1, R2, P1, P2, Q; cv::stereoRectify(M1, D1, M2, D2, imgL.size(), R, T, R1, R2, P1, P2, Q);逻辑说明:findChessboardCorners的boardSize是内角点数,9x6 对应 10x7 的格子,这个搞反了角点检测直接失败。cornerSubPix的winSize(11,11)是搜索窗口,图像分辨率高的时候可以适当加大,但太大反而会把角点拉偏。stereoCalibrate里用了CALIB_FIX_INTRINSIC,意思是单目标定已经算好了内参,立体标定阶段不再优化内参,只算R和T。这个标志在标定图数量不多的时候很关键,放开内参一起优化容易过拟合。
参数说明:squareSize是棋盘格实际方格边长,单位毫米,这个值直接决定后面测距的物理尺度。源码里没写死,需要你按自己打印的棋盘格改。calibrateCamera返回的重投影误差要盯着看,一般控制在 0.5 像素以内算合格,超过 1 像素说明标定图质量有问题,后面视差图基本没法用。
2.3 MATLAB 在校正验证里的角色
压缩包里带了校正前左右相机图像.jpg、校正后左右相机图像.jpg、左相机校正图像.jpg、右相机校正图像.jpg,这些是 MATLAB 脚本跑出来的可视化结果。MATLAB 在这套流程里不参与实时测距,它的价值是快速验证标定参数对不对。常见做法是把stereoRectifyParams.txt里的R1、R2、P1、P2读进 MATLAB,用rectifyStereoImages或者手动做映射,看校正后左右图的极线是不是水平对齐。如果校正后左右图的对应点还在同一行上对不齐,说明标定参数有问题,这时候回去查标定图,比在 C++ 里盲调效率高得多。
MATLAB 验证还有一个好处:可以叠加棋盘格角点的重投影位置,直观看到哪个角点偏了。C++ 里只能看误差数值,MATLAB 能画出来。这套源码把 MATLAB 定位成离线验证工具,不掺和实时链路,这个分工是合理的。
3. BM 局部匹配出视差图:参数怎么调才不翻车
3.1 BM 算法的参数敏感点
BM(Block Matching)是 OpenCV 里最直接的立体匹配算法,速度快,但参数敏感。源码里用 BM 输出视差图,核心参数就那么几个,但每个都能让结果天差地别。
cv::Ptr<cv::StereoBM> bm = cv::StereoBM::create(16 * 5, 21); // numDisparities 必须是 16 的整数倍,源码用 80 // blockSize 必须是奇数,源码用 21 bm->setPreFilterCap(31); // 预处理滤波截断值,默认 31 bm->setBlockSize(21); // 匹配块大小,奇数,越大越平滑但细节丢失 bm->setMinDisparity(0); // 最小视差,一般从 0 开始 bm->setNumDisparities(80); // 视差搜索范围,16 的倍数 bm->setTextureThreshold(10); // 纹理阈值,低于此值的区域不计算视差 bm->setUniquenessRatio(15); // 唯一性比率,过滤误匹配 bm->setSpeckleWindowSize(100); // 斑点窗口大小,过滤小连通区域 bm->setSpeckleRange(32); // 斑点范围 bm->setDisp12MaxDiff(1); // 左右视差一致性检查最大差值 cv::Mat disp, disp8; bm->compute(imgL_rect, imgR_rect, disp); disp.convertTo(disp8, CV_8U, 255 / (16 * 5)); // 归一化到 0-255 显示逻辑说明:numDisparities决定搜索范围,设小了近处物体视差超出范围会丢,设大了远处匹配噪声变多。源码用 80,对应大概能测到 1 米以内的场景,具体要看基线距离和焦距。blockSize越大,视差图越平滑但边缘越糊,21 是个偏大的值,适合纹理弱的场景,纹理丰富的话可以降到 11 或 15。textureThreshold是很多人忽略的参数,纹理弱的区域(比如白墙)算出来的视差全是错的,设个阈值直接跳过这些区域,比后面滤波管用。
参数说明:uniquenessRatio控制唯一性检查,值越大过滤越狠,误匹配少但有效视差也少,15 是常用起点。speckleWindowSize和speckleRange是一对,用来去掉视差图里的小斑点,窗口设 100 左右能去掉大部分孤立噪点。disp12MaxDiff做左右一致性检查,设 1 表示左右视差差超过 1 像素就标记为无效,这个对去除遮挡区域的误匹配很有效。
3.2 从视差图到深度:Q 矩阵和 reprojectImageTo3D
视差图出来只是中间结果,要得到真实距离还得用reprojectImageTo3D配合Q矩阵。
cv::Mat xyz; cv::reprojectImageTo3D(disp, xyz, Q, true); // xyz 是三通道浮点图,每个像素存 (X, Y, Z),Z 就是深度 // 取图像中心点的深度值 cv::Point center(disp.cols / 2, disp.rows / 2); cv::Vec3f point3D = xyz.at<cv::Vec3f>(center); float depth = point3D[2]; // 单位与标定时 squareSize 一致,通常是毫米逻辑说明:reprojectImageTo3D内部就是用Q矩阵做齐次变换,disp里无效视差(值为 0 或负)对应的三维点会是很大的值,取深度前要先判断视差是否有效。handleMissingValues参数设true会把无效视差对应的点设成很大的数,方便过滤。
参数说明:Q矩阵来自stereoRectify,是 4x4 矩阵。如果测出来的深度整体偏大或偏小,先检查squareSize和Q矩阵里的T分量是否一致。常见错误是标定用毫米,测距时忘了单位换算,结果差 1000 倍。
3.3 校正映射:initUndistortRectifyMap 和 remap
BM 匹配前必须先把左右图校正到极线水平对齐,这一步用initUndistortRectifyMap加remap。
cv::Mat mapLx, mapLy, mapRx, mapRy; cv::initUndistortRectifyMap(M1, D1, R1, P1, imgL.size(), CV_32FC1, mapLx, mapLy); cv::initUndistortRectifyMap(M2, D2, R2, P2, imgR.size(), CV_32FC1, mapRx, mapRy); cv::Mat imgL_rect, imgR_rect; cv::remap(imgL, imgL_rect, mapLx, mapLy, cv::INTER_LINEAR); cv::remap(imgR, imgR_rect, mapRx, mapRy, cv::INTER_LINEAR);逻辑说明:initUndistortRectifyMap把畸变校正和极线校正合并成一张映射表,remap按表采样。映射表只需要算一次,之后每帧直接remap,这是实时测距的标准做法。INTER_LINEAR是双线性插值,速度和质量平衡,追求质量可以用INTER_CUBIC但会慢不少。
参数说明:P1、P2是校正后的投影矩阵,来自stereoRectify。如果校正后图像边缘出现黑边,是正常的,因为校正会做旋转和裁剪。黑边区域不要参与匹配,可以在remap后加个掩码。
4. 避坑与排查:标定和匹配里最容易翻车的五件事
4.1 角点检测失败或角点顺序错乱
现象:findChessboardCorners返回 false,或者返回 true 但角点顺序跟世界坐标对不上,标定误差巨大。
原因:棋盘格打印不平整、光照不均、棋盘格在图像里太小或太偏,都会导致角点检测失败。角点顺序错乱通常是棋盘格被部分遮挡或者图像畸变太严重。
解决:标定图里棋盘格要占图像面积的三分之一以上,尽量放在图像中心区域。打印的棋盘格贴在硬板上,别用软纸。如果还是检测不到,换cv::CALIB_CB_FAST_CHECK标志先快速筛一遍,或者手动点角点。角点顺序问题可以在检测后画出来看一眼,drawChessboardCorners能直观看到顺序对不对。
4.2 重投影误差大但不知道哪张图有问题
现象:calibrateCamera返回的总体误差 1.5 像素,但不知道是哪几张标定图拖后腿。
原因:标定图里混了几张角点检测不准的,总体误差被拉高。
解决:calibrateCamera的perViewErrors输出参数会返回每张图的误差,逐张看,误差明显偏大的那张删掉重新标定。这套源码的标定图有 13 组,删掉一两组不影响。MATLAB 里也可以把每张图的重投影角点画出来对比,偏得明显的直接剔除。
4.3 视差图大面积黑色或全是噪点
现象:BM 输出的视差图要么大片黑色(无视差),要么全是雪花噪点。
原因:大面积黑色通常是numDisparities设小了,近处物体视差超出搜索范围;或者textureThreshold设太高,纹理弱的区域全被跳过。全是噪点一般是blockSize太小加上uniquenessRatio太低,误匹配没过滤掉。
解决:先调numDisparities,从 64 开始往上加,看黑色区域是否减少。然后降textureThreshold到 5 左右,让更多区域参与匹配。噪点多就把blockSize加到 21 以上,uniquenessRatio提到 15 到 20,speckleWindowSize设 100 以上。这几个参数要联动调,单独改一个往往顾此失彼。
4.4 测距结果整体偏大或偏小
现象:视差图看着正常,但reprojectImageTo3D出来的深度跟实际距离差一个固定倍数。
原因:squareSize和Q矩阵里的平移分量单位不一致,或者标定时棋盘格实际尺寸填错了。
解决:确认标定时squareSize用的单位,如果是毫米,Q矩阵里的T也是毫米,深度就是毫米。如果标定时填的是厘米但实际按毫米算,结果差 10 倍。重新标定前拿卡尺量一下棋盘格实际边长,别信打印时标注的尺寸,打印机缩放很常见。
4.5 VS2019 编译报 OpenCV 链接错误
现象:VS2019 里编译报LNK2019未解析的外部符号,全是 OpenCV 函数。
原因:OpenCV 库路径没配全,或者 Debug/Release 配置混了。VS2019 的 OpenCV 配置要分 Debug 和 Release 两套,opencv_world4xxd.lib是 Debug 版,opencv_world4xx.lib是 Release 版,混用就报链接错误。
解决:在项目属性里检查VC++ 目录的包含目录和库目录,Debug 配置链接带d后缀的 lib,Release 配置链接不带d的。环境变量Path里也要加上 OpenCV 的bin目录,否则运行时报找不到 dll。这套源码的 sln 里已经配好了,但换机器后 OpenCV 安装路径变了,要重新改属性表。
5. 进阶技巧:用 MATLAB 反推标定质量与视差调参的联动方法
标定和匹配调参最头疼的是「改了参数不知道哪步出了问题」。我一般会先用 MATLAB 把标定质量量化,再回到 C++ 调匹配参数,这样每一步都有依据。
具体做法是:在 MATLAB 里读calibrationresults_L.txt和calibrationresults_R.txt,用cameraParameters对象重建相机模型,然后把标定图重新投影一遍,计算每张图的重投影误差。MATLAB 的estimateCameraParameters能直接输出ReprojectionErrors,比 C++ 里手动算方便。误差超过 0.5 像素的图标记出来,回 C++ 里剔除后重新标定。
标定质量确认后,视差调参也有个联动方法:在 MATLAB 里对同一组校正后图像跑disparitySGM或者disparityBM,快速试不同DisparityRange和BlockSize,找到合适的参数区间,再把这些参数映射到 C++ 的 BM 设置。MATLAB 调参不用编译,改个数字就能看结果,比在 VS2019 里反复编译快得多。参数区间确定后,C++ 里按这个区间微调,基本一两轮就能出可用的视差图。
这里有个参数对照表,是我自己调参时整理的:
| MATLAB 参数 | OpenCV BM 对应参数 | 调整方向 |
|---|---|---|
| DisparityRange | numDisparities | 近处丢视差就加大,16 的倍数 |
| BlockSize | blockSize | 噪点多加大,边缘糊减小,奇数 |
| UniquenessThreshold | uniquenessRatio | 误匹配多加大,有效视差少减小 |
| TextureThreshold | textureThreshold | 弱纹理区域误匹配多就加大 |
| ContrastThreshold | preFilterCap | 光照不均时调整预处理截断 |
还有一点,MATLAB 的rectifyStereoImages和 OpenCV 的initUndistortRectifyMap结果应该一致,如果两边校正后图像对不齐,说明R1、R2、P1、P2的读取或转置出了问题。常见错误是 MATLAB 读文本时行列读反了,R1是 3x3,转置后校正结果完全不对。我一般会在 MATLAB 里把R1打印出来跟文本文件逐元素对一遍,确认没读错再往下走。
从那以后我每次拿到新的双目标定数据,都强制先用 MATLAB 把重投影误差和校正对齐过一遍,确认标定没问题再进 C++ 调匹配。这个习惯帮我省了大量在 VS2019 里反复编译排查的时间。希望帮到你。
本文还有配套的精品资源,点击获取