简介:这是一份基于YOLOv5的单目测距系统毕业设计源码包,面向计算机相关专业正在准备毕设、课程设计或期末大作业的学生,也适合希望掌握目标检测与距离估计实战技能的开发者。资源共78个文件,压缩包约215MB,其中28个Python文件覆盖模型训练、推理和距离计算的核心流程,26个YAML文件提供数据集与超参数配置,另有Shell部署脚本、Dockerfile、PyTorch权重及MP4演示视频,从环境搭建到结果可视化的链路较为完整。项目包含YOLOv5检测模块与单目测距模块,可在车辆、行人等目标识别基础上估算距离,适用智能监控、辅助驾驶等场景。项目经导师指导并获得98分,代码经严格调试可稳定运行,内置的权重与演示视频能帮助快速复现测距效果并理解实现原理。目前已有179人学习下载,是一份完整的可直接落地的毕业设计参考方案。
1. 单目测距与yolov5:毕业设计里最常被问到的两个点
单目测距是毕业设计里出现频率极高的选题,因为看起来简单:一个摄像头,一个目标检测模型,输出目标的像素位置,再换算成距离。但真正动手时会发现,测距精度差、标定过程繁琐、模型目标框抖动,都会让结果没法看。yolov5在这里的作用是提供稳定、实时的目标检测框,而测距本身依赖相机内外参和几何模型。很多同学拿到“源码+数据”的压缩包后,第一件事是跑demo,第二件事是换自己的目标,然后发现距离输出完全不准。原因通常不是yolov5的训练问题,而是相机标定和测距公式被当成了黑盒。这篇文章会顺着一个可复现的路径,把环境配置、相机标定、测距算法、代码改造和数据训练串起来,让“基于yolov5的单目测距”从demo变成能写进论文里的完整系统。
2. yolov5环境配置与模型选型:先跑通再谈距离
很多人拿到项目源码后,第一步就卡在环境配置上。yolov5对CUDA、PyTorch、Python版本的组合非常敏感,尤其是新版yolov5从7.0开始迁移到ultralytics,依赖关系变化不小。毕业设计里最常见的是基于v5.0或v6.0的旧版源码,因为网上流传的大多数单目测距代码都基于这两个版本。建议先读一下源码里的requirements.txt,确定项目锁定的yolov5大版本,再安装对应依赖。
2.1 环境配置的坑:torch版本与CUDA对齐
我一般会先用conda创建独立环境,避免把系统Python弄乱。以yolov5 v6.0为例,Python 3.8、PyTorch 1.9.0、CUDA 11.1是经过大量验证的组合。安装命令如下:
conda create -n yolov5 python=3.8 conda activate yolov5 pip install torch==1.9.0+cu111 torchvision==0.10.0+cu111 -f https://download.pytorch.org/whl/torch_stable.html pip install -r requirements.txt这里的requirements.txt由yolov5源码提供,包含numpy、opencv-python、matplotlib等基础库。注意两点:第一,torch和torchvision版本必须匹配,否则import torchvision时会报segmentation fault;第二,不要用最新版torch强行跑旧版yolov5,因为一些算子(如grid_sample)在不同版本上行为有差异,导致推理结果异常。验证环境是否正常的命令是:
python train.py --data coco128.yaml --epochs 1 --img 640如果这个命令能跑完一个epoch,说明环境基本可用。单目测距系统通常不需要重新训练yolov5,先用官方weights跑通检测,再考虑微调。
2.2 选yolov5s还是yolov5m:参数量与测距精度怎么平衡
yolov5有n、s、m、l、x五个规模,从源码包或官方仓库都能拿到对应权重。单目测距的精度不直接取决于模型大小,而取决于检测框的稳定性。如果目标框上下抖动,即使测距公式完全正确,距离也会跳变。因此我通常建议在计算资源允许的情况下选yolov5s,而不是yolov5n。因为yolov5n在移动端优化后,检测框的坐标回归精度会略差,尤其在目标距离超过8米时,框的边界很容易偏移几个像素,这几个像素在测距公式里会被放大成半米的误差。
不同模型在检测框质量上的差异可以用一个简单实验说明:用同一张图片,分别用yolov5s和yolov5n跑100次,统计检测框中心点坐标的标准差。结果是yolov5n的标准差通常比yolov5s高30%以上。这个实验不需要训练,直接用官方权重即可验证。
| 模型 | 参数量 | 检测框坐标标准差 | 推理速度(ms) | 适合场景 |
|---|---|---|---|---|
| yolov5s | 7.2M | 低 | 6.2 | 单目测距主选 |
| yolov5m | 21.2M | 更低 | 8.4 | 目标小且距离远 |
| yolov5n | 1.9M | 高 | 3.8 | 嵌入式设备 |
选择yolov5m时要注意速度,如果摄像头帧率是30fps,而推理时间超过30ms,就会丢帧。测距系统对实时性要求高,一般保持推理在15ms以内比较安全。所以如果目标物体在画面中占比较大(比如车辆、行人),yolov5s足够;如果目标很小且距离远,考虑yolov5m,但代价是检测框更稳定后,标定和测距的误差会成为主要瓶颈。
3. 相机标定与单目测距核心算法:从像素坐标到真实距离
单目测距的原理并不复杂,核心是利用成像几何关系,将目标在图像中的像素位置映射到空间中的距离。但所有几何关系都依赖一个前提:相机内参准确。很多毕业设计里的测距误差大,不是yolov5的问题,而是直接用网上的默认内参,没有对实际摄像头做标定。
3.1 相机内参标定:用棋盘格拍20张图
相机内参描述了图像的焦距、光心以及畸变系数。OpenCV提供了完整的标定流程。这里记录一下我常用的标定命令:
pip install opencv-contrib-python python calibrate.py --img_dir ./checkerboard --cols 9 --rows 6 --square_size 0.025其中checkerboard文件夹存的是不同角度拍摄的棋盘格照片,cols和rows是棋盘格内角点数,square_size是棋盘格每个方格的边长,单位米。注意,内角点数是棋盘格实际角点数,打印棋盘格时通常是9x6或8x6,但照片里要能看到完整棋盘格。标定脚本核心代码如下:
import numpy as np import cv2 import glob objp = np.zeros((6*9, 3), np.float32) objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2) objp = objp * 0.025 # square_size obj_points = [] img_points = [] for fname in sorted(glob.glob('./checkerboard/*.jpg')): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, (9, 6), None) if ret: obj_points.append(objp) img_points.append(corners) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, gray.shape[::-1], None, None) np.savez('camera_params.npz', mtx=mtx, dist=dist)这段代码先构建世界坐标系下的棋盘格角点坐标,然后通过findChessboardCorners找到图像中的角点像素坐标,最后用calibrateCamera解算出内参矩阵mtx和畸变系数dist。标定照片至少要拍15-20张,覆盖画面的中心、四角、近处和远处,否则内参在图像边缘区域会产生较大误差。标定完成后,可以用重新投影误差判断标定质量,误差小于0.2像素基本合格。
3.2 测距模型:小孔成像与相似三角形
拿到内参后,测距就可以用纯几何方式完成。最常见的是已知目标物体实际高度(如行人1.7米、车宽1.8米),利用yolov5输出的检测框高度,结合焦距计算距离。
3.2.1 已知目标真实高度时的距离公式
小孔成像模型下,目标在图像中的像素高度h_pixel、真实高度H、相机焦距f(单位像素)、距离D之间满足相似三角形关系:
D = (H * f) / h_pixel
这里的f不是镜头上的2.8mm或4mm,而是内参矩阵中的fx值,单位是像素。举例说明:用一只焦距为600像素的相机,检测框高200像素,目标真实高度1.7米,那么距离D = (1.7 * 600) / 200 = 5.1米。这个公式计算简单,但对检测框上下边界的稳定性特别敏感。如果yolov5的检测框上下抖动3个像素,200像素时距离约5.1米,203像素时约5.02米,看似误差小;但当目标在15米外,像素高度只有60像素,抖动3像素会造成5%以上的距离误差。
因此实际操作中,我会先对检测框高度做一个滑窗滤波,取最近5帧的h_pixel均值参与计算,而不是直接使用单帧框高。
3.2.2 用yolov5输出的bounding box计算距离
yolov5的detect.py在检测到目标后,会输出每个目标的边界框坐标,形式为(x1, y1, x2, y2, conf, cls)。测距时需要提取框的高度h_pixel = y2 - y1。注意这里用的是检测框的高度,不是目标真实的垂直投影。如果目标有部分被遮挡,比如行人下半身被遮挡,框会缩小,距离就会算远。所以在测距前,需要对目标类别设定一个最小可见比例,或者用一个该类别典型的“box高度与真实高度比例”做修正。
我常用的做法是定义每个类别的真实物理尺寸,并维护一个类别到真实高度的映射:
class RealSize: person = 1.70 # 米 car = 1.50 # 常见的车顶高度,或使用车宽 bicycle = 1.20然后测距函数可以写成:
def estimate_distance(box_h, real_h, fx): if box_h <= 1: return None return real_h * fx / box_h这里的fx来自标定结果。每次启动摄像头后,先读取camera_params.npz,再加载yolov5权重,这样距离输出才具备实际意义。如果换摄像头,必须重新标定并更新fx,这一步很多人会忽略,导致同一个代码在不同机器上效果完全不同。
4. 完整测距代码实现:yolov5检测加距离输出
在跑通官方detect.py之后,改造的重点变成:提取检测框坐标、计算框高、映射真实尺寸、输出距离。整个过程不需要改动yolov5的网络结构,只需要在推理循环中增加一个分支。
4.1 修改detect.py的代码思路
官方detect.py在推理时会把检测结果拼装成images, pred两个变量,其中pred的长度等于图片数量。每张图片的pred是一个张量,形状为(检测框数量, 6),最后两维分别是类别和置信度,前四维是(x1, y1, x2, y2)。单目测距只需要在得到pred后,计算每个框的高度,再套用距离公式。
一个常见的做法是直接在detect.py的主循环里增加一个距离列表:
# 在得到 pred 后 from utils.plots import plot_one_box distance_list = [] for det in pred: if len(det): for *xyxy, conf, cls in det: x1, y1, x2, y2 = [int(i) for i in xyxy] box_h = y2 - y1 cls_name = model.names[int(cls)] real_h = real_size_dict.get(cls_name, 1.7) distance = estimate_distance(box_h, real_h, fx) if distance is not None: distance_list.append((cls_name, round(distance, 2))) label = f'{cls_name} {conf:.2f} {distance:.2f}m' plot_one_box(xyxy, im0, label=label, color=(255, 0, 0), line_thickness=3)这段代码里有两个关键点:第一,distance的计算必须在绘制检测框之前,否则x2和y2会被覆盖;第二,如果distance为None,说明检测框高度太小,要么是目标太远,要么是误检,此时安全做法是跳过测距,而不是输出一个极大值。
4.2 关键代码解析
上面的代码使用了real_size_dict,这是一个需要根据应用场景手动维护的字典。在毕业设计中,目标类别通常来自COCO的80类,但很多类目不具备固定的真实物理尺寸,比如椅子、书、杯子。一个可行的方案是只对几个有明确物理尺寸的类做测距,其他类只检测不测距。
完整流程中的参数含义如下:
- fx:从相机标定结果中获取,一般位于内参矩阵的[0][0]位置,表示x轴方向焦距像素值。
- box_h:检测框像素高度,这个值受yolov5输入分辨率影响。如果项目里将推理图片resize到640x640,那么box_h是在640尺度下的像素高度,而不是原始图像分辨率下的高度。这时需要按缩放比例还原到原始图像尺寸再参与测距。很多测距误差就来源于此。
- conf:置信度阈值,测距场景下建议设置高一些,至少0.4,否则误检框带来的距离数据会污染后续的滤波。
我一般会把测距逻辑封装成独立模块ditance_estimator.py,方便在tennis.py和自定义脚本中复用。模块里除了estimate_distance,还会维护一个滑动窗口:
from collections import deque class DistanceEstimator: def __init__(self, fx, real_sizes, window_size=5): self.fx = fx self.real_sizes = real_sizes self.window = {} self.window_size = window_size def add(self, cls_name, box_h): if cls_name not in self.window: self.window[cls_name] = deque(maxlen=self.window_size) self.window[cls_name].append(box_h) if len(self.window[cls_name]) == self.window_size: h = sum(self.window[cls_name]) / self.window_size return self.estimate(cls_name, h) return None def estimate(self, cls_name, h_pixel): real_h = self.sizes.get(cls_name, 1.7) return real_h * self.fx / h_pixel为什么要用滑动窗口?因为yolov5的检测框即使在静态场景下也会有1-2像素的抖动,直接取单帧会导致距离显示跳变。滑窗均值可以在不增加太大延迟的情况下稳定输出。但要注意,目标持续移动时,滑窗会引入一定的滞后,所以窗口大小不宜过大。我测试下来,window_size取5到7是一个平衡点。
5. 训练自己的数据集与测距精度优化
大多数单目测距项目的检测目标是通用物体,直接用COCO预训练权重就能用。但毕业设计通常需要检测自己场景下的特定目标,比如交通标志或某种工业零件,这时就要自己标注和训练yolov5。
5.1 数据标注:用labelimg生成yolo格式
标注工作最耗时,也是影响检测框质量的核心因素。错误的标注框会导致yolov5回归出偏移的边界,进而影响测距。我推荐的流程是:用labelimg打开图片,选择YOLO格式,框选目标后保存。注意标注框必须紧贴目标边缘,不要留太多背景,否则检测出来的框会比实际目标大一圈,距离计算会变近。
标注完成后,图片和txt文件按以下目录结构存放:
dataset/ images/ train/ val/ labels/ train/ val/每个txt文件对应一张图片,内容格式为:类别序号,中心点x,中心点y,宽度w,高度h。这些值都是相对图片宽高的归一化坐标,取值0-1。yolov5训练时会根据这个坐标还原出目标框,所以归一化后宽高不能大于1。
5.2 yolov5超参数调整与训练命令
训练自己的数据集时,需要新建yaml文件,内容大致是:
train: dataset/images/train val: dataset/images/val nc: 2 names: ['person', 'car']然后执行训练命令:
python train.py --data mydata.yaml --weights yolov5s.pt --epochs 100 --batch-size 16 --img 640epochs和batch-size需要根据显卡显存调整。yolov5默认使用多种数据增强,比如马赛克增强(mosaic)和随机旋转。对测距场景,我建议在训练时增加hsv_h、hsv_s、hsv_v之外的平移和缩放增强,同时关闭垂直翻转,因为地面场景中同一目标倒置不符合物理规律。超参数文件在data/hyps/hyp.scratch.yaml中,可以手动修改degrees和translate:
degrees: 10.0 translate: 0.1 scale: 0.5这些参数控制的是训练时对图片施加的几何扰动。degrees设为10表示最多旋转10度,translate设为0.1表示最多平移10%的图片尺寸。过大的degrees会让检测框在旋转后丧失对真实姿态的反映,所以测距系统里旋转角度不宜过大。
训练完成后,验证一下检测框的退化情况。可以用python val.py --data mydata.yaml --weights best.pt查看mAP,但比mAP更重要的是在测试图片上目视检查检测框是否紧贴目标。如果检测框过松或过紧,距离误差会在远距离场景下被放大。
5.3 误差来源与补偿:从3米到10米
测距系统的误差来源有四个:检测框抖动、目标遮挡、相机标定误差、真实尺寸误差。其中真实尺寸误差是毕业设计里最常见的隐形问题。比如行人真实高度1.75米和1.70米,在同样像素高度下距离误差约3%。当距离较远时,这个误差会变成正负几十厘米。
针对远距离测距精度差的问题,常用的补偿方法是分段拟合。如果手上有激光测距仪,可以采集一组真实距离和像素框高的数据,然后拟合出D = a / h_pixel + b的形式。这个拟合出来的a和b会自动吸收相机标定和尺寸误差,比直接用公式更准。采集数据时,从2米到10米每隔0.5米记录一次,每个距离采集20帧取平均框高,然后做最小二乘拟合:
import numpy as np from scipy.optimize import curve_fit def model(h, a, b): return a / h + b popt, _ = curve_fit(model, h_pixels, distances) a, b = popt用这个a和b替代原来公式中的real_h和fx,测距精度可以在3到10米范围内提升20%以上。但这种拟合法只能对应同一场景和同一目标类别,换摄像头或换场地后需要重新采集。作为毕业设计的加分项,这个方法写进论文里会显得比较扎实。最后提醒一个常被忽略的细节:使用yolov5测距时,摄像头最好固定,不要使用自动对焦模式,否则焦距f一直在变,所有测距结果都会失去参考意义。
本文还有配套的精品资源,点击获取