尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于YOLOv5的单目测距系统:从原理到部署的毕业设计实战指南

基于YOLOv5的单目测距系统:从原理到部署的毕业设计实战指南 简介这份资源是经导师指导并通过的98分毕业设计项目围绕YOLOv5构建单目测距系统面向计算机相关专业正在做毕设、课程设计或期末大作业的学生也适合需要项目实战练习的学习者。项目包含完整源码与全部数据可直接作为毕设使用且经过严格调试确保能够运行。压缩包共78个文件约215.32MB以28个Python脚本和26个YAML配置为主辅以Shell脚本、XML配置、权重文件、演示图片与视频等覆盖模型训练、推理检测、距离计算与数据配置等环节。目录中可见detect.py、train.py、distance.py、realsensedetect.py等核心模块以及models、utils、data等结构便于理解单目测距从目标检测到距离估计的完整流程。目前已有180人学习下载适合希望快速搭建可运行项目、参考目录组织与排错思路的读者。1. 单目测距这套方案为什么成了毕业设计里的性价比之选一辆普通 USB 摄像头固定在桌面上画面里出现一个锥桶程序要在不接任何激光雷达、不接双目模组的前提下把锥桶到镜头的距离算出来误差还得控制在可接受范围内。这就是「基于 YOLOv5 的单目测距系统」要解决的核心问题也是近几年计算机、自动化、嵌入式方向毕业设计里出现频率极高的题目。它吸引人的地方很直接硬件成本低到只需要一个摄像头算法链路完整覆盖了目标检测、相机标定、几何测距、结果可视化工作量足够撑起一篇毕设又不像多传感器融合那样一上来就卡在设备采购和标定环境上。这套系统适合谁如果你正在做计算机毕业设计选题手里只有一台笔记本加一个摄像头又想做出能现场演示、有量化指标、能写进论文的东西那它非常合适。它同时踩中了 yolov5 训练自己的数据集、树莓派部署 yolov5、yolov5 后处理这几个高频检索方向意味着你遇到问题时能找到的参考资料足够多。但要注意单目测距不是「检测框画出来距离就自动有了」它背后有一套明确的几何假设假设不成立测出来的数字就是玄学。下面从原理到落地把这条链路拆开讲清楚。2. 单目测距的几何原理与 YOLOv5 检测链路怎么接2.1 相似三角形测距一个公式撑起整个系统单目测距最常用的方法是基于相似三角形。核心假设是已知目标的真实物理宽度或高度W已知相机焦距 f单位是像素检测框在图像里的像素宽度是 w那么目标到相机的距离 D 满足D (W × f) / w这个公式成立的前提有三个相机内参固定且已标定、目标真实尺寸已知、目标平面大致垂直于光轴。三个前提里任何一个崩掉测距结果就会系统性偏大或偏小。很多人第一次做的时候直接拿一个经验焦距就上结果换一台摄像头数字全乱这就是没做标定的后果。焦距 f 的获取有两种常见做法。一种是用棋盘格做相机标定通过 OpenCV 的 calibrateCamera 拿到内参矩阵取 fx 作为焦距另一种是简易标定法把已知宽度 W 的物体放在已知距离 D0 处测出它在图像里的像素宽度 w0反推 f (w0 × D0) / W。前者严谨适合写进论文后者快适合快速验证。我一般建议两种都做一遍用棋盘格的结果做基准用简易法交叉验证两者差太多说明标定环节有问题。2.2 YOLOv5 检测输出到测距输入的字段映射YOLOv5 的检测结果经过后处理后每条目标包含这些关键字段边界框左上角和右下角坐标 (x1, y1, x2, y2)、置信度 conf、类别 cls。测距模块真正需要的是像素宽度 w x2 - x1以及类别对应的真实宽度 W。所以中间要做一层映射把类别名映射到真实物理尺寸表。这里有个容易忽略的点YOLOv5 默认输出的是归一化坐标相对图像宽高的比例需要先乘以图像宽高还原成像素坐标再算像素宽度。如果你直接拿归一化宽度去套公式结果会差好几个数量级。下面这段代码展示了从推理到测距的完整衔接import cv2 import torch import numpy as np # 类别到真实宽度的映射表单位米 # 这张表必须根据你实际检测的目标来填不能照抄 REAL_WIDTH { cone: 0.28, # 锥桶底部宽度 person: 0.45, # 人体肩宽近似 car: 1.80, # 车宽 } # 相机焦距单位像素由标定得到 FOCAL_LENGTH 615.0 def detect_and_measure(img, model, img_size640): # 预处理letterbox 到模型输入尺寸 h0, w0 img.shape[:2] img_resized cv2.resize(img, (img_size, img_size)) img_rgb cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB) tensor torch.from_numpy(img_rgb).permute(2, 0, 1).float() / 255.0 tensor tensor.unsqueeze(0) # 推理 with torch.no_grad(): pred model(tensor)[0] # NMS 后处理得到 [x1, y1, x2, y2, conf, cls] pred pred[pred[:, 4] 0.4] # 置信度阈值 results [] for det in pred: x1, y1, x2, y2 det[:4].tolist() conf, cls_id det[4].item(), int(det[5].item()) # 归一化坐标还原到原图像素坐标 x1 x1 / img_size * w0 x2 x2 / img_size * w0 y1 y1 / img_size * h0 y2 y2 / img_size * h0 pixel_width x2 - x1 if pixel_width 1: continue cls_name model.names[cls_id] if cls_name not in REAL_WIDTH: continue # 相似三角形测距 distance (REAL_WIDTH[cls_name] * FOCAL_LENGTH) / pixel_width results.append((x1, y1, x2, y2, cls_name, conf, distance)) return results逻辑说明先做 letterbox 缩放保持比例推理后把归一化坐标还原回原图尺度再用像素宽度代入相似三角形公式。参数说明置信度阈值 0.4 是起点实际按场景调漏检多就降到 0.25误检多就升到 0.5FOCAL_LENGTH 必须来自你自己的标定不能直接用示例值REAL_WIDTH 表里的数值要拿卷尺实测锥桶写 0.28 还是 0.3 会直接影响结果。2.3 相机标定用棋盘格拿到可信的焦距标定这一步决定了测距的上限。常见做法是打印一张 9×6 的棋盘格每格边长 25mm用待标定的摄像头从不同角度拍 15 到 20 张覆盖画面中心和四角。张数太少会导致畸变系数估计不准四角覆盖不到会让边缘区域测距偏差变大。import cv2 import numpy as np import glob # 棋盘格内角点数量注意是内角点不是格子数 CHESSBOARD (8, 5) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp np.zeros((CHESSBOARD[0] * CHESSBOARD[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHESSBOARD[0], 0:CHESSBOARD[1]].T.reshape(-1, 2) objp * 0.025 # 每格 25mm换算成米 objpoints, imgpoints [], [] for fname in glob.glob(calib_images/*.jpg): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, CHESSBOARD, None) if ret: corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners2) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None) print(内参矩阵:\n, mtx) print(焦距 fx , mtx[0, 0], fy , mtx[1, 1]) print(重投影误差:, ret)逻辑说明objp 构造的是棋盘格在世界坐标系下的三维点imgpoints 是每张图检测到的角点像素坐标calibrateCamera 求解内参和畸变系数。参数说明重投影误差 ret 要小于 0.5 像素才算标定合格超过 1.0 说明有图拍糊了或者角点检测错了要剔除重拍fx 和 fy 理论上接近差太多说明图像被非等比拉伸过。拿到 mtx 后测距用 fx去畸变用 dist两个都别丢。3. 从零跑通数据集准备、训练与测距联调3.1 用 YOLOv5 训练自己的数据集标注到出权重数据集这块如果你做的是锥桶测距就自己拍几百张不同距离、不同光照下的锥桶照片如果做行人测距可以用公开数据集。标注用 labelImg导出 YOLO 格式的 txt每行是cls x_center y_center width height全部归一化到 0 到 1。目录结构按 YOLOv5 的要求组织dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml 内容path: ./dataset train: images/train val: images/val nc: 1 names: [cone]训练命令python train.py --img 640 --batch 16 --epochs 100 \ --data data.yaml --weights yolov5s.pt --device 0逻辑说明--weights 用预训练权重做迁移学习小数据集也能收敛--img 640 是输入尺寸和推理时保持一致--batch 16 按显存调爆显存就降到 8。参数说明epochs 100 是起点看验证集 mAP 曲线如果 60 轮后还在涨就加到 150如果早早就平了说明数据量或多样性不够加数据比加轮数有用。训练完在 runs/train/exp/weights/ 下拿 best.pt。3.2 测距联调把检测框、距离和可视化串起来训练出权重后把检测和测距接起来在画面上画出框、类别、距离这是毕设演示最直观的部分。import cv2 import torch model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/exp/weights/best.pt) model.conf 0.4 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break results detect_and_measure(frame, model) for x1, y1, x2, y2, cls_name, conf, dist in results: cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) label f{cls_name} {dist:.2f}m cv2.putText(frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(mono distance, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明逐帧读取调用前面的 detect_and_measure把距离叠加到画面上。参数说明dist 保留两位小数够用显示太多位反而显得不专业框的颜色和文字大小按演示屏幕调投影演示时字号要加大。联调阶段重点看两件事近距离1 米内框是否稳定远距离5 米外距离数字是否跳变。跳变严重通常是像素宽度太小导致分母敏感可以考虑对连续多帧的距离做滑动平均。3.3 精度验证怎么证明你的测距是准的毕设答辩一定会被问「你这个准不准误差多少」。验证方法很朴素把目标放在已知距离上比如 1m、2m、3m、5m每个距离测 20 帧记录测距值算平均绝对误差和相对误差。真实距离(m)平均测距(m)绝对误差(m)相对误差1.01.080.088.0%2.02.130.136.5%3.03.210.217.0%5.05.420.428.4%这张表是示意你的真实数据要自己测。规律通常是距离越远绝对误差越大相对误差相对稳定。如果相对误差在 10% 以内对于毕设来说已经能打想压到 5% 以内就得在标定精度和目标真实尺寸测量上下功夫。注意别只测一个距离就下结论至少覆盖近中远三档。4. 避坑与排查单目测距最容易翻车的五个地方4.1 现象换一台摄像头距离全乱原因焦距 f 是跟具体摄像头绑定的不同摄像头的焦距、传感器尺寸都不同你拿 A 摄像头标定的 f 去算 B 摄像头的距离公式里的 f 就是错的。解决每换一个摄像头就重新标定一次把 fx 写进配置文件而不是硬编码在代码里。如果毕设要演示多摄像头至少保证测距用的那个是标定过的。4.2 现象目标斜着放距离明显偏大原因相似三角形假设目标平面垂直于光轴。目标一旦倾斜图像里的像素宽度不再对应真实宽度在成像平面上的投影公式失效。解决要么约束目标姿态比如锥桶正对镜头要么改用基于目标高度的测距高度方向受倾斜影响小一些要么在论文里明确说明适用姿态范围。别假装这个问题不存在答辩老师很可能拿一个斜放的物体让你现场测。4.3 现象检测框抖动距离数字疯狂跳原因YOLOv5 逐帧检测框的边界在像素级抖动像素宽度 w 变化几个像素距离 D 就会跳几十厘米远距离尤其明显。解决对连续 N 帧的距离做滑动平均或卡尔曼滤波N 取 5 到 10。也可以对检测框坐标做平滑再算距离。代价是响应变慢快速移动的目标会有延迟按场景权衡。4.4 现象远距离目标测出来比实际近很多原因远距离时目标在图像里只有几十个像素宽w 很小公式里 D 和 w 成反比w 的微小误差被放大。同时远距离目标容易漏检或框不准。解决设定有效测距范围比如 0.5m 到 8m超出范围不显示距离或标注「超出量程」。这比硬报一个错得离谱的数字要诚实也更专业。4.5 现象训练时 mAP 很高实测测距却很差原因mAP 衡量的是检测框和真值的 IoU框稍微大一点小一点 mAP 可能还很高但测距对框的宽度极其敏感。检测指标好不等于测距指标好。解决训练时除了看 mAP还要单独看框宽度的回归误差。实测阶段用真实距离验证别用 mAP 自我安慰。如果框宽度系统性偏大测距会系统性偏小可以在标定时把这个偏差补偿进去。5. 把测距做稳的进阶技巧滑动窗口与量程自适应基础版本跑通后真正拉开差距的是稳定性处理。我一般会在测距模块外面套一层滑动窗口同时根据像素宽度动态决定是否信任当前结果。from collections import deque class DistanceSmoother: def __init__(self, window7, min_pixel_width15): self.buffer deque(maxlenwindow) self.min_pixel_width min_pixel_width def update(self, pixel_width, raw_distance): # 像素宽度太小远距离不可信直接丢弃 if pixel_width self.min_pixel_width: return None self.buffer.append(raw_distance) # 去掉最大最小值再平均抗单帧异常 if len(self.buffer) 3: vals sorted(self.buffer)[1:-1] return sum(vals) / len(vals) return raw_distance逻辑说明min_pixel_width 是量程下限的守门员低于这个像素宽度说明目标太远测出来也不可信直接返回 None 让上层不显示。buffer 存最近 7 帧去掉最大最小再平均能压掉单帧的异常跳变。参数说明window 取 5 到 10太小压不住抖动太大响应迟钝min_pixel_width 按你的焦距和目标尺寸反推比如焦距 615、锥桶宽 0.28m、想保证 8m 内有效那 8m 处的像素宽度约是 0.28×615/8 ≈ 21 像素所以设 15 到 20 比较合理。还有一个技巧是量程自适应显示距离在 3m 内显示两位小数3m 以上显示一位小数因为远距离的精度本来就不支持两位小数显示太细反而暴露误差。演示时观众看到的是「1.23m」和「5.4m」既专业又不露怯。最后说个我自己的习惯每次改完标定参数或换了摄像头我一定先拿卷尺在地上贴三个标记点1m、3m、5m跑一遍看三个点的读数三个都对了才继续往下做。这个动作花不了五分钟但能省掉后面几小时的「为什么数字不对」的排查。单目测距这东西几何假设是地基标定是钢筋检测是砖任何一层偷懒最后都会在演示现场还回来。希望帮到你。本文还有配套的精品资源点击获取
返回列表