☰
YOLOv5单目测距实战:从相似三角形原理到精度优化与避坑指南
2026/11/2 6:43:04 网站建设 项目流程

简介:面向计算机视觉初学者与自动驾驶、机器人导航方向开发者,这份资源围绕YOLOv5目标检测与单目深度估计的结合展开,解决无深度相机条件下如何从单张彩色图像推断物体距离的问题。压缩包共132个文件、约13.87MB,以34个py脚本承载检测与测距主流程,40个yaml与11个yml配置文件组织模型结构与训练参数,另含pt预训练权重、ipynb交互教程、Dockerfile与sh启动脚本,便于快速复现环境。资源已有3868人学习下载,说明其在同类实践中具备一定参考价值。读者可从中获得YOLOv5检测代码、单目测距深度网络实现、预训练权重与训练测试脚本,并借助示例图像与配置说明理解检测框与距离信息如何融合,适合作为课程设计或原型验证的起点。

1. 单目测距的真相:一个相机凭什么能测出深度

很多人第一次看到「YOLOv5 + 单目测距」这个组合,第一反应是怀疑:一个摄像头拍出来的只是二维像素,深度信息在投影那一刻就丢了,怎么可能反过来算出距离?这个疑问恰恰是理解整套机制的入口。单目测距并不是凭空恢复被丢弃的深度,而是用几何先验把深度「补」回来——最常见的就是相似三角形:已知目标的真实高度、相机焦距和成像平面上的像素高度,四者构成一个比例关系,距离就能解出来。YOLOv5 在这里的角色不是测距,而是把「目标是谁、在画面里占多少像素」这件事稳定地框出来,测距模块再拿这个像素尺寸去套几何公式。搞懂这条链路,你才会明白为什么同一个框,换个焦距、换个目标类别,算出来的距离能差出一大截,也才知道哪些误差是玄学、哪些是血泪经验换来的参数校准。

2. 相似三角形测距:像素高度怎么变成物理距离

2.1 从针孔模型推出距离公式

单目测距的数学根基是针孔相机模型。光线从物体顶端和底端出发,穿过光心,落在成像平面上,形成一个倒立的像。物体真实高度 H、物体到光心的距离 D、成像高度 h、焦距 f 之间满足 H/D = h/f,整理得 D = f·H/h。这里的 h 是成像平面上的物理高度,工程上我们拿到的是像素高度 p,两者差一个像素物理尺寸,通常把它并进焦距里,得到一个「像素焦距」fx,于是公式变成 D = fx·H/p。这个变形看着简单,但它是整篇文章所有代码的锚点:fx 和 H 是标定出来的常量,p 是 YOLOv5 每帧给出的框高,D 就是输出。理解这一点,后面所有误差分析都有了落脚点——D 对 p 是一阶反比关系,p 越小(目标越远),同样的像素抖动带来的距离误差越大,这就是远距离测距精度天然差的根源,不是代码写得烂。

2.2 焦距标定:fx 到底怎么来的

fx 不能拍脑袋填。常见的实现思路有两种:一种是用相机内参标定(棋盘格),拿到 fx、fy、cx、cy,其中 fx 直接可用;另一种是「已知距离反推法」,把某个已知高度 H 的物体放在已知距离 D0 处,量出它在画面里的像素高度 p0,反解 fx = D0·p0/H。第二种在快速验证里很常见,因为它不需要标定板,但代价是它把镜头畸变、像素非方形等误差全塞进了一个 fx 里,换个场景就飘。我一般会这么理解:fx 本质上是「这台相机在某个分辨率下的像素焦距」,分辨率一变(比如从 640 缩到 416 推理),fx 必须按比例缩放,否则距离整体偏。这一点在代码里经常被忽略,是翻车的重灾区。

# 已知距离反推焦距:把一个已知高度 H、已知距离 D0 的目标摆正 # p0 是它在当前推理分辨率下检测框的像素高度 def calibrate_fx(H_m, D0_m, p0_px): # 由 D = fx * H / p 反解 return D0_m * p0_px / H_m # 分辨率缩放:推理尺寸从 base_w 变到 cur_w,fx 同步缩放 def scale_fx(fx_base, base_w, cur_w): return fx_base * (cur_w / base_w)

上面两个函数是整个测距模块的地基。calibrate_fx里 H_m 是目标真实高度(米),D0_m 是标定时量的距离(米),p0_px 是检测框高度(像素),返回值 fx 的单位是「像素·米/米」,量纲上就是像素。scale_fx解决的是分辨率漂移问题:YOLOv5 推理时经常把输入 letterbox 到 640 或 416,检测框坐标是在缩放后的图上量的,fx 必须跟着缩放,否则距离会系统性偏大或偏小。参数怎么改:H_m 要按类别分别维护(人、车、锥桶高度完全不同),D0_m 尽量选在你要用的工作距离附近,标定点太近会让远距离误差放大。

