简介YOLOv5单目测距项目是一份结合目标检测与单目深度估计的Python实现面向自动驾驶、机器人导航、智能监控等需要实时距离感知的应用场景适合有一定深度学习基础的开发者借鉴参考。资源包共132个文件压缩包大小13.87MB包含yaml与yml配置文件、py可执行脚本、markdown说明文档等主体内容同时提供预训练权重pt、Dockerfile、ipynb示例等可完整覆盖从环境搭建、模型训练到测试推理的流程。目前已有3858人学习/浏览是该方向下较受关注的一份实战资源。借助其中完整代码不仅能看到YOLOv5检测输出与单目深度网络如何融合进而得到物体框的位置、类别和距离还能直接使用自带的数据集、训练与测试脚本复现实验单目方案不需要额外深度传感器能显著降低硬件成本便于移植到实际视觉系统中。1. 单目测距不是玄学但别指望一个摄像头测出激光雷达的精度一位做园区安防的朋友拿着 YOLOv5单目测距python的标题来问我说检测框能出来距离却忽远忽近同一辆车停在同一个位置两次读数能差出两三米。这不是个例。单目测距的原理确实简单——一个摄像头、一个已知安装高度、一条相似三角形公式几十行 Python 就能把目标框底边的像素位置换算成米。但越是简单的公式越容易在工程里翻车。这个方案适合做车辆防碰撞预警、机器人避障、交通流量统计这类对精度要求不苛刻的场景能稳定做到相对误差 10% 以内但如果不做标定、不处理检测框抖动、不知道俯仰角的影响误差会直接失控。这篇文章把原理、标定、工程实现和踩坑点全部拆开讲按步骤走完你能在自己的数据集上跑出可信的距离输出。2. 像素到米的第一步针孔模型、相机内参与固定高度假设2.1 为什么单目能测距靠的是目标落在平面上的几何约束单目相机的成像过程本质上是把三维空间压缩到二维像素平面这个过程丢失了深度信息所以一个摄像头怎么测距离听起来像悖论。真正能测距的前提是目标必须落在某个已知平面上最常见的约束就是地面平面。摄像头安装高度 H 是已知的相机光轴与地面的夹角可以通过标定得到那么图像上任意一个像素行都能唯一对应地面上的一条平行线目标与这条线的交点就是它的位置。这就是针孔相机模型加固定平面假设的完整逻辑。目标的检测框底边中心被用作目标与地面的接触点。对车辆来说这个点通常是后轮或车尾与地面的接触位置比较可靠对行人来说脚底被框住的位置会受到站立姿势和遮挡影响误差更大。公式很简单d (H × fy) / (v_bottom - cy)其中 fy 是焦距的像素值cy 是主点 y 坐标v_bottom 是检测框底边中心的像素行。这个式子在相机完全水平时成立一旦相机有俯仰角必须修正。很多工程翻车就是跳过了这个修正。2.2 相机内参标定用 OpenCV 棋盘格拿到 fy 与 cy公式里的 fy 和 cy 不是相机说明书上的物理焦距而是像素单位的内参必须在真实相机上标定得到。同一个型号的两台相机内参会略有差异更不用说换了镜头或分辨率。标定的标准做法是打印一张棋盘格在不同角度、不同距离、不同高度拍 15 到 30 张图然后用 OpenCV 的 calibrateCamera 解算。import cv2 import numpy as np CHESS_W, CHESS_H 9, 6 # 内角点数不是棋盘格格子数 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp np.zeros((CHESS_W * CHESS_H, 3), np.float32) objp[:, :2] np.mgrid[0:CHESS_W, 0:CHESS_H].T.reshape(-1, 2) obj_points [] # 世界坐标系下的棋盘格角点 img_points [] # 图像坐标系下的角点 images [fcalib_{i}.jpg for i in range(1, 21)] # 20张标定图 for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, (CHESS_W, CHESS_H), None) if ret: obj_points.append(objp) # 亚像素细化能有效提升角点精度 corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) img_points.append(corners2) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) print(mtx:, mtx) # 内参矩阵fx、fy、cx、cy 都在这一行里 print(dist:, dist) # 畸变系数测距前最好做去畸变标定的逻辑是已知棋盘格每个角点的真实相对位置又在图像上找到了同一个角点的像素位置多组对应关系就能反解出内参矩阵和畸变系数。calibrateCamera 返回的 mtx 就是你应该记住的 fy 和 cy标定程序的输出建议保存成 numpy 文件后面测距代码直接加载。一个容易忽略的点是棋盘格内角点数必须与 findChessboardCorners 的输入一致9×6 表示每行 9 个内角、每列 6 个内角打印时数清楚格子别拿错规格。标定成功的标志有三个内参矩阵的对角线量级正常畸变系数不大重投影误差低于 0.1 像素。重投影误差可以用 cv2.projectPoints 把 obj_points 反投回图像和 img_points 求均方根误差跑一次就清楚了。拍标定图时视野四个角和近距离都要覆盖到棋盘格尽量占画面三分之一以上纹理清晰、光照均匀否则角点提取会有系统性偏移。2.3 外参与安装高度俯仰角不标定测距全白搭内参解决的是像素坐标和相机坐标的关系而测距公式需要的 H 和俯仰角属于外参。相机安装高度 H 是物理测量值用卷尺量光心到地面的垂直距离量到光心位置不好确定时可以量镜头前端再补上机身的厚度误差控制在 2 厘米以内。俯仰角是最容易被忽略的参数。只要相机不是完全水平哪怕是向下倾斜两三度远距离的测距误差就会以平方级别放大。修正后的公式是d H × (cosα − u·sinα) / (u·cosα sinα)其中 u (v_bottom − cy) / fyα 是相机光轴与水平面的夹角向下俯视为正。当 α 等于 0 时这个公式退化成最简单的相似三角形版本。在实际项目中α 可以用消失点法标定在平直路面上找两条平行车道线的灭点灭点的像素行对应无穷远的地平线代入 u (v_horizon − cy) / fy就能反算出 α。这个计算在标定现场做一次运行时固定不动效果比纯目测安装角度靠谱得多。3. 把 YOLOv5 的检测框换算成距离Python 工程落地全流程3.1 检测输出解析从 xyxy 框拿到底边中心像素YOLOv5 的输出通常有两种来源直接跑 detect.py或者在自己的 Python 脚本里加载模型推理。工程上我更推荐在脚本里集成因为要拿原始坐标做测距计算。Ultralytics YOLOv5 的推理结果是一个 Results 对象其中 xyxy 属性是 [x1, y1, x2, y2, conf, cls] 的数组前四个是检测框左上角和右下角的像素坐标。取底边中心就是在 y1 和 y2 之间取中值得到 v_bottom再拿到整张图像的高度做换算。有一个常见前辈做法是直接用归一化坐标xywhn 格式的 bbox 需要乘回图像宽高才能用于像素公式。无论哪种方式都要确认坐标参考系是原图而不是缩放后的输入图。如果推理前做了 letterbox 缩放输出坐标通常已经映射回原图不要二次乘缩放系数否则距离整体漂移。排查这个问题最快的方法是打印一个框的坐标在 cv2.rectangle 绘制的图上肉眼确认框是否贴合目标。import cv2 import torch model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) model.conf 0.35 # 检测置信度阈值 model.iou 0.45 # NMS 的 IoU 阈值 results model(frame) # frame 是 BGR 图像 det results.xyxy[0].cpu().numpy() # N x 6 的检测结果 for row in det: x1, y1, x2, y2, conf, cls row v_bottom (y1 y2) / 2.0 # 底边中心像素行测距关键点代码里的 model.conf 和 model.iou 是 Ultralytics YOLOv5 提供的属性直接赋值就能生效不用改权重文件。v_bottom 取框底边是因为我们假设目标贴地取中心行或框顶边都会让距离读数偏大或出现负数。如果你的目标分类里包含多种类别建议按类别分开处理比如 person 和 car 的底边与地面接触点可靠程度完全不同。3.2 距离换算函数相似三角形公式加俯仰角修正把公式写成独立函数是整套工程里最值得复用的部分。输入是检测框底边像素行、相机内参 fy 和 cy、安装高度 H、俯仰角 α输出是米为单位的距离。函数内部先算归一化坐标 u再套俯仰角修正公式最后对结果做下限保护避免除零或负数。import math def estimate_distance(v_bottom, cam_h, fy, cy, pitch_deg): v_bottom: 检测框底边中心像素 y 坐标 cam_h: 相机安装高度单位米 fy: 内参矩阵中的焦距像素值 cy: 主点像素 y 坐标 pitch_deg: 相机俯仰角向下俯视为正单位度 u (v_bottom - cy) / fy alpha math.radians(pitch_deg) cos_a math.cos(alpha) sin_a math.sin(alpha) # 俯仰角修正的测距公式α0 时退化为 cam_h / u d cam_h * (cos_a - u * sin_a) / (u * cos_a sin_a) return max(d, 0.0)这个函数的参数说明值得细看fy 和 cy 来自标定结果cam_h 是现场物理量pitch_deg 是标定或估算的角度。当你没有标定俯仰角时先填 0 跑通流程再把错误距离作为反馈反推角度后面避坑章会写具体反推方法。公式的数值行为是目标越靠近图像底部u 越大分母越大距离越小目标靠近地平线时u 趋近某个临界值距离趋于无穷大这符合透视规律。3.3 主循环检测、测距、绘制结果一体化运行完整的主循环是把摄像头帧、检测模型、测距函数串起来。实时视频流场景里每一帧都执行检测和测距然后把框、类别、距离一起绘制到画面里。这个循环看起来简单但有几个工程细节决定了它能不能长时间稳定运行模型推理要不要降帧率、图像要不要去畸变、距离要不要做时序平滑。cap cv2.VideoCapture(0) CAM_H 1.45 # 相机安装高度米 FY mtx[1, 1] # 标定得到的内参 CY mtx[1, 2] PITCH_DEG 2.5 # 俯仰角度需要实测或消失点标定 frame_id 0 while True: ret, frame cap.read() if not ret: break # 测距对畸变敏感先做一次去畸变 frame cv2.undistort(frame, mtx, dist) if frame_id % 2 0: # 每 2 帧推理一次节省 CPU results model(frame) det results.xyxy[0].cpu().numpy() for row in det: x1, y1, x2, y2, conf, cls row v_bottom (y1 y2) / 2.0 distance estimate_distance(v_bottom, CAM_H, FY, CY, PITCH_DEG) label f{model.names[int(cls)]} {distance:.2f}m cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) frame_id 1 cv2.imshow(yolov5 mono distance, frame) if cv2.waitKey(1) 0xFF ord(q): break去畸变这一步不是可选项。彩色工业镜头和普通 USB 摄像头在画面边缘都有明显畸变畸变会让底边像素行偏离真实投影位置近距离可能看不出远距离的直接误差能到半米以上。cv2.undistort 每次都会做完整的像素重映射开销不小如果你在树莓派 4B 这类设备上部署可以把映射表预先算好用 cv2.remap 替代速度能快不少。每两帧推理一次也是一种常见的降载手法测距输出不会因此变得不可用但后面接的平滑滤波需要相应调整参数。4. 参数怎么设检测阈值、相机参数与距离平滑的取舍4.1 检测置信度阈值和 NMS 如何影响测距稳定性先想清楚一个容易搞混的关系检测框的质量直接决定测距质量。当 conf 阈值调低比如从 0.35 降到 0.15模型会输出更多低置信度框这些框的位置往往不稳定同一辆车相邻两帧的底边能差十几个像素算出来的距离跳变非常明显。反过来阈值调高到 0.7漏检率上升目标丢失时测距输出会突然归零。实战中我一般把 conf 控制在 0.3 到 0.4避免阈值本身成为误差放大器。NMS 的 IoU 阈值影响的是重叠框的去留。两个重叠框的高度略有不同被保留下来的那个直接决定了 v_bottom 的取值。保持默认的 0.45 通常够用但如果你发现目标被双层框包裹且底边不一致可以尝试调低到 0.35 让重叠框更容易合并。注意每改动一次 NMS 参数都要重新观察测距输出的稳定性而不是只看框是否贴得紧。4.2 相机参数误差如何传递到距离上测距公式里的 fy、cy、H、α 四个输入各自的误差对最终距离的影响完全不同这个敏感性差异决定了你要花多少精力去精确标定哪一项。fy 是线性项直接乘在结果上标定误差 1% 就会带来 1% 的距离误差所以标定要做得认真。cy 作为减数在 v_bottom 远离 cy 时影响变小也就是说它主要影响近距离的精度。H 是线性比例项但物理测量误差通常能控制在 2% 以内相对可靠。α 是最麻烦的它在远处通过分母起作用α 每差 0.5 度10 米外的距离可能偏 8% 以上。做成表格更直观参数测量/标定方式典型误差对测距的影响fy棋盘格标定0.5% 以内全局线性误差cy棋盘格标定1 到 2 像素主要影响近距离H卷尺测量1 到 2 厘米全局比例误差α消失点或反推0.2 到 1 度远距离放大误差在实际部署中最难控制的是 α。一个常见做法是多点反推人站在已知距离的 5 米、10 米、15 米位置记录每个位置的底边像素行代入公式反推 α取多个结果的均值。这个操作比用水平仪去量安装角度更可靠因为它是直接对测距结果做闭环修正。4.3 距离抖动怎么消EMA 平滑和它的两个参数原始测距输出几乎必然抖动来源不只是检测框的波动还有目标本身的运动、遮挡、图像噪声。直接显示原始值会让人感觉这套系统质量很差。最简单有效的平滑是一阶指数移动平均也就是 EMA一个公式就能让输出曲线变得可读alpha_ema 0.6 def lowpass_filter(measured, prev_smoothed, alpha_ema): return alpha_ema * measured (1 - alpha_ema) * prev_smoothedalpha_ema 的取值决定了平滑强度。0.6 表示当前帧占六成响应快但抖动抑制有限0.3 表示当前帧占三成曲线很平但目标快速靠近时会有明显滞后。经验值低速场景用 0.3高速来车用 0.6。EMA 的滞后在目标急停或急加速时会把距离读数拉出一个过冲这个现象不是 bug是滤波器本身的性质需要让使用方理解。如果你后续要接刹车逻辑对响应速度敏感就选 0.5 到 0.7别为了好看把 alpha 压到 0.2 以下。5. 单目测距避坑五个让距离读数失控的典型场景5.1 检测框底边不在地面接触点截断目标让距离直接跳变现象一辆车开进画面边缘时检测框的下边界突然从车轮处跳到车身中部距离读数瞬间少了 5 米。原因YOLOv5 的检测框是矩形当目标与图像边缘相交或者被前方物体部分遮挡时完整的目标框无法生成底边不再贴合地面接触点。单目测距公式默认 v_bottom 是接触点一旦这个假设破裂输出自然失真。解决对底边位置做逻辑校验只有检测框完整包含目标时才参与测距否则沿用上一帧的平滑值。更进一步的方案是给每个类别设置一个底边偏移修正比如行人的脚底通常在框底边往上 5% 到 10% 的位置用统计得出的偏移量校正 v_bottom。5.2 固定平面假设失效坡道和减速带让所有距离归零现象在园区入口有减速带的位置车辆通过减速带时测距读数突然从 10 米跳到 3 米过后又恢复到正常。原因测距公式隐含目标在地平面上这个前提减速带和坡道让车辆的触点不再处于相机安装时的基准平面v_bottom 对应的地面距离在一瞬间被压缩。解决方案本身对非平直路面无解能做的是加场景约束——在代码里限制输出范围或对单帧跳变超过阈值的距离打上不可信标签。使用方需要明确这不是标定问题是单目测距的物理边界通常只能在平直路面的场景里使用。5.3 标定图拍太少导致内参在边缘漂移现象近距离测距正常15 米外距离系统性偏近 1 到 2 米重新标定后恢复。原因棋盘格标定图拍得少且集中在画面中央时calibrateCamera 对边缘畸变参数的估计会明显失真图像边缘的像素位置投影偏差最大。解决标定图拍到 20 张以上棋盘格必须出现在画面的四个角和上下边缘避免所有标定图里的棋盘都放在画面中心。标定完成后看一眼重投影误差超过 0.1 像素就补拍重新标。这一步是最省心但最容易被偷懒的。5.4 换相机或改分辨率后没有更新内参现象代码从笔记本摄像头迁移到一个 USB 工业相机或者把视频分辨率从 1080p 改成 720p测距结果整体偏了 30%。原因fy、cx、cy 是像素单位和图像分辨率强相关。同一个相机物理焦距不变但分辨率减半后像素焦距也会减半。把原先标定的内参硬套到新分辨率上必然整体偏移。解决录制新分辨率下的标定视频重新跑标定流程或者在工程代码里把内参加载逻辑做成和分辨率绑定的配置。改分辨率是对标定结果影响最隐蔽的操作因为你可能完全不记得改过它。5.5 俯仰角用了估算值远距离系统性偏差现象5 米内距离读数准确20 米外距离普遍偏近而且距离越远偏差越明显。原因俯仰角 α 是唯一一个会随着距离放大误差的参数。安装时用目测估算角度误差可能达到 1 到 2 度这个误差在近距离被 u 的大数值稀释在远距离被分母放大。解决用已知距离反推 α。找一块平地人站在 5、10、15、20 米位置记录对应的 v_bottom代入测距公式反算 α 并取平均。反推时保证相机没有再次被移动因为 α 对安装姿态极其敏感动过一次就要重新标定。6. 从能测到测得准验证方法、误差曲线与进阶方向验证这套流程是否真正可用不能靠肉眼感觉。我在项目里常用一种土办法找一条平直马路用卷尺每隔 5 米贴一个标记站到标记处记录测距读数做一个距离-误差对照表。关键是每个距离点至少采样 20 次取均值和中位数把误差来源分成随机抖动和系统性偏差两部分。随机抖动用标准差衡量正常应该小于距离的 5%系统性偏差则需要回到第四章针对性地重新标定对应参数。进阶方向可以分两级。第一级是把测量距离绘成误差曲线观察误差是否随距离单调增加如果是优先处理俯仰角如果误差在某个区间突然恶化排查对应距离上是否有遮挡或框截断。第二级是用逆透视映射把图像坐标投影到世界坐标网格生成鸟瞰图之后每个目标的位置直接在世界坐标里读取这个方案比单点测距公式更接近地图级输出适合做多目标的车距统计和轨迹分析。如果对精度有更高追求还有一个和单目测距互补的思路利用标定出的地面平面先把目标底边投影到地面坐标然后用 PnP 解算目标相对相机的位姿。这个方案增加了计算量但对目标朝向和尺度的适应能力更强适合检测框底边经常不可靠的场景。单目测距的本质是几何假设的工程化应用一旦你理解了假设在哪、误差从哪来这套 Python 流程就能从演示级走向可用级。我自己做过的项目中这类方案也曾在雨天因地面反光和框抖动让人头疼后来靠平滑和阈值管理把问题压了下去。希望帮到你。本文还有配套的精品资源点击获取