传送带异物检测数据集:COCO JSON解析与YOLO训练实战
2026/9/23 18:05:25 网站建设 项目流程

简介:传送带异物检测识别数据集专注于工业生产线场景下的目标检测任务,可用于识别铁棍、垃圾等非预期物体,面向计算机视觉开发者、工业质检算法工程师以及相关课题研究人员,方便快速验证检测模型在真实监控画面上的表现。压缩包内共108个文件,其中105张JPG图片来自不同时段的生产线监控视频抽帧,涵盖白天、交接班等多样化画面;另有3个JSON文件,采用COCO格式标注,包含类别与边界框信息,可配合主流框架直接读取。资源整体约4.65MB,轻量易下载。目前已有413人学习使用。通过该数据集与标注文件,用户可以省去自行采集和标注的时间,直接用于YOLO、Faster R-CNN、SSD等目标检测模型的训练和评估;不同摄像头的拍摄角度和光线变化也为模型泛化能力测试提供了参考场景,可据此分析传送带异物识别的难点,为产线质检方案提供数据基础。

1. 传送带异物检测识别数据集:别急着找下载,先弄懂这份 COCO JSON 能不能直接用于训练

做工业质检的人对"异物检测"四个字都又爱又恨:传送带上除了正品,什么都有可能混进来——铁棍、纸团、塑料片、编织袋条,甚至上一道工序掉下来的螺丝垫片。目标检测模型能不能在这里落地,瓶颈从来不在算法,而在数据。而"传送带异物检测识别数据集"这个关键词能搜到的东西,多数以 COCO JSON 格式打包,带好标注框。问题在于:这些标注是用 COCO 的规则组织起来的,和 YOLO 训练直接要的 txt 格式之间还有一层转换关系,很多人就是倒在这层转换上的。COCO 是一个公开的目标检测数据集格式规范,它把每张图的路径、尺寸、标注框、类别都写进一个大的 json 文件里,结构稳定、字段清楚,但直接拿来训练并不行。这篇我按自己做工业项目的经验,从解读这份 json 开始,把校验、转换、训练、踩坑一路写到现场部署。

2. COCO JSON 格式拆解:先搞懂标注文件里每一行在说什么

2.1 COCO 数据集的信息结构:从 images、annotations、categories 三个顶层字段开始

拿到一份 COCO 格式的异物检测数据集,第一件事不是急着去看图,而是把 json 文件的结构打开看清楚。COCO JSON 是一整个字典,顶层通常有 info、licenses、images、annotations、categories 五个键,其中真正决定模型训练的只有后面三个。images 是一张列表,里面每个元素对应一张图片,包含 id、file_name、width、height 四个关键字段;annotations 是另一张列表,每个元素对应一个标注框,包含 id、image_id、category_id、bbox、area 这些字段;categories 则是类别字典的列表,通常包含 id、name、supercategory。

看一个实际标注的小例子会更直观。假设数据集里有一张生产线上拍到的传送带俯视图,上面有一根铁棍和一个纸团,对应的 json 片段大概是这个样子。这段结构里值得注意的细节是:bbox 的四个数字依次是左上角 x 坐标、左上角 y 坐标、框宽度 w、框高度 h,单位是像素,但类型是浮点数。COCO 规范里允许小数坐标,很多标注工具导出的也是小数,这在后面转 YOLO 格式时会多一个取整还是保留的决策点。

{ "images": [ {"id": 1, "file_name": "frame_0001.jpg", "width": 1920, "height": 1080} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 1, "bbox": [120.5, 340.2, 860.1, 42.3], "area": 36382.23}, {"id": 2, "image_id": 1, "category_id": 2, "bbox": [1500.1, 620.7, 96.3, 88.5], "area": 8522.55} ], "categories": [ {"id": 1, "name": "iron_bar", "supercategory": "foreign_matter"}, {"id": 2, "name": "trash", "supercategory": "foreign_matter"} ] }

