
简介基于YOLOv8的交通桥梁排水孔堵塞检测系统专为计算机视觉方向的学生和研究者设计适用于毕业设计、课程设计、大作业或项目初期演示也适合有一定基础的学习者快速进阶。系统提供完整源码、可视化界面、数据集和部署教程开箱即用运行成功后才上传能够直接生成核心指标曲线、混淆矩阵、F1分数曲线、P-R曲线、验证集预测结果及标签分布图满足答辩评审对实验完整性的要求。压缩包内共8个文件包含3个Python脚本分别对应可视化页面、模型训练与视频检测、3个模型权重文件和2个文本说明整体大小仅15.91MB轻巧易分发。作者已对代码进行充分测试并附有README.txt操作指引当前已有24人学习下载。资源不仅覆盖模型推理与训练过程还预留了二次开发接口方便在此基础上修改功能或扩展应用场景是学习YOLOv8目标检测与构建完整工程项目的实用型资料。1. 为什么是YOLOv8来盯排水孔一座桥的排水孔堵了往往没有预警。等到桥面出现裂缝、防护层鼓起维修预算早就翻了十倍。传统桥梁巡检靠人拿着摄像机对着泄水孔一张张拍一天下来只能覆盖几十个洞口夜间和恶劣天气几乎无法作业。基于YOLOv8的交通桥梁排水孔堵塞检测系统就是把这个人工盯视频的过程变成一条流水线用目标检测模型识别视频帧里的排水孔并输出它是“通畅”还是“堵塞”。如果你是做毕设、课程设计或者想为养护单位做原型验证这套系统提供了完整的可视化界面、数据集和部署脚本拿到后配好Python环境就能跑。同时理解和改造它的过程本身就是一次完整的YOLOv8工程实践。2. YOLOv8的模型结构与数据集准备2.1 yolov8模型结构中c2f和Anchor-Free检测头YOLOv8在架构上最值得关注的不是“比v5快多少”而是C2f模块和Anchor-Free检测头的组合。C2f把输入特征图拆成多个分支在通道维度上交叉堆叠让每一个卷积层都能看到不同感受野的子特征。当排水孔在画面里只有20x20像素时这种多尺度融合比单纯加深网络更容易保留小目标的边缘信息同时它没有引入额外的超参数换个权重文件就能直接复用。检测头改成Anchor-Free后预测的是物体中心到四条边的距离不再需要预设候选框的宽高比例。这带来两个直接好处一是推理时后处理少了一轮匹配GTX1660Ti这种入门卡跑yolov8s也能到30ms级二是不会因为预设anchor尺寸不符合排水孔形状而漏检。需要注意的是Anchor-Free对框边缘的回归更依赖回归损失的权重后面第3节我会专门看DFL损失曲线。在“用哪一档模型”的选择上我的经验是模型变体重量级推荐场景训练显存参考YOLOv8n最轻树莓派或纯CPU验证~1GBYOLOv8s折中课程设计、单卡训练~2GBYOLOv8m中等需要更高mAP的工程试用~4GBYOLOv8l/x重大桥批量检测~8GB以上这个表格没有写具体参数的参数量因为不同版本迭代较快只要理解n偏重速度s/m偏重平衡即可。对于该系统我用yolov8s作为基座搭配imgsz640既能满足大多数笔记本GPU的显存约束又比n档在小目标上更稳。2.2 用标注工具生成YOLO格式数据集项目自带的“完整数据集”一般都已经是YOLO的txt标注格式但如果你要扩充样本——比如加入其他沥青裂缝、树叶堆积的现场照片——就必须知道标注格式怎么转换。这里给出一个从Labelme多边形标注转到YOLO txt的脚本这是我处理桥梁类数据集时最常用的一段代码。import json import os import glob def labelme_to_yolo(json_path, out_dir, class_id_map): 把单张Labelme JSON转换为YOLO txt归一化坐标保留6位小数。 with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] txt_name os.path.basename(json_path).replace(.json, .txt) txt_path os.path.join(out_dir, txt_name) with open(txt_path, w) as out: for shape in data[shapes]: label shape[label] cls_id class_id_map[label] points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] xmin, xmax min(xs), max(xs) ymin, ymax min(ys), max(ys) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h out.write(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n) if __name__ __main__: class_id_map {blocked: 0, normal: 1} os.makedirs(labels/train, exist_okTrue) for json_file in glob.glob(labelme/train/*.json): labelme_to_yolo(json_file, labels/train, class_id_map)逻辑说明Labelme记录的是多边形的绝对像素坐标而YOLO需要的是相对图像宽高的中心点和宽高。这段脚本把所有点取最小外接矩形得到一个水平框。对排水孔这种近似圆形的目标外接矩形的交并比和圆相近足够训练使用。参数说明class_id_map的key必须和Labelme里标的名称完全一致否则会KeyError输出坐标保留6位小数因为0.000001的误差在1920宽的画面上只差不到2像素不会影响训练。2.3 数据目录和YAML配置数据集做好后目录结构按YOLO惯例组织data/drain_hole/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── drain.yamldrain.yaml内容如下path: /data/drain_hole train: images/train val: images/val test: images/test nc: 2 names: 0: blocked 1: normal路径可以用相对路径也可以写绝对路径。我在训练时踩过一个坑path写相对路径后在命令行里加cd到数据集根目录才能找到写成绝对路径后一劳永逸。接下来数据增强方面YOLOv8默认的mosaic会在训练时把4张图拼成1张对于小目标检测容易让目标变成原图1/4大小。如果你的数据只有几百张建议在训练命令中加上mosaic0或mosaic0.5同时把scale0.5调小防止排水孔被过度缩小。增强参数默认值排水孔场景建议原因mosaic1.00.5或0拼接后目标太小scale0.50.3避免目标缩到10x10以下fliplr0.50.5左右翻转不影响判断hsv_h0.0150.01颜色抖动过大会把积水反光变成噪声3. 训练自己的数据集参数、损失与曲线3.1 yolov8训练自己的数据集最小命令准备好YAML后训练命令可以写得非常短yolo detect train \ data/data/drain_hole/drain.yaml \ modelyolov8s.pt \ epochs120 \ imgsz640 \ batch16 \ device0 \ patience30 \ projectrun_drain \ nameexp1 \ mosaic0.5这里每个参数都值得说。modelyolov8s.pt会先下载预训练权重在COCO上预训练过的模型对边缘、纹理特征有很好的先验比自己从零训练快得多。epochs120是一种参考配合patience30早停当验证集mAP在30个epoch内不再上升时训练自动停止避免时间浪费。batch16取决于显存如果训练时报OOM就把batch降到8或4。imgsz640是大多数工程的标准输入但排水孔在俯拍画面中可能只有30像素如果显存充足升到800会带来明显mAP提升但推理时间也会增加。3.2 yolov8画损失函数曲线图看什么训练完以后run_drain/exp1/目录下会自动生成results.png包括两类损失box_loss和cls_loss以及两个度量指标mAP50和mAP50-95。很多新手只盯着loss是否下降其实更要看val分支。如果train loss下降而val loss在中期反弹说明过拟合需要增大数据增强或减少epochs。如果项目自带的训练脚本没有保存曲线或者你想按自己的喜好重新画下面这段可以直接用。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(run_drain/exp1/results.csv) df.columns [c.strip() for c in df.columns] for i, col in enumerate([train/box_loss, val/box_loss, metrics/mAP50(B)]): ax plt.subplot(1, 3, i 1) ax.plot(df[epoch], df[col], labelcol) ax.legend() plt.tight_layout() plt.savefig(loss_curve.png)逻辑说明读取results.csv后必须把列名两侧空格去掉因为Ultralytics导出时会在列名前后加空格。参数说明如果你用的是YOLOv8.0早期版本字段名可能没有(B)后缀可以直接用df.columns.tolist()打印实际列名。3.3 验证集评估与失败案例用yolo detect val单独验证yolo detect val \ data/data/drain_hole/drain.yaml \ modelrun_drain/exp1/weights/best.pt \ imgsz640跑完会输出precision、recall、mAP并生成confusion_matrix.png。排水孔检测场景中最常见的问题是“漏检率偏高”也就是混淆矩阵的右上角有数字。这种情况先看是否是图片中排水孔小于10x10像素如果是训练时imgsz640太小应改为imgsz800或更高。另一个可能是正负样本不均衡堵塞孔的数量远少于正常孔此时可以在数据集中复制堵塞孔样本或使用weights给少数类损失加权。4. 可视化界面与简单部署4.1 推理脚本和关键参数不管是界面还是后台服务推理核心都是这一段from ultralytics import YOLO import cv2 model YOLO(run_drain/exp1/weights/best.pt) img cv2.imread(bridge.jpg) results model.predict(img, conf0.25, iou0.45, verboseFalse) for result in results: for box in result.boxes: x1, y1, x2, y2 map(int, box.xyxy[0]) cls int(box.cls[0]) conf float(box.conf[0]) if cls 0: cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, fblocked {conf:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(result.jpg, img)conf0.25是置信度阈值排水孔检测建议先设低一点让召回率尽量高然后在界面上提供滑条调整。iou0.45用于非极大值抑制如果场景中孔洞互相紧贴可以降到0.3减少重复框。4.2 做一个PyQt可视化界面项目里的“可视化界面”通常是一个PyQt5或Tkinter弹窗。PyQt5代码量稍大但交互控件丰富。下面是一份可运行的最小框架核心是图片选择、推理绘制和QPixmap显示import sys import cv2 from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QFileDialog from PyQt5.QtGui import QImage, QPixmap from ultralytics import YOLO class MainWindow(QMainWindow): def __init__(self): super().__init__() self.model YOLO(best.pt) self.setWindowTitle(排水孔堵塞检测) self.btn QPushButton(选择图片, self) self.btn.move(20, 20) self.btn.clicked.connect(self.open_image) self.label QLabel(self) self.label.setGeometry(20, 60, 800, 600) self.label.setStyleSheet(border: 1px solid gray) def open_image(self): path, _ QFileDialog.getOpenFileName(self, Open Image, , Images (*.jpg *.png)) if path: src cv2.imread(path) results self.model.predict(src, conf0.25) for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0]) cls int(box.cls[0]) color (0, 0, 255) if cls 0 else (0, 255, 0) cv2.rectangle(src, (x1, y1), (x2, y2), color, 2) self.show_image(src) def show_image(self, cv_img): rgb cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg).scaled(self.label.size(), Qt.KeepAspectRatio)) if __name__ __main__: app QApplication(sys.argv) win MainWindow() win.resize(850, 700) win.show() sys.exit(app.exec_())逻辑说明scaled只改变显示尺寸原始图像在推理时保持原分辨率。参数说明显示区域固定为800x600实际素材如果超过用Qt.KeepAspectRatio防止拉伸变形。注意在PyQt中必须持有一份对QImage的引用否则数组内存被释放后显示会花屏所以rgb要一直保留到setPixmap之后。4.3 部署教程从源码包到一键运行项目附带的“部署教程”一般都是这样先建conda环境安装PyQt5和ultralytics再运行界面脚本。这里给出一份适用于Windows的requirements.txtultralytics8.0.0 PyQt55.15 opencv-python4.5 torch1.8 torchvision0.9 pandas matplotlib如果机器只有CPU用pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu否则默认安装CUDA版。要真正“简单部署”还可以把best.pt导出成ONNX格式yolo detect export modelbest.pt formatonnx imgsz640然后用onnxruntime替换PyTorch进行推理这样即使对方电脑没装GPU也能稳定运行但界面代码里要加一段onnxruntime的读取逻辑。对于毕设演示PyTorch直驱更直观少一道转换就少一个坑。5. 四个必踩的坑和验证技巧5.1 CUDA、镜像和GTX1660Ti的纠缠很多人的第一个坑是环境装好后import torch报告CUDA不可用。常规解决方法是先看nvidia-smi驱动版本再装对应的PyTorch。GTX1660Ti的显存只有6GB跑yolov8s batch16很容易OOM我会先设batch8并加workers4。另外从国内镜像源安装时不要混用不同渠道的torch附件否则运行时会报illegal instruction。5.2 数据集划分泄露这是排水孔检测里隐蔽性最强的问题。很多桥面俯拍视频是连拍的相邻帧几乎一模一样。如果随机划分train/val模型会利用背景记忆来“检测”在测试集上mAP虚高换一段新桥视频就暴跌。正确做法是按巡检时间切分前80%的帧进训练集后20%进验证集并且同一座桥不要横跨两个集合。写个简单脚本就能做到。import os import shutil frames sorted(os.listdir(all_images)) val_count int(len(frames) * 0.2) for i, name in enumerate(frames): if i len(frames) - val_count: shutil.move(os.path.join(all_images, name), images/val/) else: shutil.move(os.path.join(all_images, name), images/train/)5.3 小目标与mAP的取舍如果你发现损失曲线很好但mAP50不到0.6首先怀疑不是模型坏了而是目标太小。用imgsz800重新训练通常能将小目标mAP提升3-5个点但推理耗时增加约30%。另一种思路是改用更细的backbone把yolov8s换成yolov8m但要评估显存是否够用。5.4 用视频帧数说话最后给出一个对毕设答辩最有效的验证技巧不只看mAP而是用一段没参与过训练的桥面视频总共200帧人工标注每帧是否有堵塞孔再让模型逐帧推理写一个独立脚本统计精确率和召回率。tp fp fn 0 for frame_id in range(200): gt gt_labels[frame_id] # 人工标注列表 [0/1] det detect(frame_id) # 模型输出 [0/1] if det 1 and gt 1: tp 1 elif det 1 and gt 0: fp 1 elif det 0 and gt 1: fn 1 print(fPrecision: {tp/(tpfp):.2f}, Recall: {tp/(tpfn):.2f})这样的统计结果比一张PR曲线更能打动评审老师。注意这里的detect(frame_id)直接封装了4.1节的推理逻辑gt_labels则是逐帧人工标注。做完这一步再回到模型里去调整conf阈值如果fp高就调高conffn高就调低conf阈值从0.15到0.5按0.05步长扫描一遍画出来阈值-指标曲线整个系统的调参逻辑闭环就齐了。本文还有配套的精品资源点击获取