☰
垃圾桶满溢检测数据集:YOLOv5三分类实战与避坑指南
2026/9/26 2:53:51 网站建设 项目流程

简介:面向目标检测与垃圾分类场景的实用数据集,包含满溢垃圾桶、未满溢垃圾桶和垃圾三个类别,图像为400×500的RGB图片,并按YOLOV5标准目录组织,下载解压后可直接用于模型训练,无需额外转换。资源包共2000个文件、约313MB,其中1999个txt为各图片对应的标注文件,并附有3类别的类别文本,另提供1个Python可视化脚本,可随机读取一张图片绘制边界框,便于快速验证标注效果。数据已按YOLOV5惯例划分训练集与验证集,训练集2680张图片及标签、验证集669张图片及标签,可直接用于训练与评估流程。项目内show.py脚本无需修改即可运行,帮助使用者直观检查数据质量。目前已有451人学习下载,适合需要成熟标注数据集开展垃圾分类检测实验的学生、研究者与开发者。

1. 垃圾桶满溢检测数据集:为什么 3 类标注比 10 类更实用

目标检测数据集(YOLOV5目录格式):垃圾桶满溢检测数据集(3类别),这个标题放到搜索页里很常见,但真正拿到手里你会发现,满溢检测的核心不是模型多强,而是数据集怎么把“满溢”这个状态定义清楚。垃圾桶是固定场景里的静态物体,检测任务从“物体有没有”变成了“状态是什么”,所以 3 个类别反而比 10 个类别更好训练。适合正在用 YOLOv5 训练自己的数据集、又不想在环卫和物业场景里反复翻车的算法工程师。下面从目录格式、标注规则、训练参数到踩坑点,给你一条可以直接复现的路径,而不是只给一份“看起来能用”的压缩包。

2. 先看懂 YOLOV5 目录格式:一张图对应一个 txt,坐标全部归一化

拿到数据集先不要急着解压跑训练,先看目录结构。YOLOV5 目录格式和 COCO、VOC 不一样,它把标注信息拆成每个图片同名的 txt 文件,统一放在 labels 目录下。不了解这一层,后面 data.yaml 写错一个路径就是No labels found,训练直接停摆。很多从公开数据集转过来的新手会把 VOC 的 XML 硬塞进 YOLOv5,结果训练时一条标注都读不到,这就是典型的“目录格式没先立住”。

2.1 目录结构:images 和 labels 必须严格对齐

一份标准的 YOLOV5 目录格式数据集,长这样:

dataset/ ├── images/ │ ├── train/ │ │ ├── img_001.jpg │ │ ├── img_002.jpg │ │ └── ... │ ├── val/ │ │ └── ... │ └── test/ │ └── ... ├── labels/ │ ├── train/ │ │ ├── img_001.txt │ │ ├── img_002.txt │ │ └── ... │ ├── val/ │ │ └── ... │ └── test/ │ └── ... ├── data.yaml └── classes.txt

images和labels下面要有相同的子目录名,而且图片文件名和 txt 文件名必须完全一致,包括大小写和后缀。比如图片是img_001.jpg,对应标注就是img_001.txt。如果图片是.jpeg后缀,txt 名的基准是去掉后缀后的主文件名,跟后缀无关。txt 里没有内容说明这张图没有目标;但是训练时 YOLOv5 会把这样图片当作背景图,如果背景图太多,模型会偏向“什么都不检”。

这里有一个常见误解:classes.txt不是 YOLOv5 训练必需的。真正决定类别的是data.yaml里的names列表。如果你拿到的数据集只有classes.txt,也不要直接把它改成data.yaml,因为 YOLOv5 的 train.py 只认 yaml 格式的配置文件。

2.2 3 个类别怎么定:normal、full、overflow

标题里写的是“3类别”,那这三类到底是什么,必须在一开始就定死。按环卫巡检最常见的需求,我一般建议这样定义:

类别编号类别名判定标准
0normal垃圾桶内垃圾没有超过桶口平面,桶盖能正常闭合,桶周围没有散落垃圾
1full垃圾已经堆到桶口平面以上,桶盖无法闭合,但垃圾还没有散落到桶外地面
2overflow垃圾已经散落到桶外地面,桶周围有明显堆弃物,或者垃圾袋挂在桶边

这个定义一定要落到标注规则上,否则标注员会自由发挥。很多数据集翻车就翻在 full 和 overflow 的边界上:有人觉得垃圾高过桶口就算 overflow,有人觉得必须掉到地上才算 overflow。模型学到的边界是混乱的,最后表现为同一段视频里状态在 full 和 overflow 之间来回跳。

为什么是 3 类而不是更多?因为满溢检测最终要触发的动作不一样:normal 不用处理,full 需要安排清运但可以缓几个小时,overflow 需要尽快处理。类别设计要和业务动作一一对应,而不是为了“看起来厉害”去细分垃圾类型。细分垃圾类型是垃圾分类检测的事,和满溢检测是两个任务。

2.3 标签文件里到底写了什么:归一化坐标计算

YOLOv5 的 txt 每一行代表一个目标框,格式是:

# class_id x_center y_center width height # 坐标均为归一化值,即像素值除以图片的宽或高 0 0.512345 0.678901 0.123456 0.234567 2 0.300000 0.800000 0.150000 0.200000

第一列是类别编号,后面四个数分别是目标框中心点的 x、中心点的 y、框宽、框高,全部相对于图片尺寸做了归一化。比如一张 1920x1080 的图片,某个垃圾桶框的左上角是 (500, 300),右下角是 (800, 600),那么:

x_center = (500 + 800) / 2 / 1920 = 0.3385 y_center = (300 + 600) / 2 / 1080 = 0.4167 width = (800 - 500) / 1920 = 0.1563 height = (600 - 300) / 1080 = 0.2778

注意是中心点坐标,不是左上角坐标。很多人手动改的时候把 x_min 和 y_min 直接写进去,训练出来框全部偏到右下角。这种错误用可视化脚本一查就能发现,后面会讲。

还有一点:坐标如果出现小于 0 或大于 1 的值,YOLOv5 会把它当异常框过滤掉,导致该目标直接不参与训练。所以转换脚本里必须做边界处理,但不能简单地裁一刀了事,因为这说明原始标注本身越界了,最好回到原始图片上修正框再转换。

2.4 从 VOC 标注转成 YOLOV5 格式:转换脚本与边界处理

如果拿到的数据集是 VOC 的 XML 格式,需要用脚本转成 YOLOv5 的 txt。这里给一个我常用的转换核心函数:

import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_txt_path, class_list): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_list: continue class_id = class_list.index(name) bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 防止单个标注越界导致整行被过滤 x_center = max(0.0, min(1.0, x_center)) y_center = max(0.0, min(1.0, y_center)) w = max(0.0, min(1.0, w)) h = max(0.0, min(1.0, h)) lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, 'w') as f: f.write("\n".join(lines))

这里class_list的顺序必须和data.yaml里的names一致,否则类别号全错位。脚本里过滤掉了不在类别列表里的目标,比如如果 XML 里混了person,就不会写进 txt。边界处理我用了min/max裁剪,但这只是兜底,真的遇到越界框,裁剪完看似合法,实际上框已经不代表原目标了,最好还是把图片和原框导出来人工看一眼。

如果你拿到的数据本身就是 YOLOV5 目录格式,比如这个垃圾桶满溢检测数据集已经分好了 images 和 labels,那么转换步骤可以跳过。但建议你仍然抽查至少 10 张图的 txt,因为公开数据集里经常混着空的、错位的、类别号对不上的文件,提前发现比训练到一半再排查省事得多。

3. 从采集到标注:制作一份不翻车的垃圾桶满溢检测数据集

数据集的质量决定了模型的上限,这个话在满溢检测上尤其成立。因为垃圾桶本身是低动态目标,真正变化的是垃圾的状态,而状态识别对光照、角度、遮挡极其敏感。如果你只是在网上找了一堆“垃圾桶”图片,随手标注一个“垃圾桶”类别,那即使训练出模型,也只是在检测“有垃圾桶”,而不是在检测“垃圾桶满了”。这一步要把场景覆盖和状态边界都做到位。

