ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv5单目测距实战:原理、代码与避坑指南

YOLOv5单目测距实战:原理、代码与避坑指南 简介这是一份基于PyTorch的YOLOv5目标检测与单目测距Python实现工程面向计算机视觉初学者以及希望在普通摄像头场景下快速实现“检测—测距”完整流程的开发者。它解决的问题是利用单目视觉估算前方目标距离无需双目或深度相机即可获得大致距离信息。资源压缩包约13.91MB共132个文件其中py为算法实现yaml/yml与xml对应模型结构及参数配置pt为预训练权重sh与Dockerfile用于运行环境部署md、txt提供说明ipynb为教程式笔记另有少量图片可作测试样例整体目录结构清晰便于查阅与复现。目前已有2287人浏览学习可迁移到自动驾驶、安防监控、工业测量等应用场景。包内覆盖YOLOv5网络结构、训练参数配置、加载推理与NMS后处理同时结合相机内参和物体高度完成单目距离估计原理讲解比较简洁配合示例图片和Jupyter教程能帮助理解特征提取、高度估计、三角测量等关键环节对掌握PyTorch工程组织和目标检测落地实现都有参考价值且代码结构便于按需修改。1. YOLOv5 单目测距为什么说原理简单但落地全是坑用一句话说清楚这个项目用YOLOv5检测出目标在图像里的边界框再结合相机焦距和目标真实高度通过相似三角形算出目标与相机的距离。整个过程就是小孔成像加初中几何原理确实很简单。但真拿python和PyTorch跑起来你会发现检测框稍微抖一下测距结果就能从3米跳到5米换个相机焦距不重新标定结果直接翻车。这篇笔记把YOLOv5推理、单目测距公式、代码实现和排错经验串成一条完整链路适合刚接触目标检测和python测距的入门者也适合想快速在现有项目里加一个测距模块的工程师。配套资源里已有YOLOv5-6.1源码目录、tutorial.ipynb示例和bus.jpg测试图照着跑一遍就能看到检测加测距的完整效果。2. YOLOv5 推理流程检测框是怎么从模型变成坐标的2.1 模型加载PyTorch Hub 与本地源码两种方式YOLOv5-6.1这个版本我拆过几次源码最省事的加载方式是PyTorch Hub它会自动下载预训练权重把模型结构和权重一起加载回来。如果是离线环境或者你要改模型结构做自己的训练就克隆官方源码到本地用sourcelocal加载。资源包里那份yolov5-6.1就是源码目录直接可以用第二种方式。import torch # 方式一在线加载自动下载 yolov5s.pt 权重 model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) # 方式二本地源码加载资源包里已包含 yolov5-6.1 目录 model torch.hub.load(./yolov5, yolov5s, sourcelocal, pretrainedTrue)这段代码里第一个参数是来源标识第二个是模型名称。yolov5s是small版本推理速度快显存占用低适合先跑通流程如果检测精度不够再换yolov5m或者yolov5l。pretrainedTrue表示加载在COCO数据集上预训练好的权重能识别80个常见类别bus.jpg里那辆公交车就是其中一类。加载完之后模型会自动进入eval模式之后只需要把图像路径传进去就能拿到检测结果。很多人加载完模型直接丢图忘记设置置信度阈值结果画面里出现一大堆置信度只有0.1的噪声框后处理的时候NMS又删掉一批最后测距用的框可能根本不是最稳的那个。我一般会在加载之后立刻把推理参数设好model.conf 0.5 # 置信度阈值低于此值的检测结果直接丢弃 model.iou 0.45 # NMS 的 IoU 阈值控制重叠框的抑制力度 model.max_det 20 # 每张图最多保留 20 个检测框这三个参数直接影响测距质量。conf设置太低画面里的杂物会被当成目标测出毫无意义的距离设置太高目标漏检测距直接没有输出。单目测距场景我建议conf不要低于0.4因为检测框高度是后续距离计算的唯一几何输入一个不确定的框带来的误差会被距离公式放大得很难看。2.2 后处理从 25200 个候选框过滤到最终检测结果YOLOv5的原始输出不是直接的[x1, y1, x2, y2]。模型在最后会输出一个形状为(1, 25200, 85)的张量25200是三个尺度下anchor数量之和85由4个坐标(cx, cy, w, h)加1个置信度加80个类别概率组成。要拿到标准的边界框需要两步后处理第一步用conf阈值过滤掉低分结果第二步对每个类别做NMS把同一个目标上重叠的候选框合并成一个。YOLOv5源码里这两步封装在non_max_suppression()函数中detect.py调用时一行就完成了。但如果你要自己写后处理需要注意YOLOv5的坐标是中心点加宽高格式得转换成常规的左上角右下角矩形import torch # 模拟模型原始输出中的一条预测中心x, 中心y, 宽, 高, 置信度, 各类别得分 pred torch.tensor([[100.0, 200.0, 80.0, 120.0, 0.85, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9]]) # 中心点宽高格式转左上角右下角格式 x1 pred[..., 0] - pred[..., 2] / 2 y1 pred[..., 1] - pred[..., 3] / 2 x2 pred[..., 0] pred[..., 2] / 2 y2 pred[..., 1] pred[..., 3] / 2 box torch.stack([x1, y1, x2, y2], dim-1) print(box)这段代码演示了坐标格式转换的核心逻辑。YOLOv5训练时预测的是边界框中心坐标和宽高推理输出需要转成(x1, y1)和(x2, y2)。这一步做错的话画出来的框和真实目标位置会差得很明显测距时取的pixel_height自然也是错的。NMS的执行逻辑是按置信度从高到低排序先取最高分框把所有和它IoU超过阈值的框删掉然后处理剩余框中最高分的框直到没有候选框为止。可以用torchvision.ops.nms直接调用也可以翻YOLOv5源码里那份实现。只需要记一个经验NMS的IoU阈值越大保留的框越多同一目标可能出现多个重叠框阈值越小抑制越激进小目标容易漏检。2.3 解析 results 对象把检测数据拉成干净的列表用模型跑完推理之后返回的是一个Detections对象里面封装了渲染后的图片、原始检测数组、裁剪子图等一堆内容。对测距来说关键是results.xyxy它给的是原图分辨率下的像素坐标import numpy as np # 跑YOLOv5推理 results model(bus.jpg) # 取出第一张图的检测结果每行为 [x1, y1, x2, y2, conf, cls] detections results.xyxy[0].cpu().numpy() print(detections) # 逐框解析 for det in detections: x1, y1, x2, y2, conf, cls det pixel_height y2 - y1 # 边界框高度单位像素 pixel_width x2 - x1 # 边界框宽度单位像素 print(f类别ID: {int(cls):d}, 置信度: {conf:.2f}, f框高: {pixel_height:.1f}px, 框宽: {pixel_width:.1f}px)这段代码里的detections是一个二维数组每一行对应一个检测框。xyxy坐标是原始图像像素不是模型输入的640x640缩放坐标。YOLOv5推理时会等比缩放图像检测完成后内部已经做了坐标映射。我建议你在取高度时确认是在原图坐标系里取因为后面标定的焦距也是以原图分辨率为基准的坐标系不一致算出来的距离全是错的。这里还有个容易被忽略的点results.xyxy[0]取出的是第一张图的检测结果如果你传入的是一个包含多张图片的列表就要遍历results.xyxy的每一张图。实时视频流里每帧就是一张图所以基本只操作[0]。3. 单目测距原理小孔成像、相似三角形与焦距标定3.1 小孔成像模型物体成像大小和距离的数学关系单目测距的核心依据是相机的透镜成像模型。可以把它理解成一个暗箱光线透过镜头照射到感光元件上远处一个高为H的物体经过透镜后在图像平面上形成一个高为h的像。两者的几何关系是h / f H / d其中f是焦距单位是像素d是物体到相机的距离。这个式子变形后就得到距离公式d (f × H) / h这就是单目测距的全部数学基础。目标在图像里的像素高度越高说明离相机越近越低离相机越远。这是一个反比关系前提是相机内参固定、目标实际高度已知、目标与相机主光轴的夹角可以忽略。这里需要特别说明式中的f是像素焦距不是镜头上标注的毫米数。毫米焦距要通过传感器尺寸换算成像素值更实用的做法是忽略镜头标签直接用标定反推。我就见过有人拿手机镜头的等效焦距带入公式算出距离差了一倍还把锅扣在YOLOv5检测不准上。3.2 焦距标定已知距离反推像素焦距的实操方案焦距是所有单目测距误差的最大来源也是最容易被忽略的一步。很多人看教程直接拿一个假设焦距值套进去结果测出来距离偏得离谱。其实焦距不需要查参数用已知距离反推法几分钟就能标定出来。操作方法找一个实际高度已知的目标放在一个实际距离已知的位置用YOLOv5检测出像素高度代入公式反推焦距# 标定焦距的过程 KNOWN_DISTANCE 3.0 # 米用卷尺量 KNOWN_HEIGHT 1.7 # 米人的实际身高 # 在3米位置检测一个人YOLOv5输出的像素高度 pixel_height 280.0 # 像素 # 反推像素焦距 focal_length (pixel_height * KNOWN_DISTANCE) / KNOWN_HEIGHT print(f标定得到的像素焦距: {focal_length:.2f} px)这段代码就是标定全过程。KNOWN_DISTANCE是目标到相机的实际距离KNOWN_HEIGHT是目标实际高度pixel_height来自YOLOv5的检测框。标定出来之后把这个值写进配置文件后续所有测距共用这个焦距。标定有几个关键点。第一标定距离别太远3到5米最合适太远检测框高度受像素量化影响大反推出来的焦距误差大。第二目标尽量和相机成像平面平行如果人是斜着站的检测框高度会比真实成像高度小标定出来的焦距偏小。第三镜头畸变会影响边缘区域结果所以标定时把目标放在画面中心附近。提示更严谨的方案是用棋盘格做张正友标定拿到fx、fy、cx、cy完整内参矩阵。但对于只需要给测距模块一个能用焦距的场景已知距离反推法已经够用。3.3 高度比例法为什么用高度而不是宽度单目测距公式里的H选什么直接决定测距稳定性。我一般用目标高度原因有两方面一是很多目标的实际高度先验是稳定的成年人身高在1.7米左右公交车高度在3米左右查标准或实地测一下就有二是目标宽度会随运动和姿态变化一辆卡车斜着开过来检测框宽度一直在变但高度基本不变。高度比例法需要维护的完整参数见下表参数含义获取方式误差敏感度focal_length像素焦距已知距离反推或棋盘格标定高real_height目标实际高度(米)查标准数据或现场实测高pixel_height检测框高度(像素)YOLOv5检测结果中distance目标距离(米)公式计算输出-这张表里最影响结果的其实是real_height。如果你把公交车实际高度设成2.5米而真实值是3米所有测距结果会系统性偏小16%左右。这个偏差是乘法性质的不随距离变化滤波也消不掉只能靠准确的目标高度先验。实际场景还有一个限制YOLOv5只给检测框不会告诉你框内目标是完整还是被截断。目标在画面边缘被切掉一半时检测框高度变成真实成像高度的一半测距结果直接翻倍。这个坑我在第五章单独展开因为它是单目测距项目里最容易被忽视的误差来源之一。4. 代码实现把 YOLOv5 检测输出变成距离数据4.1 环境准备PyTorch、OpenCV 与 YOLOv5 源码目录先确认环境。这个项目基于python和PyTorch核心依赖就四个torch、torchvision、opencv-python、numpy。YOLOv5-6.1源码目录里自带requirements.txt可以直接安装。资源包里的setup.cfg是打包配置Dockerfile是容器化配置tutorial.ipynb是官方入门笔记bus.jpg是测试图整套环境都是齐的。# 创建虚拟环境并安装依赖 conda create -n yolov5 python3.8 -y conda activate yolov5 pip install torch torchvision pip install opencv-python numpy pip install -r requirements.txtrequirements.txt是YOLOv5源码根目录下的依赖清单包含tqdm、pyyaml、matplotlib等。装完之后先验证模型能不能跑通把bus.jpg丢进去做一次检测。如果这一步报错常见原因是torch和torchvision版本不匹配或者CUDA版本不对导致torch无法调用GPU。CPU也能跑只是推理速度慢一些不影响测距逻辑验证。4.2 单目测距核心模块封装一个可复用的类测距逻辑不复杂但写成可复用代码比散写在一堆循环里干净得多。我习惯封装成一个相机类把焦距和目标高度字典塞进去。这样换场景、换相机只需要改配置不用动算法代码。class MonoCameraDistance: def __init__(self, focal_length, real_height_dict): self.focal_length focal_length self.real_height_dict real_height_dict def calculate_distance(self, class_id, pixel_height): # 该类别没有先验高度则返回 None real_height self.real_height_dict.get(int(class_id)) if real_height is None or pixel_height 0: return None # 单目测距公式d f * H / h return (self.focal_length * real_height) / pixel_height这段代码里focal_length是标定得到的像素焦距real_height_dict是一个字典key是YOLOv5的类别IDvalue是对应目标的实际高度。calculate_distance先查字典没有这个类别的先验高度就返回None避免测出没有物理依据的距离。类别ID不能跨数据集混用。YOLOv5默认权重在COCO上训练bus的类别ID是5。如果你加载自己的训练权重类别表就变了real_height_dict的key也要跟着改。我之前就犯过这个错用COCO的ID去查自己数据集的类别测距结果全是None排查了大半天。4.3 整合完整流程检测、测距、结果输出一条链有了模型和测距类接下来把检测和测距串成完整流程读图模型推理解析xyxy提取像素高度算距离输出结果。import torch # 加载本地YOLOv5源码模型 model torch.hub.load(./yolov5, yolov5s, sourcelocal) model.conf 0.5 model.iou 0.45 # 初始化测距器焦距来自标定类别5对应COCO的bus实际高度按城市公交约3米 distance_estimator MonoCameraDistance( focal_length875.0, real_height_dict{5: 3.0} ) image_path bus.jpg results model(image_path) detections results.xyxy[0].cpu().numpy() for det in detections: x1, y1, x2, y2, conf, cls det pixel_height y2 - y1 dist distance_estimator.calculate_distance(cls, pixel_height) if dist is not None: print(f类别ID: {int(cls)} 置信度: {conf:.2f} f像素高度: {pixel_height:.1f}px 距离: {dist:.2f}m) else: print(f类别ID: {int(cls)} 没有先验高度跳过测距)这段代码是完整的测距主线。model(bus.jpg)返回Detections对象results.xyxy[0]取第一张图的检测框x1到y2是原图像素坐标pixel_height由y2减y1得到最后套公式算距离。focal_length填875.0是我在某台相机上标定的值换相机必须重新标定直接抄这个数是错的。运行之后你会看到类似这样的输出类别ID: 5, 置信度: 0.87, 像素高度: 213.5px, 距离: 12.29m。这个结果可不可信取决于标定和检测框质量。想验证就把bus.jpg换成一段视频看连续帧的距离输出是否稳定同时把目标摆到已知位置做比对。如果你想把测距结果直接可视化可以用OpenCV在目标框上方写距离文本import cv2 frame cv2.imread(bus.jpg) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, fdistance: {dist:.2f}m, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(bus_result.jpg, frame)这里cv2.rectangle画检测框cv2.putText把距离文本放在框上方y1减10是让文字和框线保持间距。最后写盘保存方便和原图对比确认测距结果是否合理。注意这段可视化代码里直接用x1、y1、dist变量是因为它紧跟在上一段循环内部使用。如果你要把可视化单独封装记得通过参数把检测框坐标和距离传进去。4.4 视频流里的实时测距帧循环版本静态图跑通之后实时视频流只是在外层加一个循环import cv2 cap cv2.VideoCapture(0) video_distance MonoCameraDistance( focal_length875.0, real_height_dict{0: 1.7} # 类别0对应COCO的person实际高度约1.7米 ) while True: ret, frame cap.read() if not ret: break # 把单帧画面交给YOLOv5推理 results model(frame) detections results.xyxy[0].cpu().numpy() for det in detections: x1, y1, x2, y2, conf, cls det pixel_height y2 - y1 dist video_distance.calculate_distance(cls, pixel_height) if dist is not None: cv2.putText(frame, f{dist:.2f}m, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()实时这个版本把模型推理和测距放在每一帧里执行detections解析和距离计算和静态图完全一样。真正跑起来你大概率会遇到距离抖动的问题这就是第五章第一个要讲的核心坑。5. 避坑与常见问题单目测距翻车现场实录5.1 检测框抖动导致测距结果跳变现象视频流里目标静止不动测距输出却在3米和5米之间来回跳像心电图一样。原因YOLOv5每一帧独立做检测边界框回归本身存在像素级的随机性。检测框高度波动几个像素经过距离公式里除以h的放大效应后距离值就被放大成大幅跳变。解决对测距输出做时间维度的平滑我用得最多的是EMA指数滑动平均。把当前帧距离和上一帧平滑值按权重混合就能显著压低抖动alpha 0.9 smooth_distance 0.0 def filtered_distance(raw_distance, smooth_distance, alpha0.9): return alpha * smooth_distance (1 - alpha) * raw_distancealpha设置0.9时历史值占主导轨迹平稳但目标突然运动时响应偏慢alpha调到0.7左右响应快但抖动压制效果弱。目标静置时可以用高alpha目标运动时调低。5.2 距离越远误差越大10米外基本是玄学现象5米以内测距误差在0.3米左右10米开外误差能到1到2米30米外结果只能当作定性参考。原因距离公式里pixel_height在分母上。目标距离翻倍成像高度减半像素量化误差的相对比例也随之翻倍。640分辨率下一个15米外的3米高目标可能只占二三十个像素检测框差1个像素距离就偏出接近1米。解决从源头上限制单目测距的有效范围。我的经验是10米内的结果可以参考配合滤波后能用于预警类业务超过这个范围只当作目标存在性判断不要当作精确距离。如果你必须用单目做远距离测距可以把模型推理分辨率从640提到1280让目标在图像里占据更多像素但推理时间会明显上涨。5.3 换相机或换分辨率后测距全部失效现象同一套代码在笔记本摄像头上标定好焦距换成USB摄像头后测距结果整体偏大或偏小。原因每个相机的镜头焦距、传感器尺寸不同像素焦距不是通用常数值。改变分辨率也会改变同一物理焦距对应的像素值640分辨率下标定的875到1280分辨率下就不再是875。解决换相机或改分辨率后重新标定而且要把标定结果和分辨率绑定记录。标定流程不长几分钟就能跑完但能省掉后面排错的大量时间。我在多个项目里踩过这个坑后来养成的习惯是每次换硬件先跑一遍已知距离反推焦距再继续测距。5.4 目标被截断导致测距翻倍现象人站在画面边缘身体被图像边界切掉一半测距结果变成实际距离的两倍。原因YOLOv5检测框会只包住可见区域目标被裁掉一半时检测框高度也只有一半。代入距离公式后距离翻倍。这类错误不是随机噪声是系统性的滤波也救不回来。解决测距前加一个检测框位置检查。如果检测框的某个边紧贴图像边界或者检测框宽高比突然变化明显直接丢弃这一帧的测距结果。更稳妥的做法是在业务上要求目标处于画面中央区域比如只在图像宽度中间60%范围内参与测距。5.5 小目标检测框漂移距离时近时远现象远处小目标目标本身没动但检测框在上下浮动测出来的距离一会近一会远。原因小目标在特征图上的有效特征少回归头对边界框定位置信度低同一目标在不同帧里的检测框位置会有几个像素到几十像素的随机摆动。解决小目标测距不要用单帧值连续取5到10帧的检测框高度用中位数或分位数做距离计算。也可以把检测置信度当作可信度标签conf低于0.4的结果标记为低可信度在上层业务里不参与决策。6. 进阶距离平滑、误差验证与单目测距的边界6.1 EMA 滤波让距离曲线稳下来实时视频流的距离输出做好平滑项目可用性会立刻上一个台阶。EMA是最轻量的实现一行代码就能跑。实际项目中我还会再加一层突变保护当前帧距离相比上一帧变化超过30%就认为检测框异常直接沿用上一帧结果。MAX_CHANGE_RATE 0.3 def robust_filter(raw_distance, prev_distance): if prev_distance 0: return raw_distance change abs(raw_distance - prev_distance) / prev_distance if change MAX_CHANGE_RATE: return prev_distance return 0.9 * prev_distance 0.1 * raw_distance这样既压制了高频抖动又挡住了检测框异常导致的单帧跳变。系数0.9和0.3按场景调整目标运动快就放宽突变阈值目标静止就拉紧。6.2 误差验证与单目测距的现实边界验证方法是找一个已知距离的场地每隔2米放一个高度已知的目标用相机测一遍记录测距值和实际距离算相对误差。如果误差曲线在近距离区间平稳远距离快速发散说明是量化误差主导如果在所有距离都是一个固定偏移说明焦距或先验高度标定错了。最后说句实在话单目测距本质上是在做一个强假设就是目标实际高度已知且稳定。单个相机无法恢复绝对尺度所谓测距是把固定先验代入几何公式做推理。目标类型超出先验字典、目标高度假设错误、姿态变化导致成像高度异常都会让测距失真。想要更通用的方案应用双目相机或者深度相机。但如果在受限场景里只需要一个参考距离YOLOv5加单目这条链路是成本最低、见效最快的组合。从那以后我每次跑测距项目都强制走一遍流程先标定焦距再验证误差最后才谈优化希望帮到你。本文还有配套的精品资源点击获取
返回列表