
简介这是一份基于OpenCV3.2的双目校正与双目测距C工程源码面向学习机器视觉的开发者演示了从相机标定、极线校正到BM立体匹配、视差图生成及深度测距的完整流程。压缩包共38个文件约9.43MB包含Visual Studio解决方案与C源代码、双目内外参数yml配置以及编译生成的exe运行程序和pdb、obj等调试中间文件便于直接研究或二次调试。已有1774人学习下载可作为双目视觉入门与算法验证的参考。项目通过棋盘格标定获取相机参数使用BM算法进行像素匹配并计算视差进一步转换为深度距离同时采用线性拟合对测距结果进行后处理使误差控制在几厘米以内适合需要开发测距、避障或三维感知功能的工程人员借鉴。1. 为什么把OpenCV3.2 BM当入门路线目标与整体设计先说结论如果你只是想快速验证双目相机能不能测出距离OpenCV3.2 BM算法仍然是一条性价比最高的路线。尽管现在OpenCV 4.x早就普及SGBM算法性能更好但很多工业相机SDK、老版本ROS包、嵌入式板卡自带的环境还是停留在3.2网上能搜到的中文资料和毕设代码也大量基于这个版本。我见过太多人一上来就追新结果在环境配置上耗掉一周最后发现手里的设备SDK根本不兼容。这篇文章要做的事情很明确把双目校正 双目测距 BM算法这条完整链路跑通并给出可以直接抄的C代码框架。具体解决三个问题两台相机拍出来的左右图像怎么对齐到同一平面让极线变成水平线BM算法怎么配置参数为什么有些人跑出来的视差图全是噪点视差图怎么换成真实距离以及换出来的距离为什么看着不怎么准。文章使用的环境是OpenCV 3.2.0 C相机为普通USB双目模组左右目固定基线出厂带标定参数文件。这套代码在Ubuntu 16.04/18.04和Windows 10 VS2015环境下都实测通过。适合正在做毕业设计、机器人避障、简单测距demo的读者不涉及深度学习。代码整体框架分四步读取标定参数 → 双目校正 → BM视差计算 → 三角测量。每一块我会给出参数选择和踩坑点最后附上完整的main函数。2. 环境与数据准备OpenCV 3.2下的三个前置条件2.1 标定参数文件怎么准备BM算法本身是不需要标定的它只是对两张校正后的图像做块匹配。但校正这一步必须有标定参数。常见的做法有两种用OpenCV自带的stereo_calib.cpp对棋盘格拍照标定生成stereoParams.xml买双目模组时厂家直接提供左右相机的内参、畸变系数、旋转矩阵、平移向量。无论哪种来源最终需要提取出这些东西左目内参矩阵M1和畸变系数D1右目内参矩阵M2和畸变系数D2左右目之间的旋转矩阵R和平移向量T图像分辨率校正前后要保持一致。一个很常见的坑很多厂家给的标定文件是MATLAB标定工具箱的格式或者只有R和T但没给R1、R2、P1、P2。这没关系stereoRectify函数会帮你算出来你只需要把原始参数读进来传给它就行。我之前接过一个项目对方说标定文件丢了但相机没动过结果折腾半天还是得重新标定。说实话如果你手里的双目模组是固定基线的出厂标定参数一般够用不需要自己重标但如果相机摔过、镜头拧动过一定要重新标否则校正出来的图像会出现明显的上下错位。2.2 图像采集用VideoCapture还是读图片OpenCV 3.2的VideoCapture对双目USB相机的支持不太好很多免驱的双目模组在Windows下会被识别成两个独立的摄像头设备/dev/video0和/dev/video1。这时你有两个选择如果你的相机驱动可以把左右图拼成一张图输出很多双目模组出厂就是左右拼接模式直接用VideoCapture读取单路视频流然后按宽度切成左右两张图。如果左右目是独立设备需要开两个VideoCapture分别读取。注意这时候左右图的曝光、白平衡可能不一致会导致BM匹配质量严重下降最好手动关闭自动曝光固定同一组参数。代码里我用的是左右拼接模式因为这种方式时序同步最好。独立双摄的软同步问题很麻烦不是这篇的重点但可以提一句如果坚持用双设备采集建议用硬触发或者至少保证帧率一致。// 读取左右拼接图并切分 cv::Mat frame, left, right; cap frame; if (frame.empty()) return; int width frame.cols / 2; left frame(cv::Rect(0, 0, width, frame.rows)).clone(); right frame(cv::Rect(width, 0, width, frame.rows)).clone();注意clone()不能省。cv::Rect切出来的是原图的ROI是浅拷贝后续的校正和匹配操作会修改数据如果不clone原图会被污染导致下一帧数据错乱。这个错误我犯过调试时发现第一帧正常第二帧开始图像撕裂排查了半小时才反应过来。2.3 3.2版本的一个典型坑stereoRectify的输出格式OpenCV 3.2里stereoRectify的Q矩阵输出是cv::Mat类型但到了4.x有些版本默认输出变了。如果你是从网上抄的代码要注意检查到底是stereoRectify(m1, d1, m2, d2, ...)返回Q还是通过引用参数输出Q。3.2的经典签名如下cv::stereoRectify(M1, D1, M2, D2, imageSize, R, T, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, 0, imageSize);最后两个参数是flags和alpha。其中alpha0表示对图像进行裁剪去掉黑色无匹配区域alpha1保留所有像素但会出现黑边。做BM匹配时建议alpha0否则黑边会被当成无效区域影响视差图边缘质量。另外一个容易忽略的点imageSize必须和实际读入的图像尺寸一致。如果你标定用的图像是1280x480但运行时切成左右图后每张是640x480校正映射表就会出错。最典型的症状是校正后的图像拉伸变形或者视差图出现大量横向条纹。3. BM算法的匹配原理与参数调节逻辑3.1 BM到底在做什么用窗口找相似块BMBlock Matching的核心思想其实特别朴素在左图上取一个像素点以它为中心选一个小窗口然后沿着极线在右图上扫同一行比较哪个位置的窗口和左图窗口最像那个位置和原点的水平偏移量就是视差。窗口怎么比较像不像BM原始实现用的是SAD绝对差值和即窗口内所有像素差的绝对值之和。OpenCV的BM是加了点优化的变种但本质还是这个思路。所以它的前提假设是左右图上同一个物理点的颜色灰度是接近的。这就是为什么光照不一致、曝光不一致会导致匹配崩掉。用生活化的方式理解假设你在左图看到一个带纹理的墙面BM就是拿着这块墙面的拼图块去右图同一行找一样的纹理找到后算一下两块拼图的位置差这个差就是视差。如果墙面是纯白色没有纹理那在右图一整行都能匹配上算法就懵了——这也是为什么白墙、天空、光滑地面都是BM的重灾区。3.2 参数逐项拆解从resolution到uniquenessRatioOpenCV 3.2的StereoBM创建方式有两种旧的CvStereoBMState和新的cv::StereoBM::create。3.2已经支持create推荐用它参数更清晰。cv::Ptrcv::StereoBM bm cv::StereoBM::create(numDisparities, blockSize);这里最关键的两个参数是numDisparities和blockSize但我实际调参的经验是后面那六个参数才是决定视差图质量的关键。挨个说numDisparities最大视差值必须是16的倍数。它决定了你测距的有效范围。视差和距离成反比numDisparities设得越大能测的近距离越小远距离测量精度越差。实测下来基线6cm的相机numDisparities64时有效测距范围大约0.5m~5m。如果想测更远单纯增大这个值没有意义因为远距离的视差可能只有1~2个像素像素级误差会导致距离误差爆炸。blockSize匹配窗口大小必须是奇数。窗口越小细节越丰富但噪点越多窗口越大视差图越平滑但边缘越糊。我一般在11~21之间选纹理丰富的场景用小窗口墙面等弱纹理场景用大窗口。minDisparity最小视差值。默认0表示匹配从右图最左侧开始。如果场景里都是近距离物体视差整体偏大可以设一个正值来缩小搜索范围、加快速度同时减少误匹配。uniquenessRatio匹配唯一性比率默认5~10。它表示最优匹配值比次优匹配值好多少才算有效。这个参数对噪点抑制非常有效。设得太低比如0你会看到视差图上一片雪花设得太高比如30好点都会被删掉视差图变得稀疏。我通常先设10再根据效果微调到5~15之间。speckleWindowSize和speckleRange这两个是去椒盐噪点的组合。speckleWindowSize表示视差连通区域的最小像素数小于这个值的连通块会被删掉speckleRange表示连通性判断时允许的视差差异。实践中speckleWindowSize100, speckleRange32的组合比较稳。textureThreshold纹理阈值。它过滤掉窗口内像素变化太小的区域也就是弱纹理区域直接不匹配。这个参数很实用但设太高会把远处有纹理的物体也过滤掉。我一般设10~20。bm-setNumDisparities(64); bm-setBlockSize(15); bm-setMinDisparity(0); bm-setUniquenessRatio(10); bm-setSpeckleWindowSize(100); bm-setSpeckleRange(32); bm-setTextureThreshold(15);3.3 BM和SGBM怎么选速度与质量的取舍很多人一开始就纠结用BM还是SGBM。我的看法是如果你的项目只是测距demoBM完全够用如果要做三维重建或精度要求高直接上SGBM。BM快在640x480分辨率下大约20~30ms一帧SGBM慢同样分辨率约80~150ms但视差图好得多尤其对弱纹理区域和倾斜表面更友好。这里有个为什么值得说清楚SGBM做半全局匹配会考虑相邻路径上的视差平滑性相当于加了周围像素视差应该差不多的约束BM完全没有这种约束每个点是独立匹配的。所以BM在纹理重复区域容易出现视差跳变SGBM会好很多。但SGBM参数更多调起来更费劲而且它对内存占用高一个量级。如果你是初学者先把BM跑通、把测距链路搭好再切SGBM只要替换一行代码// SGBM替换BM cv::Ptrcv::StereoSGBM sgbm cv::StereoSGBM::create( 0, 64, 9, 8 * 9 * 9, 32 * 9 * 9, 1, 63, 25, 15, 100, 32, 1);这一行里8 * 9 * 9是P132 * 9 * 9是P2分别对应视差平滑惩罚的弱约束和强约束。P2大约是P1的4倍是经验值。9是匹配窗口大小。参数含义和BM里的blockSize类似。注意P1、P2跟窗口大小有关系如果你改窗口这两个值也得跟着变。4. 从视差到距离三角测量的数学和代码实现4.1 为什么距离和视差是反比关系双目测距的数学基础是相似三角形。左右相机光心距离基线为b焦距为f某个物理点在左右图像上的像素坐标差视差为d则深度z b * f / d。这就是为什么物体越近视差越大测距越准物体越远视差趋近于0一点点像素误差都会被放大。实际操作中我们很少直接拿b和f的物理值计算而是用stereoRectify输出的Q矩阵。Q矩阵已经把焦距、基线、主点位置都整合进去了。视差图经过reprojectImageTo3D就能得到三维坐标。cv::Mat disparity, depth; bm-compute(leftRectified, rightRectified, disparity); // 归一化视差图以便显示 cv::Mat disparity8U; double minVal, maxVal; cv::minMaxLoc(disparity, minVal, maxVal); disparity.convertTo(disparity8U, CV_8U, 255.0 / (maxVal - minVal)); // 转换为三维点云 cv::Mat points3D; cv::reprojectImageTo3D(disparity, points3D, Q, true);注意reprojectImageTo3D的第四参数handleMissingValues一定要设为true它会自动处理视差图中-999这样的无效值否则生成的点云里全是黑洞洞的NaN点。另外disparity必须是CV_32F类型如果compute输出的不是浮点型需要convertTo转换。4.2 提取某个点的距离写个辅助函数在实际项目中你往往只关心某个目标物的距离没必要把整帧点云都存下来。我习惯写一个函数输入像素坐标输出该点的三维距离double getDistance(const cv::Mat points3D, int x, int y) { if (x 0 || y 0 || x points3D.cols || y points3D.rows) { return -1; } cv::Vec3f point points3D.atcv::Vec3f(y, x); double z point[2]; if (z 0 std::isfinite(z)) { return z; } return -1; }这里有个很多人会踩的坑points3D.atcv::Vec3f取出来的是什么坐标reprojectImageTo3D输出的points3D它的通道0、1、2分别对应x、y、z。但要注意单目相机坐标系下的x、y是相机坐标系的测量值不是当前像素离图像中心多少像素。所以在求目标距离时应该直接取z值而不是算sqrt(x^2 y^2 z^2)。为什么因为x和y在相机坐标系里代表的是目标的横纵位置单位毫米和深度z是正交关系。如果你算欧氏距离得到的其实是目标到相机光心的直线距离而视觉上说的测距通常指的是深度距离——即目标在相机正前方多远也就是z。二者在近距离大偏移时会差很多你需要先想清楚自己的需求。4.3 为什么我建议用鼠标点选而不是全图测距测距demo最实用的交互方式就是在显示窗口上鼠标点击目标物实时显示距离。全图测距看起来高大上实际上帧率会被拖累而且你很难验证每个点的准确性。所以我建议先做点选测距配合一个交叉十字线人工判断目标中心这样调试起来非常直观。实现鼠标回调的代码很简单static void onMouse(int event, int x, int y, int flags, void* userdata) { if (event cv::EVENT_LBUTTONDOWN) { std::paircv::Mat*, cv::Mat** data (std::paircv::Mat*, cv::Mat**)userdata; double dist getDistance(*data-first, x, y); double disp (*data-second).atfloat(y, x); printf(点(%d, %d) 视差%.2f 距离%.2f米\n, x, y, disp, dist / 1000.0); } }然后把points3D、disparity塞进userdata注册回调函数即可。注意回调函数里printf打印而不是用cv::imshow弹窗因为回调里弹窗容易导致事件冲突。5. 实测效果与调参实战一套可复现的验证流程5.1 用标定板验证校正质量双目校正做得对不对最直观的验证方法把左右校正图拼在一起显示观察棋盘格或者场景中的水平边缘是否在同一水平线上。cv::Mat pair; cv::hconcat(leftRectified, rightRectified, pair); cv::imshow(rectified pair, pair);然后拿鼠标在左图找一个特征点看右图同一行有没有对应点。如果校正正确特征点应该出现在同一行允许1~2像素误差。如果是歪的说明stereoRectify的参数不对或者标定数据本身有问题。一个更好用的调试技巧在拼合图上画一条水平直线左右两个画面的同一特征点如果都能落在直线上说明极线校正到位。5.2 从全是雪花到轮廓清晰的调参顺序我总结了一套BM调参的顺序按这个顺序来出问题的概率最小先固定numDisparities64, blockSize15其他参数用默认值跑一遍看视差图的整体轮廓如果视差图大范围雪花先调uniquenessRatio到10~15这会去掉一大半误匹配如果还有成片的孤立噪点调speckleWindowSize100, speckleRange32如果近距离物体的视差断裂严重适当增大blockSize到21如果远处区域全是空洞可能是textureThreshold设太高降到5~10试试。注意一个反直觉的点BM算法的视差图有时看起来很平滑但精确度一般看起来很多洞的视差图反而在可见区域更准确。不要追求视差图好看而要追求有效区域多且稳定。5.3 距离测量误差分析2米内的惊喜与2米外的惊吓我的双目模组是6cm基线、720p分辨率实测下来0.5m处误差约1~2%非常准1m处误差约3~5%可以接受2m处误差约8~12%勉强能用3m以上误差超过20%基本只能用来判断远/近不能用来做精确测距。这个规律是物理决定的不是算法问题。因为视差在远距离时只有几个像素像素量化误差本身就很大。想提高远距离精度唯一有效的办法是增大基线或者提高分辨率。基线翻倍相同距离下视差就翻倍精度随之提升。这也是为什么无人驾驶的双目相机基线都做得很宽。测距前一定要先做一件事测量目标与相机的真实距离用手或者卷尺都行然后把程序输出的距离和真实距离画成曲线。这样你能直观看到这个系统的有效测距范围和误差趋势。不要等到项目后期才发现3米外全是废数据。6. 完整代码框架OpenCV 3.2双目校正与BM测距main函数最后给出可以直接编译运行的完整代码框架。这段代码把上面所有模块串起来了读入标定文件、切分左右图、校正、BM匹配、点选测距一气呵成。代码默认你的双目相机输出的是左右拼接图。#include opencv2/opencv.hpp #include iostream #include cstdio static void onMouse(int event, int x, int y, int flags, void* userdata) { if (event ! cv::EVENT_LBUTTONDOWN) return; auto* data (std::paircv::Mat*, cv::Mat**)userdata; double dist 0; if (x 0 y 0 x >g stereo_bm.cpp -o stereo_bm pkg-config --cflags --libs opencvWindows的VS环境下记得把OpenCV 3.2的include目录、lib目录配置到项目属性里Debug/Release模式下的附加依赖项要区分opencv_world320d.lib和opencv_world320.lib。这个区分特别坑链接时报一堆无法解析的外部符号大多数情况就是Debug/Release lib选错了。6.2 运行效果与进一步优化代码跑起来后正常情况下你应该看到伪彩视差图里近距离物体偏红视差大远距离偏蓝视差小鼠标点选时控制台打印距离值。如果运行中遇到disparity全是黑色或者所有距离都是无穷大几乎可以确定是标定参数文件路径问题或左右切图尺寸问题。先打印第一帧的左右图看看切分是否正常再打印M1矩阵和Q矩阵确认非空。最后说一个优化方向如果对实时性有要求把remap输出的CV_32FC1映射表提前算好运行时只做内存拷贝不做重算把bm-compute的输入分辨率降一半可以换来近4倍的提速。但分辨率降太多视差精度也会跟着降需要根据实际场景做取舍。本文还有配套的精品资源点击获取