基于YOLOv5的单目测距技术:原理与工程实现
2026/9/16 16:43:17 网站建设 项目流程

简介:基于YOLOv5的单目测距系统源码包,面向计算机视觉方向的毕业设计开发者,尤其适合需要快速搭建目标检测与距离估计一体化项目的学生。压缩包共78个文件,整体约215.3MB,文件类型覆盖28个Python脚本、26个YAML配置、6个Shell脚本、5个XML工程文件、2个pt权重、2个mp4演示视频,以及Dockerfile和依赖清单等,能够支撑环境部署、模型训练、推理检测与距离计算等完整流程。已有3146人学习下载,适用于从模型原理理解到实际工程落地的进阶学习。通过这份资源,读者可以系统掌握YOLOv5的模型组织结构、数据配置方式、训练及检测脚本的编写技巧,同时借助权重文件和演示视频快速验证单目测距效果,为毕业设计答辩或项目开发提供可运行的代码基础和清晰的技术思路。

1. 单目测距不是玄学,是几何

如果你搜过「yolov5 单目测距」,大概率会看到两种声音:一种是毕设源码打包,跑起来就给你一个距离数字;另一种是实验室里拿着双目相机或激光雷达的人说单目测距不靠谱。做这个题目之前,你只需要搞明白一件事:单目测距本质上是「已知物体真实尺寸 + 相机成像关系」的三角换算,YOLOv5 负责的只是第一跳——从图像里拿回目标的像素框和类别,距离是从框的像素尺寸反推出来的。误差存在,但可控,且足够满足毕设和很多安防、巡检、辅助提醒场景。这篇不是给你复述某份源码的 readme,而是把单目测距从原理、标定、代码到误差修正完整拆开,让你拿到「基于 yolov5 的单目测距系统源码」这类工程时,知道每一行在算什么,改了哪里会影响精度。

2. 单目测距的原理:从 YOLOv5 的 bbox 到一个 distance 值

2.1 为什么 bbox 配一个固定焦距就能算距离

常见做法是把相机当成一个针孔模型,一个目标物在画面里占的像素越多,离镜头越近。对于宽度为 W(米)的目标,在图像中宽度为 w(像素),相机焦距为 f(像素),距离 D 满足相似三角形:

D = (W * f) / w

YOLOv5 输出的检测框是归一化坐标(cx, cy, bw, bh),取值范围 0~1,乘上图像宽高就得到像素值。这里的bw就是上式里的wW是你预先知道的该类别真实宽度,f来自相机内参标定。这就是基于 yolov5 的单目测距系统里绝大多数源码实现所采用的核心公式。

代码落地时,YOLOv5 官方 detect.py 里拿到的是detections,每一行是x1, y1, x2, y2, conf, cls。改造最小的一版测距逻辑长这样:

import torch import cv2 class DistanceEstimator: def __init__(self, focal_px, real_width_table): self.focal_px = focal_px # 相机焦距,单位像素 self.real_width_table = real_width_table # 每类目标的真实宽度,单位米 def estimate(self, detections, img_shape): """ detections: torch.Tensor, shape (N, 6) -> x1, y1, x2, y2, conf, cls img_shape: (H, W) 返回列表,每个元素是 (cls, box, distance) """ h, w = img_shape results = [] for *xyxy, conf, cls in detections: x1, y1, x2, y2 = [int(v) for v in xyxy] box_w = x2 - x1 if box_w <= 0: continue cls_id = int(cls) if cls_id not in self.real_width_table: continue distance = (self.real_width_table[cls_id] * self.focal_px) / box_w results.append((cls_id, (x1, y1, x2, y2), distance)) return results

逻辑说明:从 bbox 的像素宽度反推距离,前提是你知道这个目标类别的实际宽度,并且该宽度在画面中近似等于目标与相机垂直时的投影宽度。real_width_table是人工维护的先验知识,比如汽车按 1.8 米、行人按 0.5 米,不同类目对应不同宽度,这是后期调精度最容易改的地方。