这里每个字段的语义要抠清楚:image_id是把标注框和图片关联起来的外键,没有它标注框就找不到对应的图;category_id是类别在 categories 里的编号,这个编号可以从 1 开始不连续,但不能和 categories 里的 id 对不上;area字段在 COCO 官方工具里会被用来筛选小目标,但在 YOLO 训练流程里它基本不参与计算,转格式时可以不保留。实际项目里我收到过不少标注文件,images 是齐的,annotations 里却混着个别不在 images 里的 image_id,这种脏数据会在数据加载时直接报 KeyError,而且报错位置离问题数据很远,非常难排查。所以在动任何训练代码之前,先把这个 json 当成数据库表来检查外键关系,是省时间的第一步。

2.2 类别定义与 ID 映射:为什么建议把"铁棍"和"垃圾"分开,再留一个"未知异物"

类别怎么定,直接决定了模型能学到什么边界。标题里写了"可识别铁棍,垃圾",但落到 COCO 的 categories 字段时,有一个很多人忽视的设计问题:id在 COCO 里从 1 开始,而在 YOLO 训练格式里类别编号必须从 0 开始。如果拿到手的 COCO json 里铁棍是category_id: 1,垃圾是category_id: 2,那么转换成 YOLO 标签时,铁棍的类别编号要写成 0,垃圾写成 1。这个减一操作看起来简单,但一旦数据集里有三类、四类,或者标注软件导出的 id 是从 0 起的,映射关系就很容易错位。我的习惯是写转换脚本时强制用 categories 列表的索引顺序来重建映射,而不是直接拿category_id减一,前者的结果是确定的,后者依赖标注工具的实现。

另一个更贴近工业现场的问题是类别粒度。传送带上的"垃圾"其实是个很宽泛的概念,纸团和塑料瓶盖在图像上差异极大,放在同一个类里,模型学的是它们和传送带背景的差异,这在初期够用,但一旦现场出现某种训练集里完全没有的垃圾形态,模型大概率会漏。我的经验是:如果数据集允许,把类别拆成"铁棍""软质垃圾""硬质碎片"三到四类会更有价值。软质垃圾包括纸团、布料、塑料袋,硬质碎片包括石子、金属片、玻璃渣。拆完之后,训练阶段可以针对样本少的类做增强补偿,部署阶段也能根据类别做不同的报警策略——铁棍对设备有物理损坏风险,需要急停;纸团可能只需要记录和吹除。

还有一点值得专门说:要不要加一个"未知异物"类。常见做法是加,因为传送带异物检测本质上是个开放集问题,总有没见过的垃圾形态。把这个类放进训练集有个副作用,它会吸收大量难例,导致其他类别的精确率下降。我一般会在数据集版本 v1 不加,等上线后收集两个月的误检和漏检样本,再决定是否引入。这个决策可以先不做,但 categories 的supercategory字段建议统一写成foreign_matter,为以后无论怎么调整类别结构都留好兼容空间。

2.3 数据采集与标注工具:从现场抽帧到生成 COCO 的完整流程

这份数据集的标注格式是 COCO JSON,但数据从哪来、怎么标,才是决定它能不能用的根本。采集部分有个关键点:传送带是运动的,异物出现在画面里的姿态几乎每帧都不同,如果按视频连续帧取数据,相邻帧高度相似,训练集的信息量会被严重高估。我之前做过一次统计,连续帧采 1000 张图,去重后有效样本不到 300 张,因为在传送带速度恒定的情况下,同一根铁棍会被拍到几十帧。正确做法是设定固定的抽帧间隔,比如每 5 秒取一帧,或者按传送带运行距离来抽样,保证同一个物体最多只出现在两三帧里。另外一定要采至少 15% 的负样本,也就是没有任何异物的空传送带图片,负样本对减少误检的作用远比想象中大,很多模型在现场疯狂报警,就是因为训练集里全是正样本,模型不知道"没有异物"长什么样。

