简介:本资源面向计算机视觉初学者与自动驾驶、机器人导航方向的开发者,提供一套基于YOLOv5与单目深度估计的距离测算Python实现方案,帮助在无额外深度硬件的情况下从单张彩色图像中推断目标距离。压缩包共132个文件,约13.87MB,以34个py脚本、40个yaml配置、16个pyc编译文件为主,辅以yml、xml、sh、md及Dockerfile等,覆盖模型代码、预训练权重、训练与测试脚本、数据集配置等模块,目录结构清晰便于按功能检索。目前已有3861人学习下载,说明该方案在目标检测与三维测距结合场景中具备一定参考价值。读者可据此理解YOLOv5检测框与深度映射函数的结合方式,掌握从图像特征提取到距离输出的完整流程,并借助预训练权重快速复现预测与可视化结果,为自动驾驶、无人机或智能监控中的环境感知与决策规划提供可落地的技术参考。
1. 单目测距:一个 bounding box 凭什么能算出深度
很多人第一次看到 YOLOv5 加单目测距的代码,心里都会犯嘀咕:一个二维的检测框,四个像素坐标,凭什么能反推出目标离相机多少米?这听起来像是玄学,但背后其实是一套非常朴素的几何约束。核心结论先给出来:单目测距不是靠“猜”,而是靠已知的真实尺寸 + 像素比例关系,把二维框的宽度或高度映射回三维空间中的距离。它解决的是一个非常实际的问题——在没有激光雷达、没有双目相机、没有深度传感器的条件下,仅凭一颗普通摄像头,就要对画面里的目标做粗略但可用的距离估计。这件事值得搞懂,因为它是自动驾驶、机器人避障、智能监控里成本最低的深度感知方案之一,也是理解“视觉如何恢复三维信息”的最佳入口。你会在很多边缘计算设备上看到这套组合,原因很简单:YOLOv5 够快,单目测距够轻,两者拼在一起,能在嵌入式板子上跑出实时效果。
2. 小孔成像模型:像素坐标到三维距离的数学桥梁
2.1 从相似三角形说起
单目测距的底层原理,来自初中几何里的相似三角形。想象一个理想的小孔成像模型:现实世界里的物体高度为 H,它到相机光心的距离为 D,相机焦距为 f,物体在成像平面上的高度为 h。根据相似关系,有 H / D = h / f。把这个式子变形,就能得到 D = (H × f) / h。这里的 h 是物体在图像中的像素高度,f 是相机的焦距(以像素为单位),H 是物体的真实物理高度。只要知道 H 和 f,再测出 h,距离 D 就出来了。这就是单目测距的全部数学基础,没有更复杂的东西。很多人觉得单目测距“不准”,其实不是原理不准,而是 H 和 f 这两个参数在实际场景里很难精确获得。
2.2 焦距 f 到底怎么来的
焦距 f 的单位是像素,不是毫米。相机出厂标称的焦距通常是毫米,比如 3.6mm、6mm,但公式里需要的是像素焦距。换算方式有两种:第一种是用标定板做相机标定,得到内参矩阵,直接读出 fx 和 fy;第二种是用一个已知尺寸的物体,放在已知距离上拍一张照,量出它在图像里的像素宽度,反推 f = (h × D) / H。第二种方法更接地气,适合没有标定条件的场景。常见的实现思路是:拿一个 A4 纸大小的目标,放在离相机 1 米的位置,拍一张图,用 YOLOv5 检测出它的像素宽度,然后代入公式算出 f。这个 f 一旦确定,只要相机不换、分辨率不改,就可以一直用。
2.3 为什么选宽度而不是高度
YOLOv5 输出的 bounding box 有四个值:x1, y1, x2, y2。宽度 w = x2 - x1,高度 h = y2 - y1。理论上用宽度或高度都可以,但实际代码里更常用宽度。原因有两个:第一,很多目标(比如车辆、行人)在画面中的高度容易被遮挡或截断,而宽度相对稳定;第二,宽度对应的真实尺寸 H 更容易估计,比如一辆轿车的宽度大约 1.8 米,一个成年人的肩宽大约 0.45 米,这些数值的方差比高度小。当然,如果目标是竖直细长的物体,比如电线杆,用高度反而更合适。选择哪个维度,取决于目标本身的几何特性。
# 单目测距核心公式实现 # 输入:bbox 的像素宽度 pixel_width,目标真实宽度 real_width(米) # 相机焦距 focal_length(像素),由标定或反推得到 def monocular_distance(pixel_width, real_width, focal_length): # 相似三角形:real_width / distance = pixel_width / focal_length # 变形得:distance = (real_width * focal_length) / pixel_width if pixel_width <= 0: return float('inf') # 防止除零,返回无穷大表示无效 distance = (real_width * focal_length) / pixel_width return distance # 示例:假设焦距为 700 像素,目标真实宽度 1.8 米 # 检测框像素宽度为 200 像素 f = 700 W = 1.8 w_pixel = 200 d = monocular_distance(w_pixel, W, f) print(f"估计距离:{d:.2f} 米") # 输出 6.30 米这段代码就是整个测距模块的心脏。pixel_width来自 YOLOv5 的检测结果,real_width是预先设定的先验知识,focal_length是相机参数。三个输入,一个输出,没有迭代,没有优化,纯解析解。代码后的逻辑说明:pixel_width越小,说明目标在画面里越远,算出的距离越大;pixel_width越大,目标越近,距离越小。参数说明:real_width的准确性直接决定测距精度,如果实际目标宽度是 1.8 米但你填了 2.0 米,距离就会偏大 11% 左右。focal_length的误差也会等比例传递到距离上。所以这套方法的精度天花板,在参数标定阶段就已经决定了。
3. YOLOv5 检测框到距离估计:工程落地的关键细节
3.1 检测框的稳定性决定测距的抖动程度
YOLOv5 每一帧输出的 bounding box 并不是完全稳定的,即使目标静止不动,框的像素宽度也会有 1 到 3 个像素的波动。这个波动经过公式放大后,会导致距离估计出现几十厘米的跳动。常见的处理思路是加一个滑动窗口滤波:维护一个长度为 N 的队列,每次新来的距离值入队,队满后取平均值或中位数作为最终输出。N 一般取 5 到 10,太小滤波效果不明显,太大则响应延迟增加。如果目标在快速移动,比如车辆,N 要适当减小,否则测距会滞后。这是一个典型的“平滑 vs 响应”权衡,没有标准答案,只能根据场景调。
3.2 目标真实尺寸的先验表怎么建
单目测距的精度很大程度上取决于real_width的准确度。实际项目里,通常会建一个先验表,按类别存储典型尺寸。比如:
| 类别 | 真实宽度(米) | 真实高度(米) | 备注 |
|---|---|---|---|
| 行人 | 0.45 | 1.70 | 肩宽,高度用于验证 |
| 轿车 | 1.80 | 1.50 | 宽度稳定,高度易截断 |
| 卡车 | 2.50 | 3.00 | 宽度方差较大 |
| 自行车 | 0.60 | 1.20 | 宽度随姿态变化 |
这张表不是万能钥匙。同是轿车,不同车型宽度从 1.6 米到 2.0 米都有,如果你统一用 1.8 米,误差就是 ±10%。更精细的做法是:如果检测框能区分车型,就按车型查表;如果分不了,就用类内平均值。还有一种思路是用高度和宽度联合估计,取两个距离的加权平均,权重根据目标的遮挡程度动态调整。遮挡严重时,被遮挡的维度权重要降低。
3.3 相机俯仰角带来的系统性偏差
前面的公式假设相机光轴与地面平行,目标在画面中的成像高度只与距离有关。但实际安装时,相机往往有俯仰角,尤其是车载场景,相机稍微向下倾斜。这时候,目标在画面中的垂直位置会引入额外的距离信息,但同时也让简单的相似三角形关系产生偏差。修正方法有两种:第一种是几何修正,根据俯仰角和目标在画面中的 y 坐标,重新计算目标在相机坐标系下的实际投影;第二种是查表修正,事先在不同距离上采集标准目标的像素宽度,建立“像素宽度-距离”查找表,直接插值。第一种更通用,第二种更简单。如果俯仰角小于 5 度,偏差通常在 3% 以内,可以忽略;超过 10 度,就必须修正,否则近处目标会偏近、远处目标会偏远。
# 带俯仰角修正的距离估计 import math def distance_with_pitch(pixel_width, real_width, focal_length, pitch_angle, bbox_center_y, image_height): # 先按无俯仰角公式算一个基准距离 base_distance = (real_width * focal_length) / pixel_width # 计算目标在画面中的垂直偏移(相对于光心) # 光心通常在图像中心,即 image_height / 2 offset_y = bbox_center_y - image_height / 2.0 # 俯仰角修正:将垂直偏移转换为距离修正量 # 原理:offset_y 对应一个角度,该角度与俯仰角叠加后影响距离 angle_offset = math.atan2(offset_y, focal_length) corrected_angle = pitch_angle + angle_offset # 修正后的距离(近似) corrected_distance = base_distance * math.cos(corrected_angle) / math.cos(pitch_angle) return corrected_distance # 示例:俯仰角 10 度,目标在画面下方 100 像素处 d_corrected = distance_with_pitch(200, 1.8, 700, math.radians(10), 400, 720) print(f"修正后距离:{d_corrected:.2f} 米")这段代码展示了俯仰角修正的基本思路。pitch_angle是相机光轴与水平面的夹角,向下倾斜为正。bbox_center_y是检测框中心点的 y 坐标,image_height是图像总高度。offset_y为正表示目标在画面下方,为负表示在上方。angle_offset是目标偏离光心的角度。修正后的距离用余弦比值调整。参数说明:pitch_angle需要事先测量,可以用水平仪或标定板;bbox_center_y直接从 YOLOv5 输出取。注意这个修正模型是近似,角度越大误差越大,超过 30 度建议用完整投影模型。
4. 避坑与排查:单目测距常见的五个翻车现场
4.1 距离跳变剧烈,目标静止但读数乱飞
现象:画面里目标没动,但输出的距离值在几米范围内反复跳动。原因:YOLOv5 的检测框像素宽度每帧都在变,哪怕只差 2 个像素,经过公式放大后就是几十厘米的波动。解决:加滑动窗口滤波,取最近 5 帧的中位数。如果还不行,检查是不是检测框本身在抖动,可以降低 NMS 阈值或提高置信度阈值,让框更稳定。
4.2 近处准远处偏,误差随距离线性增长
现象:1 米处误差 0.1 米,5 米处误差 0.5 米,10 米处误差 1.5 米。原因:像素宽度与距离成反比,距离越远,同样的像素误差对应的距离误差越大。这是单目测距的固有特性,不是 bug。解决:接受远处精度下降的事实,或者对远距离目标改用其他传感器。如果必须用单目,可以在远距离时切换到高度维度估计,因为远处目标的宽度可能只有几个像素,但高度相对更可靠。
4.3 换一个相机,所有距离全错
现象:同一套代码,换了一颗摄像头,测距结果完全不对。原因:焦距 f 变了,分辨率也可能变了,但代码里还用的旧参数。解决:每次换相机都要重新标定 f。标定方法很简单:拿一个已知宽度的物体,放在已知距离上,拍一张照,量像素宽度,反推 f。如果分辨率也变了,还要按比例缩放 f。血泪经验:把 f 和分辨率写进配置文件,换相机时只改配置,不改代码。
4.4 目标被遮挡时距离突然变大
现象:目标被部分遮挡后,检测框变小,距离读数突然增大。原因:YOLOv5 在遮挡时输出的框只覆盖可见部分,像素宽度变小,公式算出距离变大。解决:如果遮挡是暂时的,用滤波队列平滑掉;如果遮挡是持续的,需要引入跟踪算法,用历史框预测当前完整框的宽度。常见的做法是结合卡尔曼滤波,用运动模型估计真实框尺寸。
4.5 类别混淆导致先验尺寸用错
现象:把卡车识别成轿车,距离偏小;把行人识别成自行车,距离偏大。原因:real_width查表时用错了类别。解决:提高 YOLOv5 的分类置信度阈值,或者在测距模块前加一个类别校验,如果置信度低于某个值,就不输出距离。另外,可以在先验表里为易混淆类别设置相近的宽度值,减少误判带来的误差。
5. 精度天花板与设计取舍:单目测距到底能走多远
单目测距的精度天花板,在参数标定的那一刻就已经注定了。real_width的方差、focal_length的标定误差、检测框的像素抖动,这三个误差源叠加在一起,决定了这套方案在 10 米内的误差通常在 5% 到 15% 之间,超过 20 米基本只能给出“很远”的定性判断。这不是代码写得不好,而是方法本身的物理限制。如果你需要厘米级精度,单目测距做不到,必须上双目、激光雷达或结构光。但如果你需要的是低成本、低功耗、实时性好的粗略距离估计,单目测距加 YOLOv5 就是最优解之一。
我一般会这么理解这套方案的取舍:它用先验知识换掉了硬件成本,用解析公式换掉了复杂优化,用滤波平滑换掉了瞬时精度。每一个选择都有代价,但组合在一起,恰好卡在了一个非常实用的平衡点上。你可以在嵌入式设备上跑,可以在普通摄像头上跑,可以在没有深度传感器的旧系统上跑。这种“够用就好”的工程哲学,才是单目测距真正的价值所在。
从那以后我每次做单目测距,都强制走一遍参数标定流程:先测 f,再建先验表,最后加滤波。这三步少一步,后面就要花十倍时间调参。希望帮到你。
本文还有配套的精品资源,点击获取