
简介面向计算机视觉入门与进阶开发者的OpenCV双目测距系统资源围绕立体视觉核心技术系统梳理了双目测距的技术原理与实现路线涵盖双摄像头同步采集、特征点提取与匹配SIFT、SURF、ORB、立体匹配获取视差、基础矩阵与单应性矩阵求解、三角测量、深度图重建及目标距离计算等关键环节既有理论铺垫也有工程实现思路。资源以RAR压缩包形式打包整体约80.48MB便于集中下载与离线学习。内容基于Ubuntu环境下的Qt与OpenCV进行整体设计覆盖了从环境配置、项目模块划分到多线程性能优化的完整开发流程对机器人导航、自动驾驶、工业检测等真实应用场景有直接参考价值。已有737人浏览学习适合希望动手构建双目视觉测距系统的读者参考借鉴。通过该项目可深入理解双目立体视觉的计算链路与常见工程化处理手法有效降低独立开发时的排错与试错成本。 做双目测距项目时最让人焦躁的往往不是核心算法有多深奥而是从“听说过视差”到“真机实测能准”中间隔着标定、校正、参数调优、环境配置一整串泥坑。我去年给移动机器人做近距离避障就选择了OpenCV 双目相机这套方案前后折腾了三周最后把测距误差稳定在5米内2%左右。这篇就基于“基于OpenCV的双目测距系统”这个项目把从原理到落地会踩的坑和能直接抄的代码整理出来。适合刚入手双目测距、以及在OpenCV里做过图像处理但没搞过3D重建的人参考。1. 双目测距在做什么原理与优势1.1 从“两只眼睛”到三角测量双目测距的核心说穿了就是三角测量。人的两只眼睛看向同一个物体时因为位置有左右偏移学名叫基线距离B物体在左右视网膜上的位置存在差异这个差异就是视差d。物体越近视差越大越远视差越小。数学关系用相似三角形推导深度Z f × B / d其中f是焦距像素单位B是左右相机光心的距离d是同一个点在左右图像上的像素横坐标之差。OpenCV在这个系统里的角色很明确它提供了从相机标定、图像畸变校正、极线校正到视差图计算BM、SGBM等算法以及重投影到三维空间的完整工具链。换句话说我不想自己造算法轮子OpenCV帮我把“从两张图到深度图”的每一段路都铺好了我要做的是把每一段路的参数调对。1.2 为什么不用单目或深度相机做测距有很多技术路线单目相机加目标检测也可以估计距离但本质上靠先验尺寸或地面平面假设换个场景就可能翻车。结构光或ToF深度相机在室内短距离效果好但室外强光下容易失效而且成本也高。双目相机是被动视觉只要环境有纹理就能算室内外都能用硬件成本可能只有两个普通USB摄像头。当然双目也有自己的短板对无纹理的纯色墙、重复纹理的格子布料视差匹配会失效对光照变化敏感基线大小直接限制有效测量距离。搞清楚这些边界才能设计出能用的系统而不是纸上谈兵。2. 搭建系统前必须先想清楚的四件事2.1 相机选型与基线设计相机选型不是越贵越好而是要和测量距离匹配。我当时的机器人需要在0.5到5米范围避障所以基线选了6厘米左右这个宽度在短距离内能提供足够视差同时减少近处物体的遮挡。如果你要测50米外的目标基线可能要拉大到几十厘米甚至更宽否则视差会小到亚像素级精度惨不忍睹。分辨率也很关键。同样的光学配置分辨率越高单像素对应的视差精度越高但计算量也成倍上涨。我当时用的720P摄像头标定后效果已经不错因为OpenCV的SGBM算法在1080P下实时性会吃紧。另外建议尽量选同型号、同一批次、固定焦距的镜头两个镜头焦距差太大会让后面的标定和校正非常痛苦。2.2 OpenCV环境搭建与语言选择PythonOpenCV是开发调试的最快路径但性能瓶颈明显。我用的标准流程是先在Python里跑通标定和参数验证再把最终视差计算部分用C重写嵌入项目。如果你只是做实验Python完全够用如果是产品化建议直接上C。安装方面Python环境直接pip install opencv-python opencv-contrib-python注意contrib版本里才有后续要用的SGBM、WLS滤波器等模块。C环境我试过在VS里配置最省事的方式是用vcpkg安装opencv然后链接必要的库。别自己从源码编除非你要定制某些模块否则纯粹浪费时间。注意Python和OpenCV版本之间偶尔有ABI兼容问题特别是opencv-contrib-python和opencv-python混装时可能出现module cv2 has no attribute StereoSGBM_create这类报错。解决办法是卸载后统一重装同一个版本。3. 棋盘格标定精度从源头决定3.1 标定为什么绕不过去很多人想跳过标定直接跑视差代码结果出来的深度图一片噪声。原因很简单任何镜头都有畸变左右相机光轴也不可能完全平行如果直接拿原始图像算视差对应点根本不在同一水平线上SGBM会匹配到一堆错误点。标定的目标是求出每个相机的内参焦距、主点、畸变系数和两个相机之间的外参旋转矩阵R、平移向量T然后通过立体校正让左右图像达到“数学上的理想双目”状态。我把标定比作给系统打地基地基歪了后面盖多少层算法都摇摇晃晃。上面热搜词里有“opencv棋盘格标定的c代码”说明这是初学者普遍卡壳的地方。其实OpenCV提供findChessboardCorners和calibrateCamera流程固定关键是采集足够多有效的标定图片。3.2 从单目标定到立体标定的流程我的标定流程分三步每一步都有筛选标准采集10到20对左右棋盘格图片必须是左右相机同时拍摄同一块棋盘在不同姿态下的照片。棋盘格我用A4纸打印贴在硬纸板上格数选9×6或7×10方格边长20毫米。采集时让棋盘尽量覆盖画面各个区域倾斜角度从0度到45度都要有尽量避免纯正面对准镜头。分别用cv2.calibrateCamera对左、右相机做单目标定得到各自的内参矩阵和畸变系数。这里要留意重投影误差如果误差大于0.2像素建议剔除那几张图片重新标定。用cv2.stereoCalibrate同时传入左右相机的内参、畸变系数和对应角点求两个相机之间的R和T。注意stereoCalibrate的flags里可以设置cv2.CALIB_FIX_INTRINSIC意思是只优化外参这样更稳定。下面是我整理出的代码骨架采集和角点检测部分都做了注释import cv2 import numpy as np # 棋盘格参数 pattern_size (9, 6) square_size 0.020 # 20mm # 准备棋盘格世界坐标单位米 objp np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size obj_points [] # 世界坐标系中的棋盘格点 img_points_l [] # 左图角点 img_points_r [] # 右图角点 # 假设已经读取左右图对 image_l, image_r gray_l cv2.cvtColor(image_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(image_r, cv2.COLOR_BGR2GRAY) ret_l, corners_l cv2.findChessboardCorners(gray_l, pattern_size, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, pattern_size, None) if ret_l and ret_r: # 亚像素细化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_l cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points_l.append(corners_l) img_points_r.append(corners_r) # 单目标定 ret_l, mtx_l, dist_l, _, _ cv2.calibrateCamera(obj_points, img_points_l, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera(obj_points, img_points_r, gray_r.shape[::-1], None, None) # 立体标定 flags cv2.CALIB_FIX_INTRINSIC ret_s, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], flagsflags, criteria(cv2.TERM_CRITERIA_MAX_ITER cv2.TERM_CRITERIA_EPS, 100, 1e-5))单目标定里我习惯先把两张图的畸变系数打印出来看看是否量级正常如果畸变系数大到离谱多半是角点检测失败或者棋盘格照片数量不够。3.3 极线校正的意义与验证标定完外参后还要做立体校正。OpenCV提供了cv2.stereoRectify和cv2.initUndistortRectifyMapcv2.remap最终目的是让左右图像同一行像素严格对齐这样计算视差时只需要在同一行搜索匹配点从二维问题降成一维问题速度和准确率都大幅提升。验证校正效果最直观的方法是把左右图并排画出来再用cv2.drawMatches或者直接画水平线观察同一特征点是否落在同一行上。我当时的验证方法是取30个不同的特征点打印左右图对应点的y坐标差值要求绝对值小于1像素。如果偏差很大说明标定参数有问题或者双目标定用的图片质量不够。stereoRectify还会输出一个重投影矩阵Q后面从视差图计算三维坐标时直接用它。下面这行是常用调用R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, alpha0, newImageSize(0, 0))alpha参数我建议设0它会尽可能裁剪黑色无效区域输出的图像更干净代价是有效视野略微缩小。如果设1则会保留全部原始像素但边缘会出现大片黑色变形区域视差计算时还要做ROI裁剪。4. 视差计算与深度映射核心代码实战4.1 SGBM参数设置与视差图后处理OpenCV里最常用的稠密匹配算法是SGBM半全局块匹配它在精度和速度之间比较平衡。BM更快但精度差适合实时性要求极高的简单场景。SGBM最关键的参数是numDisparities、blockSize、P1、P2。numDisparities表示允许搜索的最大视差范围必须是16的倍数。设小了会在大视差区域直接出现黑斑设大了匹配歧义增加算力也上去。我用720P分辨率、基线6cm、最近距离0.5m时算出来的最大视差大约是60像素所以设成64比较稳。blockSize是匹配窗口大小必须为奇数。我常用blockSize5或7窗口太小纹理匹配噪声大窗口太大在物体边缘处容易把前景和背景黏在一起。P1和P2是惩罚系数控制相邻像素视差变化的平滑度。手感上P1取8 * 通道数 * blockSize^2P2取4 * P1再根据噪声情况微调。还有uniquenessRatio用于过滤相似匹配点一般设5到15设太高会大片区域被认为是无效点。视差图直接用SGBM跑出来会有一堆噪声和空洞我后续做了几步处理先用cv2.medianBlur去孤立噪声再用置信度判断把不可靠点置为0最后用cv2.createDisparityWLSFilterWLS滤波结合左右一致性检查来滤波。WLS滤波属于optional module在opencv-contrib-python里原理是用左图作为引导图在平滑视差的同时尽量保留边缘轮廓效果比纯中值滤波好不少。核心代码大致是left_matcher cv2.StereoSGBM_create( minDisparity0, numDisparities64, blockSize7, P18 * 3 * 7 ** 2, P24 * 8 * 3 * 7 ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange2, preFilterCap63, modecv2.STEREO_SGBM_MODE_SGBM_3WAY, ) # 计算视差时注意返回的是int16需要除以16才是真实像素视差 disparity_left left_matcher.compute(rectified_l, rectified_r).astype(np.float32) / 16.04.2 从视差到真实距离的计算有了视差图后最简单的测距方式是用cv2.reprojectImageTo3D它会根据重投影矩阵Q把每个像素转换为三维坐标返回的三维坐标里z值就是该点到左相机光心的深度。points_3d cv2.reprojectImageTo3D(disparity_left, Q, handleMissingValuesTrue) # points_3d是HxWx3第三维是(x, y, z)单位为米 distance_map points_3d[:, :, 2]如果你想只测某个目标点的距离也可以不生成整个三维点云直接取左右图中对应的匹配点按视差和焦距计算Z f * B / d。其中f可以用左相机内参的fxB是立体标定得到的平移向量T中x分量的绝对值单位要与棋盘格输入单位一致。这里有个容易踩的坑T的单位取决于标定时棋盘格世界坐标的单位我之前用毫米作单位结果算出来的距离全大了1000倍后来统一成米才正常。我一般会在图上用鼠标点选感兴趣的点取该点周围5×5邻域的中值深度作为最终距离这样能抗个别错误匹配点的干扰。实际测试时1米、2米、3米、5米的真实距离与测量距离能对得上而且误差基本集中在目标物体边缘和反光区域。5. 实测数据与常见问题排查5.1 我踩过的坑与排查方法双目测距的坑远不止标定。下面这张表是我自己遇到并解决过的高频问题基本都可以直接对号入座。现象常见原因排查思路视差图大面积黑色numDisparities设置太小或目标太近增大numDisparities或增加基线距离目标边缘出现“毛边”blockSize过大窗口跨越了前景背景边界调小blockSize启用后处理WLS滤波每秒处理帧数极低图像分辨率太高且SGBM没有用3WAY模式降分辨率设置mode为STEREO_SGBM_MODE_SGBM_3WAY同一距离不同时刻测出的值跳动大光照变化导致匹配点偏移固定曝光时间整理视差可信度掩码低可信度点不输出左右图校正后边缘明显不同步双目标定图片太少或棋盘格未覆盖边缘重新采集15组图像特别是四角和中心区域另外强烈建议在代码里加一个自动化检查在计算完disparity_left后统计无效像素占比如果超过50%直接抛运行时警告说明当前场景不适合双目匹配而不是继续给用户一个伪距离结果。5.2 提高测距精度的小技巧最后分享几个我用真金白银换来的经验。测距精度首先依赖标定的准确性不要在窄走廊里随手拍几张棋盘就完事多花半小时覆盖各种姿态后面调试能省一整天。其次是曝光控制两个相机务必设置为手动曝光并且让左右曝光参数一致我用的是把两个摄像头都锁在同一曝光值肉眼看起来可能不是最佳亮度但视差匹配稳定性大幅提升。材质方面带反射的地砖、玻璃窗、亮面金属都会产生错误匹配目前这依然是双目方案的通路痛点实际项目里最好告诉使用方“哪些物体测不准”提前划清能力边界。根据我个人经验这个系统最理想的扩展方向是通过标定文件冻结好所有参数后把SGBM运算放到CUDA上720P分辨率实时跑到25帧以上很轻松。如果只是验证原理纯Python版本也足够用关键是先把标定质量做扎实再谈算法优化。本文还有配套的精品资源点击获取