简介:面向计算机视觉入门与进阶开发者的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环境搭建与语言选择
Python+OpenCV是开发调试的最快路径,但性能瓶颈明显。我用的标准流程是:先在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], flags=flags, criteria=(cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 100, 1e-5))单目标定里我习惯先把两张图的畸变系数打印出来看看是否量级正常,如果畸变系数大到离谱,多半是角点检测失败或者棋盘格照片数量不够。
3.3 极线校正的意义与验证
标定完外参后,还要做立体校正。OpenCV提供了cv2.stereoRectify和cv2.initUndistortRectifyMap+cv2.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, alpha=0, 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是匹配窗口大小,必须为奇数。我常用blockSize=5或7,窗口太小纹理匹配噪声大,窗口太大在物体边缘处容易把前景和背景黏在一起。P1和P2是惩罚系数,控制相邻像素视差变化的平滑度。手感上,P1取8 * 通道数 * blockSize^2,P2取4 * P1,再根据噪声情况微调。还有uniquenessRatio用于过滤相似匹配点,一般设5到15,设太高会大片区域被认为是无效点。
视差图直接用SGBM跑出来会有一堆噪声和空洞,我后续做了几步处理:先用cv2.medianBlur去孤立噪声;再用置信度判断把不可靠点置为0;最后用cv2.createDisparityWLSFilter(WLS滤波)结合左右一致性检查来滤波。WLS滤波属于optional module,在opencv-contrib-python里,原理是用左图作为引导图,在平滑视差的同时尽量保留边缘轮廓,效果比纯中值滤波好不少。
核心代码大致是:
left_matcher = cv2.StereoSGBM_create( minDisparity=0, numDisparities=64, blockSize=7, P1=8 * 3 * 7 ** 2, P2=4 * 8 * 3 * 7 ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2, preFilterCap=63, mode=cv2.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, handleMissingValues=True) # points_3d是HxWx3,第三维是(x, y, z),单位为米 distance_map = points_3d[:, :, 2]如果你想只测某个目标点的距离,也可以不生成整个三维点云,直接取左右图中对应的匹配点,按视差和焦距计算:Z = f * B / d。其中f可以用左相机内参的fx,B是立体标定得到的平移向量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版本也足够用,关键是先把标定质量做扎实,再谈算法优化。
本文还有配套的精品资源,点击获取