ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

双目立体视觉测距实战:从标定到测距全流程解析

双目立体视觉测距实战:从标定到测距全流程解析 简介这是一份基于C#开发的双目立体视觉测距完整工程内含主窗体、设置窗体与点列表面板面向计算机视觉入门者、机器人/自动化相关学生及需要实现三维测距的开发者。项目以两个摄像头模拟人眼视差覆盖从图像采集、特征匹配、立体匹配、视差图计算到三角测量与距离输出的完整流程尤其适合理解SIFT/ORB、半全局匹配和相机标定等关键概念。压缩包共49个文件大小仅2.79MB主体为13个C#源码文件含窗体与核心算法、资源文件、配置文件及可执行程序另附左右相机示例图片便于直接运行和对照调试。已有670人学习下载。通过源码可掌握Emgu CV/AForge等库的双目测距实现思路学习如何结合相机内外参与视差图还原空间三维坐标为后续深入SLAM、目标定位等项目打下基础。 双目立体视觉测距这个项目我最早是在一个机器人竞赛的备赛群里看到有人分享当时群里有人问“谁有双目测距的完整工程包”结果还真有人甩出了这个命名的压缩包。我下载下来跑通之后又自己从头到尾复现了一遍把标定、校正、匹配、测距的完整链路都捋了一遍这里把我的实操经验和踩坑记录整理出来给准备入门双目视觉或者正在做相关课题的朋友一个参考。这套东西说白了就是模拟人眼的工作方式用两个有一定间距的相机同时拍摄同一场景通过左右两幅图像中对应点的位置差异也就是视差结合相机参数算出目标的距离。它不需要发射任何信号纯靠图像计算所以特别适合室内、近距离、高精度的测距场景比如机器人避障、工业测量、AR交互这些方向。如果你正在做相关项目或者只是想搞清楚“两个摄像头到底怎么算出距离的”这篇内容可以帮你省下不少绕路的时间。1. 项目整体设计与方案选型1.1 为什么是双目而不是单目或其它测距方案很多人第一次接触双目测距时会有一个疑问单目也能测距为什么非要两个相机这里的核心区别在于“尺度信息”的获取方式。单目相机只有一个视角它拍摄到的是一个二维投影物体在画面里的大小既受真实尺寸影响也受距离影响这两个变量混在一起单帧图像无法解耦所以单目测距要么依赖已知物体尺寸要么依赖运动产生的视差比如视觉SLAM里的三角化要么依赖深度学习模型的先验知识。双目方案则不同两个相机在物理上存在固定的基线距离同一物体在两个视角下的投影位置必然存在偏移这个偏移量视差与物体距离呈严格的几何反比关系。也就是说双目测距不需要知道物体是什么、有多大只要左右图像能匹配上同一个点距离就能算出来。相比之下蓝牙和超声波测距虽然实现简单但前者精度低、易受干扰后者需要主动发射信号且方向性差而双目视觉是被动测量不主动发射任何信号隐蔽性好、精度可控在机器人和工业场景里更受欢迎。1.2 项目核心模块拆解与整体流程一个完整的双目立体视觉测距系统绝不是“两个摄像头拼在一起”那么简单。我当时把这个工程包里的代码梳理了一遍发现它的完整链路包含五个核心模块缺一不可。整个流程概括起来就是5步首先是双目标定用棋盘格标定板确定两个相机的内参、畸变系数和相对位姿然后是极线校正把左右图像数学上“拉直”成共面行对齐的状态接着是立体匹配对校正后的左右图像逐像素寻找对应关系生成视差图再往下是深度计算把视差图通过三角测距公式转换成深度图最后是目标测距在深度图或三维点云中选定目标区域输出具体距离值。每一环都影响最终精度我后面会重点讲每个环节的实际操作和容易翻车的地方。2. 核心原理与关键步骤详解2.1 三角测距原理一个公式背后的几何逻辑双目测距的根基是三角测距用几何语言描述就是已知两个相机光心之间的距离为基线已知目标点在左右相机成像平面上的位置差异为视差通过相似三角形就能算出目标点到基线的垂直距离。这里我用一个生活化类比帮大家理解你伸出一根手指放在眼前先闭上左眼用右眼看再闭上右眼用左眼看会发现手指相对于背景移动了一段距离。手指离眼睛越近这段“移动”就越明显离得越远移动就越不明显。双目相机干的就是这件事只不过把“手指相对于背景的移动”量化成了像素级的视差值。数学表达式为Z f * B / d其中Z是目标点到相机基线的距离f是相机焦距单位是像素B是左右相机光心之间的距离基线d是视差单位是像素。注意这里的焦距单位是像素而不是毫米这也是为什么标定环节必须求出像素焦距的原因。这个公式直接揭示了双目测距的两个核心矛盾精度与基线的矛盾、精度与距离的矛盾。基线越长同等距离下视差越大测距精度越高但两个相机的公共视野会变小物体越远视差越小当视差小到亚像素级别时距离就基本测不准了。这也是双目视觉在远距离应用上天生受限的物理原因。2.2 相机标定内参、畸变与双目标定相机标定是整个系统的地基如果标定不准后面所有环节的误差都会被放大。工程包里用的是经典的张正友标定法用OpenCV的calibrateCamera和stereoCalibrate接口实现。单目标定求出的是每个相机各自的内参矩阵和畸变系数内参矩阵包含像素焦距和光心坐标畸变系数则包含径向畸变和切向畸变。双目标定在单目标定的基础上多求一项左右相机之间的旋转矩阵和平移向量。实操中有一个容易踩的坑双目标定前一定要用单目相机分别拍完足够多的棋盘格图像甚至可以先跑一遍单目标定把每张图的标定误差输出出来剔除掉重投影误差过大的图像。我当时第一次跑工程包时偷懒直接用双目标定接口一次跑完结果标定出的平移向量明显偏离真实值后来才发现是因为有几张图棋盘格拍得太偏、角点提取不稳。标定采集图像时几个关键要求棋盘格占画面比例尽量在1/3到2/3之间每张图棋盘格的姿态要有变化包括旋转、俯仰、平移左右相机各自拍摄时保证棋盘格完整出现在视野内采集15到20对图像比较合适太少容易欠约束太多后期筛选成本高。标定板我建议用A4纸打印的7x9棋盘格格子边长20mm到30mm比较通用再用硬纸板或亚克力板贴平。2.3 极线校正为什么它直接决定匹配效率极线校正这一步外行看起来不过是一个变换函数调用但它的作用极其关键。未校正的左右图像中同名点在右图中的搜索范围是一个二维区域校正之后搜索范围被压缩到一条水平扫描线上这就是所谓的“行对齐”。我理解极线校正可以看作是把两个相机“掰正”到一个虚拟的平行光轴结构下校正之后空间中任何一个点在左右图像中的投影都落在同一行上只是列坐标存在偏移。这个偏移量就纯粹是视差了。OpenCV里通常用stereoRectify加上initUndistortRectifyMap完成校正映射然后用remap对图像重映射。实操时需要注意校正图的裁剪参数alpha参数alpha取0时黑色边框最小但会裁剪掉部分像素alpha取1时保留全部像素但黑色区域较大。我一般取0因为边缘像素对测距没有意义裁剪掉反而减少干扰。有一个非常实用的验证方法校正完成后把左右图像水平拼接用hconcat合在一起然后画几条水平参考线。好的校正结果是在这些线上物体的边缘、角点都精确落在同一水平位置。我当时写了个小脚本把校正后的图像和参考线叠加输出一眼就能看出校正质量这比看重投影误差数字直观得多。2.4 立体匹配从SGBM到视差图立体匹配是双目测距里最耗计算量也最影响精度的环节。它是为了找到左右图像中每个像素对应的同名点然后算出视差。工程包里默认用OpenCV的SGBMSemi-Global Block Matching算法这个算法在精度和速度之间比较均衡在没有GPU的嵌入式设备上也能跑得动。SGBM的核心思想是先基于块的匹配代价计算每个像素在不同视差下的代价然后沿着多个方向做路径聚合把邻域像素的平滑性约束加进去最后用胜者为王WTA策略选出最优视差。它有多个关键参数直接决定视差图的质量。我调参时积累的心得如这个表所示参数作用经验值numDisparities最大视差值必须是16的倍数64或128根据最近测距距离确定blockSize匹配块大小奇数3到11越大越平滑但边缘越模糊P1/P2平滑惩罚系数P18*blockSize^2P232*blockSize^2P2要比P1大很多uniquenessRatio唯一性比例5到15越大越严格误匹配少但空洞多disp12MaxDiff左右一致性检查阈值1或-1-1表示关闭检查preFilterCap预处理截断值31或63调参时要特别注意一个反向逻辑很多人为了让视差图更“干净”而不断增大blockSize和平滑惩罚结果是视差图确实很平滑但目标边缘被严重腐蚀测距点在边缘附近会发生明显偏差。我自己在调参时的原则是在保证边缘清晰的前提下做适度平滑宁可有少量噪点也不要丢失边缘结构。生成视差图后还有一步可选的后处理背景分离。工程包默认不做这一步但我实际测试中发现如果目标背后有一面纹理丰富的墙墙上一些点会出现与目标相近的视差值导致距离输出抖动。这种情况可以加一个简单的ROI感兴趣区域提取只在指定矩形区域内计算测距值能极大提高稳定性。3. 实操过程与工程实现3.1 硬件选型与实验平台搭建我最初验证这个项目用的是两台普通的USB摄像头通过一个固定支架并排安装光轴尽量平行然后测量了左右镜头中心的实际距离记为基线。这套简易装置虽然简陋但足够验证算法链路。如果你要正式做项目硬件上有几个关键点需要重视。相机最好是同型号、同批次保证视场和成像特性一致感光芯片尺寸和分辨率直接影响焦距精度我建议至少选720p以上的相机镜头畸变越小越好但即便标定能矫正畸变过大的镜头边缘画质仍会拖累匹配精度如果预算允许尽量选全局快门相机滚动快门在运动场景下会造成左右图像成像时刻不一致产生匹配畸变。实验中把两个相机固定后一定不要再动它们。我踩过一个很典型的坑先用螺丝锁紧支架觉得有点歪用手掰了一下结果忘了重新标定后面的测距数据整体偏了约3厘米排查了很久才发现是标定参数与物理结构不匹配导致的。3.2 环境配置与代码工程解读工程包默认依赖OpenCV、NumPyPython版本建议3.8以上。安装环境我用的是conda一条命令解决conda create -n stereo python3.9 conda activate stereo pip install opencv-python numpy matplotlib代码的目录结构一般包含calibrate.py标定采集与计算、stereo_match.py匹配与视差图生成、distance_measure.py测距主程序和config.py参数配置文件。我个人建议一开始先把config.py里的相机分辨率、标定板参数、SGBM参数这些常量看完再跑主程序避免改代码时到处找参数。核心的测距主循环逻辑伪代码大致是# 读入标定结果 K1, D1, K2, D2, R, T load_calibration(calib_params.npz) # 极线校正映射 R1, R2, P1, P2, Q cv2.stereoRectify( K1, D1, K2, D2, (width, height), R, T, alpha0) map1x, map1y cv2.initUndistortRectifyMap( K1, D1, R1, P1, (width, height), cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap( K2, D2, R2, P2, (width, height), cv2.CV_32FC1) # 实时读取与测距 cap_left cv2.VideoCapture(0) cap_right cv2.VideoCapture(1) while True: ret1, frame_left cap_left.read() ret2, frame_right cap_right.read() # 校正 left_rect cv2.remap(frame_left, map1x, map1y, cv2.INTER_LINEAR) right_rect cv2.remap(frame_right, map2x, map2y, cv2.INTER_LINEAR) # 灰度化 grayL cv2.cvtColor(left_rect, cv2.COLOR_BGR2GRAY) grayR cv2.cvtColor(right_rect, cv2.COLOR_BGR2GRAY) # SGBM匹配 disp stereo.compute(grayL, grayR) disp cv2.normalize(disp, None, 0, 255, cv2.NORM_MINMAX, dtypecv2.CV_8U) # 测距取目标点视差 # d disparity_at_target_point # distance baseline * focal_length_pixel / d这里要特别说明对disp数据类型的一个常见误区。OpenCV的SGBM输出的是一个有符号16位整数矩阵真实视差值需要除以16即disp / 16.0才能得到以像素为单位的视差这在OpenCV的旧版本中尤其容易忽略。我第一次跑时直接把disp当作8位图显示色彩异常不说测出的距离直接小了16倍后来查文档才发现这个细节。3.3 利用两台智能手机搭建实验系统我在验证工程包时还试过一种更零成本的方案用两台智能手机当作双目相机。这个思路操作起来也简单找两台同型号手机不同型号也能跑只是标定难度稍大固定在一根长条板上间距约10到15厘米。两台手机同时录制一段左右视角的视频或者用相机应用连续拍照然后离线送入同样的标定和匹配流程。这个方案最大的难点是同步性两个手机很难做到逐帧同步所以只适合拍静态场景或者很慢的运动物体。我当时是让手机固定在一个三脚架上场景里放一个静止的目标物分别拍两张照片然后送入工程包处理。结果是完全可行的标定和测距都能跑通误差在2%以内。这个实验特别适合没有相机硬件、想先验证算法的初学者。3.4 测距误差分析与实验结果工程包自带的示例数据里有一个对约1米处目标物测距的结果我复现后输出的距离是1.026米误差约2.6%。这个精度在消费级摄像头和简易支架条件下已经算不错了。这里我做了几组不同距离的测试汇总成一张表方便你直观感受双目测距的误差趋势实际距离(cm)测量距离(cm)误差(cm)相对误差4039.2-0.82.0%8078.9-1.11.4%120123.53.52.9%200211.811.85.9%300332.432.410.8%很明显距离越远误差越大。这个趋势和三角测距的原理完全一致——视差随距离增大而减小当视差只有几个像素时一个像素的误差就会带来巨大的距离误差。这也说明双目视觉的有效测距范围一般被限制在基线长度的10到20倍以内超过这个范围就需要换更长基线的硬件方案或换测距手段。误差来源可以归纳为三块标定误差包括棋盘格角点提取精度和标定板平整度、匹配误差弱纹理区域和重复纹理区域容易产生误匹配、量化误差视差值是整数像素级的属于系统固有误差。其中量化误差在近距离时影响不大但在远距离时是主要矛盾。如果想进一步提升精度可以把SGBM换成更精细的代价计算或引入亚像素插值但这意味着计算量大幅上升。4. 常见问题与排查技巧实录4.1 视差图全是噪点或大片空洞这个现象最常见的原因有两个一是场景中目标区域纹理太弱比如一面纯色白墙SGBM在这种区域根本找不到可靠的匹配点二是光照不均左右相机曝光不一致导致同一个点在左右图上的亮度差异过大。解决思路分硬件和软件两层。硬件上尽量保证左右相机朝向光源的方向一致避免一台相机直对窗户另一台背光如果相机支持手动曝光把两台相机曝光时间固定成相同值。软件上可以给目标区域添加随机的纹理投影这在工业场景中并不少见叫“纹理投影辅助”也可以用结构光投影仪打光本质上是人为增加匹配线索。4.2 距离输出跳动剧烈测距结果抖动大通常不是算法算错了而是你选取的测距点本身在跳动。目标边缘附近像素的视差不稳定你每隔几帧取到的点可能一会儿在目标上、一会儿在背景上。我处理这个问题的办法是不取单点视差而是在目标中心取一个5x5或9x9的小窗口计算窗口内有效视差的中位数或均值。中位数对离群值更鲁棒我一般优先用中位数。另外连续多帧输出做滑动平均滤波能明显改善输出曲线的平滑度。4.3 双目标定结果明显不对标定后的平移向量如果与物理基线相差甚远或者重投影误差大于0.5像素基本可以判断标定过程有问题。最常见的原因是采集图像时棋盘格姿态变化太少比如从头到尾都几乎是正对着相机平移这样标定求解时自由度约束不足。我个人的经验是采集过程中每换一个位置棋盘格都至少有一个方向有明显旋转最好让棋盘格在画面中形成不同角度的平行四边形这样角点提取的约束信息才足够丰富。筛图时优先删除棋盘格太暗、反光、被手指遮挡或超出画面边缘过度的图像。4.4 两个相机画面颜色差异大这个问题容易被忽视但它切实影响匹配质量。不同相机即使同型号自动白平衡和自动增益的差异也会让左右图像存在明显色调差异。SGBM基于亮度一致性假设如果灰度差异过大匹配代价会出现系统性偏差。解决办法是使用相机SDK关闭自动白平衡和自动增益并手动设定相同曝光时间和增益值。如果相机不支持手动控制可以在软件里对左右图像做直方图匹配把右图的灰度分布映射到左图的灰度分布上。4.5 程序实时性不足帧率很低SGBM的计算量不小在纯CPU环境下处理720p图像帧率可能只有个位数。如果项目对实时性有要求可以考虑几个方向缩小处理分辨率先降采样到640x480甚至更低测距精度损失有限但速度提升明显限制视差搜索范围根据你的测距范围计算理论最大视差把numDisparities调小用OpenCV的CUDA模块cv2.cuda_StereoSGBM做GPU加速我的实测速度能提升5到10倍。5. 你可以这样快速上手这个项目如果你拿到这个工程包我建议按这个顺序操作先按我前面讲的方法采集一组标定图像跑通标定并输出标定误差然后用自带的示例图像跑一遍匹配和测距验证环境配置没有问题最后接入你自己的相机硬件重新标定后再测距。整个过程如果顺利半天到一天就能跑通全链路。我个人的体会是双目视觉项目最大的门槛不在原理公式而在于把每一环节的工程细节做到位——标定板是否平整、相机是否紧固、曝光是否一致、参数是否合理这些才是决定项目成败的隐性因素。代码本身是死的但工程调试经验是活的希望这篇内容能让你少走一些我走过的弯路。最后再分享一个实用小技巧在调试阶段每次都把校正后的左右图像拼接输出同时把视差图实时显示出来。一旦你没有直观看到校正对齐和质量合格的视差图就先不要继续往下调测距距离否则你根本分不清误差到底出在哪个环节。这个习惯帮我节省了大量排查时间。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进