2.3 检测框高度 p 的取法决定精度上限

公式里 p 是「目标在画面中的像素高度」,但 YOLOv5 给的框是带抖动的。同一帧里框高可能因为遮挡、姿态、置信度阈值变化而跳几个像素。直接拿单帧框高代入,距离曲线会像心电图。常见的实现思路是做时间维度的平滑:对连续 N 帧的框高做滑动平均或卡尔曼滤波,再代入公式。另一个细节是框的上下边界要不要裁掉阴影或脚部误检——框高多算 10 像素,近距离时距离可能少算半米。这些不是公式问题,是工程问题,但恰恰决定了这套方案能不能落地。

3. YOLOv5 检测输出到测距输入的衔接

3.1 从推理结果里抠出可用的框

YOLOv5 的推理输出是一堆 (x1, y1, x2, y2, conf, cls),测距模块只关心两件事:这个框属于哪个类别(决定 H),以及框的像素高度 p = y2 - y1。衔接层要做三件事:按类别过滤(只对已知真实高度的类别测距)、按置信度过滤(低置信框的边界不可靠)、按框的完整性过滤(贴边、被截断的框高度失真,不能直接用)。这一步看着琐碎,但它是「检测」和「测距」两个世界的接口,接口没设计好,后面公式再准也白搭。

# 把 YOLOv5 的原始输出整理成测距可用的结构 CLASS_REAL_HEIGHT = {0: 1.70, 2: 1.50, 5: 0.80} # 类别 -> 真实高度(米),示例值 def boxes_to_measure(detections, img_h, img_w, conf_thres=0.5, edge_margin=5): results = [] for x1, y1, x2, y2, conf, cls in detections: if conf < conf_thres: continue if cls not in CLASS_REAL_HEIGHT: continue # 贴边或截断的框高度不可信,直接丢弃 if y1 < edge_margin or y2 > img_h - edge_margin: continue p = y2 - y1 if p <= 0: continue results.append({ "cls": int(cls), "p": float(p), "H": CLASS_REAL_HEIGHT[int(cls)], "center_x": (x1 + x2) / 2.0, }) return results

这段代码的关键不在语法,在三个过滤条件的设计动机。conf_thres过滤掉低置信框,因为低置信往往意味着框边界模糊,p 的误差被直接放大进距离。edge_margin处理的是截断问题:一个只露出上半身的人,框高只有真实成像高度的一半,代入公式距离会翻倍,这是最常见的「明明很近却报很远」的原因。CLASS_REAL_HEIGHT是类别到真实高度的映射,必须按你的实际场景维护,人、车、非机动车的高度差异巨大,混用会让误差失控。参数怎么改:conf_thres 在远距离场景可以适当降低,但要配合后面的平滑;edge_margin 按分辨率比例给,640 输入下 5 像素左右比较合理。

3.2 坐标系与 letterbox 的坑

YOLOv5 推理前会做 letterbox:保持长宽比缩放,再补灰边到正方形。检测框坐标是在 letterbox 后的图上量的,而 fx 是在原始分辨率下标定的。如果直接混用,距离会整体偏。正确做法是把框坐标映射回原始图像坐标系,或者把 fx 换算到 letterbox 后的尺度。常见的实现思路是记录 letterbox 的缩放比 r 和填充偏移 (dw, dh),框高 p 在 letterbox 前后只差一个 r,所以 p_orig = p_letterbox / r,fx 用原始分辨率的即可。这个换算漏掉,误差是系统性的,而且随分辨率变化,排查起来很折磨。

3.3 多目标测距的输出组织

一帧里往往有多个目标,测距模块要输出的是「每个框一个距离」。这里有个容易忽略的点:距离应该挂在框的中心还是底边?对于站在地面上的目标,底边中点更接近「目标与地面的接触点」,用它做后续的平面投影(比如鸟瞰图)更合理;而框中心更适合做屏幕标注。工程上通常两个都留,标注用中心,几何反投影用底边。这个选择不影响单目测距公式本身,但影响下游怎么用这个距离,属于设计取舍而非对错。

4. 误差从哪来:单目测距的精度边界与排查

4.1 距离越远误差越大的数学原因

回到 D = fx·H/p,对 p 求导得 dD/dp = -fx·H/p² = -D²/(fx·H)。也就是说,距离 D 的误差对像素误差 p 的敏感度随 D² 增长。近距离时 p 大,几个像素的抖动无所谓;远距离时 p 小,同样的抖动能让距离跳好几米。这不是实现缺陷,是单目几何的固有属性。理解这一点,你就不会去追求「远距离高精度单目测距」这种不存在的目标,而是把方案定位在「中近距离、精度够用」的场景,远距离交给多传感器或双目。

4.2 真实高度 H 的不确定性