标注工具的选择上,常见做法是用 Labelme 或 COCO Annotator。Labelme 默认导出的是自己的 JSON 格式,需要写一次转换脚本转成 COCO;COCO Annotator 可以直接产出 COCO 格式,用浏览器打开,支持多人分任务标注,比较适合几十万张图的中型项目。我自己的项目用的是 Labelme,因为团队标注员更熟悉它的快捷键,但每次标注完都要合并所有 JSON 文件再转 COCO,这中间容易出编码问题,后面避坑章节会专门说。标注规范里必须写死三点:第一,bbox 要框住目标的完整轮廓,包括被遮挡时能推断出的部分,而不是只画露出来的部分;第二,铁棍这种长条形目标,框必须是贴合的矩形,但允许它倾斜着被框成一个大矩形——这种情况下框里包含大量背景,模型初始化时可能会被干扰,这是旋转框检测能解决的问题,但对于 v1 版本,先把贴合度做好就够了;第三,目标被传送带边缘裁切时,如果可见部分不足 30%,直接跳过不标,只标完整可见和大部分可见的目标,避免给模型喂入大量残缺样例。

3. 让数据先通过体检:COCO 文件校验与转 YOLO 格式的实操脚本

3.1 先跑一个 JSON 体检脚本:检查结构、缺字段、BOM 与非法值

从网上下载的或者标注团队交付的 COCO 文件,质量参差不齐。常见的问题包括:JSON 文件带 UTF-8 BOM 头导致json.load直接报错;annotations 里的 image_id 指向了不存在的图片;bbox 出现负值或超出图像边界;某些标注框的 area 字段缺失或为 0。这些问题不是说模型完全不能训练,而是会在训练过程中以各种奇怪的形式暴露出来,比如 loss 突然变成 nan,或者某个类别完全学不出来。所以在转换格式之前,我会一律先把检测脚本跑一遍,当作数据集体检。下面这个脚本在项目里我会保存为check_coco.py,每次拿到新数据先跑一次。

import json from pathlib import Path def check_coco(json_path): with open(json_path, 'r', encoding='utf-8-sig') as f: coco = json.load(f) images = {img['id']: img for img in coco['images']} categories = {cat['id']: cat['name'] for cat in coco['categories']} print(f"图片数量: {len(images)}, 标注框数量: {len(coco['annotations'])}, 类别: {categories}") # 检查 image_id 和 category_id 的外键完整性 bad_image_ref, bad_cat_ref = 0, 0 for ann in coco['annotations']: if ann['image_id'] not in images: bad_image_ref += 1 if ann['category_id'] not in categories: bad_cat_ref += 1 # 检查 bbox 是否越界、面积是否为负 bbox_out = [] for ann in coco['annotations']: img = images.get(ann['image_id']) if not img: continue x, y, w, h = ann['bbox'] if x < 0 or y < 0 or w <= 0 or h <= 0: bbox_out.append((ann['id'], '非正尺寸')) elif x + w > img['width'] or y + h > img['height']: bbox_out.append((ann['id'], '越界')) print(f"无效 image_id: {bad_image_ref}, 无效 category_id: {bad_cat_ref}, 异常 bbox: {len(bbox_out)}") for bid, reason in bbox_out[:10]: print(f" annotation {bid}: {reason}") if __name__ == "__main__": check_coco("annotations/instances_train.json")

逻辑说明:用utf-8-sig编码打开 JSON 文件,可以自动剥离 BOM 头,这是从 Windows 上交付的标注文件最常踩的一个编码坑;随后把 images 和 categories 构建成字典,用image_idcategory_id做外键检查,能定位到 dangling reference;bbox 检查过滤三类问题:负起点、非正尺寸、超出图像边界。参数上可以通过命令行传入 json 路径,也可以在脚本里写死先跑通一次。这个脚本跑出来的异常数量如果超过标注总数的 1%,我一般会退回标注环节修数据,而不是在训练时硬扛。

还有一个容易被忽略的问题:COCO 的 annotations 里的segmentation字段。在目标检测数据集中,它可以是多边形坐标的嵌套列表,也可以是多边形内部的 RLE 编码;如果这份异物数据集只用于检测不使用实例分割,可以完全忽略这个字段,转换时直接跳过。但要注意的是,很多标注工具导出 COCO 时会把segmentation字段写成空列表或 None,这会造成 json 内容不一致,转换脚本里读取字段时必须用ann.get('segmentation', [])的方式防 KeyError。