参数说明:focal_px的单位是像素,不是毫米,它由内参矩阵里的 fx 得到;box_w是目标在图像中的像素宽度;real_width_table的准确度直接决定距离误差,标定真实宽度时建议测量常见目标的中位数尺寸,而不是极端值。注意这里没有考虑任何畸变和偏航角,是纯垂直正对场景下的简化模型,适合做毕设第一版。

2.2 相似三角形、相机内参与坐标系映射的关系

如果你的毕设要求里写了「坐标系映射」或「世界坐标」,那就不能只用一个 bbox 宽度了事。深入一层的做法是把像素坐标转换到相机坐标系,再利用地面几何约束求距离。相机内参矩阵 K 把相机坐标系下的点映射到像素坐标系:

s * [u, v, 1]^T = K * [Xc, Yc, Zc]^T

其中 K = [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]。如果用地面投影法,已知相机安装高度 H 和俯仰角 pitch,目标检测框底边对应的地面点在图像中的 v 坐标可以用下式直接算出距离:

D = H / tan(θ + α)

θ 是目标底边像素对应的俯仰角,α 是相机光轴的俯仰角。这个公式不需要知道目标真实尺寸,缺点是需要精确测量相机的安装姿态。两者对比见下:

方法需要的先验误差敏感项适用场景
bbox 宽度法目标真实宽度 W、焦距 fxW 测量值、目标朝向车、行人、路牌等宽高先验稳定的目标
地面投影法相机高度 H、俯仰角 pitch安装角度震动、地面不平固定视角的交通监控、车底检测
两者融合W + H + pitch全部上两者的敏感项要求角速度稳定的巡检机器人,常做卡尔曼融合

我做毕设的时候选的是 bbox 宽度法为主,地面投影法辅助校验。原因很实际:bbox 宽度法只需要标定一次内参,换场景不用重新量相机角度,对「源码」类项目来说部署步骤最少。如果你在源码里看到代码同时引用了focal_pxpitch_angle两个参数,那就是这两种方法的结合。

2.3 为什么 YOLOv5 的网络结构本身不影响测距公式

很多刚接触这个的人会以为换 YOLOv5s 为 YOLOv5m 能直接提升测距精度。这其实是误解。网络结构决定的是「检测框能不能完整框住目标」,而测距公式用的是box_w。如果 YOLOv5s 把汽车框得略微小一圈,那个box_w偏小,距离就会偏大。所以提升测距精度的直接路径不是加大网络,而是让你的检测框更贴合目标边缘。

这里有一个容易被忽略的细节:YOLOv5 在 NMS(非极大值抑制)之前会输出多个候选框,最终框的位置是回归出来的,并不保证紧贴物体的真实轮廓。你可以考虑在距离计算之前对框做一次轻微的边缘修正,比如对框的 x1, x2 同时向外扩展 1 到 2 个像素,来抵消检测头的回归偏移。扩展多少依据你验证集上的平均 IoU 来定。具体做法后面第 5 章会讲。

3. 相机标定与参数准备:让焦距 fx 变成可用的像素值

3.1 用 OpenCV 棋盘格标定拿到内参矩阵

单目测距里最容易出错的一步不是检测,而是标定。没有准确的 fx,公式D = W * fx / w就是一个拍脑袋乘出来的数。我一般让你用 OpenCV 自带的标定流程,采 15 到 20 张不同角度的棋盘格照片,每张保证棋盘完全在画面内,光照均匀,距离覆盖你要测距的主要区间。

采集完图片后跑下面的标定代码:

import cv2 import numpy as np import glob # 棋盘格内部角点数量,例如 9x6 checkerboard = (9, 6) objp = np.zeros((checkerboard[0] * checkerboard[1], 3), np.float32) objp[:, :2] = np.mgrid[0:checkerboard[0], 0:checkerboard[1]].T.reshape(-1, 2) obj_points = [] # 世界坐标系中的角点 img_points = [] # 图像坐标系中的角点 for fname in sorted(glob.glob('calib_images/*.jpg')): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, checkerboard, None) if not ret: continue criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners2) cv2.drawChessboardCorners(img, checkerboard, corners2, ret) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, gray.shape[::-1], None, None) fx = mtx[0, 0] print("内参矩阵:\n", mtx) print("fx =", fx)