3.1 采集场景:昼夜、天气、角度和遮挡一个都不能少

满溢检测的摄像头通常装在环卫车、路灯杆、小区出入口、垃圾房旁边,安装角度千奇百怪。我一般建议采集时至少覆盖这几个维度:

  • 时间段:白天、傍晚、夜间带灯光、夜间无灯光。夜间是个大坑,很多项目白天 mAP 很高,一上夜间摄像头就全线漏检。
  • 天气:晴天、阴天、雨天。雨天的垃圾桶周围会有反光和水渍,模型容易把反光当成垃圾。
  • 垃圾桶类型:铁皮圆桶、塑料方桶、带盖桶、无盖桶、不同颜色的桶。颜色差异会影响模型对“垃圾溢出”的判断。
  • 拍摄角度:平视、俯视、斜视。现实中摄像头经常是俯拍,框的形状差别很大。
  • 距离:近景、中景、远景。远景下垃圾桶可能只有几十个像素,满溢和正常的差异更小。
  • 遮挡:行人、车辆、树木遮挡,以及垃圾桶被部分挡住的情况。不要只拍无遮挡的干净画面。

采集数量上没有绝对标准,但如果自己从头做,建议每个类别至少要有 500 到 1000 个标注框,不是图片数。一张图里往往有多个垃圾桶,所以图片数可以比框数少。如果场景多,宁可在夜间多拍些,因为夜间样本一旦缺了,后面想靠数据增强补回来很难。

3.2 标注工具与状态边界:LabelImg 保存 YOLO 格式

标注工具最常见的是 LabelImg,它可以直接保存 YOLO 格式的 txt,不需要再转换。打开一张图,画框,选择类别,保存,这个流程对标注员几乎没有学习成本。如果你需要更强大的功能,可以用 X-AnyLabeling 或者 Labelme,但 LabelImg 在 YOLOv5 这个固定流程里已经够用。

标注规则比工具更重要。给标注员发一份带图例的规则说明,比发一个 README 有效得多。这里是我的经验版本:

  • 只有明显可见的垃圾桶才标,模糊到看不清状态的远处垃圾桶直接忽略,不要硬标。
  • 框要贴着垃圾桶的外沿,包含桶身和溢出物。注意 overflow 的框要覆盖散落在地面的垃圾,而不是只框桶身。
  • 如果同一个桶同时存在 full 和 overflow 的特征,按 overflow 标,因为状态优先级最高。
  • 不要框影子,不要框倒影,不要把两个紧挨着的桶框成一个框。
  • 拿不准的边界按低一级标。比如垃圾高出桶口一点但不确定满没满,标 full 而不是 overflow。

这些规则要写进标注文档里。标注员如果只看类别名,就会用自己脑补的“满了”来标,最后你得到的标签噪声会直接变成模型输出的抖动。

3.3 数据划分:按场景分组,不要让同一摄像头进两个集合

训练集、验证集、测试集的划分看起来是几行代码的事,但满溢检测数据集里有很多视频抽帧出来的图片,如果直接随机划分,同一摄像头同一时间段的前后帧会被拆到 train 和 val 里,模型相当于提前看过验证集,指标会虚高得离谱。正确做法是按场景分组划分。

import os import random from shutil import copyfile random.seed(42) src_images = "raw_images" src_labels = "yolo_labels" train_ratio, val_ratio = 0.8, 0.1 # 假设文件名前缀是摄像头ID+日期,例如 cam01_20240115_0001.jpg all_files = [f for f in os.listdir(src_images) if f.endswith(('.jpg', '.png', '.jpeg'))] groups = {} for f in all_files: group_key = f.split('_')[0] + '_' + f.split('_')[1] # 按摄像头+日期分组 groups.setdefault(group_key, []).append(f) group_keys = list(groups.keys()) random.shuffle(group_keys) train_groups = group_keys[:int(len(group_keys) * train_ratio)] val_groups = group_keys[int(len(group_keys) * train_ratio):int(len(group_keys) * (train_ratio + val_ratio))] test_groups = group_keys[int(len(group_keys) * (train_ratio + val_ratio)):] for split, gkeys in [("train", train_groups), ("val", val_groups), ("test", test_groups)]: os.makedirs(f"dataset/images/{split}", exist_ok=True) os.makedirs(f"dataset/labels/{split}", exist_ok=True) for gk in gkeys: for f in groups[gk]: base, ext = os.path.splitext(f) copyfile(os.path.join(src_images, f), f"dataset/images/{split}/{f}") txt = os.path.join(src_labels, base + ".txt") if os.path.exists(txt): copyfile(txt, f"dataset/labels/{split}/{base}.txt")