3.2 把 COCO JSON 转成 YOLO 训练格式:转换脚本与四个边界坑

YOLO 系列的训练格式和 COCO 完全不同,它是每个图片对应一个同名 txt 文件,每行代表一个目标框,格式是class_id x_center y_center width height,其中中心点坐标和宽高都归一化到 0 到 1 之间。这个转换在逻辑上很简单,但实操中几乎每个项目都会遇到问题。最常见的四个坑:一是类别 ID 减一问题,前面已经说过 COCO 的 id 从 1 开始,而 YOLO 的 class_id 从 0 开始;二是坐标变换公式容易写错,需要把左上角坐标加宽高的一半得到中心点,再除以图片宽度和高度归一化;三是浮点数精度,归一化后的值至少要保留 6 位小数,否则小目标的定位精度会受影响;四是文件名对齐问题——YOLO 的 txt 标签文件必须和图片放在同一个命名空间下,如果图片叫frame_0001.jpg,标签就必须叫frame_0001.txt,但 COCO json 里的file_name可能带有子目录前缀,转换时需要约定最终的数据目录结构。

下面是我在项目里持续在用的转换脚本。它不是最简版本,但把上面这些坑都堵住了,输出目录结构可以直接喂给 YOLOv8 或其他 YOLO 版本使用。

import json from pathlib import Path def coco_to_yolo(coco_path, img_root, out_root): with open(coco_path, 'r', encoding='utf-8-sig') as f: coco = json.load(f) images = {img['id']: img for img in coco['images']} cats = sorted(coco['categories'], key=lambda c: c['id']) # 用类别列表顺序重建从 COCO id 到 YOLO id 的映射 cat_id_map = {cat['id']: idx for idx, cat in enumerate(cats)} out_root = Path(out_root) (out_root / 'labels').mkdir(parents=True, exist_ok=True) # 按图聚合标注框 anns_by_img = {} for ann in coco['annotations']: anns_by_img.setdefault(ann['image_id'], []).append(ann) for img_id, anns in anns_by_img.items(): img = images[img_id] stem = Path(img['file_name']).stem label_path = out_root / 'labels' / f"{stem}.txt" lines = [] w_img, h_img = img['width'], img['height'] for ann in anns: x, y, bw, bh = ann['bbox'] cx = (x + bw / 2.0) / w_img cy = (y + bh / 2.0) / h_img nw = bw / w_img nh = bh / h_img # 过滤完全越界的框,保留部分越界的框并做裁剪 if nw <= 0 or nh <= 0: continue cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) lines.append(f"{cat_id_map[ann['category_id']]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") label_path.write_text("\n".join(lines), encoding='utf-8') print(f"写入 {label_path} 共 {len(lines)} 个目标") if __name__ == "__main__": coco_to_yolo("annotations/instances_train.json", "images", "yolo_dataset")

参数说明:img_root是图片目录,转换脚本本身不复制图片,只负责生成标签,图片软链到out_root/images下即可,这样可以省一份存储空间;cats排序后按索引映射,确保cat_id_map稳定;对cxcy做了 0 到 1 的截断,对完全越界的框直接丢弃,这能避免 YOLO 训练时出现 anchor 匹配异常。转换完成后,我会随机抽 20 张图做可视化检查,把标注框画回图片上,确认坐标没有错位。这一步必不可少,因为脚本逻辑正确不代表原始标注就正确。

3.3 数据划分与增强:先按图片切分,再用针对性增强缓解类别不平衡

数据集的划分方式在目标检测里直接影响评估可信度。一个常见的错误做法是按标注框的数量做随机划分,结果同一个铁棍出现在训练集和验证集的不同图片里,导致验证指标虚高。因为传送带异物数据集里同一个物体经常出现在连续帧中,按图片划分也不完全安全。我通常的做法是先用时间戳或视频片段编号把相近的帧分到同一个组,按组划分,确保训练集和验证集之间没有来自同一个物体运动轨迹的样本。如果数据集本身没有视频来源信息,那就只能按图像哈希去重后,随机按 8:1:1 划分训练、验证、测试,并在训练时关闭 shuffle 的随机种子来保证可复现性。

