简介:基于YOLOv5的单目测距系统源码毕业设计项目,面向计算机视觉相关专业学生、毕业设计者及目标检测初学者,解决利用单目摄像头实时检测目标并估算距离的问题。压缩包共79个文件,约215.35MB,包含28个py源码(涵盖模型训练、推理、距离计算、数据预处理等)、26个yaml配置(模型结构、数据集与超参数)、6个sh脚本、2个pt权重文件,以及mp4演示视频、Dockerfile、xml标注、requirements依赖和说明文档,目录模块清晰,便于按需查阅。项目完整覆盖YOLOv5的数据准备、模型定制、训练、评估与部署流程,附有coco128/argoverse等数据集配置和预训练权重,其中集成了单目测距模块,可直接运行detect脚本进行实践与二次开发。已有373人学习下载,适合作为毕业设计参考,也可用于快速掌握目标检测与单目测距的工程实现。
1. 拿到压缩包先别急着训:这个项目真正值钱的是 A4 纸先验
单目测距一直被框定在“尺度不确定性”这个死结里:没有深度传感器,同一辆车在 5 米和 20 米处投影到 CMOS 上的物理尺寸差很多,仅凭单帧像素无法直接还原真实距离。这套基于 YOLOv5 的毕业设计源码给的解法很务实——用已知物理尺寸的物体做尺度基准。项目中detect_A4.py的存在说明它把 A4 纸当作标准参照物:长宽已知、到处都有、几乎零成本,正好能塞进“先验尺寸 → 焦距比 → 距离”这条闭环。
适合拿这个人入手的是两类人:一类是计算机视觉方向的毕业生,想把目标检测和空间几何结合起来做完整系统;另一类是已经跑通 YOLOv5 检测、但还不会把 bounding box 转换成物理距离的工程师。压缩包里code目录下同时出现了kerman.py(卡尔曼类的滤波与平滑)、realsensedetect.py(实时视频流检测)和detect_A4.py(A4 测距主流程),意味着这不是单一算法文件,而是从检测到测距后处理的完整链路。接下来按工程落地的顺序拆:模型怎么选、测距公式怎么落进代码、实时视频怎么调、最后一层误差怎么压。
2. YOLOv5 模型选型与训练配置:拿哪版权重、anchor 和超参怎么调
2.1 为什么是这个项目选 YOLOv5 而不是 Faster R-CNN 或 YOLOv8
单目测距对检测器的要求不是极限精度,而是三个工程属性:帧率、小目标稳定性、部署简单。Faster R-CNN 在 COCO 上的 AP 可能略高,但两阶段结构在前向推理时的 ROI 池化和第二次分类开销,在 CPU 或者低端 GPU 上很难跑到实时。YOLOv8 虽然新,但它的解耦头和 anchor-free 策略在某些自定义小数据集上反而更难收敛,尤其是 A4 纸这类背景占比小、形态单一的目标。YOLOv5 的 anchor-based 检测头配合多尺度特征图(P3/P4/P5),在 640×640 输入下对中小尺寸物体有天然的召回优势,而且hyp.scratch.yaml里的数据增强配置已经被 Ultralytics 在 COCO 上验证过,挑出来直接用,不必从零调增广策略。
这个压缩包里的models/yolov5s.yaml就是默认的 s 版本:depth_multiple=0.33、width_multiple=0.50。对 A4 纸检测来说,跑yolov5m或者yolov5l会浪费算力,因为 A4 纸是平面物体,纹理和形状特征都很简单,不需要深层网络去提取复杂语义;而yolov5s在单张 1080Ti 上能轻松跑到 80 FPS 以上,把帧率预算留给后端的距离滤波。
2.2 训练前的数据集准备:不一定要从零标,但要明白格式
data/coco128.yaml已经给出了标准 COCO 格式的目录映射方式,data/argoverse_hd.yaml则是针对自动驾驶数据集 Argoverse-HD 的配置。这两个 yaml 文件不是让你直接用,而是提供结构参考:训练自己的 A4 纸数据集时,标注文件应放在labels/目录下,每个 txt 文件名与图片名一致,内容是class x_center y_center width height,坐标全部做归一化。如果是单人毕设,拍摄 A4 纸时带上标尺,先用 LabelImg 框出纸张四个角生成矩形框,再写脚本把它转成 YOLO txt 格式。类别的 id 不要用 0,因为 COCO 里 0 是 person,容易在后续分析时误判;建议在数据配置里把 A4 纸这一类的 id 设为 1(从coco.yaml的类别顺序看,第 0 类 person 占据优先位置,A4 纸作为新增类别排在后面更合理)。
2.3 超参数配置文件里真正值得动的几个开关
hyp.scratch.yaml里有几十个超参数,但针对 A4 纸检测,注意力只需要放在 4 个位置:
| 参数 | 默认值 | 推荐调整方向 | 理由 |
|---|---|---|---|
lr0 | 0.01 | 0.005~0.02 | A4 纸类别的正样本数量少,学习率大容易震荡 |
mosaic | 1.0 | 0.5~0.8 | Mosaic 把 4 张图拼接,A4 纸容易变形,降低概率让几何特征稳定 |
degrees | 0.0 | 5.0~10.0 | 加一点旋转增广,模拟手持摄像头角度变化 |
fliplr | 0.5 | 0.5 保留 | 水平翻转不改变 A4 纸的物理尺寸语义 |
训练命令通常长这样:
python train.py \ --data data/a4.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp hyp.scratch.yaml \ --project runs/train_a4逻辑说明:--weights yolov5s.pt加载的是 COCO 预训练权重,能提供通用的特征提取能力(边缘、纹理),A4 纸检测本质上只需要在最后一层新增输出头;--img 640控制模型输入分辨率,分辨率太高(如 1280)会显著增加推理延迟,对测距场景不划算;--hyp指定超参文件,如果显存只有 8GB,--batch 16在 640 分辨率下已经是上限。训练完看runs/train_a4/weights/best.pt的 mAP@0.5 是否超过 0.95,如果卡在 0.9 以下,优先检查标注框是否紧贴纸张边缘,YOLO 对标签框偏移比较敏感,框大了或小了 10 个像素,距离误差都会在后续计算中被放大。
2.4 一个容易忽视的坑:A4 纸的方向和纵横比会影响检测置信度
A4 纸的宽高比是 1:1.414(210mm×297mm),当摄像头正对纸面时检测框几乎是标准的矩形,置信度很高;但倾斜超过 30° 时,YOLOv5 的矩形框会把纸张连同桌面背景一起框进去,导致 bounding box 宽度偏大。这个现象在测距阶段会直接带来距离偏小的误差,因为宽度变大,按相似三角形算出的距离就变小。训练阶段用degrees: 10.0增强倾斜鲁棒性,代码处理时还需要配合离群点滤波——压缩包里的kerman.py就是在做这件事。
3. 从 bounding box 到距离值:针孔成像原理、焦距比与代码落位
3.1 测距公式怎么推导:像素宽度、真实宽度和焦距的关系
单目测距不是从模型里直接回归距离(那是深度估计网络做的事),而是利用透视投影的几何关系。针孔相机模型下,物体在图像上的像素宽度 $w_{pixel}$、真实物理宽度 $W_{real}$、相机焦距 $f$(像素单位)和物距 $D$ 满足:
$$ D = \frac{f \times W_{real}}{w_{pixel}} $$
这里的 $f$ 不是镜头标识上的 4mm 或 6mm,而是焦距在像素坐标系下的表达:$f_{pixel} = f_{mm} / \mu$,其中 $\mu$ 是单个像素的物理尺寸(通常是 1.12μm 到 3μm 之间)。很多初学者直接拿标称焦距算,结果距离偏大或偏小,原因就是没换算成像素质点。
实际操作中更推荐用标定法直接得到 $f_{pixel}$:在已知距离 $D_{known}$ 处放置 A4 纸,检测出像素宽度 $w_{pixel}$,反推 $f_{pixel} = D_{known} \times w_{pixel} / W_{real}$。这个标定过程在项目启动时做一次,把算出的 $f_{pixel}$ 存成常量写进配置。
3.2 为什么用宽度而不是高度做测距基准
从公式上看宽度和高度都行,但工程上 A4 纸的宽度更稳。因为实际拍摄时摄像头与纸面之间往往存在俯仰角,高度会因为透视收缩明显变小,而宽度方向的收缩只有在水平偏航角存在时才体现。检测框的底部中心更贴近物体与地面的接触点,正好对应到真实场景中可以视为基准的支撑面位置;边框底部被杂物遮挡的概率相对较小,靠近下方的像素也受镜头畸变影响更集中,因此以底部矩形框宽度(x2 - x1)作为 $w_{pixel}$ 是最常见的处理方式。注意不要用检测框的[0]和[2]索引直接取坐标,因为 YOLOv5 输出的是归一化坐标,要先乘上图像宽度。YOLOv5 返回的结果中xyxy格式是未归一化的像素坐标(输入--img 640时坐标范围在 0~640 之间),直接拿x2 - x1即可作为像素宽度参与计算。
3.3 YOLOv5 推理流程里插入距离计算的代码结构
假设使用detect.py的模型加载方式,拿到预测结果后需在非极大值抑制之后、可视化之前插入距离计算。核心代码如下:
import torch import numpy as np def estimate_distance(box, img_width, f_pixel, real_width_mm=210.0): """ 利用针孔相机模型估算目标距离 box: [x1, y1, x2, y2] 像素坐标,来自 YOLOv5 的 xyxy 输出 img_width: 输入图像宽度(像素),用于校验坐标合法性 f_pixel: 标定得到的焦距(像素单位) real_width_mm: 目标真实宽度,A4 纸为 210mm """ x1, y1, x2, y2 = box pixel_width = abs(x2 - x1) if pixel_width < 5: return -1.0 # 框太小时信号不可信 # 像素宽度与真实宽度正比于像素焦距与真实距离之比 distance_mm = (f_pixel * real_width_mm) / pixel_width return distance_mm / 1000.0 # 转成米 # 推理循环中的调用片段 # results.xyxy[0] 是当前帧所有检测框的张量 # 每行格式: [x1, y1, x2, y2, confidence, class_id] for det in results.xyxy[0]: cls_id = int(det[5]) if cls_id == 1: # 假设 1 是 A4 纸的类别 id box = det[:4].cpu().numpy() dist = estimate_distance(box, img_width=640, f_pixel=980.0) print(f"A4 paper distance: {dist:.2f} m")逻辑说明:estimate_distance的核心是(f_pixel * real_width_mm) / pixel_width,这一步把“宽度在像素空间和物理空间的比例”还原成三角形相似关系。f_pixel是第 3.1 节标定得到的值,这里举 980.0 作为示例;正式使用时不要硬编码,应该从标定文件读取。box[:4]提取的是坐标张量,.cpu().numpy()是因为 YOLOv5 推理时数据可能在 GPU 上,直接传给 numpy 函数会报类型错误。pixel_width < 5的过滤条件是为了防止远距离目标框退化后产生异常大距离值。
3.4 镜头的畸变影响:为什么距离越近误差越小
这个公式成立的前提是近似针孔模型,真实镜头存在径向畸变和切向畸变。标定后的f_pixel实际上已经包含了畸变的平均影响,但在画面边缘,畸变导致像素宽度产生非线性伸缩。用cv2.undistort对每帧图像做去畸变处理可以让测距值更平滑:
import cv2 # 假设标定得到了相机内参矩阵 mtx 和畸变系数 dist # 这只在摄像头广角大、画面边缘变形明显时有必要做 map1, map2 = cv2.initUndistortRectifyMap( mtx, dist, None, mtx, (img_width, img_height), cv2.CV_32FC1 ) frame_undistorted = cv2.remap(frame, map1, map2, interpolation=cv2.INTER_LINEAR)距离越远,目标在图像上的像素宽度越小,畸变带来的绝对误差占比越高。在 1 米距离时 A4 纸可能占 300 个像素宽,即使畸变修正误差 2%,算出的距离只有 2% 的波动;到 10 米距离时像素宽度缩到 30 个像素,同样 2 个像素的抖动带来 6%~10% 的距离误差。这也是后面引入滤波器的根本动机。
4. 实时视频流测距:realsensedetect.py 的工程化思路与参数调优
4.1 从单张图片到视频流:检测频率和测距频率的分离设计
realsensedetect.py这类实时脚本和单帧detect.py的主要区别在于:视频流里相邻帧高度相关,目标检测可以以较低的频率运行,而距离输出需要更高频率的更新。常见做法是检测线程以 15~20 FPS 运行,测距线程以 30 FPS 运行,检测框的位置通过线性插值或最近邻结果传给测距模块。这样做能显著降低 GPU 占用,因为检测模型一次前向推理 640×640 分辨率图像的成本远高于一次简单的除法计算。
import threading import queue import numpy as np class FrameProcessor: def __init__(self, model, f_pixel): self.model = model self.f_pixel = f_pixel self.detect_queue = queue.Queue(maxsize=4) self.distance_result = 0.0 self.det_box = None self.lock = threading.Lock() def inference_worker(self): """检测线程:从队列拿帧,推理更新检测框和置信度""" while True: frame = self.detect_queue.get() results = self.model(frame[None, ...], size=640) # 从 results.xyxy[0] 中挑出最大置信度的 A4 纸框 with self.lock: self.det_box = max( (d for d in results.xyxy[0] if int(d[5]) == 1), key=lambda x: x[4], default=None )逻辑说明:inference_worker是独立线程,它把当前帧推入模型做完整推理,但只在拿到结果时更新共享变量det_box。测距主循环只需要读取这个共享变量,不需要每次都重新跑模型,这使视频流代码能在 CPU 上也能达到实时效果。queue的maxsize=4限制了积压帧数,防止检测速度跟不上输入帧率时内存被无限撑高。一个约定:检测线程里不要放任何print或文件写入,所有 IO 操作留在测距主线程,否则帧率会被 IO 拖垮。
4.2 RealSense 摄像头在单目模式下的标定注意事项
realsensedetect.py这个名字虽然带 RealSense,但单目测距场景下只使用它的 RGB 流,不需要启用深度流。RealSense 的内参可以在运行时通过 SDK 直接获取,这是它比普通 USB 摄像头方便的地方:
import pyrealsense2 as rs pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) profile = pipeline.start(config) # 获取内参,这里返回的 fx 就是像素焦距 intr = profile.get_stream(rs.stream.color).as_video_stream_profile().get_intrinsics() f_pixel = intr.fx # 单位是像素,直接用于测距公式说明:intr.fx是 x 方向的焦距,正常情况下fx和fy非常接近(差异小于千分之一),用fx作为测距公式里的 $f$ 即可。需要注意:如果启用了 RealSense 的深度流并做了自动对齐,RGB 流和深度流的分辨率要求可能不一致,而测距代码只需要 RGB 的fx,不要误把深度内参当作 RGB 内参使用。对普通 USB 摄像头则只能靠棋盘格标定,推荐用cv2.calibrateCamera,采集 15~20 张不同角度的棋盘格图,得到内参矩阵mtx,再取mtx[0, 0]作为f_pixel。
4.3 实时测距主循环:要不要每一帧都做距离计算
上面的代码已经把检测和测距分离,但实际做主循环时还有一个选择:距离计算放不放阈值逻辑。在接近 5 米以上时 A4 纸的像素宽度可能低于 40 个像素,单帧测距值波动达到 0.5 米以上。我一般会在主循环里加一个置信度和像素宽度的双重闸门:
# 推荐做法:只在像素宽度高于阈值时才计算距离 MIN_PIXEL_WIDTH = 50 # 低于该值,测距结果抖动过大,视为无效 while True: frame = pipeline.wait_for_frames().get_color_frame() if not frame: continue img = np.asanyarray(frame.get_data()) with processor.lock: det = processor.det_box if det is not None: x1, y1, x2, y2 = det[:4].cpu().numpy() pixel_w = abs(x2 - x1) if pixel_w >= MIN_PIXEL_WIDTH: dist = estimate_distance(det[:4], img_width=640, f_pixel=f_pixel) # 将 dist 送入 kerman.py 的平滑器,替代直接输出 smoothed_dist = smooth_distance(dist) print(f"A4: {dist:.2f} m -> smoothed: {smoothed_dist:.2f} m")逻辑说明:MIN_PIXEL_WIDTH是根据实际摄像头焦距和视频分辨率决定的经验值。若标定f_pixel=980、真实宽度 210mm,像素宽度 50 对应距离约 4.1 米(0.21m × 980 / 50px = 4.116m)。超过这个距离后 A4 纸在图像中的细节减少,单帧检测框上下跳动幅度加大,直接输出会让读者觉得系统不稳定。这里的smooth_distance就是kerman.py暴露的入口。保持距离计算和帧读取同步,是为了保证测距结果的实时性不受检测线程里 NMS 耗时的抖动影响。
5. 距离抖动的压平技巧:用滤波、尺度校验框住误差边界
5.1 误差链路上最脆弱的环节不是模型,是坐标抖动
把全套管线跑通后,最常见的现象是:目标在 3 米处不动,测距输出却以 0.1 秒为周期在 2.7 米到 3.3 米之间跳。这个波动的不是测距公式本身,而是 YOLOv5 每帧给出的 bounding box 有 1~2 个像素的随机偏移,这部分偏移被除法放大后形成距离噪声。kerman.py在项目中的角色就是把这一层的跳变压平。它本质上是带置信度加权的平滑器:置信度越高权重越大,这是比单独用均值滤波更合理的设计,因为低置信度帧往往伴随着检测框退化,它们输出的距离值应该被压低贡献。
关于kerman.py的具体实现,可以按卡尔曼滤波或指数滑动平均两种方式组织。卡尔曼滤波要维护状态向量[distance, velocity],观测模型直接把distance映射到输出;指数滑动平均更简单,smoothed = alpha * raw + (1-alpha) * smoothed。对毕设场景我推荐先写指数滑动平均版本,调参直观:
class DistanceSmoother: def __init__(self, alpha=0.35): self.alpha = alpha self.last_value = None def update(self, raw_distance, confidence): # 置信度越低,平滑系数越大,越信任历史值 if confidence < 0.5: alpha = min(self.alpha * 1.5, 0.85) else: alpha = self.alpha if self.last_value is None: self.last_value = raw_distance else: self.last_value = alpha * raw_distance + (1 - alpha) * self.last_value return self.last_value代码说明:alpha决定当前帧与历史帧的权重。alpha=0.35意味着历史值占 65%,能有效滤除高频抖动;代价是目标快速移动时距离输出有约 3~5 帧的滞后。confidence < 0.5时把alpha放大到 0.525,目的是对低置信度帧更保守,避免异常检测框导致距离突跳。
5.2 动态校准窗口与离群点判定
平滑滤波只能处理随机噪声,处理不了瞬间的大跳变——比如某一帧检测框误框到背景里。这种离群点可以用滑动窗口内的中值偏差来判定剔除。
from collections import deque import numpy as np class OutlierRejecter: def __init__(self, window_size=7, threshold_m=0.5): self.window = deque(maxlen=window_size) self.threshold = threshold_m def filter(self, distance): if len(self.window) < 3: self.window.append(distance) return distance median_dist = np.median(self.window) if abs(distance - median_dist) > self.threshold: # 用中值替代异常值,而不是丢弃到没有输出 fixed = median_dist else: fixed = distance self.window.append(fixed) return fixed这里的threshold_m=0.5表示超过当前中位数 0.5 米的帧会被替换为中位数,这个值可以根据场景调整:目标静止时调到 0.2 米更灵敏,目标移动时调到 0.8 米避免把真实位移误判成异常。注意deque在未填满时直接返回原始距离,前 3 帧不做过滤,这是为了避免初始化阶段误伤。
5.3 距离校验:标尺验证法
室内标定场景里放一把卷尺,摄像头从 1 米移到 5 米,每 0.5 米记录一次:检测框中心是否对准 A4 纸中心、误差是否低于 15%。如果 1 米处误差大而 5 米处误差小,说明焦距标定值偏小;反过来则说明标定值偏大,按线性比例修正f_pixel即可,不需要重新拍摄。注意摄像头光轴必须尽量与纸面垂直,倾斜视角下测距误差会迅速上升,这种现象与检测置信度无关,纯几何误差。
5.4 A4 纸的实际摆放与先验库扩展:一个把测距扩展到任意已知尺寸物体的技巧
A4 纸作为先验物是入门方案,但它有两个缺点:纸张太轻遇风容易抖动,远距离时像素宽度过小。把方法泛化时,可以维护一个“先验尺寸库”:
known_objects.yaml # 目标名称: [实际宽度(米), 高度(米)] a4_paper: [0.210, 0.297] car_side: [1.8, 1.5] traffic_cone: [0.36, 0.7]推理时根据检测到的类别 id 索引这个 yaml,把对应宽度传入estimate_distance,测距公式本身不用改。训练时给这些目标分别建类,但不建议把person放进去,因为人的宽度变化太大(正对、侧对差距明显),用固定先验宽度测距的误差会很大。选参照物时要遵循两个条件:宽度在直角视角下稳定、目标不能被压缩形变。这正是这套源码最核心的延伸方向。
本文还有配套的精品资源,点击获取