简介:这份资源面向计算机、人工智能、通信工程、自动化等专业的在校学生与教师,提供一套可直接运行的交通高速公路路面抛洒物检测方案,适合作为毕业设计、课程设计或大作业的完整参考。压缩包共8个文件,约15.91MB,包含3个Python脚本、3个模型权重文件与2个说明文本,分别承担可视化界面、模型训练与视频检测、预训练权重加载及部署指引等用途,结构精简、上手门槛低。项目基于YOLOv8实现,配套完整数据集与可视化页面,可生成核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图,便于答辩时系统展示实验过程与效果。目前已有91人学习下载,代码均经运行验证后上传,下载后按README说明即可完成部署,也可在此基础上修改扩展功能,用于学习进阶或项目初期立项演示。
1. 路面抛洒物检测:为什么YOLOv8成了高速场景的默认选项
高速公路上一个掉落的轮胎皮、一袋散落的水泥,留给后车的反应时间往往不到两秒。这类抛洒物检测和普通的目标检测任务有本质区别:目标极小、背景高速运动、正负样本极度不均衡。我最早用传统帧差法做过一版,白天误报率还能忍,一到夜间或者雨天,虚警直接把监控室淹了。后来换成YOLOv8,才算把召回率和误报率同时压到可接受的范围。
这套「基于YOLOv8的交通高速公路路面抛洒物检测系统」要解决的,就是从监控视频流里实时框出路面异物并触发告警。它适合三类人:做毕设或课程设计的学生,需要一个功能完整、能跑通全流程的项目;刚接触YOLOv8的工程师,想拿一个真实场景练手数据集处理和部署;还有做交通信息化的从业者,评估这套方案能不能落到自己的路段上。标题里提到的源码、可视化界面、完整数据集、部署教程,本质上是在降低从零搭建的门槛——但门槛低不代表没有坑,后面几章我会把每个环节的边界条件讲清楚。
2. 数据集怎么处理才能让抛洒物检测不翻车
抛洒物检测的效果,七成取决于数据集质量,三成才是模型和调参。很多人拿到标注文件直接开训,结果mAP卡在0.4上不去,回头查才发现标注框大量漏标、类别定义混乱。这一章把数据集的清洗、转换、增强讲透。
2.1 抛洒物数据集的类别定义与标注规范
高速抛洒物的类别划分不能太细。我见过有人把「塑料袋」「纸箱」「轮胎皮」「石块」「金属件」分成五类,训练出来每一类的样本都不够,模型根本学不动。常见做法是合并成两类:debris(一般抛洒物)和large_object(大件抛洒物,比如轮胎、家具)。如果只做二分类检测,直接用一个debris类也够用。
标注时用Labelme或者LabelImg都行,导出成YOLO格式。关键规范有三条:第一,小于16×16像素的目标要么放大标注区域,要么直接丢弃,YOLOv8的P3特征图对这么小的目标响应很弱;第二,运动模糊严重的帧不要标,标了也是噪声;第三,同一目标在连续帧里只标关键帧,不要每帧都标,否则正样本冗余严重。
标注完成后目录结构应该是这样:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml的内容:
path: ./dataset train: images/train val: images/val test: images/test nc: 2 names: ['debris', 'large_object']这里nc是类别数,names的顺序必须和标注文件里的class_id一一对应。我踩过的坑是:标注时先标了large_object再标debris,结果class_id反了,训练出来模型把大件识别成小件,排查了半天。
2.2 用脚本把VOC和COCO格式转成YOLO训练格式
很多公开数据集是VOC的XML格式或者COCO的JSON格式,不能直接喂给YOLOv8。我一般写一个转换脚本统一处理。以VOC转YOLO为例:
import xml.etree.ElementTree as ET import os from pathlib import Path def voc_to_yolo(xml_dir, img_dir, out_label_dir, class_map): """ xml_dir: VOC标注文件目录 img_dir: 对应图片目录,用于读取宽高 out_label_dir: 输出YOLO标签目录 class_map: {'debris': 0, 'large_object': 1} """ os.makedirs(out_label_dir, exist_ok=True) for xml_file in Path(xml_dir).glob('*.xml'): tree = ET.parse(xml_file) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) lines = [] for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in class_map: continue cls_id = class_map[cls_name] bbox = obj.find('bndbox') x1 = float(bbox.find('xmin').text) y1 = float(bbox.find('ymin').text) x2 = float(bbox.find('xmax').text) y2 = float(bbox.find('ymax').text) # 转成YOLO的归一化中心点+宽高 cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_file = Path(out_label_dir) / (xml_file.stem + '.txt') out_file.write_text('\n'.join(lines)) if __name__ == '__main__': voc_to_yolo( xml_dir='raw/Annotations', img_dir='raw/JPEGImages', out_label_dir='dataset/labels/train', class_map={'debris': 0, 'large_object': 1} )逻辑说明:遍历每个XML文件,读取图片宽高,把VOC的左上右下坐标转成YOLO需要的归一化中心点坐标和宽高。class_map控制类别映射,不在映射表里的类别直接跳过。参数上注意cx和cy是除以图片宽高,不是除以2,这是新手最容易写错的地方。
转换完成后,一定要抽查几张图片的标签是否对齐。我一般用下面的脚本可视化验证:
import cv2 import numpy as np def visualize_label(img_path, label_path, class_names): img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:]) x1 = int((cx - bw/2) * w) y1 = int((cy - bh/2) * h) x2 = int((cx + bw/2) * w) y2 = int((cy + bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('check', img) cv2.waitKey(0) visualize_label('dataset/images/train/001.jpg', 'dataset/labels/train/001.txt', ['debris', 'large_object'])如果框的位置明显偏移,大概率是宽高读反了或者归一化除错了。
2.3 针对小目标的增强策略与负样本采样
抛洒物在画面里往往只占几十个像素,默认的Mosaic增强对小目标其实不友好——四张图拼在一起后,小目标被进一步缩小。我的做法是:训练前期用Mosaic,后期最后10个epoch关掉,让模型在原始尺度上收敛。另外开启copy_paste增强,把标注好的抛洒物复制粘贴到其他帧的路面上,能有效增加正样本密度。
负样本同样重要。高速路面上的车道线、阴影、水渍、路面修补痕迹,都是常见的误报来源。我一般会从没有抛洒物的视频里抽2000到3000帧作为背景负样本,混进训练集。YOLOv8对纯背景图的处理是:如果一张图没有任何标注,它会被当作负样本参与分类损失计算,不会报错。
注意:负样本比例不要超过总样本的30%,否则模型会偏向于「什么都不检测」。
3. YOLOv8训练抛洒物检测模型的完整命令与参数
数据集准备好之后,训练本身反而是最标准化的环节。但标准化不代表可以无脑跑,学习率、batch size、输入尺寸这几个参数直接决定模型能不能收敛到可用水平。
3.1 环境配置:从零搭好YOLOv8训练环境
我一般用conda建一个独立环境,避免和系统里的其他包冲突。Python版本选3.9或3.10,太新的版本有些依赖还没跟上。
conda create -n yolov8 python=3.10 -y conda activate yolov8 # 安装PyTorch,根据你的CUDA版本选对应命令 # CUDA 11.8的情况 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装ultralytics pip install ultralytics # 验证安装 yolo checksyolo checks会输出当前环境的信息,包括PyTorch版本、CUDA是否可用、GPU型号。如果CUDA显示不可用,检查驱动版本和PyTorch版本是否匹配。CPU版本也能跑,但训练速度会慢一个数量级,GTX 1660 Ti这种6GB显存的卡,用imgsz=640、batch=8大概能跑起来。
3.2 训练命令拆解:每个参数为什么这么设
最基础的训练命令:
yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ device=0 \ workers=4 \ project=runs/debris \ name=exp1逐参数说明:
| 参数 | 含义 | 抛洒物场景建议值 |
|---|---|---|
model | 预训练权重 | yolov8s.pt起步,显存够用yolov8m.pt |
epochs | 训练轮数 | 150到300,看早停 |
imgsz | 输入尺寸 | 640起步,小目标多用960 |
batch | 批大小 | 6GB显存用8到12,12GB用24 |
lr0 | 初始学习率 | 0.01是默认,小数据集降到0.005 |
lrf | 最终学习率因子 | 0.01,即最终lr=lr0×lrf |
patience | 早停耐心值 | 30,验证集不涨就停 |
workers | 数据加载线程 | 4到8,Windows下设0避免报错 |
为什么选yolov8s而不是yolov8n?nano版本虽然快,但对小目标的特征提取能力明显不足,抛洒物检测里mAP会低5到8个点。s版本在速度和精度之间平衡得最好,如果部署到RK3588这类边缘设备,再考虑用n版本或者做量化。
3.3 训练过程监控与损失曲线判读
训练启动后,runs/debris/exp1/目录下会生成results.csv和一系列曲线图。重点看三个指标:train/box_loss、val/box_loss、metrics/mAP50。
正常的收敛过程是:box_loss在前20个epoch快速下降,之后缓慢下降;mAP50持续上升,到后期趋于平缓。如果val/box_loss开始上升而train/box_loss还在降,说明过拟合了,要么加数据,要么加正则化(调大weight_decay)。
画损失曲线的脚本:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/debris/exp1/results.csv') df.columns = df.columns.str.strip() fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(df['epoch'], df['train/box_loss'], label='train') axes[0].plot(df['epoch'], df['val/box_loss'], label='val') axes[0].set_xlabel('epoch') axes[0].set_ylabel('box_loss') axes[0].legend() axes[1].plot(df['epoch'], df['metrics/mAP50'], label='mAP50') axes[1].plot(df['epoch'], df['metrics/mAP50-95'], label='mAP50-95') axes[1].set_xlabel('epoch') axes[1].set_ylabel('mAP') axes[1].legend() plt.tight_layout() plt.savefig('loss_curve.png', dpi=150)这个脚本直接读results.csv,列名可能有空格,所以先strip一下。如果mAP50在0.7以上,基本可以进入部署阶段;低于0.5的话,回头查数据集标注质量。
4. 可视化界面与推理部署:从权重文件到可交互系统
训练出best.pt只是第一步,真正交付的是一个能打开摄像头、能上传视频、能实时框出抛洒物并告警的系统。可视化界面用PyQt5或者Gradio都行,我倾向于PyQt5,因为打包成exe更方便。
4.1 用PyQt5搭一个抛洒物检测可视化界面
界面不需要花哨,核心功能就三个:视频显示区、检测结果列表、告警日志。下面是一个最小可运行版本:
import sys import cv2 from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QTextEdit) from PyQt5.QtCore import QTimer, Qt from PyQt5.QtGui import QImage, QPixmap from ultralytics import YOLO class DebrisDetector(QMainWindow): def __init__(self): super().__init__() self.model = YOLO('runs/debris/exp1/weights/best.pt') self.cap = None self.timer = QTimer() self.timer.timeout.connect(self.update_frame) self.setWindowTitle('高速抛洒物检测系统') self.resize(900, 700) central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) self.video_label = QLabel('等待视频输入...') self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setMinimumSize(800, 500) layout.addWidget(self.video_label) self.btn_open = QPushButton('打开视频') self.btn_open.clicked.connect(self.open_video) layout.addWidget(self.btn_open) self.log = QTextEdit() self.log.setReadOnly(True) layout.addWidget(self.log) def open_video(self): self.cap = cv2.VideoCapture(0) # 0为摄像头,也可换成视频路径 self.timer.start(30) def update_frame(self): ret, frame = self.cap.read() if not ret: return results = self.model(frame, conf=0.4, iou=0.5, verbose=False) annotated = results[0].plot() # 记录检测到的目标 boxes = results[0].boxes if boxes is not None and len(boxes) > 0: for cls_id in boxes.cls.tolist(): name = self.model.names[int(cls_id)] self.log.append(f'检测到: {name}') rgb = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg)) def closeEvent(self, event): if self.cap: self.cap.release() self.timer.stop() if __name__ == '__main__': app = QApplication(sys.argv) win = DebrisDetector() win.show() sys.exit(app.exec_())逻辑说明:QTimer每30毫秒触发一次update_frame,从视频源读一帧,送入YOLO推理,results[0].plot()直接返回画好框的numpy数组,转成QImage显示。conf=0.4是置信度阈值,iou=0.5是NMS的IoU阈值。检测到目标时往日志区追加一条记录。
参数上,conf设太低会误报,设太高会漏报。抛洒物场景我一般用0.35到0.45之间,根据实际路测调整。iou保持0.5默认值即可,除非发现同一目标被重复框选。
4.2 推理参数调优:置信度、IoU与输入尺寸的联动
推理阶段有三个参数需要联动调整:conf、iou、imgsz。它们不是独立的,改一个往往要跟着改另一个。
imgsz在推理时可以和训练时不同。训练用640,推理可以用1280来提升小目标召回,但速度会下降。如果部署在边缘设备上,反而要降到416或320来保帧率。我的经验是:服务器端用1280,边缘端用640,极端情况用416。
conf和iou的联动关系:调低conf会让更多候选框进入NMS,此时如果iou也低,NMS会过滤掉大量重叠框,可能导致漏检;如果iou高,保留的框多,误报增加。抛洒物检测里,我一般先固定iou=0.5,然后从0.5往下调conf,直到召回率满足要求,再看误报能不能接受。
# 批量测试不同conf下的检测结果 from ultralytics import YOLO model = YOLO('best.pt') for conf in [0.25, 0.35, 0.45, 0.55]: results = model('test_video.mp4', conf=conf, iou=0.5, stream=True) total = 0 for r in results: total += len(r.boxes) print(f'conf={conf}, 总检测框数={total}')这个脚本能快速看出不同置信度下的检测框数量变化,帮你找到拐点。
4.3 部署到边缘设备:RK3588与CPU版本的取舍
如果标题里的部署教程要落到实际硬件上,RK3588是常见选择。它自带NPU,能把YOLOv8的推理速度从CPU上的2到3帧提升到25帧以上。但模型需要先转成RKNN格式,转换过程中有几个坑:第一,YOLOv8的输出层需要做后处理适配,不能直接用ONNX的输出去转;第二,量化时要用真实抛洒物图片做校准集,否则量化后小目标精度掉得厉害;第三,RKNN Toolkit的版本要和板端驱动匹配,版本不对会报init_runtime失败。
如果只是做毕设演示,CPU版本足够了。Ubuntu 20.04上装好PyTorch CPU版,推理一张640×640的图大概200到400毫秒,用视频流的话做个跳帧处理也能跑。
5. 抛洒物检测落地时最容易踩的五个坑
这一章记录我在实际部署中翻车最多的几个问题,每个都按「现象→原因→解决」写清楚。
5.1 夜间误报率飙升:路灯和车灯被当成抛洒物
现象:白天跑得好好的模型,一到夜间监控画面里,路灯的光斑、对向车灯的眩光被大量框成debris,误报率从5%涨到40%以上。
原因:训练集里夜间样本太少,模型没见过这种高对比度、大面积过曝的负样本。另外,YOLOv8的默认色彩增强没有针对夜间做特殊处理。
解决:从夜间监控视频里抽2000帧以上作为负样本加入训练集,同时开启hsv_v增强,让模型对亮度变化更鲁棒。如果还不行,在前处理里加一个自适应直方图均衡化,把夜间画面的动态范围压一压。
5.2 小目标漏检:远处抛洒物在640输入下只剩几个像素
现象:距离摄像头50米以外的抛洒物,在640×640输入下只有8×8像素,模型完全检测不到。
原因:YOLOv8的P3特征图下采样8倍,640输入下P3的特征图是80×80,8像素的目标在上面只占1个格子,特征太弱。
解决:推理时把imgsz提到1280,P3特征图变成160×160,小目标占4个格子,召回率明显提升。代价是推理速度降一半左右。另一个办法是在训练时开启copy_paste,人为增加小目标样本。
5.3 类别不平衡导致大件抛洒物被忽略
现象:large_object类的AP只有0.3,而debris类有0.75,模型几乎不检测大件。
原因:大件抛洒物在数据集里占比不到10%,分类损失被小类主导。
解决:在data.yaml里给每个类加权重,或者在训练时用fraction参数控制采样比例。更直接的办法是过采样大件样本,把包含large_object的图片复制2到3份放进训练集。
5.4 视频流推理延迟累积导致画面卡顿
现象:用stream=True处理长视频时,内存持续上涨,画面越来越卡。
原因:stream=True返回的是生成器,如果不及时消费,结果会堆积在内存里。另外OpenCV的VideoCapture缓冲区默认会缓存多帧。
解决:设置cv2.VideoCapture的缓冲区大小为1:
cap = cv2.VideoCapture('video.mp4') cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)推理时用stream=True并逐帧消费,不要一次性把结果转成list。
5.5 模型导出ONNX后精度下降
现象:PyTorch权重推理正常,导出ONNX后用onnxruntime推理,mAP掉了3到5个点。
原因:导出时的opset版本和推理引擎不匹配,或者动态轴设置不对导致resize行为不一致。
解决:导出时指定opset=12,并且用dynamic=False固定输入尺寸:
yolo export model=best.pt format=onnx opset=12 imgsz=640 dynamic=False导出后用onnxruntime跑一遍验证集,和PyTorch的结果对比,确认精度一致再部署。
6. 把mAP再提5个点:几个我常用的进阶技巧
训练到mAP50在0.75左右时,常规调参已经很难再涨了。这时候需要从数据和模型结构两个层面找突破口。
第一个技巧是难例挖掘。用训练好的模型跑一遍训练集,把置信度在0.1到0.4之间的预测框对应的图片挑出来,人工复核。这些是模型「犹豫」的样本,把它们重新标注后加入训练集,对精度的提升比随机加数据明显得多。我一般做两轮难例挖掘,mAP能涨3到5个点。
第二个技巧是多尺度训练。在data.yaml里设置imgsz为一个范围而不是固定值,比如imgsz=640,960,训练时每10个epoch随机换一次尺度。这样模型对不同距离的抛洒物都有适应性。代价是训练时间增加20%左右。
第三个技巧是替换检测头。YOLOv8默认的检测头是解耦头,对小目标已经比较友好。如果还想再进一步,可以换成带注意力机制的检测头,比如在P3特征图上加一个CBAM模块。改动不大,但小目标的AP能涨2个点左右。
# 在YOLOv8的head部分插入CBAM的示意 import torch import torch.nn as nn class CBAM(nn.Module): def __init__(self, channels, reduction=16): super().__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) self.fc = nn.Sequential( nn.Conv2d(channels, channels // reduction, 1, bias=False), nn.ReLU(), nn.Conv2d(channels // reduction, channels, 1, bias=False) ) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = self.fc(self.avg_pool(x)) max_out = self.fc(self.max_pool(x)) return x * self.sigmoid(avg_out + max_out)这个模块加在P3的卷积层后面,通道数对齐即可。注意加了之后要重新训练,不能直接加载原权重。
最后一个习惯:每次训练完,我都会用验证集里mAP最低的那10张图做一次可视化,看看模型到底在哪些场景下失败。十次里有八次能发现标注错误或者场景遗漏。模型不会骗人,它学不会的东西,要么是数据没给够,要么是标注有问题。
希望帮到你。
本文还有配套的精品资源,点击获取