这段代码的关键是按摄像头ID_日期分组后,再把组整体划分到 train、val、test。这样同一个摄像头同一天的连续帧不会同时出现在训练集和验证集里。如果你是从视频里抽帧,不要把连续帧全部放进去,可以每隔 5 帧抽一张,减少冗余。

还有一点:没有对应 txt 的图片也会被复制到 images 里,但不会复制标签。如果这类图片太多,训练集里背景图比例会过高,模型会变得“不敢检测”。建议单独统计一下背景图数量,控制在总数 10% 以内。

3.4 可视化检查:标注坐标还原到图片上看

训练前必须做一次可视化检查,把 txt 坐标还原到图片上,确认框的位置和类别没有错位。很多问题光看数据统计是看不出来的,比如框整体偏移、类别号对不上、框只框住半个桶,这些只能通过看图发现。

import cv2 import os def draw_boxes(img_path, txt_path, class_names): img = cv2.imread(img_path) h, w = img.shape[:2] if not os.path.exists(txt_path): return img with open(txt_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls = int(parts[0]) x_c, y_c, bw, bh = map(float, parts[1:]) x1 = int((x_c - bw / 2) * w) y1 = int((y_c - bh / 2) * h) x2 = int((x_c + bw / 2) * w) y2 = int((y_c + bh / 2) * h) # 防止画框时越界 x1 = max(0, min(w, x1)) y1 = max(0, min(h, y1)) x2 = max(0, min(w, x2)) y2 = max(0, min(h, y2)) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img # 抽查 val 目录下的图片 class_names = ['normal', 'full', 'overflow'] for img_name in os.listdir('dataset/images/val'): base, _ = os.path.splitext(img_name) img = draw_boxes( f"dataset/images/val/{img_name}", f"dataset/labels/val/{base}.txt", class_names ) cv2.imwrite(f"check/{base}_check.jpg", img)

这段脚本会生成带框的图片,重点看两类:一类是框和桶的贴合度,另一类是 full 和 overflow 的框有没有把散落的垃圾包含进去。如果发现大量框偏移到一个方向,很可能是在转换坐标时把中心点写成了左上角,回到归一化公式检查即可。可视化检查这一步不要省,它能帮你省掉后面至少 3 轮无效训练。

4. 用 YOLOv5 训练自己的数据集:data.yaml 和训练参数

数据准备好之后,训练阶段真正要操心的是data.yaml、模型尺寸和超参数。很多数据集本身标注没问题,但训练时因为 yaml 写错、batch 太大显存溢出、或者不知道看哪些指标,导致反复跑实验却得不到一个稳定结果。这一章把训练前的配置和训练后的验收串起来。

4.1 写 data.yaml:路径用相对路径,类别名顺序别乱

YOLOv5 训练时读的是data.yaml,不是classes.txt。一个能直接跑的 yaml 长这样:

# 数据集根目录,可以填相对路径或绝对路径 path: ../dataset train: images/train val: images/val test: images/test nc: 3 names: 0: normal 1: full 2: overflow

path是数据集根目录,train和val是相对于path的路径。我一般建议用相对路径写,这样整个数据集拷到另一台机器上不用改配置换绝对路径。names的顺序必须和 txt 文件里的 class_id 一一对应,如果0: normal而标注文件里0是 overflow,那训练出来的模型预测结果会整体错位,而且混淆矩阵上看不出来。

写 yaml 时最容易踩的坑有两个:一是train写成images/train/带末尾斜杠,YOLOv5 也能接受,但路径拼接时容易出问题;二是nc和names的数量对不上,YOLOv5 会在训练启动时报错,或者干脆静默地只使用前nc个类别。写完 yaml 可以先用一行 Python 验证一下:

import yaml with open('data.yaml') as f: cfg = yaml.safe_load(f) print(cfg['nc'], len(cfg['names'])) assert cfg['nc'] == len(cfg['names'])

4.2 选模型大小和预训练权重:从 s 开始,m 更稳

YOLOv5 有n/s/m/l/x几个型号,满溢检测的垃圾桶通常不是小目标,但摄像头俯拍的时候桶的像素可能不大,综合来看从yolov5s起步比较稳。如果推理设备是树莓派这类嵌入式板子,选yolov5n压帧率;如果显存够、对精度要求高,可以上yolov5m。l和x在满溢检测这个任务上收益有限,反而会把训练时间和部署体积拉高。

预训练权重建议使用 COCO 上训好的yolov5s.pt。注意:COCO 里的garbage bag或bottle类别权重对满溢检测只有迁移学习的意义,不能直接拿来推理。因为满溢检测的输出类别是状态,不是物体本身。第一次训练时用预训练权重可以显著提速,但如果数据集很少,预训练权重反而可能让模型陷入对 COCO 特征的过拟合,这时候可以考虑从头训,但需要更多 epoch。

4.3 训练命令:关键参数和显存踩坑

训练命令是标准 YOLOv5 启动方式:

python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --workers 8 \ --project runs/train \ --name trash_overfill \ --hyp data/hyps/hyp.scratch-low.yaml

--img 640表示把输入图片统一 resize 到 640x640,YOLOv5 会自动加灰边保持原始比例。如果你的数据集里垃圾桶在远景下很小,可以把--img提高到 1280,但显存占用和训练时间会明显上升。--batch 16是一个安全起步值,如果你在 8G 显存的卡上遇到CUDA out of memory,直接减到 8 或 4,不要硬扛。--workers 8是数据加载线程数,Windows 上太高会卡死,改成 4 或者 2 更稳。

超参数文件默认用hyp.scratch-low.yaml就好。如果你的数据集小,不要急着调学习率,先跑一轮默认的看结果。满溢检测有一个值得关注的是--multi-scale,开启后模型会随机缩放输入图,对远景检测有好处,但训练时间会增加。也可以加--label-smoothing 0.1,对全和满这种状态型类别有时能提升一点鲁棒性,但不是万能药。

训练过程中如果 loss 始终不动,先别急着调超参数,检查一下数据集里是不是大量标签为空,或者data.yaml的train路径下根本没图片。这是最常见的“玄学”问题,九成是数据路径问题,不是模型问题。

4.4 训练产物:先看哪些指标再决定要不要调

训练结束后,在runs/train/trash_overfill/下会生成weights/best.pt、weights/last.pt、results.png和confusion_matrix.png。先看results.png里三条曲线:mAP_0.5、mAP_0.5:0.95、val_cls_loss。如果mAP_0.5和mAP_0.5:0.95还在上升,说明欠拟合,可以增加 epoch;如果已经平了,说明训练到位,不要再硬跑。

指标含义经验参考
mAP@0.5IoU 阈值 0.5 下的平均精度满溢检测建议至少 0.85
mAP@0.5:0.95更严格的 IoU 范围平均精度比 mAP@0.5 低 0.1~0.2 正常
Precision预测为正例中有多少正确低的时候误检多
Recall实际正例有多少被检出低的时候漏检多

对于满溢检测,漏检和误检都很麻烦,但漏检更致命。因为一个满溢的桶没检出来,环卫调度系统就不会触发工单。如果 recall 比 precision 低不少,优先补数据集里 overflow 的样本,而不是去调置信度阈值。混淆矩阵也要看,重点看 full 和 overflow 的互扰程度,如果溢出样本大量被预测成 full,说明标注时两者的边界没有拉开,这时候加训练集不如先修标注规则。

到这一步,模型已经能跑通了。但训练完不是终点,接上摄像头才是真正的开始。

5. 避坑:垃圾桶满溢检测数据集最容易翻车的 5 个细节

这一章是血泪经验。我见过太多项目挂在同样的地方,而且都是训练结束之后才暴露出来。下面这 5 个问题,每一个都值得你在启动训练之前先对照检查一遍。

5.1 训练报 No labels found:目录和文件名不匹配

现象:python train.py启动后,YOLOv5 直接提示No labels found in .../labels/train,或者训练日志里Scanning labels显示 0 个标签,但 images 目录里明明有 txt 文件。

原因:最常出现在两种情况。一是data.yaml里train写成了images/train,但labels目录不在images/train同级路径下,YOLOv5 会根据图片路径自动推断标签路径,如果图片路径和标签路径相对关系不对就找不到。二是图片文件名和 txt 文件名大小写不一致,比如图片是IMG_001.JPG,txt 是img_001.txt,YOLOv5 在 Linux 下对大小写敏感,直接跳过。

解决:先跑一个检查脚本,列出所有缺少 txt 的图片文件:

import os for split in ['train', 'val', 'test']: img_dir = f'dataset/images/{split}' lbl_dir = f'dataset/labels/{split}' missing = [] for f in os.listdir(img_dir): base, _ = os.path.splitext(f) if not os.path.exists(os.path.join(lbl_dir, base + '.txt')): missing.append(f) print(split, 'missing labels:', len(missing)) if len(missing) < 20: print(missing)

这个脚本会快速定位是文件名问题还是路径问题。如果缺失数量很大,优先检查data.yaml的路径和目录结构,而不是逐个改名。我一般会在训练前把这个脚本的结果存成日志,免得后面想追溯都没有记录。

5.2 full 和 overflow 分不清:标注规则没有量化

现象:训练完看混淆矩阵,full 和 overflow 的互混非常严重,大约三成 overflow 被预测成 full。在视频实测里,垃圾桶已经堆到地上了,模型还输出 full。

原因:标注时没有一个可以量化的边界。标注员 A 认为垃圾掉了 3 块在地上就算 overflow,标注员 B 坚持只要桶盖盖不上就算 overflow。模型同时学习到两套标准,自然在两者之间摇摆。

解决:把判定标准写成“硬指标”。我的做法是:以桶口平面为界,桶口以下为 normal;垃圾接触桶口平面以上但未接触地面,为 full;垃圾任何一部分接触地面,或者垃圾袋/散落物超出桶身的垂直投影,为 overflow。规则越具体越好,甚至可以配图说明。如果两个标注员对同一张图的标注一致性测试达不到 95%,就不要开始训练。

5.3 夜间图片全部漏检:场景比例严重失衡

现象:白天测试集上 mAP@0.5 有 0.9,夜间测试集直接掉到 0.3,甚至垃圾桶完全检测不到。训练集图片数量明明有几千张,为什么夜间效果这么差。

原因:大部分公开数据集和自采数据都偏重白天,夜间图片可能只占 5%。YOLOv5 虽然有 Mosaic、HSV 等数据增强,但 HSV 增强改变的是色相和饱和度,模拟不了夜间整体低亮度、强噪点的视觉效果。

解决:采集时强制按场景配额,一般我会要求夜间图片占比不低于 20%。如果实在补拍不了,可以对夜间图做一次离线增强,比如降低亮度、加高斯噪点、提高对比度,把一张夜间图扩成 3 张参与训练。但增强只是补救,最可靠的还是真实夜间样本。夜间漏检还有一个隐藏原因:标注框在低亮度下看不清,标注员凭感觉画框,框偏了模型自然学不好。所以夜间图必须单独抽检可视化结果。

5.4 验证集虚高:同一场景的视频帧被切进 train 和 val

现象:训练时验证集 mAP 很高,模型看起来完美,但接到实际摄像头视频上表现很差,同一个场景反复跳检测结果。

原因:数据划分时直接random.shuffle了所有图片,而图片大多来自同一批视频抽帧。同一个摄像头同一个时间段的前后帧,一张进了 train,一张进了 val,模型在训练时已经见过高度相似的画面,验证集指标自然虚高。

解决:用第 3 章里的分组划分,按摄像头 ID 和日期分 group,group 之间整体划分。如果没有摄像头 ID,可以根据文件名的时间戳前缀分组,或者按照拍摄地点手工分组。更严格一点,测试集应该来自完全没参与训练的摄像头,这样才贴近真实上线场景。验证集的作用是模拟没见过的环境,不是考记忆。

5.5 状态输出抖动:标注边界不一致导致模型置信度摇摆

现象:模型在连续视频帧上,同一垃圾桶的状态在 normal、full、overflow 之间来回跳,单帧看置信度都不低,但整体输出非常不稳定,没法触发工单。

原因:除了前面说的标注规则不统一,还有一个原因是垃圾桶状态本身在视频里是渐变的,满溢不会在某一帧突然从 normal 变成 overflow。模型在边界附近置信度天然低,如果没有时序处理,输出就会抖。

解决:第一优先级是把标注规则拉齐,减少训练标签里的噪声。第二是推理端加时序平滑,比如用滑动窗口对最近 N 帧的状态投票,输出出现次数最多的状态。这个技巧会在下一章详细展开。不要试图单靠提高置信度阈值来解决抖动,因为阈值提高会导致状态切换响应变慢,full 已经出现很久了还不触发。

这些坑每一个都真实存在,而且很多是训练之后才暴露的。提前检查它们,比多跑 100 轮 epoch 有价值得多。

6. 别只信 mAP,用连续帧投票验证满溢检测才算落地

训练跑完、best.pt 也出来了,但如果只盯着测试集 mAP,你很可能在真实视频流上摔跟头。满溢检测是一个典型的状态检测任务,业务系统需要的是“稳定地知道这个桶现在是满还是溢”,而不是每一帧都独立输出一个类别。解决抖动的最简单方法,就是加一个连续帧投票模块。

from collections import deque class FrameVote: def __init__(self, window=5): self.window = window self.queue = deque(maxlen=window) def add(self, detections): # detections: 当前帧的目标列表,每个元素为 (class_id, conf) if not detections: self.queue.append(-1) return # 取当前帧置信度最高的目标类别 best = max(detections, key=lambda x: x[1]) self.queue.append(best[0]) def get_status(self): if len(self.queue) < self.window: return None # 返回窗口内出现次数最多的状态 return max(set(self.queue), key=self.queue.count)

这段代码会维护一个长度为 5 的队列,每帧把最高置信度的类别放进去,然后统计最近 5 帧里出现最多的状态作为最终输出。窗口太短比如 3 帧,抖动压不明显;窗口太长比如 10 帧,状态变化响应慢,垃圾桶已经 overflow 了,还要等好几秒才触发。我一般从 5 帧开始调,如果摄像头是 25fps,5 帧意味着 0.2 秒内决策一次,对工单系统足够快。

验证的时候也不要只看单帧 mAP。拿一段包含“正常→满→溢”完整过程的视频,跑模型,记录每一帧的状态,检查状态跃迁路径。好的结果应该是normal -> full -> overflow按顺序推进,如果中间出现normal -> overflow的跳变,或者 full 和 overflow 反复交替,说明标注边界仍然有噪声,回去改标注规则,而不是继续调投票窗口。这个习惯帮我在很多项目里避免了“测试集分数很好看,上线后工单乱飞”的尴尬。

我之前做过一个环卫项目,模型在 mAP 上表现不错,一接摄像头,满溢状态在 normal 和 overflow 之间来回跳,当时第一反应是调阈值,折腾了两天都没改善。最后重新翻了标注文件,才发现一批 overflow 的框只框了桶身,没把地面散落垃圾包进去。重新修正后,再配合 5 帧投票,输出就稳了。这个教训让我现在做任何数据集,第一件事永远是可视化检查加状态边界评审,而不是急着跑训练。希望帮到你。

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

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

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

立即咨询