逻辑说明:objp是把棋盘格角点的世界坐标按毫米为单位建模,角点间距不需要显式传入单个格子的边长,因为在标定过程中它会被内参矩阵吸收为 fx 的单位是像素。如果你想更严谨,可以指定每个格子的宽度 square_size,并把objp[:, :2]乘以它。

参数说明:checkerboard = (9, 6)指的是内部角点数,不是棋盘格总数,买棋盘格的时候要看准。cornerSubPix这一步使用亚像素精度细化角点位置,直接影响 fx 的有效位数。采集图片时建议把棋盘格放在多组距离和角度下,但不要剧烈倾斜,否则外参计算不稳定。标定完直接读mtx[0, 0]作为 fx,mtx[1, 1]是 fy。

3.2 标定焦距、畸变系数与安装参数怎么填进源码

标定输出的内容有三样:内参矩阵、畸变系数dist、每张图的外参。在你的测距系统里,外参不是用标定板算出来的,而是根据相机的实际安装位置手工输入。这里建议在参数配置文件config.yaml里维护一类一参:

camera: fx: 1055.43 # 像素焦距,标定得到 fy: 1053.82 cx: 640.5 cy: 360.3 distortion: [0.045, -0.32, 0.001, -0.0007, 0.12] # k1 k2 p1 p2 k3 height_m: 1.2 # 相机安装高度,单位米 pitch_deg: 0.0 # 光轴俯仰角,单位度,水平为0 category_width: person: 0.5 car: 1.8 bicycle: 0.6 truck: 2.4

畸变对测距的影响容易被低估。如果你用的是广角摄像头,图像边缘目标的 bbox 宽度会因桶形畸变而失真,距离计算自然偏大。正确的流程是先用cv2.undistortcv2.remap把帧校正,再喂给 YOLOv5。畸变校正本身也会引入边缘区域的重采样误差,所以有经验的方案会优先用窄视角镜头放在固定位置,弱化畸变影响。

安装参数里,pitch_deg决定地面投影法会不会参与。当相机带有明显俯仰角时,bbox 底边所在的实际地面距离和画面中心距离不再线性,此时必须回落到 2.2 里的地面投影公式才能修正。源码里如果没做这个修正,你会看到远处的目标距离跳变剧烈,就是因为目标的像素高度变化率不一致。

3.3 环境配置里最容易踩的坑:YOLOv5 与 OpenCV 版本冲突

到了环境配置环节,许多人卡在「torch 版本 + CUDA + opencv」的三角关系里。YOLOv5 官方仓库在 requirements.txt 里对 opencv 没有严格 pin 版本,但如果你直接pip install opencv-python拿到 4.9 或更高版本,在部分 torch 2.0 以上的组合里cv2.dnntorchvision.ops.nms会有小概率冲突,运行时不报错,但检测的 NMS 输出顺序有差异。

我的建议是:

conda create -n yolodepth python=3.8 conda activate yolodepth pip install torch==1.13.1 torchvision==0.14.1 pip install opencv-python==4.8.1.78 pip install -r requirements.txt

逻辑说明:Python 3.8 是为了兼容 YOLOv5 常见版本里对torch1.13 的依赖;opencv 4.8 系列对cv2.findChessboardCornerscv2.remap的行为最稳定,且不会强制AVX512指令集。

参数说明:torch==1.13.1对应 CUDA 11.7 的预编译包,opencv-python==4.8.1.78是纯 CPU wheel,如果你的机器没有 N 卡也能跑,只是推理速度慢。测距本身不依赖 GPU,检测部分用 CPU 的话,YOLOv5s 在 640x640 输入下大约 1 秒一帧,毕设演示足够。如果你要实时测距,就换 YOLOv5n 或做 TensorRT 导出。

4. 源码怎么组织:从检测到测距的模块串联

4.1 一个可运行的目录结构与每个文件的职责

毕设源码最常见的陋习是把所有逻辑堆在detect.py里,跑是能跑,答辩和后期改起来很痛苦。我会按「采集 → 检测 → 测距 → 可视化」四层拆分,目录结构如下:

single_camera_depth/ ├── config.yaml # 相机内参、类别宽高、阈值参数 ├── camera.py # 相机读取与畸变校正 ├── detector.py # YOLOv5 模型加载、推理与目标过滤 ├── distance.py # 测距算法,含距离修正 ├── visualizer.py # 画框、画距离标签、深度信息叠加 ├── main.py # 主循环,组合各模块 ├── weights/ │ └── best.pt # 训练好的模型权重 ├── utils/ │ └── calibration.py # 棋盘格标定脚本 └── requirements.txt

每个文件保持单一职责,这是为了让你能用最小代价替换部分实现。比如后来我想把 YOLOv5 换成 YOLOv8,只需要改detector.py里的model加载接口,distance.py完全不动。这也是为什么网上那些「源码」工程虽然版本老,但拆得好的反而更有参考价值。

4.2 加载 YOLOv5 权重并输出带距离标签的帧

核心主循环用一个生产者消费者思路:相机线程只管出帧,检测线程在 GPU 上推理,测距和绘制放到主线程里做。这样避免一个卡顿拖垮整条链路。

import cv2 import torch import yaml from detector import YOLODetector from distance import DistanceEstimator from visualizer import draw_depth_info with open("config.yaml") as f: cfg = yaml.safe_load(f) detector = YOLODetector("weights/best.pt") estimator = DistanceEstimator( focal_px=cfg["camera"]["fx"], real_width_table=cfg["category_width"] ) cap = cv2.VideoCapture(0) while cap.isOpened(): ret, frame = cap.read() if not ret: break # 先做畸变校正,再做检测 frame = cv2.undistort(frame, mtx, dist) detections = detector.infer(frame) results = estimator.estimate(detections, frame.shape[:2]) frame = draw_depth_info(frame, results) cv2.imshow("yolov5 mono depth", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:detector.infer内部调用了 YOLOv5 的torch.hub.load或者直接加载best.pt后跑model(frame),返回的是经 NMS 后的(x1, y1, x2, y2, conf, cls)格式张量。estimator.estimate用的是第 2 章的距离公式。draw_depth_info把距离值写在 bbox 正下方,建议同时显示类别名和最大置信度,便于你观察误检对测距的干扰。

参数说明:cv2.undistort(mtx, dist)里的 mtx 和 dist 是第 3 章标定得到的,注意这句代码在每帧都做,会消耗 CPU。如果你测距目标是 20 米以上的远处目标,可以把undistort改成只做一次映射表预计算cv2.initUndistortRectifyMap+cv2.remap,帧率能提升一倍。

4.3 训练自己的数据集:标注、划分与超参数选择

毕设源码附带的权重通常是在 COCO 上预训练的,能检测 80 类目标,但具体到你的应用场景,比如校园里的车、行人、电动车,预训练模型可能漏检或框得不稳,此时就要训练自己的数据集。数据集的准备绕不开标注。常见做法是先用一个已经训练好的 YOLOv5 模型自动预测一批图片,导出 txt 标注,再手动修正,这就是「如何通过界面操作 yolov5 完成数据集的自动标注」这类流程的落地方案。

标注完的数据目录如下:

datasets/ ├── images/ │ ├── train/ # 2000张 │ └── val/ # 300张 └── labels/ ├── train/ # 每张图对应一个同名txt └── val/

训练命令一般长这样:

python train.py --data dataset.yaml --weights yolov5s.pt --img 640 --batch-size 16 --epochs 100 --hyp hyp.scratch-low.yaml

逻辑说明:dataset.yaml里写类别数和类别名,hyp.scratch-low.yaml是 YOLOv5 的默认超参数文件。对于测距系统,你更关心的是检测框能不能贴合目标,而不是类别有多丰富,所以类别数控制在 3 到 5 类最好。超过 5 类会让模型在有限数据下和测距任务争抢容量。

参数说明:--img 640是训练分辨率,如果你的相机画面里小目标多,可以改成 960,但推理时间会增加。--hyp里和测距最相关的参数是hsv_hhsv_sdegrees,这些数据增强如果过分,会让 bbox 抖动更剧烈,反而拉低测距稳定性。建议保持默认增强只有轻微修改。

