YOLOv5+双目测距实战教程:从相机标定到三维坐标输出,附完整代码与调参技巧

📅 发布时间:2026/9/7 5:33:00
YOLOv5+双目测距实战教程:从相机标定到三维坐标输出,附完整代码与调参技巧
简介面向有一定深度学习与OpenCV基础的开发者基于YOLOv5和双目相机实现三维测距适用于自动驾驶、机器人导航、无人机避障等实时感知场景既能用于算法验证也可作为工程落地的起点。压缩包共141个文件以Python脚本和yaml配置为主另含pt权重、mp4演示视频、Dockerfile、Markdown说明等整体约40.38MB目录结构清晰便于按模块调用与二次开发。目前已有2417人学习下载。包内整合了数据预处理、模型训练、目标识别与视差深度计算等流程并附有预训练权重和相机参数配置有助于减少立体匹配误差、提升测距精度。通过源码研读与样例运行可快速掌握YOLOv5与双目测距的结合思路理解立体匹配、深度计算与目标框融合的关键环节为实际项目中的环境感知与空间定位提供可复用的工程参考。 最近在做一个移动平台的视觉测距小项目方案定的是“YOLOv5 双目相机”这条路。说实话两年前就有人这么玩但那时候的坑特别多——标定流程繁琐、YOLOv5版本迭代快、网上教程代码对不上号是常事。这阵子重新捡起来做了一版新的踩了不少坑也把整个流程理顺了从相机标定、模型训练到最终的测距输出都能跑通而且精度比旧版方案稳定不少。这篇东西我会把完整的实现思路、关键代码、参数计算过程和常见坑都写出来适合正在做毕设、或者搞机器人/自动驾驶相关项目的朋友参考。不需要你有太深的底子只要能跑通Python和基本的深度学习环境跟着做基本上能复现出一个能用的双目测距Demo。1. 整体方案设计为什么选了“YOLOv5双目”这条路1.1 双目测距解决的是什么问题先聊一个最基础的问题为什么测距要上双目而不是直接用单目相机加深度学习单目方案在自动驾驶里用得很多但它的本质是“从2D图像回归3D信息”这是个病态问题——没有几何约束模型只能靠数据里的先验去猜。比如同一个物体在图片里大小一样但实际距离可能差两倍单目模型全靠数据集里统计出来的“平均尺寸”去估换一个场景就容易翻车。双目方案不一样。它是基于三角测量原理纯粹靠两幅图像之间的视差来计算深度几何关系是确定的不依赖训练数据里的先验尺寸。这就意味着只要相机标定得准、匹配做得好测距精度是有保障的而且换场景、换目标物体都能保持稳定。我选的组合是YOLOv5负责在左目图像上检测目标输出目标的类别和2D边界框双目视觉负责算视差图再把视差图转换成深度图。两个模块各自独立、串联工作检测框告诉我们“目标在哪”深度图告诉我们“那个位置有多远”。1.2 为什么是YOLOv5而不是其他检测器目标检测这一层可选的方案其实挺多。YOLOv8、RT-DETR、甚至是传统视觉方案都有。但说实话对于“三维测距”这个任务来说YOLOv5仍然是最舒服的选择原因有三个。第一部署生态太成熟了。YOLOv5的ONNX导出、TensorRT加速、OpenCV DNN推理网上资料一大把遇到问题基本都能搜到解决方案。这在做项目的时候太重要了省下来的时间足够你多调几次参。第二检测精度和速度的平衡很好。v5s模型在GPU上能做到2-3ms一帧在树莓派5这种边缘设备上也能跑实时后面会讲具体优化方法。而测距任务对检测框的稳定性和召回率要求比较高v5在这方面的表现比很多轻量级网络要可靠。第三便于改造和源码级调试。YOLOv5的代码结构很简单直观全是PyTorch写的没有花里胡哨的抽象封装。你可以直接修改它的推理代码把检测结果和视差图对接。1.3 “新版本”方案改进了什么旧版方案的核心问题有三个一是没有统一坐标映射检测框坐标和深度图坐标经常错位二是没有针对小目标做优化距离稍远就漏检三是标定代码和YOLOv5的模型版本不匹配加载模型时出现各种兼容性报错。新版方案做了几个关键改进统一的坐标变换工具类把所有2D-3D坐标映射逻辑收拢到一起避免各处散落导致Bug引入图像校正后的ROI裁剪机制只对检测框所在区域计算视差省算力还能减少误匹配把YOLOv5模型升级到了最新的6.2版本也就是v7发布前的最后一个稳定版推理代码适配了新版API。2. 核心原理与前置准备2.1 双目测距原理视差是如何变成距离的在写代码之前原理必须吃透。双目测距的核心公式就一个depth (f * baseline) / disparity其中f是相机焦距单位是像素不是毫米baseline是两个相机光心之间的距离单位是毫米或米disparity是同一个空间点在左右两幅图像上的像素横坐标之差。这个公式做一下量纲验证你就能明白焦距单位是像素基线单位是毫米除以视差像素得到的深度单位就是毫米。很好理解对不对但这里有个容易踩的坑这个公式用的是经过立体校正后的理想状态。实际相机存在镜头畸变两个相机的光轴也不是完全平行的所以必须做标定和立体校正把左右图像“矫正”成严格的共面行对准状态。不校正的话视差计算出来的深度误差会大到你怀疑人生。2.2 相机标定误差的大半都出在这里三维测距项目的误差来源标定占了大概70%。标定做不好后面做再多优化都是白搭。标定需要做三件事单目内参标定每个相机各自的焦距、主点、畸变系数、双目外参标定两个相机之间的旋转矩阵和平移向量、立体校正计算左右视图的校正映射表。工具方面我用的是OpenCV的calibrateCamera和stereoCalibrate。标定板用棋盘格我用的是9x6的格子边长24mm。采集了大概30对左右不同角度、不同距离的图像对。说一个经验值标定图像的覆盖范围很重要。一定要包含画面边缘区域的图像因为畸变在边缘最明显。如果只在画面中心采集图像标定出来的畸变系数会很不准。2.3 YOLOv5环境配置与模型准备YOLOv5的环境配置其实挺简单关键就是版本要对齐。我的环境是Python 3.9PyTorch 2.0.1CUDA 11.8OpenCV 4.8克隆YOLOv5仓库后直接pip install -r requirements.txt就能装好大部分依赖。需要注意的一点是YOLOv5的requirements.txt里没有锁版本有些依赖装了最新版会报错。我实测下来numpy不能超过1.24matplotlib不能超过3.7否则跑训练或推理的时候会出现奇奇怪怪的警告甚至报错。模型这块如果只是做测距Demo直接用官方预训练权重yolov5s.pt就行它能在COCO数据集上检测80个类别常见的行人、车辆、桌椅都在里面。如果要做特定目标的检测比如检测某种零件或者动物那就需要训练自己的数据集这个我后面实操环节会说。3. 实操从标定到测距的完整流程3.1 第一步双目相机标定与校验这一节我直接给出可以跑的代码框架。我用的是OpenCV自带的标定流程核心步骤如下先采集标定板图像对。我的相机是两颗USB摄像头固定在一个支架上基线长度约60mm。采集代码很简单import cv2 cap_left cv2.VideoCapture(1) cap_right cv2.VideoCapture(2) # 棋盘格参数 pattern_size (9, 6) # 内角点数 square_size 0.024 # 格子边长单位米 frames [] for i in range(50): # 采集50对 ret_l, frame_l cap_left.read() ret_r, frame_r cap_right.read() if ret_l and ret_r: frames.append((frame_l, frame_r)) cv2.imshow(left, frame_l) cv2.imshow(right, frame_r) cv2.waitKey(500) cv2.destroyAllWindows()采集的时候记得把标定板放在画面的不同位置左上、右上、中心、近处、远处、倾斜一些角度。我一般一轮采集30~40张足够用了。然后是单目标定和双目标定。OpenCV有现成函数不再重复写API细节。重点说几个操作细节单目标定完成后注意看每个相机的重投影误差。我自己的标准是小于0.15像素才算合格。如果大于这个值通常是检测棋盘格角点的精度不够检查一下标定板图像是否清晰、有没有反光。双目标定要基于单目结果进行用stereoCalibrate优化两个相机之间的外参。标定之后做立体校正# 基于stereoRectify计算校正映射 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, (width, height), R, T, alpha0 ) map1_l, map2_l cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, (width, height), cv2.CV_32FC1 ) map1_r, map2_r cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, (width, height), cv2.CV_32FC1 )Q矩阵是重投影矩阵后面从视差图算三维坐标要用到它。校验标定是否成功的办法很直观把左右目图像校正后并排显示然后画几条水平线。如果左右图中同一个物体在两条水平线上基本对齐视差只在x方向y方向几乎为0说明标定质量没问题。3.2 第二步训练自己的目标检测模型如果你只需要检测常规物体人、车、杯子等官方预训练权重直接搞定。但如果你的检测目标和COCO类别差异很大就得自己训练。训练自己的模型分几步第一步准备数据集。用LabelImg或Labelme标注目标框保存为YOLO格式类别ID 归一化的中心坐标和宽高。我建议至少准备500张图每类目标不要低于200个标注实例否则模型学不到稳定的特征。第二步写数据配置文件。YOLOv5的数据集配置文件是YAML格式train: /path/to/train/images val: /path/to/val/images nc: 2 names: [person, car]第三步选预训练权重做迁移学习。不要从零开始训练用yolov5s.pt作为初始权重只训练自己的类别。强制迁移学习能大幅缩短训练时间而且收敛效果更好。python train.py --data mydata.yaml --weights yolov5s.pt --img 640 --epochs 100 --batch-size 16训练的时候注意两个超参数hyp[anchors]和hyp[lr0]。如果你的目标物体尺寸比较特殊比如特别大或者特别小建议先跑一遍自动锚框计算python train.py --data mydata.yaml --noautoanchor这个其实不要加直接跑就行YOLOv5会自动计算锚框。学习率一般默认0.01如果loss震荡严重可以降到0.005。3.3 第三步检测与测距的串联实现这是整个项目最核心的环节。检测和测距的串联逻辑上分四步用YOLOv5检测左目图像得到目标类别和边界框坐标对左目图像计算视差图用SGBM算法根据检测框在左图上的坐标在视差图上取对应区域的视差值用视差值和相机参数换算深度。先看视差图计算import numpy as np import cv2 def compute_disparity(img_l, img_r): # 转为灰度图 gray_l cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) # SGBM参数 stereo cv2.StereoSGBM_create( minDisparity0, numDisparities128, blockSize11, P18 * 3 * 11 ** 2, P232 * 3 * 11 ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32 ) # 计算视差 disparity stereo.compute(gray_l, gray_r).astype(np.float32) / 16.0 return disparity这几个SGBM参数是我的经验值每个场景可能需要微调。numDisparities越大能测的距离越远但计算量也越大。blockSize越大视差图越平滑但会损失边缘细节测量小目标的距离时容易把背景视差混进来。然后是YOLOv5推理import torch # 加载模型 model torch.hub.load(ultralytics/yolov5, custom, pathbest.pt, force_reloadTrue) model.conf 0.4 # 置信度阈值 model.iou 0.45 # NMS IoU阈值 def detect_objects(img): results model(img) dets results.xyxy[0].cpu().numpy() boxes [] for x1, y1, x2, y2, conf, cls in dets: boxes.append({ cls: int(cls), conf: float(conf), box: [int(x1), int(y1), int(x2), int(y2)] }) return boxes关键的一步来了怎么在视差图上取深度。这里我踩过一个大坑——如果直接取检测框中心点的视差值遇到目标中心区域纹理较弱比如纯色衣服、或者目标中心正好被遮挡时中心点的视差值会变成噪声测出来的距离忽远忽近。我试过多方案后最终采用了一个更稳定的策略取检测框下半部分区域的有效视差值的中位数。原因是目标底部通常更接近地面或支撑面这个区域的深度比较稳定而目标顶部比如人的头、车的顶部容易受背景干扰。中位数比平均值更抗噪即使区域里有几个极端的错误视差也不会对结果产生致命影响。def get_depth_from_box(disparity, box): x1, y1, x2, y2 box # 取检测框下半部分作为ROI roi disparity[y1 (y2-y1)//2 : y2, x1:x2] # 过滤无效视差0的值是无效值 valid roi[roi 0] if len(valid) 0: return None # 取中位数 median_disp np.median(valid) # 根据视差计算深度 # depth (f * baseline) / disparity # f是焦距像素baseline是基线米 depth (camera_params[f_pixel] * camera_params[baseline]) / median_disp return depth最后是主循环cap_l cv2.VideoCapture(1) cap_r cv2.VideoCapture(2) while True: ret_l, frame_l cap_l.read() ret_r, frame_r cap_r.read() # 校正 frame_l_rect cv2.remap(frame_l, map1_l, map2_l, cv2.INTER_LINEAR) frame_r_rect cv2.remap(frame_r, map1_r, map2_r, cv2.INTER_LINEAR) # 检测 boxes detect_objects(frame_l_rect) # 视差 disparity compute_disparity(frame_l_rect, frame_r_rect) # 测距 for bbox in boxes: depth get_depth_from_box(disparity, bbox[box]) if depth is not None: label f{names[bbox[cls]]} {depth:.2f}m cv2.rectangle(frame_l_rect, (bbox[box][0], bbox[box][1]), (bbox[box][2], bbox[box][3]), (0, 255, 0), 2) cv2.putText(frame_l_rect, label, (bbox[box][0], bbox[box][1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(Distance, frame_l_rect) if cv2.waitKey(1) 0xFF ord(q): break cap_l.release() cap_r.release() cv2.destroyAllWindows()到这里一个最基本的检测加测距管线就跑起来了。3.4 三维坐标输出不只是距离标题里写的是“三维测距”但严格来说上面那套流程只能算是“单点深度测量”也就是输出目标到相机的距离这是“一维”的信息。真正的三维测距要输出目标在相机坐标系下的三维坐标(x, y, z)。这一步要用到前面stereoRectify计算出来的Q矩阵。OpenCV提供了一条很方便的路径——reprojectImageTo3D函数可以把整张视差图直接转换为三维点云图def compute_3d_points(disparity, Q): # 输入视差图单位像素 # 输出三维点云图shape(H, W, 3)每个像素对应一个(x,y,z)坐标 # 注意disparity需要是float32而且负值会得到错误的3D坐标 disp disparity.copy() disp[disp 0] 0.1 # 防止除零 points_3d cv2.reprojectImageTo3D(disp, Q) return points_3d拿到三维点云后针对目标检测框坐标直接采样框内区域的三维坐标同样做中位数滤波def get_3d_from_box(points_3d, box): x1, y1, x2, y2 box roi points_3d[y1 (y2-y1)//2 : y2, x1:x2, :] # 过滤无效点深度为0或极大值 valid roi[(roi[:, :, 2] 0) (roi[:, :, 2] 50)] if len(valid) 0: return None # 分别取xyz的中位数 x np.median(valid[:, 0]) y np.median(valid[:, 1]) z np.median(valid[:, 2]) return x, y, z输出(x, y, z)之后你可以很方便地做很多扩展比如计算目标相对相机的偏航角、计算两个目标之间的相对距离、画三维散点图等等。根据我实测的结果在2米范围内这套方案测距误差能控制在1-3厘米左右5米范围内大约5-10厘米误差再远误差会快速增大这受限于相机基线长度和分辨率。60mm的基线对5米以上的目标来说视差太小了——这个物理限制没法靠算法完全解决要么加长基线要么换更高分辨率的相机。4. 常见问题与排查技巧实录4.1 标定相关的问题现象可能原因解决方法重投影误差大于0.3像素角点检测不准、标定板反光换哑光标定板重新采集图像校正后左右图水平线对不齐单目标定误差累积重新做单目标定确认无问题后再做立体标定标定结果每次跑都不一样采集的图像太少或覆盖不足保证30张以上覆盖画面边缘和近远距离这里有个小技巧标定板尽量买陶瓷材质的不要用打印纸贴在硬纸板上。打印纸和硬纸板的热胀冷缩系数不一样时间一长棋盘格就不是严格平面了标定精度直接受影响。4.2 测距结果抖动这是最常见的问题。我遇到过两种情况一种是固定目标不动测出来的距离在±5cm范围内随机跳动另一种是偶尔跳变到明显错误的值。第一种情况的主要原因是SGBM视差图本身有噪声。解决办法是给深度值加时序滤波——我用的是一阶低通滤波alpha 0.3 smoothed_depth alpha * current_depth (1 - alpha) * smoothed_depthalpha值越小曲线越平滑但反应越迟钝。实测0.3是个不错的平衡点。第二种情况跳变到错误值通常是ROI区域里有无效视差或遮挡边缘。除了用中位数滤波外还可以加一个“可信度校验”如果当前帧的深度和上一帧深度的差值超过一定阈值比如20%就用上一帧的值。这个逻辑对应对短暂遮挡特别有效。4.3 检测框漂移导致测距偏差YOLOv5在视频流中检测时检测框会有轻微抖动尤其是目标运动较快或光线变化时。检测框位置哪怕只有几个像素的偏移ROI区域就变了深度值也会跟着变。一个有效的解决办法是“检测框平滑”。我维护了一个简单的EMA平滑器对每个目标的检测框坐标做滑动平均def smooth_box(old_box, new_box, alpha0.4): return [int(alpha * n (1 - alpha) * o) for o, n in zip(old_box, new_box)]对多目标追踪场景建议给YOLOv5接一个目标追踪器比如DeepSORT或ByteTrack这样不仅检测框更稳还能给每个目标分配ID方便持续测距。4.4 性能优化边缘设备部署如果你要把这套方案部署到树莓派5或者Jetson上有几个必做的优化第一模型轻量化。把YOLOv5s换成YOLOv5n或者用--half开启FP16推理速度能提升一倍左右。第二SGBM很吃CPU可以缩小输入分辨率比如从1280x720降到640x480视差计算量能减少好几倍。第三在树莓派5上建议用OpenCV的C接口写推理和视差计算Python的GIL在多线程环境下会让性能折损严重。我在树莓派5上实测YOLOv5n 640x480分辨率 SGBM全流程能做到大约15-20FPS基本满足实时性要求。4.5 低纹理区域的视差空洞墙面、纯色地面这种低纹理区域SGBM根本匹配不到有效视差测出来的深度全是空洞。解决办法是开启WLS滤波# 用WLS滤波平滑视差图 right_matcher cv2.ximgproc.createRightMatcher(stereo) disparity_left stereo.compute(gray_l, gray_r) disparity_right right_matcher.compute(gray_r, gray_l) wls_filter cv2.ximgproc.createDisparityWLSFilter(stereo) wls_filter.setLambda(8000) wls_filter.setSigmaColor(1.5) filtered_disp wls_filter.filter(disparity_left, gray_l, disparity_rightdisparity_right)WLS滤波的效果立竿见影视差图会平滑很多空洞也会被周围的有效值填补。但注意它只是“看起来合理”填补出来的深度其实是插值的结果精度有限关键目标还是要有有效视差才行。4.6 YOLOv5推理时“no detections”的排查思路如果你发现自己训练的模型在某个场景下什么都检测不到先检查三件事。第一置信度阈值——conf设到0.4还漏检先降到0.1试一下看看是模型没检出还是置信度不高被过滤了。第二输入分辨率——YOLOv5默认用640x640推理如果你的目标很小建议把推理尺寸提高到1280小目标检测效果会明显改善。第三训练数据的过拟合问题——如果训练集里全是相近角度、相近距离的图像模型学到的特征就太局限了。我在做车辆检测的毕设项目时就遇到过这个问题训练集里全是正前方的车侧后方的车几乎全部漏检补齐数据分布后这个问题才解决。写在最后的一些心得做这个项目最大的体会是视觉测距的难点从来不在深度学习那一侧而在传统视觉的标定和匹配。YOLOv5再复杂也就是一个前向推理的事反而是相机标定、立体校正、SGBM参数调优这些“老古董”技术决定了你最终测距精度的上限。很多人一上来就盯着模型训练调参标定马马虎虎就过去最后测距结果差了又回头找模型的麻烦方向完全搞反了。如果你要在这个项目基础上继续扩展我建议从两个方面切入一是把目标追踪接进去让系统能持续锁定目标测距而不是每帧独立检测二是加入串口或ROS通信把检测和测距结果送给下位机或者机器人决策模块这样就能直接从“能测”跨越到“能用”了。先把基础流程跑通再谈其他的。本文还有配套的精品资源点击获取