目标检测里类别不平衡是个老生常谈的问题,但在传送带异物场景里格外严重——铁棍出现频率可能只有垃圾的十分之一,而某些罕见垃圾可能只有十几个标注框。针对这种数据集,单纯靠损失函数里的类别权重是救不回来的,常见做法是在训练时做类别感知的采样:让每个 batch 里都保证包含一定比例的少数类样本。YOLOv8 的数据加载器本身不直接支持这个功能,我的处理是在预处理阶段把少数类样本重复复制几份,或者用复制粘贴增强,把铁棍的标注框连同像素区域剪贴到不含异物的背景图里,这样既增加了样本量,又保留了真实标注质量。复制粘贴增强在传送带场景特别好用,因为传送带背景高度重复,异物区域和背景的边界清晰,粘贴一个铁棍到另一张空传送带图上,视觉上几乎没有破绽。对于垃圾这类形状不规则的目标,粘贴效果稍差,但对训练而言也远好于没有样本。

4. 用 YOLOv8 训练自己的数据集:命令、参数与第一次评估

4.1 第一步先把目录组织和数据配置做好,再考虑调参

YOLOv8 训练自己的数据集的目录结构以 data.yaml 为入口,这个文件决定了训练时去哪里找图片和标签。很多初学者拿到 COCO 数据集转换完就直接开始训练,结果报错找不到标签文件或者类别数量对不上,绝大多数是 data.yaml 里路径写错或者类别列表顺序和转换脚本不一致。我统一用的目录布局是:yolo_dataset 下包含 images 和 labels 两个目录,每个目录里再按 train、val、test 分子目录;图片路径用相对路径,配合 data.yaml 里的绝对路径前缀,这样换机器训练时只需要改一行配置。

data.yaml 的内容很关键,它把数据集的物理路径和类别语义绑定在一起。假设我们转换后的类别顺序是铁棍(索引 0)和垃圾(索引 1),data.yaml 里的 names 必须和这个顺序一致,否则模型学到的标签就会张冠李戴。以下是一个可以直接用的最小配置,训练和验证图片路径指向各自的子目录,nc 表示类别数。

# data.yaml path: /data/foreign_matter/yolo_dataset # 数据集根目录,按机器改 train: images/train val: images/val test: images/test nc: 2 names: 0: iron_bar 1: trash

逻辑说明:path是根目录,trainval是相对于根目录的子路径。这里有一个细节:YOLOv8 允许 train 和 val 是图片文件列表的 txt 文件路径,也允许是目录,我个人推荐用目录,省去了维护 txt 列表文件的麻烦。nc是类别总数,必须和 names 的条目数一致;names 的键值顺序必须是 0、1、2 这样的连续整数,这个顺序直接对应训练输出的类别索引,也对应标签文件里每行开头那个数字。

然后就是启动训练。训练命令我在项目里是固定的写法,第一次跑的时候先不加任何花哨的参数,目标是跑通流程并获得一个基线指标。下面这个命令用 yolov8m 作为预训练权重,输入图像尺寸设成 1280,批次大小根据显存调整,训练 150 个 epoch。

yolo detect train \ data=/data/foreign_matter/yolo_dataset/data.yaml \ model=yolov8m.pt \ epochs=150 \ imgsz=1280 \ batch=16 \ device=0 \ project=experiments \ name=foreigm_matter_v1

参数说明:imgsz=1280听上去只是分辨率,但它决定小目标还能不能被模型看到。传送带异物检测的铁棍宽度可能只有 30 像素,如果用默认的 640,缩放到 1280 之后铁棍的特征还能保留下来一些。代价是显存占用翻倍,batch 从 32 降到 16 是正常操作。projectname指定训练结果输出目录,每次训练的结果都会存到这里,包括最后的权重文件 weights/best.pt 和 weights/last.pt。第一次训练不必追求最优,重点是确认 loss 曲线能够下降、验证集的 mAP 不是 0。

