ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

yolov5双目测距实战:标定与视差才是精度核心

yolov5双目测距实战:标定与视差才是精度核心 简介这是一套基于YOLOv5实现双目摄像头测距的完整Python项目面向计算机视觉和Python开发者适用于需要快速搭建双目视觉测距方案、进行算法验证或二次开发的学习与研究场景。压缩包共含131个文件包括23个py源码、21个yaml模型配置、15张jpg图片、pt预训练权重以及mp4视频演示等整体大小约46.75MB目录结构清晰便于按模块查阅和复用。目前已有189人浏览学习三个核心脚本camera_config.py、dis_count.py、video_remain.py分别负责摄像头参数配置、深度图与距离矩阵计算、主流程控制配合文档和视频可以完整理解双目标定、YOLOv5检测与测距结合的技术链路文档中还对双目摄像头的标定方法给出了实用建议视频演示则能直观看到实际运行效果。除代码外资料整理了作者在实操中对比不同标定方案、处理双设备号冲突等经验提示能帮助使用者避开开发中的常见问题同时pyc文件也可作为对照学习的参考。整体适合用作实战项目模板、课程设计参考或毕业设计素材拿到后即可对照运行、修改和扩展。1. yolov5双目测距高分项目检测只是幌子标定和视差才是核心“基于yolov5对双目摄像头进行测距”这一类项目拆开看就是两件事yolov5负责在画面里找到目标双目摄像头负责算出目标离相机多远最后用Python把这两条链路串成一个可运行的demo。压缩包里通常还附带文档说明、视频演示和训练好的模型权重所以很多第一次接触的人会以为拿到手就能复现视频里的测距效果。实际上这类项目的真实难度不在yolov5检测而在标定和视差参数这两块检测只是把画面里“该测哪里”交代清楚而已。适合的读者是有yolo检测经验、想扩展测距能力或者正准备做巡检机器人避障、车载预警、仓储测量的工程师。不适合的是指望解压即得工业级测量结果的人那类期望往往会在第一次看到跳动距离数字时破灭。2. 双目测距原理先立住视差才是距离yolov5只负责找目标2.1 为什么单目测距在这个项目里会翻车很多人在接触双目测距之前先用单个摄像头做过“单目测距”。网上流传的单目方案无外乎两种一种是找一个已知尺寸的目标比如车牌宽度用相似三角形换算距离另一种是假设目标在地平面上通过相机高度和俯仰角做几何约束把像素坐标投影到地面。这两种方案在固定场景里都能跑出数据但换场景就翻车。原因在于单目方案对先验极度敏感目标尺寸一旦改变相似三角形的换算系数立刻失效地平面约束一旦遇到坡度、颠簸误差会成倍放大。这也是为什么机器人或车载场景里测距最终会回到双目或深度相机——双目不需要目标尺寸先验它直接测量同一个物理点在左右两张图上的位置差这个差值在几何上是确定的。标题里的项目用yolov5做前端检测、双目做后端测距恰好绕开了单目的硬伤。有一点必须先说清楚yolov5在项目里的职责是定位目标框提供距离信息的是双目视差。换句话说就算把yolov5换成其他检测器测距主链路照样成立。如果项目文档把注意力全放在检测效果上反而要警惕测距部分是否只是拿SGBM跑了个最基础demo。2.2 视差测距公式里的三个参数焦距、基线、像素误差双目测距的核心公式是 Z f * B / d。三个变量f是相机焦距像素单位B是两个相机光心之间的距离基线建议统一用mmd是同一个匹配点在左右图中的像素差视差。这个公式成立的前提是左右图像完成了极线校正也就是说同名点只在同一行上只在横坐标上有偏移。举个例子焦距 f 600 像素基线 B 120mm某个目标点视差 d 40 像素算出来 Z 600*120/40 1800mm。公式看起来简单但它最坑的地方在于距离和视差成反比。视差每差1个像素近距离和远距离对应的绝对误差完全不同3米处视差大约24像素差1像素对应约4%的距离误差8米处视差大约9像素差1像素对应约11%的误差。这就是“远处天生不准”的根源因为硬件和算法能提供的亚像素匹配精度是有限的视差误差随距离增加被放得越来越大。所以拿到一个双目测距项目先别急着跑检测先找它的标定产出物左右相机内参、畸变系数、基线长度、焦距。没有标定文件后面所有距离数字都只能当参考不能当结论。2.3 深度图算法选型SGBM而不是BM理由不只是效果双目测距在实现层面可以选两类算法拿深度图BMBlock Matching和SGBMSemi-Global Block Matching。BM的做法是在一块小窗口内做穷举匹配速度快但噪声大生成的深度图经常像撒了一把盐。SGBM在匹配代价之外加入平滑惩罚项让视差在相邻像素之间保持连续边缘跳变也比BM干净得多。在OpenCV里对应的类是cv2.StereoSGBM_create参数虽然多默认值在室内场景基本能出效果。BM我只在需要跑实时且对精度要求极低的场合用其他情况一律SGBM。另外注意SGBM输入的是灰度图左右图的亮度差异太大会导致匹配失败率高这也是为什么后面会强调固定曝光而不是自动曝光。视觉算法之外还有一个容易忽略的点遮挡边缘和无纹理区域是视差空洞的重灾区。目标边框内部如果是一整片同色区域SGBM在那一块很容易匹配失败深度图里出现黑色空洞。后面测距时直接取空洞里的像素距离输出就是无效值。这也是为什么真正落地时不能只依赖一个中心点的视差而要取一个小邻域做统计。3. 相机标定与极线校正先把左右两张图拉齐再谈距离3.1 棋盘格拍摄一张都不能糊标定是整个项目里最花时间但最不能省的一步。先说棋盘格常见做法是用一张9x6内角点的标定板对应OpenCV的findChessboardCorners参数就是(9,6)。拍摄数量上左右各15到30张比较稳妥要求标定板在画面里占1/3以上覆盖中间、四周、倾斜等不同角度避免反光、模糊和运动拖影。双USB摄像头方案还要注意一个前提没有硬件同步的情况下软件同时read左右图只能做到“近似同时”。拍摄标定图片时保证相机和目标都静止这个近似就是够用的如果目标在动问题会显性化具体现象放到第5章排查。另外左右相机的自动曝光、自动白平衡最好关掉左右图亮度不一致会直接拉低SGBM的匹配成功率。这个细节在标定阶段容易被忽略但它对深度图质量的影响比想象中大得多。3.2 标定代码stereoCalibrate与stereoRectify怎么用标定脚本的核心逻辑是先找角点再计算内参和外参最后生成极线校正映射。下面是常见标定脚本里最核心的一段假设左右图已经分别放在left/和right/目录下单目内参K1、D1、K2、D2已经通过calibrateCamera得到。import cv2 import numpy as np import glob chessboard_size (9, 6) # 棋盘格内角点数 square_size 25.0 # 方格边长单位mm只影响尺度不影响畸变 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp * square_size objpoints, imgpoints_l, imgpoints_r [], [], [] for lf, rf in zip(sorted(glob.glob(left/*.jpg)), sorted(glob.glob(right/*.jpg))): img_l cv2.imread(lf) img_r cv2.imread(rf) gray_l cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) ret_l, corners_l cv2.findChessboardCorners(gray_l, chessboard_size, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, chessboard_size, None) if not (ret_l and ret_r): continue # 亚像素细化角点坐标更精确重投影误差能降一个台阶 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) objpoints.append(objp) imgpoints_l.append(corners_l) imgpoints_r.append(corners_r) # 双目标定固定已标定的内参只优化外参 ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, K1, D1, K2, D2, gray_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC) # 极线校正输出左右视图的映射矩阵和Q矩阵 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K1, D1, K2, D2, gray_l.shape[::-1], R, T, alpha0, flagscv2.CALIB_ZERO_DISPARITY) mapLx, mapLy cv2.initUndistortRectifyMap( K1, D1, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1) mapRx, mapRy cv2.initUndistortRectifyMap( K2, D2, R2, P2, gray_l.shape[::-1], cv2.CV_32FC1) # 保存映射和测距参数基线取平移向量T的x分量单位mm np.savez(calib_result.npz, mapLxmapLx, mapLymapLy, mapRxmapRx, mapRymapRy, baselineabs(T[0][0]), focalK1[0, 0])这里说两个重点。一是stereoCalibrate的flags用CALIB_FIX_INTRINSIC意思是内参不再参与优化只解左右相机之间的旋转R和平移T这样比放开全部参数稳定得多不容易收敛到错误解。二是T[0]是右相机光心在左相机坐标系下的x方向位移对于左右排列的双目相机这个值的绝对值就是基线已经足够用于测距公式。3.3 极线校正验证重投影误差与同一行上的目标标定跑完后不能直接进入测距先验证两件事。第一件是重投影误差也就是stereoCalibrate返回的ret值低于0.5像素说明标定质量不错如果超过1像素基本可以断定标定板照片里混入了模糊图或反光图需要重新挑图。第二件是极线校正效果直接把左右图remap之后并排显示用鼠标在一个特征点上看左右图的y坐标是否一致或者用cv2.line在两张图上画一条水平线观察墙面、柜子边缘是否在同一条线上。常见问题是校正后图幅两边出现黑色无效区域这是stereoRectify里alpha0裁剪的结果。无效区域会干扰检测框因为目标一旦进入黑色区坐标和视差全是错的。处理办法是后续计算时把ROI范围限制在有效区域或者把alpha调大一点但代价是图像边缘出现拉伸变形。项目里如果只在画面中心区域测距用默认的alpha0裁剪反而省事。验证通过后标定映射可以序列化保存运行时直接加载不需要每次都重新标定。接下来的检测与测距融合就是在这个校正后的坐标系里进行的。4. 把yolov5检测框和深度图融合测距代码怎么落地4.1 为什么用yolov5s而不是更大的模型项目标题只写了yolov5没写具体规模。做测距场景检测模型只是定位工具不是精度瓶颈我一般用yolov5s权重而不是yolov5x。原因很直接测距项目的实时性比检测精确度更敏感模型变大帧率下降测距精度不会有任何提升。如果目标就是行人、车辆这类常见类别yolov5自带的预训练权重直接可用如果目标比较偏才需要走“yolov5训练自己的数据集”的流程准备标注数据微调。部署时建议用conda建一个干净环境PyTorch、OpenCV、yolov5依赖全部装进环境里版本冲突这种事在yolov5环境配置里最常见混用系统Python环境翻车概率很高。顺带提醒一句yolov5的超参数比如置信度阈值、NMS IoU阈值调来调去只影响框的质量不影响距离精度。如果目标框已经稳定框住目标再去花时间调检测超参数对测距结果没有任何帮助。4.2 把校正、检测、SGBM串起来一份最小融合代码下面是一份最小可跑的融合代码省略了摄像头读取和结果可视化保留了核心链路加载标定结果 → 极线校正 → yolov5检测 → SGBM视差 → 换算距离。import cv2 import numpy as np import torch # 加载本地yolov5权重项目包里通常自带 model torch.hub.load(yolov5, custom, pathweights/yolov5s.pt, sourcelocal) model.conf 0.45 # 检测置信度阈值只影响框的过滤 model.iou 0.45 # NMS的IoU阈值 # 加载第3章标定的映射与测距参数 calib np.load(calib_result.npz) mapLx, mapLy calib[mapLx], calib[mapLy] mapRx, mapRy calib[mapRx], calib[mapRy] baseline float(calib[baseline]) # 单位mm focal float(calib[focal]) # 单位像素 # SGBM参数先用这组初始值跑通再按深度图调 stereo cv2.StereoSGBM_create( numDisparities96, # 必须是16的倍数 blockSize11, # 必须为奇数 P18 * 3 * 11 ** 2, # 平滑惩罚随blockSize缩放 P232 * 3 * 11 ** 2, disp12MaxDiff1, # 左右一致性检查最大允许差值 uniquenessRatio10, # 匹配唯一性阈值 speckleWindowSize100, # 去噪点窗口 speckleRange32 ) def measure_frame(left, right): # 第1步先做极线校正保证左右图行对齐 left_rect cv2.remap(left, mapLx, mapLy, cv2.INTER_LINEAR) right_rect cv2.remap(right, mapRx, mapRy, cv2.INTER_LINEAR) # 第2步在校正后的左图上检测目标坐标和视差图严格对应 results model(left_rect) if results.xyxy[0].numel() 0: return [] # 第3步SGBM计算视差注意除以16恢复真实像素差 disparity stereo.compute(left_rect, right_rect).astype(np.float32) / 16.0 disparity[disparity 0] 0 # 无效视差清零 output [] for det in results.xyxy[0].tolist(): x1, y1, x2, y2, conf, cls det cx int((x1 x2) / 2) cy int((y1 y2) / 2) # 取中心附近5x5窗口的视差均值比单像素稳定得多 roi disparity[max(0, cy-2):cy3, max(0, cx-2):cx3] valid roi[roi 0] if valid.size 0: output.append((cls, conf, x1, y1, x2, y2, -1)) continue d valid.mean() distance baseline * focal / d # Z f * B / d output.append((cls, conf, x1, y1, x2, y2, distance)) return output这段代码有三个设计点值得对照检查。第一先校正再检测而不是在原始图上检测再去视差图里取值因为校正后的图像坐标和视差图坐标才是严格对齐的混用坐标系是新手最容易踩的坑。第二SGBM的compute输出是int16需要除以16.0才是真实视差忘掉这一步距离会差16倍。第三中心点取5x5窗口均值而不是单像素单像素取到空洞或边缘噪点时距离会瞬间跳飞窗口能把这个风险摊薄。4.3 SGBM参数调节先看深度图再调数字SGBM参数是测距项目里最像“玄学”的部分其实有明确顺序可循。以下参数表按优先级排列前两个决定基础形态后三个负责擦屁股。参数含义对结果的影响起步建议numDisparities最大视差值必须是16的倍数决定最近测距范围值越大近距离覆盖越广96起步blockSize匹配窗口边长奇数窗口越大视差越平滑边缘越模糊9~15P1/P2视差平滑惩罚系数控制相邻像素视差跳变的容忍度83k^2和323k^2kblockSizeuniquenessRatio最优匹配与次优匹配的差异阈值越大误匹配越少有效像素也减少5~15speckleWindowSize噪点滤波窗口过滤小面积错误视差块50~200调试顺序我建议固定成一套先跑一帧把视差图用cv2.applyColorMap显示出来。视差图大片黑色空洞说明匹配失败多先调大numDisparities或减小blockSize视差图噪点细碎调大speckleWindowSize和uniquenessRatio物体边缘有横向拖影确认P1/P2是否偏小。只看最终距离数字调参很难判断问题出在哪一步视差图是唯一能看清问题位置的中间产物。这一条是血泪经验跳过它调参基本靠猜。4.4 帧率优化检测跳帧和ROI限定视差计算把yolov5s和SGBM串起来在普通笔记本上直接跑全分辨率帧率常常掉到10fps以下。做实时演示前先做两个改动。一是检测不需要每帧跑每3到5帧检测一次中间帧沿用上一次的目标框再在框附近做小范围搜索修正因为目标在相邻帧之间移动量很小这个近似足够安全。二是视差计算只算检测框内的ROISGBM计算量跟图像尺寸成正比把全图计算缩小到几十个像素见方的框内速度提升非常明显。如果做完这两步还是慢可以把输入图像缩小0.5倍后再跑。注意缩小图像后焦距也必须跟着除以2距离公式里的f要同步改否则算出来的距离全部偏大。最保险的做法是缩小前先把f从标定结果里读出来缩小后直接用f/2。5. 测距翻车现场5个常见问题与排查清单5.1 现象目标没动距离数字来回跳距离读数跳动的直接原因是取视差时取到了边缘或空洞区域。SGBM在遮挡边界、低纹理区域会产生错误匹配中心点5x5窗口如果跨到这种区域均值就会波动。更隐蔽的原因是左右图自动曝光不一致左边亮右边暗导致匹配代价整体偏移。解决分两步代码层面把5x5窗口里的大于0的像素做统计后去掉最大最小值再取均值能进一步压缩噪声硬件层面把左右相机的曝光和增益锁定成手动值保证两路图像亮度一致。这个坑在室内灯光下不明显到了户外自然光环境会被无限放大。5.2 现象近距离准远距离漂移这不是bug是物理限制。距离和视差成反比关系远处视差可能只有几个像素1个像素的量化误差对应10%以上的距离误差。普通USB双目基线只有6到12厘米远处根本形成不了有效的视差梯度。解决方法是先确认自己的测量范围预期。0.5到5米是常规USB双目的舒适区超过5米的数据只能定性不能定量。想测远唯一的硬件出路是加大基线比如把两个相机拉开到30厘米以上或者换更高分辨率的相机。代码层面可以加一个最大可信距离阈值超过这个距离输出不可信标记而不是继续给一个看起来精确的数字。5.3 现象目标在画面边缘时距离明显偏差边缘区域偏差大通常是镜头畸变在边缘最严重但标定照片没有覆盖到边缘导致的。很多人生成标定板图片时习惯把棋盘格放在画面中间拍边缘区域的畸变参数没有被约束校正后残留误差自然大。解决方法是重新拍摄标定板保证棋盘格移动到画面的四个角和四边位置让畸变模型在边缘也有数据支撑。如果不方便重拍可以在预处理时按stereoRectify返回的roi1、roi2做边缘裁剪把校正不充分的无效区域裁掉。精度需求不高的话裁剪是成本最低的方案。5.4 现象目标一旦动起来测距结果就乱套双USB摄像头没有硬件同步是这类方案的硬伤。软件同时read两个VideoCapture实际拿到的两帧可能在时间上差了十几毫秒甚至更多目标一移动左右图里拍到的不是同一个时刻的目标视差自然乱。这是用普通双USB方案做动态测距最大的坑。解决思路有三个。第一静态目标测距没问题动态场景优先换带硬件同步的双目模组这类模组左右图由同一块电路触发帧对齐是硬件保证的。第二如果只能用双USB软件上尽量做时间戳对齐用帧ID或系统时间戳匹配左右流差得远的帧直接丢弃。第三控制使用场景把目标约束在低速运动比如巡检机器人低速行进帧间位移控制在几个像素内误差会小很多。5.5 现象换了自己的检测模型测距结果没变化这个现象不是问题但值得单独说。测距误差的来源基本全在标定和视差环节检测模型只负责给出目标框框的稳定性不影响视差的计算公式。如果目标框已经稳定框住物体继续花时间提升检测精度对距离精度没有任何帮助这叫典型的资源错配。真正想提升距离精度回去重新标定把重投影误差压进0.5像素以内再把SGBM参数按第4章的顺序调一轮。想验证距离精度用卷尺在1米、2米、3米、5米处各测10次记录误差这个动作比调任何参数都更能说明问题。6. 从能测距到测得准几个值得动手的改进方向6.1 目标底部中心比几何中心更稳对地面上的目标检测框底部中心点的视差往往比几何中心更可靠因为底部更接近地面左右图之间的遮挡概率更低纹理也更丰富。代码里把取点从cy改成y2附近具体可以取y1加上框高度的0.7到0.85倍之间实测误差通常能小一截。这个改动十分钟就能做完但要注意目标脚部如果超出检测框取点要往回收一点避免取到背景地面。6.2 时间维度的中值滤波比均值更稳连续帧的距离输出做一个时间维度的滤波可以用最近5帧距离的中值代替当前帧中值滤波对野值不敏感目标被短暂遮挡或视差突然跳飞时不会出现剧烈的单帧抖动。均值滤波会拖尾中值滤波能把这些毛刺直接滤掉。实现上也简单维护一个固定长度的deque每帧append新距离取sorted后的中间值输出即可。6.3 验证习惯1米、3米、5米各测十次最后说一个我自己一直在用的验证习惯。每次标定完或调完参数拿卷尺量好1米、3米、5米三个位置每个位置测十次记录均值和标准差。标准差超过5%就说明当前配置不过关需要回炉。这个验证动作花不到二十分钟但能让你清楚知道这套系统在哪个距离区间可用、误差是多少。很多标着“高分”的项目演示视频效果确实顺滑但实际精度只有做这个验证才能暴露出来。希望这篇能帮你少走几步弯路也祝你能把距离精度做成自己心里有数、可随时复现的状态。本文还有配套的精品资源点击获取
返回列表