H 是假设值,不是测量值。人的身高在 1.5 到 1.9 米之间波动,车的高度因车型差异更大。用一个固定 H 去测所有同类目标,误差是结构性的。常见的缓解思路是按类别给一个区间,取中位数,并在输出里带上置信范围;更精细的做法是用目标的其他几何特征(比如宽高比)辅助判断子类别。这一步没有银弹,只能靠场景约束把 H 的方差压小。

4.3 俯仰角与安装高度的影响

公式 D = fx·H/p 隐含一个假设:相机光轴水平,且目标高度是从地面算起的完整高度。实际安装时相机往往有俯仰角,甚至装在杆子上有高度。俯仰角会让成像高度 p 与真实几何关系偏离,尤其在画面边缘。严格的解法是把像素坐标反投影到地面平面,需要相机外参(俯仰角、安装高度)。很多快速实现直接忽略这一项,在画面中心区域误差可接受,越靠边缘越离谱。如果你的场景相机是斜装的,这一项必须补,否则调参调到怀疑人生。

5. 避坑与排查:单目测距落地时的常见问题

5.1 距离整体偏大或偏小

现象:所有目标的测距结果都系统性偏大(或偏小)一个固定比例。原因:fx 标定时的分辨率与推理分辨率不一致,或者 letterbox 缩放比没换算。解决:确认 fx 对应的基准分辨率,用 scale_fx 换算到当前推理尺度;检查 letterbox 的 r 是否参与 p 的还原。这类误差是乘性的,表现为「近的偏一点、远的偏很多」,和加性误差(整体平移)要区分开。

5.2 同一目标距离剧烈跳动

现象:静止目标在连续帧里距离上下跳一两米。原因:检测框高度 p 逐帧抖动,单帧直接代入公式放大了噪声。解决:对 p 做滑动平均或卡尔曼滤波,滤波窗口按帧率调,30fps 下 5 到 8 帧比较稳;同时提高 conf_thres 过滤掉边界不稳的框。注意滤波会引入延迟,快速移动目标要权衡。

5.3 近距离目标报出天文数字

现象:明明目标就在眼前,距离却算出几十米甚至上百米。原因:目标被画面边缘截断,框高只有真实成像高度的一小部分,p 偏小导致 D 偏大;或者类别映射错,把矮目标当成了高目标。解决:加 edge_margin 过滤截断框;核对 CLASS_REAL_HEIGHT 映射;对 p 设一个下限,低于阈值直接标记为不可测而非硬算。

5.4 换分辨率后精度崩了

现象:模型从 640 换到 416 推理,测距整体失准。原因:fx 没随分辨率缩放,或者标定时的 p0 是在旧分辨率下量的。解决:fx 按宽度比例缩放;重新在目标分辨率下标定一次 p0;把分辨率作为配置项显式管理,别让它隐式漂移。

5.5 多类别混测时误差忽大忽小

现象:人和车同时测距,人的误差可接受,车的误差很大。原因:车的真实高度方差远大于人,固定 H 对车不成立;或者车的检测框常包含阴影、反光,框高虚高。解决:按类别分别标定和评估误差;对框高做异常值剔除;必要时对车用「车顶到地面」的稳定特征而非整框。

6. 设计取舍:单目测距该用在哪儿、不该用在哪儿

把整条链路拆完,会发现单目测距的本质是「用先验换硬件」:它用已知的真实高度和标定好的焦距,替代了双目或多传感器方案里的深度测量硬件。这个取舍决定了它的能力边界。它的优势是成本低、部署简单、和现有 YOLOv5 检测管线天然兼容,一份 Python 代码就能跑起来;代价是精度强依赖 H 和 fx 的准确性,且误差随距离平方增长,远距离和边缘区域天然不可靠。所以合理的定位是:中近距离(大致十几米内)、目标类别高度相对稳定、相机安装姿态可控的场景,比如室内人员距离估计、近距离车辆预警、固定工位的行为分析。反过来,需要米级远距离精度、或者相机姿态频繁变化的场景,单目方案不该硬扛,应该上双目或融合方案。

验证这套实现是否可信,我一般会做三件事:一是用已知距离的标定物在多个距离点回测,画出「真实距离 vs 测距输出」的散点,看偏差是乘性还是加性;二是固定目标录一段视频,统计距离的帧间方差,评估平滑是否够用;三是把画面分成中心区和边缘区分别统计误差,确认俯仰角影响是否在可接受范围。这三步做完,你对这套代码的能力边界就有数了,不会在错误场景里反复调参。

血泪经验是:单目测距的坑几乎都不在公式,而在标定和坐标系换算这些「看起来不重要」的环节。从那以后我每次接这类项目,都强制先把 fx 的基准分辨率、letterbox 缩放比、类别高度表这三项写成配置并在启动时打印出来核对一遍,再谈精度优化。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询