如果显存不够跑 1280,还有一个更通用的下位替代方案:保持 imgsz=640 训练,然后在推理时用更大的 imgsz,因为 YOLO 的推理分辨率可以和训练分辨率不一致。但要注意,训练和推理分辨率差距太大会导致目标尺度失配,小目标的召回率反而下降。升级显卡或者用切图训练是更治本的做法,切图就是把 1920x1080 的原图切成四块 960x540 的图分别训练,可以变相提高小目标的分辨率占比,后面避坑章节会展开说。

4.2 影响异物检测效果的关键参数:imgsz、batch、anchor 与数据增强强度

当基线跑通之后,真正需要反复调的是下面这组参数。首先是imgsz,它的影响在小目标场景里是决定性的。传送带异物数据集的宽度和高度与目标尺寸的比例跨度很大,垃圾分类里既有几百像素的塑料瓶,也有几十像素的碎石子,如果模型只在 640 分辨率下训练,感受野基本决定了碎石子这种小目标很难被正确检测。用 1280 训练之后,小目标的召回率通常能提升十几个百分点,这是我在多个工业视觉项目里反复验证过的结论,不是玄学。

第二个关键参数是 mosaic 数据增强。YOLOv8 默认开启 mosaic=1.0,它把四张图拼在一起训练,对小目标检测很有帮助,但对细长目标有副作用——铁棍一旦被拼图的边界切分,训练样本里的目标形态就被破坏掉了。我的经验是把 mosaic 降到 0.5,同时开启 copy_paste 增强,也就是把少数类目标复制到其他图上,对协调各类别样本数量更有效。YOLOv8 的增强参数可以在命令行直接覆盖,比如mosaic=0.5 copy_paste=0.3。另外传送带现场的光照是相对固定的,大幅度的 HSV 扰动反而会让模型学到不真实的变化,所以我把 hsv_h、hsv_s、hsv_v 都调低到 0.01 左右,保持背景纹理的稳定性。

anchor 超参数往往被忽略,但对长宽比极端的铁棍来说很关键。YOLOv8 在训练时默认会重新计算 anchor,如果是自定义数据集,训练日志里会出现 AutoAnchor 的信息。我的建议是让模型自动算一遍,然后看一下打印出来的 anchor 尺寸,如果最大 anchor 的宽高比和铁棍的长宽比差距太大,手动指定一组带明显长条形状的 anchor 可以显著加速收敛。当然这属于进阶操作,第一次跑不必动它。rag 还有一个容易翻车的地方是close_mosaic这个参数,它控制在最后 10 个 epoch 关闭 mosaic 增强,让模型在接近真实的分布上收敛。这个默认行为保持开启就好,不要关掉,否则模型会很长时间在拼接图上拟合,推理时性能不佳。

4.3 评估指标先看漏检率再看 mAP:样本不均衡时的评估陷阱

模型训练完后,命令行会打印出一串指标,其中 mAP50 和 mAP50-95 是最显眼的。但在异物检测这种类别极不均衡的工业场景里,平均精度很容易欺骗人。如果垃圾有 1000 个样本,铁棍只有 50 个样本,模型把铁棍全部漏掉,mAP50 仍然可能维持在一个不错的数值,因为垃圾类别把整体指标拉上去了。所以在评估阶段,我会先单独看每个类别的召回率,尤其是铁棍和罕见垃圾的召回率,确认模型没有把某个类别直接放弃掉。

另一个评估陷阱是把验证集和训练集的类别分布搞成一样的。如果训练集里的图片大多只有垃圾,验证集也这样分布,模型大概率在真实场景里面对铁棍时表现骤降。所以划分数据集时就要刻意保证验证集里保留足够比例的稀有类别图片,宁可验证集规模小一点,也要让每个类别至少有 30 个样本。如果数据量实在不够,可以用测试时增强对稀有类别的数据做多尺度推理,也能得到更稳定的评估结果。评估阶段的阈值默认是 0.25,我一般会另外导出一份置信度从 0.05 到 0.95 的 PR 曲线数据,后续做阈值校准要用,这比只看一个 mAP 数字可靠得多。