5. 误差分析与精度校验技巧

5.1 三个必调参数:conf_thresiou_thresimg_size

detector.py里模型推理的三个参数直接影响测距稳定性。

参数默认值对测距的影响建议
conf_thres0.25阈值过低会产生假框,假框的像素宽度随机,距离值乱跳提高到 0.35 到 0.45
iou_thres0.45较高的 iou 会把重叠框较少的物体合并得更彻底,减少双框对距离计算干扰保持 0.45 或略微加到 0.5
img_size640分辨率越低,远端目标的像素宽度越小,量化误差越大10 米以上的测距场景优先用 960

有一个经常被忽视的细节:在距离计算中,如果置信度很低但 bbox 还在被送入estimate,那么你看到的高置信度下距离合理的输出,可能是碰运气。我一般会在estimate之前多做一个筛选,去掉conf < 0.3的框,避免这种随机性出现在距离通道里。

5.2 画一条误差-距离曲线来验证精度

写代码的人容易高估自己系统的精度,所以我建议你在代码里集成一个「相对误差采集」开关,专门在做验证的时候打开:在固定距离摆一个已知宽度的目标,记录测距值 D_est,和真实距离 D_gt 对比。

import json def validate(distance_results, ground_truth): err_list = [] for d_est in distance_results: for d_gt, w_gt in ground_truth: if w_gt is not None and abs(d_gt - d_est) < 2.0: # 距离关联匹配 err = abs(d_est - d_gt) / d_gt err_list.append((d_gt, err)) break return err_list

逻辑说明:ground_truth是用卷尺量好的若干距离点,误差采集时让目标处于画面中间,避免畸变影响。误差 - 距离曲线出来后,近端 5 米内相对误差低于 8%,10 米到 20 米区间误差 15% 到 25%,都是正常范围;如果低频段误差在 30% 以上,先检查你real_width_table里的宽度对不对,而不是去换模型。

参数说明:误差匹配用的阈值2.0表示只关联真实距离与估算距离相差 2 米内的点,防止一个目标被记到另一个目标上。实际做误差曲线时,每个距离点采集 50 帧取均值,比单帧可信得多。

5.3 亚像素估计和卡尔曼平滑让输出更可信

像素宽度在小目标上只有几个像素,量化误差呈距离的平方放大。一个有效的改善手段是对 bbox 宽度做亚像素估计:把检测框的宽度从整数提升到浮点数,用 YOLOv5 输出里的归一化宽度直接乘图像宽度,不取整,这样 0.1 像素的差异也能反映到距离上,虽然物理意义有限,但能显著减少画面抖动引起的距离显示波动。

另一个稳健做法是给距离输出加卡尔曼滤波。常见做法是把距离作为观测值,目标速度作为隐变量,用恒定速度模型做平滑。你可以用现成的filterpy库,不用自己推导矩阵推导:

from filterpy.kalman import KalmanFilter f = KalmanFilter(dim_x=2, dim_z=1) f.x = np.array([0.0, 0.0]) # 距离,速度 f.F = np.array([[1.0, 0.1], [0.0, 1.0]]) # dt=0.1, 恒定速度模型 f.H = np.array([[1.0, 0.0]]) f.P *= 50 f.R = 1.0 f.Q = np.array([[0.01, 0.0], [0.0, 0.1]]) def smooth_distance(z): f.predict() f.update([z]) return f.x[0]

逻辑说明:这里把距离的观测值直接丢进卡尔曼滤波,F矩阵中的0.1是时间步长,约等于你视频帧间隔;R是观测噪声,设得越小越信任当前距离,设得越大输出越平滑但响应越迟钝;Q里的速度噪声系数决定系统对突然远近变化的敏感度。

参数说明:如果你发现平滑后距离滞后严重,比如车停下后距离还在变小,把R调大到 5 到 10,或者把F里的0.1改成 0.05。反过来,如果仍然剧烈抖动,就把R降到 0.3。利用这套滤波和上面的误差校验流程,你可以在 20 米范围内把单目测距的相对误差压到 10% 以内,对毕设来讲是足够体面的精度。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询