1. 从“框住人”到“算出距离”:这个项目到底在解决什么问题
行人检测这件事,做过的人都知道,YOLO 系列把“框住人”这个任务已经做得相当成熟了。但框住人只是第一步,真正落到实际场景里,比如辅助驾驶预警、公共场所人流密度估计、智能监控里的安全距离判断,光知道“画面里有人”远远不够,你还需要知道“这个人离镜头有多远”。这就是行人距离检测要解决的核心问题。
我最初接触这个方向,是因为一个模拟项目里需要判断画面中行人与摄像头的相对距离,用来做分级预警。当时第一反应是:能不能直接用现成的检测模型输出距离?答案是不能。YOLOv3 这类目标检测网络输出的是边界框的坐标和类别置信度,它压根不知道“深度”这个概念。所以整个项目的技术路线就变成了两条腿走路:一条腿用 PaddleX 快速搭建行人检测能力,另一条腿用几何方法把像素坐标换算成物理距离。
这个项目适合谁看?如果你已经跑通过 YOLO 的 demo,但不知道检测结果怎么进一步利用;或者你想找一个把深度学习检测和传统几何计算结合起来的练手项目;再或者你在做智能监控、辅助驾驶相关的原型验证,需要一套能跑通的距离估计方案,那这篇内容应该能给你省不少时间。我会把 PaddleX 和 YOLOv3 的选型逻辑、距离计算的数学原理、实际调参中踩过的坑,以及怎么把这两块拼成一个完整流程,都掰开揉碎讲清楚。
需要提前说明的是,行人距离检测的精度受很多因素影响,单目视觉测距本身就有理论上的模糊性,所以这个项目定位是“学习项目”和“原型验证”,不是工业级测距方案。但正因为它是学习项目,反而能把每个环节的原理讲透,让你真正理解从像素到米数的每一步是怎么来的。
2. 为什么是 PaddleX 加 YOLOv3 这个组合
2.1 PaddleX 在项目里扮演的角色
PaddleX 是百度飞桨生态里的一个全流程开发工具,它的定位是让开发者用比较少的代码完成模型训练、压缩和部署。在这个项目里,我用它主要做两件事:一是快速加载预训练好的 YOLOv3 模型,二是利用它的推理接口把检测结果标准化输出。
为什么不用原生 PaddleDetection 或者直接手写推理脚本?因为 PaddleX 把很多繁琐的预处理和后处理封装好了。比如图像归一化、通道转换、非极大值抑制这些步骤,如果自己写,代码量不小而且容易出错。PaddleX 的predict接口直接返回检测框的坐标、类别和置信度,格式统一,拿来就能进入下一步的距离计算。对于学习项目来说,减少无关代码的干扰,把精力集中在核心逻辑上,这一点很重要。
另外 PaddleX 支持多种部署方式,导出成推理模型后可以在不同平台上跑。虽然这个学习项目主要是在本地环境验证,但如果你后续想把它放到边缘设备上,PaddleX 的部署工具链能省不少事。这一点在选型时我是有考虑的:学习项目也要留出扩展空间,不能只图当下跑通。
2.2 YOLOv3 作为检测 backbone 的取舍
YOLOv3 放到今天来看,确实不算最新的模型了。YOLOv5、YOLOv8 在精度和速度上都有提升。但我选 YOLOv3 有几个很实际的理由。
第一,YOLOv3 的结构清晰,三个不同尺度的特征图分别负责大、中、小目标检测,这个多尺度机制对于行人检测特别重要。行人在画面里可能很近很大,也可能很远很小,单尺度检测很容易漏掉远处的小目标。YOLOv3 的 FPN 结构天然适合这种场景。
第二,YOLOv3 的预训练权重在 PaddleX 里直接可用,不需要自己从头训练。对于学习项目,用 COCO 数据集预训练的权重已经能检测出行人这一类,直接拿来就能看到效果,省去了数据标注和训练的时间。当然如果你有特定场景的需求,比如夜间红外图像或者特定角度的监控画面,还是需要自己微调,但至少基线是现成的。
第三,YOLOv3 的推理速度在中等算力设备上表现稳定。我实测在一张普通显卡上,输入 416x416 的图像,单帧推理能跑到 30 FPS 以上,对于视频流处理来说够用了。如果你追求更高精度,可以换成 YOLOv5 或者 PP-YOLOE,PaddleX 也支持,替换成本不高。
提示:选 YOLOv3 不是因为它最好,而是因为它在“够用”和“好理解”之间平衡得最好。学习项目优先考虑的是能跑通、能看懂、能改,而不是刷榜。
2.3 检测与测距的衔接点在哪里
整个流程的衔接点就是检测框的坐标。YOLOv3 输出的是[x_min, y_min, x_max, y_max, confidence, class_id]这样的结构,其中x_min, y_min是左上角坐标,x_max, y_max是右下角坐标。距离计算需要的是行人在画面中的位置信息,具体来说,我用到的是检测框的底部中心点,也就是((x_min + x_max) / 2, y_max)。
为什么用底部中心点而不是框的中心?因为行人的脚部位置最能反映他与地面的接触点,而地面与摄像头的几何关系是测距的基础。如果用框的中心,当行人被遮挡或者检测框有偏差时,中心点会漂移,导致距离估计不稳定。底部中心点相对更鲁棒,这也是很多单目测距方案里的常见做法。
这个衔接点看起来简单,但实际调试时我发现,检测框的底部坐标对置信度阈值很敏感。阈值设低了,误检框的底部会乱跳;阈值设高了,远处的小目标又检测不到。这个平衡后面会专门讲。
3. 单目测距的几何原理:从像素坐标到物理距离
3.1 相机成像模型与相似三角形
单目测距的核心原理其实就一个:相似三角形。想象你站在窗前看外面的人,人在玻璃上会形成一个投影。如果人离窗户近,投影就大;离得远,投影就小。相机成像也是同样的道理,物体的实际尺寸、在图像中的像素尺寸、物体到相机的距离,这三者之间存在一个比例关系。
具体来说,假设行人的实际高度是 H,在图像中的像素高度是 h,相机的焦距是 f(以像素为单位),那么距离 D 可以用这个公式估算:
D = (H * f) / h这个公式的推导来自相似三角形:实际高度与距离的比值,等于像素高度与焦距的比值。整理一下就得到上面的式子。这里的关键参数是 f,它不是一个物理长度,而是把物理焦距换算成像素单位后的值。换算方式是:
f = (物理焦距 / 传感器宽度) * 图像宽度像素数如果你用的是普通 USB 摄像头或者手机摄像头,物理焦距和传感器宽度通常可以在设备规格里查到。如果查不到,也可以用标定方法反推,后面会讲。
3.2 为什么用行人高度而不是宽度
理论上,用行人的高度或者宽度都可以做相似三角形计算。但实际项目中我选择用高度,原因有几个。
行人的宽度变化太大。一个人侧身站和正面站,宽度能差一倍。而且手臂摆动、背包、撑伞这些因素都会让宽度检测框变得不稳定。高度虽然也受姿态影响,但相对而言,站立行人的高度变化范围更可控。除非是蹲着或者躺着,否则高度不会出现数量级的变化。
另外,行人的平均身高有一个比较稳定的先验值。成年男性平均身高大约 1.7 米,女性大约 1.6 米,如果考虑不同地区差异,可以取 1.65 米作为默认值。这个先验值虽然粗糙,但在学习项目里足够用了。如果你要做更精确的估计,可以根据检测框的宽高比来推断行人的姿态,进而修正高度估计,但那是进阶内容了。
注意:用固定身高先验会带来系统性误差。比如画面里同时有小孩和成年人,用同一个身高值算距离,小孩的距离会被高估。实际项目中可以考虑用检测框的宽高比做简单分类,或者直接接受这个误差,因为学习项目的目标是理解原理,不是追求极致精度。
3.3 焦距标定的两种实用方法
焦距 f 的准确性直接决定测距精度。我试过两种标定方法,各有适用场景。
第一种是查规格书。大部分摄像头模组的规格书里会给出焦距(比如 3.6mm)和传感器尺寸(比如 1/2.7 英寸)。1/2.7 英寸的传感器宽度大约是 5.27mm。假设图像宽度是 1920 像素,那么:
f = (3.6 / 5.27) * 1920 ≈ 1311 像素这个方法简单,但问题是很多廉价摄像头的规格书不准确,甚至根本没有规格书。这时候就需要第二种方法。
第二种是实验标定。找一个已知身高的人,站在已知距离处,拍一张照片,测量他在图像中的像素高度,然后反推 f:
f = (h * D) / H比如一个人身高 1.7 米,站在 5 米处,在图像中高度是 400 像素,那么 f = (400 * 5) / 1.7 ≈ 1176 像素。为了减小误差,可以多测几组不同距离的数据,取平均值。我一般会测 3 米、5 米、8 米三组,然后看反推的 f 是否一致。如果差异很大,说明测量过程有问题,比如距离没量准或者行人没站直。
实测下来,实验标定得到的 f 值往往比规格书计算的更靠谱,因为它把镜头畸变等实际因素也间接包含进去了。但要注意,标定时的图像分辨率必须和实际使用时一致,否则 f 值要按比例缩放。
4. 把检测结果变成距离数字:完整实现链路
4.1 环境搭建与模型加载
先说一下环境。PaddleX 的安装不算复杂,但版本兼容性要注意。我用的组合是 Python 3.8 加 PaddlePaddle 2.4 加 PaddleX 2.1,这个组合在我这边跑下来最稳定。安装命令大致是这样:
pip install paddlepaddle-gpu==2.4.0 pip install paddlex==2.1.0如果你没有 GPU,把paddlepaddle-gpu换成paddlepaddle就行,CPU 推理也能跑,只是速度慢一些。安装完成后,加载 YOLOv3 模型只需要几行代码:
import paddlex as pdx model = pdx.load_model('yolov3_coco')这里yolov3_coco是 PaddleX 内置的预训练模型名称,它会自动下载权重。第一次运行需要联网下载,之后会缓存到本地。如果你在离线环境,可以提前把模型文件下载好,用本地路径加载。
加载完成后,可以用一张测试图片验证模型是否正常工作:
result = model.predict('test.jpg') print(result)输出是一个列表,每个元素对应一个检测框,包含category、bbox、score等字段。bbox就是[x_min, y_min, x_max, y_max]格式的坐标。确认能正常输出后,就可以进入下一步了。
4.2 从检测框提取有效行人目标
模型输出的结果里包含各种类别,我们只关心行人。COCO 数据集里行人的类别 ID 是 0,类别名称是person。所以第一步是过滤:
person_boxes = [r for r in result if r['category'] == 'person' and r['score'] > 0.5]这里的score > 0.5是置信度阈值。这个阈值我调过很多次,0.5 是一个比较平衡的值。设成 0.3 会引入很多误检,设成 0.7 又会漏掉远处的小目标。实际使用时可以根据场景调整:如果画面里行人比较清晰,可以提到 0.6;如果光线不好或者行人较远,可以降到 0.4。
过滤之后,对每个行人框计算底部中心点:
for box in person_boxes: x_min, y_min, x_max, y_max = box['bbox'] bottom_center_x = (x_min + x_max) / 2 bottom_center_y = y_max pixel_height = y_max - y_minpixel_height就是行人在图像中的像素高度,它是距离计算的关键输入。这里有个细节:YOLOv3 输出的坐标是浮点数,直接用就行,不需要取整。取整会引入不必要的误差。
4.3 距离计算与结果平滑
有了像素高度和标定好的焦距,距离计算就是一行代码的事:
D = (H * f) / pixel_height其中 H 是行人真实身高先验值,我默认取 1.65 米;f 是标定得到的焦距像素值。
但实际跑视频的时候,你会发现单帧计算出来的距离会跳动。原因很简单:检测框在每一帧都会有微小变化,像素高度跟着变,距离就跟着跳。如果直接把每帧的距离显示出来,数字会闪得没法看。
解决办法是做滑动平均。我一般维护一个长度为 5 的队列,每次把新计算的距离放进去,然后取平均值作为输出:
from collections import deque distance_queue = deque(maxlen=5) def smooth_distance(new_distance): distance_queue.append(new_distance) return sum(distance_queue) / len(distance_queue)这个简单的平滑能显著改善显示效果。窗口长度 5 是我试出来的经验值:太短了平滑效果不够,太长了响应延迟明显。如果行人在快速移动,可以缩短到 3;如果画面比较静止,可以加到 7 或 10。
提示:平滑只应该用在显示和预警判断上,如果你需要记录原始数据做分析,建议把原始距离和平滑后的距离都保存下来,方便后续对比。
4.4 可视化与预警逻辑
把距离数字标在画面上,最直观的方式是用 OpenCV 画框和文字:
import cv2 cv2.rectangle(frame, (x_min, y_min), (x_max, y_max), (0, 255, 0), 2) cv2.putText(frame, f'{D:.2f}m', (x_min, y_min - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)预警逻辑可以根据距离分级。比如距离小于 2 米用红色框,2 到 5 米用黄色,大于 5 米用绿色。这样一眼就能看出哪些行人进入了危险区域。分级阈值要根据实际场景定,辅助驾驶场景可能 2 米才预警,公共场所人流管理可能 1 米才需要关注。
这里有个容易忽略的点:画面边缘的行人检测框可能不完整,导致像素高度偏小,距离被高估。我一般会过滤掉检测框贴近画面边缘的目标,或者对边缘目标降低置信度权重。这个处理不是必须的,但能让结果更稳定。
5. 实测中那些让人头疼的问题与解法
5.1 检测框抖动导致距离跳变
这是最常见的问题。即使行人是静止的,检测框也会在几个像素范围内抖动,导致距离计算值上下波动。除了前面说的滑动平均,还有一个办法是增大检测的输入分辨率。YOLOv3 默认输入是 416x416,如果改成 608x608,检测框会更精细,抖动会小一些,但推理速度会下降。我实测 608 输入下,单帧推理时间从 30ms 增加到 60ms 左右,如果对实时性要求不高,这个代价可以接受。
另一个思路是对检测框做卡尔曼滤波,把检测框的坐标当作状态量来平滑。这个方法效果更好,但实现复杂度也更高。学习项目里用滑动平均就够了,卡尔曼滤波可以作为进阶练习。
5.2 远处小目标检测不到
YOLOv3 对小目标的检测能力有限,画面里远处只有几十个像素高的行人,很容易被漏掉。我试过几种改善方法。
降低置信度阈值是最直接的,但会引入误检。比较平衡的做法是把阈值降到 0.3,同时开启 YOLOv3 的多尺度检测。PaddleX 的 YOLOv3 默认就是多尺度输出,但后处理时的 NMS 阈值会影响小目标的保留。把 NMS 的 IoU 阈值从 0.45 调到 0.5,能让更多邻近的小目标框保留下来。
如果还是检测不到,可以考虑用图像金字塔:把原图放大一倍再检测一次,然后把结果映射回原图坐标。这个方法能显著提升小目标检测率,但计算量翻倍。我一般只在特定区域(比如画面中央远处)做放大检测,而不是全图放大。
5.3 身高先验带来的系统性偏差
前面提到过,用固定身高先验会导致小孩的距离被高估、高个子的距离被低估。在实际测试中,一个 1.2 米的小孩站在 3 米处,用 1.65 米先验算出来可能是 4.1 米,误差超过 30%。这个误差在预警场景里可能是致命的。
改善方法有几个。最简单的是根据检测框的宽高比做粗略分类:宽高比大于 0.5 的可能是成年人,小于 0.4 的可能是小孩。然后对小孩用 1.2 米的身高先验。这个方法不精确,但比一刀切好。
更复杂的方法是用行人重识别或者姿态估计来推断身高,但那已经超出这个学习项目的范围了。我的建议是:如果你的应用场景里行人构成比较单一(比如都是成年人),固定先验够用;如果构成复杂,要么接受误差,要么上更复杂的方案。
5.4 相机俯仰角带来的误差
前面的距离公式假设相机光轴是水平的,行人站在与相机等高的地面上。但实际安装时,相机往往有俯仰角,比如监控摄像头通常向下倾斜。这时候底部中心点的像素坐标对应的实际地面位置会偏移,距离计算会出现系统性误差。
修正的方法是引入相机高度和俯仰角,用更完整的投影几何来计算。具体来说,需要知道相机离地高度 h_cam 和俯仰角 θ,然后根据底部中心点的像素纵坐标 y 来计算距离:
D = h_cam / tan(θ + arctan((y - c_y) / f))其中 c_y 是图像主点的纵坐标,通常近似为图像高度的一半。这个公式比简单相似三角形更准确,但需要额外标定相机高度和俯仰角。我在实际项目里会先用简单公式跑通,然后如果发现距离系统性偏大或偏小,再引入俯仰角修正。
注意:俯仰角修正对参数很敏感,θ 差一度,距离可能差几十厘米。标定俯仰角时可以用一个已知距离的地面标记来反推,比直接测量角度更靠谱。
6. 从学习项目到可用原型:还能怎么优化
6.1 换用更轻量的检测模型
YOLOv3 的精度够用,但如果你要在边缘设备上跑,模型大小和计算量还是偏大。PaddleX 里可以换成 PP-YOLO Tiny 或者 MobileNet 作为 backbone 的检测模型,速度能提升一倍以上,精度下降在可接受范围内。替换模型后,检测框的坐标格式不变,距离计算部分完全不用改,这就是模块化设计的好处。
6.2 多帧信息融合提升稳定性
单帧测距的局限性很明显,如果能利用多帧信息,稳定性会好很多。一个简单的做法是跟踪检测框:用 SORT 或者 ByteTrack 给每个行人分配 ID,然后对同一个 ID 的距离序列做滤波。这样即使某一帧检测框跳了,也不会影响整体输出。PaddleX 本身不包含跟踪模块,但可以配合 PaddleDetection 里的跟踪器使用,或者自己写一个简单的 IoU 匹配跟踪。
6.3 用深度相机做交叉验证
如果你手头有深度相机,比如基于结构光或者 ToF 的设备,可以用它来验证单目测距的精度。深度相机直接输出每个像素的深度值,取检测框底部中心点的深度作为参考距离,和单目计算值对比。我做过几组对比,在 2 到 6 米范围内,单目测距的误差大约在 10% 到 20% 之间,深度相机的误差在 5% 以内。这个对比能帮你判断单目方案是否满足需求。
6.4 部署时的性能考量
如果要把这个流程做成实时视频处理,性能优化绕不开。几个实用的点:一是用多线程,检测和显示分开跑,避免显示阻塞检测;二是降低显示分辨率,检测用原图,显示可以缩小一半;三是用 PaddleX 的推理加速接口,开启 TensorRT 或者 OpenVINO 后端,速度能提升不少。我实测在开启 TensorRT 后,YOLOv3 的推理速度从 30ms 降到 15ms 左右,对于 30 FPS 的视频流来说绰绰有余。
最后分享一个我在调试过程中总结的小技巧:把检测框的像素高度和计算出的距离同时打印出来,观察它们的变化关系。如果像素高度突然变小但距离没有相应变大,说明检测框出问题了;如果像素高度稳定但距离跳动,说明焦距或者身高先验有问题。这个简单的日志能帮你快速定位问题出在检测环节还是计算环节。