5. 传送带异物数据集的 5 个常见坑与排查思路:现象、原因、解法

5.1 json 解析直接报错,代码看起来没问题

现象:跑json.load时抛JSONDecodeError,或者报错信息是json parse error: cannot deserialize value一类,检查引号和括号都没有明显问题。

原因:这种问题十有八九出在编码和不可见字符上。Windows 上标注工具生成的 COCO 文件可能带 UTF-8 BOM 头,encoding='utf-8'读出来第一个字符是\ufeff,json 解析器不认识它就报错;另一类是文件里有全角引号,是标注软件在 Windows 上写入时格式漂移,或者有人手工编辑过 JSON 文件。

解决:读取文件时统一用encoding='utf-8-sig'而不是utf-8,BOM 会被自动剥离。全角引号问题可以先跑一遍字符检查脚本,把所有非 ASCII 的引号替换成标准引号再解析。我在实际项目中还遇到过 Windows 的记事本把 json 存成 UTF-16 的情况,这种必须用转换工具统一转成 UTF-8 再做后续处理。血泪经验:任何标注工具交付的 COCO 文件,先脚本体检再动手,永远不要相信"应该没问题"。

5.2 bbox 越界或面积为 0,训练 loss 变成 nan

现象:模型训练的 loss 曲线在某个 epoch 突然变成 nan,或者某个类别的 AP 一直是 0;检查数据时发现少量标注框的坐标是负数,或者 w 和 h 为 0。

原因:标注工具在拖拽框时容易出现误操作,或者程序导出时坐标计算错误。COCO 官方评测对这类问题是容忍的,因为评测只算指标;但 YOLO 训练时数据增强会把归一化坐标再做缩放和平移,脏坐标会被放大成完全不合理的目标框,直接导致数值溢出。

解决:在转换脚本里对 bbox 做检查,按本文 3.1 节的方式先过滤完全越界的框,再对部分越界的框做裁剪。关键点:wh必须严格大于 0,面积为 0 的框直接丢弃,不要保留后硬编码成最小尺寸,那样会在训练时引入一个永远无法预测的噪声目标。

5.3 铁棍这种细长目标在缩放后几乎看不见

现象:训练集单独看各类别 mAP 都正常,但一到验证集,铁棍的召回率明显低于垃圾;把训练图片画出来看,铁棍在 1280 分辨率下还能辨认,但在模型实际输入的分辨率下非常细。

原因:铁棍的宽度只有 20 到 40 像素,长宽比接近 1:20,YOLO 的默认 anchor 是基于自然图像设定的,对这种极端长宽比的回归目标,anchor 匹配阶段的 IoU 很难达标,导致大量铁棍样本在训练时作为背景被忽略。

解决:三个层面组合使用。第一是提升 imgsz,这个前面提到了;第二是使用切图策略,把原图切成带重叠的块,让铁棍在切块后的图上占据更多有效像素;第三是检查自动生成的 anchor 尺寸,如果训练日志里显示的 anchor 没有明显长条形的,手动给 YOLO 配置一组,比如在模型配置文件的 anchor 列表里加入长宽比为 1:8 和 1:16 的尺度。如果数据集里铁棍还有明显的倾斜角度,并且多数是 45 度左右的斜向出现,那么旋转目标检测是更好的方向,但 v1 阶段先用切图和 anchor 调整就能拿到可用的效果。

5.4 类别严重不平衡,垃圾学得很好,铁棍几乎不报

现象:训练完看每类的混淆矩阵,垃圾的召回率在 0.9 以上,铁棍只有 0.3;加大训练轮数两个指标都不动。

原因:数据分布就是几百比几千,模型交叉熵损失对多数类的梯度压制了少数类,即使类别权重也无法完全纠正;加上铁棍本身就是细长目标,学习难度本来就比块状垃圾高,双因素叠加导致这个结果。

