
简介面向计算机视觉初学者与自动驾驶、机器人导航方向开发者这份资源围绕YOLOv5目标检测与单目深度估计的结合展开解决无额外深度硬件条件下如何从单张彩色图像推断物体距离的问题。压缩包共132个文件约13.87MB以34个py脚本、40个yaml配置、16个pyc缓存及11个yml环境文件为主另含pt预训练权重、ipynb教程笔记、sh运行脚本与Dockerfile部署文件覆盖模型代码、训练与测试脚本、数据集配置等模块。已有3868人学习下载说明其在目标检测与三维测距交叉场景中具备一定参考价值。读者可据此了解YOLOv5的锚框预测与特征提取流程掌握深度网络将二维像素映射为深度值的实现思路并借助预训练权重与示例图像快速跑通检测加测距的完整链路为自动驾驶、无人机与智能监控中的距离感知提供可复用的工程起点。1. 单目测距为什么总被说“不准”先看清这套 YOLOv5 方案到底在算什么一个摄像头、一张图、一个检测框就想把前方目标的距离估出来这事听起来像玄学其实背后是一套很朴素的几何关系。这套 YOLOv5 加单目测距的 Python 实现核心不是让模型直接“学会”距离而是用检测框的像素高度去反推目标在三维空间里的纵深。它解决的是低成本场景下的距离感知问题没有双目、没有激光雷达只有普通 RGB 摄像头却要给出一个可用的距离估计值。适合谁做嵌入式视觉 Demo 的、搞智能小车避障原型的、想在人脸或车辆检测之后补一层空间信息的从业者。三维测距这个词听着唬人但单目方案的天花板就在那里先把原理立住后面复现才不会翻车。这套代码的骨架分两块YOLOv5 负责把目标框出来测距模块负责把框的像素尺寸换算成米。很多人第一次跑会发现距离跳得厉害不是代码写错了而是没搞清它依赖的假设——目标物理尺寸已知、相机内参标定过、目标大致在光轴附近。这三个条件缺一个输出就只能当参考值看。下面从工程落地的角度把这份资源拆成能照着跑、能改参数、能判断边界的样子。2. 把检测框变成距离单目测距的几何模型与参数标定2.1 相似三角形模型像素高度到物理距离的换算链单目测距最常用的模型是针孔相机下的相似三角形。设目标真实高度为 H米在图像中的像素高度为 h像素相机焦距为 f像素目标到相机的距离为 D米则有D (H × f) / h这个式子就是整套测距模块的心脏。YOLOv5 输出的检测框给出 hH 由你按目标类别预设f 来自相机标定。代码里通常把 f 和 H 做成可配置项按类别查表。比如行人取 1.7 米小汽车取 1.5 米这些值是经验值不是测量值所以同一类别里高矮胖瘦都会带来误差。焦距 f 的获取有两种常见做法。一种是查相机规格书用像素焦距近似另一种是拿已知距离的标定物反算。我一般会后者因为规格书给的往往是物理焦距换算成像素焦距还要乘传感器像素密度中间容易出错。反算公式很直接f (D_known × h_pixel) / H_known拿一个已知高度、已知距离的物体拍一张量出它的像素高度代进去就得到 f。这个 f 是整段代码里最该认真对待的参数它偏 5%距离就偏 5%。2.2 相机标定与焦距反算用棋盘格把 f 钉死如果手头有棋盘格走标准标定流程更稳。OpenCV 的 calibrateCamera 会同时给出内参矩阵和畸变系数内参矩阵里的 fx、fy 就是像素焦距。下面这段是标定脚本的核心逻辑拍 15 到 20 张不同角度的棋盘格图跑一遍即可。import cv2 import numpy as np import glob # 棋盘格内角点数量注意是内角点不是格子数 pattern_size (9, 6) square_size 0.025 # 每个格子边长单位米 objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) * square_size obj_points [] img_points [] images glob.glob(calib_images/*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: obj_points.append(objp) # 亚像素角点优化提升标定精度 corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) img_points.append(corners2) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera(obj_points, img_points, gray.shape[::-1], None, None) print(内参矩阵:\n, mtx) print(畸变系数:\n, dist)这段代码里 pattern_size 必须和实际棋盘格内角点数一致写成格子数是最常见的翻车点。square_size 只影响外参平移量的量纲对 fx、fy 没影响但填错会让后续重投影误差看着很怪。标定完成后把 mtx[0][0] 和 mtx[1][1] 取出来就是 fx 和 fy单目测距用其中一个即可通常取 fx。畸变系数建议在检测前先做 undistort尤其是广角镜头边缘目标的框会被拉变形像素高度直接失真。2.3 检测框高度提取YOLOv5 输出怎么接进测距模块YOLOv5 的推理输出是 xyxy 格式的框坐标测距只需要框的高度。但这里有个细节框的上下边是否稳定直接决定距离曲线抖不抖。常见做法是取连续多帧的中位数做平滑而不是单帧直接用。下面是把检测结果接进测距的典型写法。import numpy as np # 按类别预设目标真实高度单位米 REAL_HEIGHT { person: 1.70, car: 1.50, truck: 3.00, bicycle: 1.10, } def estimate_distance(box, cls_name, focal_px): x1, y1, x2, y2 box h_pixel y2 - y1 if h_pixel 0: return None H REAL_HEIGHT.get(cls_name) if H is None: return None # 相似三角形D H * f / h return (H * focal_px) / h_pixel # 多帧平滑窗口长度 5 class DistanceSmoother: def __init__(self, window5): self.buf {} self.window window def update(self, track_id, dist): if dist is None: return None self.buf.setdefault(track_id, []).append(dist) if len(self.buf[track_id]) self.window: self.buf[track_id].pop(0) return float(np.median(self.buf[track_id]))estimate_distance 里 h_pixel 为 0 或负值要直接返回 None否则会除零或出负距离。REAL_HEIGHT 这张表是误差的主要来源之一行人 1.7 米对成年人尚可对小孩就偏大距离会被低估。DistanceSmoother 用中位数而不是均值是为了抗单帧框跳变窗口取 5 是折中太大延迟高太小平滑不够。如果检测器带跟踪 ID按 ID 分别平滑没有跟踪就退化成全局平滑效果会差一些。3. 把 YOLOv5 和测距串起来推理流程、类别映射与可视化3.1 推理主循环从读帧到画距离标签整套流程的主循环不复杂读帧、推理、取框、算距离、画图。难点在于把 YOLOv5 的输出格式和测距模块对齐以及控制每帧的耗时。下面是一个可运行的主循环骨架基于 ultralytics 接口模型权重按实际路径替换。import cv2 from ultralytics import YOLO model YOLO(yolov5s.pt) # 权重文件按实际路径替换 FOCAL_PX 800.0 # 由标定得到示例值 smoother DistanceSmoother(window5) cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break results model(frame, verboseFalse)[0] for i, box in enumerate(results.boxes): xyxy box.xyxy[0].cpu().numpy() cls_id int(box.cls[0]) cls_name model.names[cls_id] dist estimate_distance(xyxy, cls_name, FOCAL_PX) dist smoother.update(i, dist) x1, y1, x2, y2 map(int, xyxy) label f{cls_name} {dist:.2f}m if dist else cls_name cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(mono distance, frame) if cv2.waitKey(1) 0xFF 27: break cap.release() cv2.destroyAllWindows()FOCAL_PX 必须换成你自己标定的值直接抄示例值距离会整体偏。smoother.update 的第一个参数这里用了框的索引 i严格说应该用跟踪 ID否则目标顺序一变平滑就串了。verboseFalse 是为了压掉每帧日志实测对帧率有可见帮助。画标签时距离保留两位小数足够再多是假精度。这套循环在普通笔记本上跑 yolov5s 加 640 输入帧率大概在十几到二十几帧具体看硬件别指望实时高帧率。3.2 类别映射表哪些类能测、哪些类测了也白测不是所有检测类别都适合单目测距。能测的前提是目标有一个相对稳定的物理高度且这个高度和像素高度大致成正比。行人、车辆、自行车这类直立或规则目标可以桌子、椅子、杯子这类姿态多变、遮挡严重的目标框高度和真实尺寸关系很弱测出来基本是噪声。类别建议真实高度米适用性说明person1.60 到 1.75较好站姿稳定蹲坐时误差大car1.45 到 1.55较好轿车高度差异小truck2.80 到 3.20一般车型差异大需分细类bicycle1.00 到 1.15一般骑行姿态影响框高chair不固定差姿态和遮挡导致框高失真这张表不是让你照抄而是提醒你每加一个类别就要问一句“它的框高和真实高度是不是稳定对应”。我见过有人把杯子也加进去结果距离在 0.3 米到 3 米之间乱跳血泪经验就是类别表宁少勿滥。真要测矮小目标单目这套模型本身就不合适得换方案。3.3 可视化与阈值距离标签怎么画才不误导可视化不只是画个框写个数字。距离值本身有误差带画的时候最好把“可信区间”也体现出来至少别把小数点后两位当成真值。常见做法是距离超过某个阈值就变色比如 5 米内绿色、5 到 15 米黄色、15 米以上红色提示远距离估计已经不可靠。另一个细节是标签位置。框太靠上时 y1-8 会画出画面得做边界判断。还有框重叠时标签互相压盖简单做法是只给置信度最高的几个目标画距离其余只画框。这些不是核心算法但直接影响这套 Demo 能不能拿去给人看。实测下来远距离目标的框高只有十几个像素一两个像素的抖动就能让距离跳一两米所以超过 20 米的目标标签写个“20m”比写具体数值诚实得多。4. 避坑与排查单目测距最常见的五类翻车现场4.1 距离整体偏大或偏小先查焦距和真实高度现象是所有目标的距离都系统性偏一个比例近处偏、远处也偏。原因几乎总是 FOCAL_PX 或 REAL_HEIGHT 填错。焦距用了规格书物理焦距没换算或者真实高度按“印象”填。解决方法是拿一个已知距离的标定物在画面里量像素高度反算 f再拿另一个距离验证。两个点都对上参数才算可信。4.2 距离随帧剧烈跳动框高不稳或没做平滑现象是同一个目标距离在 2 米到 6 米之间来回跳。原因是检测框上下边逐帧变化尤其目标边缘和背景对比弱时。解决方法是加多帧中位数平滑并检查是否误用了均值。如果检测器本身框就不稳先换更大输入尺寸或更高精度权重再谈测距。4.3 远处目标距离完全不可信像素高度太小现象是 20 米外目标距离乱报。原因是框高只剩十几像素量化误差被相似三角形放大。解决方法是设距离上限超过阈值只显示“远”不显示数值或者直接不测。别试图用插值或滤波把远距离“救回来”那是自欺欺人。4.4 广角镜头边缘目标距离偏差大畸变没校正现象是画面中心目标距离还行边缘目标明显偏。原因是广角畸变让边缘目标的框被拉伸像素高度不等于校正后的高度。解决方法是在推理前对帧做 undistort用标定得到的畸变系数。代价是每帧多一次重映射帧率会掉一点但边缘精度提升明显。4.5 多目标平滑串号没用跟踪 ID现象是两个目标交叉后距离标签互换。原因是平滑器按框索引存历史索引一变历史就错位。解决方法是接入跟踪器拿稳定 ID按 ID 平滑。没有跟踪器时至少按类别加空间距离做简单关联别直接用索引。5. 进阶验证与参数微调让距离值经得起追问这套东西跑起来容易被人问“你这距离准吗”就露馅。我的习惯是做一个简易验证台地上每隔 1 米贴一个标记把目标放在 2、4、6、8、10 米处各拍一段记录估计值和真值算平均绝对误差。误差在 10% 以内这套参数就算能用超过 20%别急着调代码先回去查标定和真实高度。微调的顺序也有讲究。先固定焦距不动调真实高度表让中距离段误差最小再回头微调焦距看整体比例是否改善最后才动平滑窗口。顺序反了会互相干扰调半天不知道是谁的锅。下面这个小脚本可以批量跑验证图输出误差表。import os import numpy as np from ultralytics import YOLO model YOLO(yolov5s.pt) FOCAL_PX 800.0 GT {img_2m.jpg: 2.0, img_4m.jpg: 4.0, img_6m.jpg: 6.0, img_8m.jpg: 8.0, img_10m.jpg: 10.0} errors [] for fname, gt in GT.items(): path os.path.join(verify, fname) res model(path, verboseFalse)[0] if len(res.boxes) 0: continue # 取置信度最高的框 idx int(res.boxes.conf.argmax()) xyxy res.boxes.xyxy[idx].cpu().numpy() cls_name model.names[int(res.boxes.cls[idx])] est estimate_distance(xyxy, cls_name, FOCAL_PX) if est is None: continue err abs(est - gt) / gt errors.append(err) print(f{fname}: 估计 {est:.2f}m, 真值 {gt:.2f}m, 相对误差 {err*100:.1f}%) print(f平均相对误差: {np.mean(errors)*100:.1f}%)这段脚本的关键是“取置信度最高的框”验证阶段画面里最好只有一个目标否则取错框会让误差表失真。GT 字典里的文件名和真值要按实际拍摄替换。跑完看平均相对误差再决定是调参数还是接受现状。我一般会把误差表留一份后面换相机、换镜头时拿来做对比基线。还有一个容易被忽略的点这套单目测距的输出是沿光轴方向的纵深估计不是目标到相机的直线距离。目标偏离画面中心越远两者差异越大。如果下游逻辑要的是直线距离得用框中心像素坐标加内参做一次反投影把横向偏移补进去。这一步不是必须但被人追问“斜前方的车距离多少”时光轴距离会答错。从那以后我每次交付这类 Demo都会在文档里写清“本距离为光轴纵深估计”并附上验证误差表省得后面扯皮。希望帮到你。本文还有配套的精品资源点击获取