解决:数据层面做复制粘贴增强,把稀有类别的目标粘贴到空传送带背景图上;训练层面对每个 batch 做类别感知采样,让稀有类别在每个 batch 中的占比不低于 20%。这两步做完之后,铁棍类别的召回率通常能从 0.3 提高到 0.7 以上。如果数据增强已经做足还不够,最后的手段是采集更多铁棍样本,这是成本最高但也最有效的方案,不要指望模型能从无到有变出特征。

5.5 现场漏检集中在光照变化段,训练集里看不出问题

现象:模型在白天测试效果不错,傍晚或者传送带上方灯光开启后,漏检率突然升高;同一批测试图片在训练时数据增强后的表现却正常。

原因:这是典型的域偏移。训练集抽帧时间集中在白天,如果采样时没有覆盖不同时段的光照条件,模型会把亮度当作背景特征的一部分,一旦现场光照变化,分布就和训练集产生了偏移。

解决:在采集阶段按时间段分层抽样,早上、中午、傍晚、灯光全开四个条件下各抽一部分帧。如果数据集已经固定没有条件补采,可以在训练时把亮度增强适当调到 0.05 到 0.1,用数据增强模拟光照变化,这个方法能缓解但不能根治。真正上线前,最好留出一批涵盖不同光照条件的现场视频作为最终验收集,用的是模型从未见过的金标准数据,防的就是这种自欺欺人的评估。

6. 从训练集到传送带现场:用置信度阈值校准和样本回流兜底

模型训练好只是第一步,把权重放到传送带现场能稳定运行,还要过两道坎:一是置信度阈值怎么定,二是现场漏检的样本怎么回流进训练集。

置信度阈值不能直接用默认的 0.25。在实验室数据上,0.25 可能达到最优的 F1;但现场对误检的容忍度极低,频繁报警会让工段长直接关掉系统。我的做法是留出 200 到 300 张现场图片,包含正负样本,跑一遍推理,收集每个预测框的置信度,然后用一小段脚本遍历不同阈值下的精确率和召回率,找到目标漏检率和最大可接受误报数对应的阈值。下面的脚本就是做这个事的,输入是模型跑出来的结果文件,每行包含图片名、类别、置信度和坐标。

import json def find_threshold(preds, gt, target_recall=0.95): # preds: 模型输出,每个元素是 [img_id, cls, conf, x, y, w, h] # gt: 真实标注,按图片聚合后的字典 scores = sorted([p[2] for p in preds], reverse=True) best_t, best_f1 = 0.25, 0.0 for t in scores: tp = fp = 0 for img_id, cls, conf, *_ in preds: if conf < t: continue if img_id in gt and cls in gt[img_id]: tp += 1 else: fp += 1 fn = sum(len(v) for v in gt.values()) - tp recall = tp / (tp + fn) if (tp + fn) else 0 precision = tp / (tp + fp) if (tp + fp) else 0 if precision + recall > 0: f1 = 2 * precision * recall / (precision + recall) else: f1 = 0 if recall >= target_recall and f1 > best_f1: best_t, best_f1 = t, f1 return best_t, best_f1

参数说明:target_recall=0.95表示默认要求模型把 95% 的真实目标都找出来,然后在这个前提下选 F1 最高的阈值。这个脚本的关键在于结果要和真实标注对齐,这里用的是简单的图片 + 类别匹配,更严格的做法是按 IoU 匹配预测框和真实框。实际场景里,漏检的代价往往高于误检,所以阈值不要只看 F1,要按业务风险调低或调高。项目里最终选定的阈值往往比默认 0.25 高不少,因为现场负样本占比远高于训练集。

部署之后,持续收集误检和漏检样本,每个星期把新增样本合并进训练集重新训练一轮,这种迭代习惯比任何调参都管用。我经历过最大的教训就是以为训练完权重就结束了,结果现场跑了一个月,漏检的塑料袋形态从来没见过,模型毫无办法。这些现场样本才是数据集里最值钱的部分。希望这个方向能帮你在自己的生产线上少走两步弯路。

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

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

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

立即咨询