传送带异物检测实战:COCO数据集转YOLOv8全流程解析

发布时间:2026/9/16 5:58:44
传送带异物检测实战:COCO数据集转YOLOv8全流程解析 简介这是一份面向工业传送带异物检测场景的目标检测数据集用于识别铁棍、垃圾等常见异物适合算法工程师、科研人员及自动化质检项目开发者训练与验证模型。压缩包内共108个文件其中105张为从监控视频中抽帧得到的jpg图像3个为json标注文件整体大小约4.65MB。json文件按COCO格式组织包含类别、边界框等关键字段可直接接入MMDetection、Detectron2或YOLO系列框架免去数据格式转换步骤。文件名保留了NVR录像的时间段信息便于追溯图像采集场景真实监控视角带来的光照变化与复杂背景也有助于提升模型鲁棒性。特别是传送带上的小目标异物利用该数据集可评估不同检测器的性能差异。标注结构清晰适合作为迁移学习预训练数据或算法效果对比基准。目前已有412人学习下载可用作传送带异物检测课题的快速起步数据。1. 传送带异物检测为什么先用 COCO 格式数据集跑通传送带混入铁棍和零散垃圾靠人工盯 NVR 画面既慢又漏。这套数据集把问题归约成监督目标检测固定机位俯拍皮带面用 COCO JSON 格式标出铁棍和垃圾两类目标。文件名里保留着 NVR 通道、录像时间段和帧偏移帧是从 2022 年 9 月 12 日的监控录像中直接抽取的场景单一、视角固定恰好是工业视觉里最容易落地的一类。适合刚接手工业质检、想用真实监控画面跑通检测流程的人也适合拿它练 COCO JSON 标注转 YOLO 训练数据的脚本。真正常踩的坑不在模型选型而在长条铁棍和稀疏小垃圾之间极端不均衡的几何分布。2. NVR 帧文件名与 COCO JSON 标注结构2.1 文件名拆开看NVR 抽帧流程拿到数据集先别急着训练先看文件名。NVR_ch1_main_20220912160000_20220912170000-dav_000252-044_jpg.rf.b35c48b41b3287406e861ab231bdeeb7.jpg这一段命名比标注文件本身更能说明数据来源。NVR是网络录像机ch1代表通道 1也就是说所有帧来自同一个固定机位main是主码流分辨率通常是子码流的四倍左右20220912160000_20220912170000是一个小时的录像窗口说明抽帧不是随机抓拍而是按时间片段批量产出dav是录像封装格式的标识000252-044大致对应窗口内第 252 秒附近的第 44 帧。最后那段.rf.加 32 位哈希是 Roboflow 导出时加的标记原始图片名和哈希必须一起保留因为 JSON 里的 file_name 指向的就是这串完整名字。这类帧本身就是从 NVR 一小时录像里切出来的同一目标会以不同姿态、不同光照出现在多帧里好处是样本够真实坏处是背景几乎不动模型容易把皮带纹理和廊架阴影当背景记忆。实际使用时我会按文件名里的时间窗口划分训练集和验证集而不是随机打散否则同一目标几乎同时出现在训练集和验证集里评估出来的 mAP 虚高。NVR 抽帧数据集的另外一个共性是夜间帧和日间帧的亮度差异极大如果这份数据里两类光照都有建议在训练前按文件名时间粗略分桶确认白天和黑夜帧没有集中在同一个子集里。2.2 COCO JSON 顶层结构与校验方式COCO JSON 顶层是 info、licenses、images、annotations、categories 五个字段训练脚本实际用到的只有后三个。images 里每个元素是id、file_name、width、heightannotations 里每个元素是id、image_id、category_id、bbox、area、iscrowdcategories 把id映射到name。拿到数据先跑下面这段确认 JSON 没有损坏并看两类样本数量是否均衡。import json from collections import Counter with open(annotations.json, r, encodingutf-8) as f: coco json.load(f) cat_id2name {c[id]: c[name] for c in coco[categories]} cat_counter Counter() ann_per_image Counter() for ann in coco[annotations]: cat_counter[cat_id2name[ann[category_id]]] 1 ann_per_image[ann[image_id]] 1 print(顶层字段:, list(coco.keys())) print(图片数:, len(coco[images]), 标注数:, len(coco[annotations])) print(类别统计:, dict(cat_counter)) print(平均每图目标数:, sum(ann_per_image.values()) / max(len(coco[images]), 1))这里用Counter统计每个类别的标注数量用ann_per_image统计每张图的平均目标数。传送带场景通常每帧只有零到几个目标平均目标数如果超过 10说明标注里可能混入了大量重复框或者背景框需要做第 4 章的清洗。顺便说一句检查 COCO 文件内容时 jq 是最顺手的 json 格式化工具在命令行里可以快速看结构不用反复打开 Pythonjq .categories annotations.json jq .images[0] annotations.json jq .annotations | length annotations.json第一行输出类别表第二行看第一张图的宽高和文件名第三行直接数标注总量。没有 jq 就用 Python 的 json.load只要不抛异常说明文件是完整的 JSON。2.3 标注字段与传送带场景的对应关系COCO 格式在这个数据集里的实际含义可以对照下面这张表训练前把每个字段都确认一遍能省下后面排错的时间。字段在这个数据集里的含义训练时要关注的坑images.id帧的唯一编号annotations 的 image_id 必须能在这里找到否则就是孤儿标注images.file_name带 .rf. 哈希的完整 jpg 名转 YOLO 时不要把 basename 截错哈希丢了会找不到对图images.width / height抽帧原始分辨率转 YOLO txt 的归一化分母不能写死annotations.bboxx, y, width, height 绝对像素顺序是 xywh不是 YOLO 的 cxcywhannotations.category_id铁棍和垃圾对应的编号编号可能从 0 也可能从 1 开始不能假设annotations.iscrowd是否密集人群/物体为 1 的标注转 YOLO 时建议跳过类别只有铁棍和垃圾两类时category_id 通常从 0 或 1 开始转 YOLO 前我会统一重排一次编号避免中途引入偏移。另一个容易被忽略的点是监控画面的宽高比如果 NVR 主码流是 16:9而训练时直接 resize 到 640x640长条铁棍会被等比压缩后补边标注坐标必须跟着预处理走。最稳妥的做法是先按原始分辨率转好 YOLO txt让训练时的 letterbox 逻辑统一处理。提示转格式前先统计各类别数量如果两类数量差异超过 5:1不要直接训先看第 4 章的清洗和增强。3. 用 YOLOv8 训练自己的数据集COCO JSON 转 YOLO txt3.1 为什么把 COCO 转成 YOLO 而不是直读Ultralytics YOLOv8 官方并不直接读 COCO JSON它要求每张图对应一个同名.txt标签文件。MMDetection 的 CocoDataset 能直读 COCO JSON但 mmcv 与 torch 版本绑定太紧经常卡在编译环境上。从一个只有两类、单视角的数据集出发我的习惯是写一个二十行脚本转 YOLO txt流程最轻后续换 YOLOv5 训练自己的数据集时这套标签也能直接复用不用再改第二遍。这个 json 转换的本质是坐标系的重新表达COCO 的 bbox 是左上角 x、y 加宽高YOLO 是归一化到 0 到 1 之间的中心点 x、y 加宽高。转换时最容易错的是忘记归一化用图片真实宽高而不是 640 或 1280。另一个常见错误是类别 id 直接用原始值如果 COCO 的 categories id 从 1 开始而 YOLO 的类别索引从 0 开始训练时 loss 会静默错位类别数大于 2 时尤其隐蔽。3.2 转换脚本类别重编号与归一化import json import os def coco_to_yolo(coco_path, label_dir): os.makedirs(label_dir, exist_okTrue) with open(coco_path, r, encodingutf-8) as f: coco json.load(f) cat_remap {c[id]: i for i, c in enumerate(coco[categories])} for img in coco[images]: w, h img[width], img[height] base os.path.splitext(os.path.basename(img[file_name]))[0] lines [] for ann in coco[annotations]: if ann[image_id] ! img[id]: continue if ann.get(iscrowd, 0): continue x, y, bw, bh ann[bbox] if bw 0 or bh 0: continue cx (x bw / 2) / w cy (y bh / 2) / h lines.append( f{cat_remap[ann[category_id]]} {cx:.6f} f{cy:.6f} {bw / w:.6f} {bh / h:.6f} ) with open(os.path.join(label_dir, base .txt), w, encodingutf-8) as f: f.write(\n.join(lines)) coco_to_yolo(annotations.json, labels)逻辑说明cat_remap用 enumerate 把 COCO 类别 id 重排成 0 到 N-1防止原始 id 不连续每个标注目标输出一行中心点坐标和宽高全部除以图片真实宽高。空框和iscrowd标注直接跳过。这段代码里base取了文件名的 basename 再去掉扩展名图片的.rf.哈希会被完整保留在 txt 文件名里因为 YOLO 训练时就是靠 basename 匹配图片和标签。如果 JSON 里的 file_name 还带了子目录前缀需要把相对路径也拼进 base否则训练时提示找不到标签文件这类问题在 Windows 和 Linux 路径分隔符不一致时更容易出现。3.3 data.yaml 和训练命令转换完成后新建一个conveyor.yaml内容如下path: ./conveyor train: images/train val: images/val names: 0: iron_bar 1: trashpath 指向数据集根目录train 和 val 写图片文件夹的相对路径。如果数据没有划分可以按 NVR 文件名时间人工分不要用随机划分因为同一目标的多帧不能同时落在两个集合里否则验证集失去意义。启动训练yolo detect train modelyolov8n.pt dataconveyor.yaml epochs120 imgsz640 batch16 patience20 device0先用 yolov8n 热身跑通整个 pipeline再根据显存换 yolov8s 或 yolo m。传送带场景里目标普遍不大n 和 s 的精度差距没有想象中明显。120 个 epoch 对单一场景数据集往往偏多这类数据通常 60 到 80 轮就收敛patience 20 会提前停在最佳权重上。3.4 训练参数在这个数据集上的取舍监控素材的增强参数不能照搬 COCO 预训练那套下面这组是我在这种固定机位数据集上常用的一组基线和理由。参数推荐值这么设的原因imgsz640 起步小垃圾需要更大输入显存不够时先 640后面再微调到 960mosaic1.0皮带背景重复mosaic 能模拟更多目标摆放组合close_mosaic10最后 10 轮关闭 mosaic避免长条铁棍被切碎scale0.5固定视角下目标尺度变化小scale 太大会把小垃圾缩得更小fliplr0.5左右镜像不改变「铁棍横放」的语义安全hsv_h0.015NVR 色调偏稳定色相扰动要小hsv_s / hsv_v0.4 / 0.4车间灯光频闪和反光是主要变化来源饱和度和明度扰动比色相更有效这里说一个很多人忽略的点mosaic 对长条目标有副作用。一根铁棍横跨画面时mosaic 会随机裁切图片铁棍被切成两半后标注框要么变成残段要么把整个图幅都包进去。close_mosaic10让最后 10 个 epoch 回到真实分布收敛会更干净。如果你之前跑过 YOLOv5 训练自己的数据集会发现这两套概念完全一致txt 标签和 data.yaml 可以互相复用。4. 长条铁棍与稀疏小垃圾从 bbox 分布调整训练策略4.1 面积分位与宽高比统计直接训练前先对 COCO JSON 里的 bbox 做一次几何分布统计。这个步骤很多人跳过但在这个数据集里恰恰是关键铁棍是典型的长条目标垃圾是典型的小目标两种几何形状完全不同的目标混在一起锚框、增强策略和评估口径都要分开看。import json import numpy as np with open(annotations.json, r, encodingutf-8) as f: coco json.load(f) areas, ratios [], [] for ann in coco[annotations]: x, y, w, h ann[bbox] areas.append(w * h) ratios.append(max(w / h, h / w)) areas np.array(areas) ratios np.array(ratios) print(面积分位:, np.percentile(areas, [25, 50, 75, 95])) print(小于32x32占比:, float(np.mean(areas 32 * 32))) print(宽高比大于5占比:, float(np.mean(ratios 5)))输出里的面积分位可以告诉我们目标到底有多小、长条有多极端。假设图像宽度在 1920 附近32x32 的垃圾面积大约占全图的 0.03%这种目标在 640 输入下只剩约 10x10 像素几乎全是纹理信息宽高比大于 5 的基本就是铁棍这类目标的预测框 IoU 对角度偏差极其敏感。如果第 95 分位面积比第 25 分位大两个数量级以上说明这个数据集不能只用一个尺度策略覆盖。4.2 标注清洗重复框、越界框、失效帧几何分布统计完下一步是清洗标注。监控抽帧数据常见的三类脏数据是重复框、越界框和纯黑帧。重复框来自多人协作标注时同一目标被标两次特征是同类别、同位置、IoU 高越界框来自目标在画面边缘只有一半纯黑帧来自录像文件时间轴空洞或 NVR 夜间无信号。def iou(a, b): ax1, ay1, aw, ah a bx1, by1, bw, bh b x1, y1 max(ax1, bx1), max(ay1, by1) x2, y2 min(ax1 aw, bx1 bw), min(ay1 ah, by1 bh) inter max(0, x2 - x1) * max(0, y2 - y1) union aw * ah bw * bh - inter return inter / union def find_duplicates(anns, thr0.8): dup set() for i in range(len(anns)): for j in range(i 1, len(anns)): if anns[i][category_id] anns[j][category_id] and iou(anns[i][bbox], anns[j][bbox]) thr: if anns[j][area] anns[i][area]: dup.add(j) else: dup.add(i) return dupiou计算两个矩形框的交并比find_duplicates把同类别且 IoU 大于 0.8 的标注里面积更小的那个标记出来。两个不同类别在同一位置出现时不要删铁棍搭在垃圾上是完全合法的目标重叠删了反而让模型学错关系。越界框我一般看归一化后的中心点是否落在-0.02到1.02之外轻微越界的保留明显出画面的删掉。纯黑帧则用 OpenCV 读灰度图算方差方差小于 10 的直接丢掉这类帧放进训练集只会让模型学会输出空检测。4.3 两类目标各自的增强策略清洗完再看增强。传送带数据集和一般目标检测数据集的增强策略差异很大因为相机不动、背景高度重复通用增强里的随机裁剪和大幅旋转都会破坏目标结构。问题现象处理手段注意点铁棍被截断后漏检训练后期关闭 mosaicrotate 限制 ±15°铁棍横放是常态大角度旋转会引入不存在的姿态小垃圾漏检复制粘贴小目标到无标注区域粘贴目标不要与原标注重叠也不要贴在传送带边缘阴影上两类数量不均衡对垃圾类做上采样复制验证集不能同步做增强否则评估结果失真夜间帧漏检对低光帧做 gamma 增强不要只调 hsv_vNVR 夜视模式的色偏是全局的复制粘贴小目标是我在这个数据集上提点最明显的手段。把垃圾 bbox 抠出来随机缩放到 0.9 到 1.1 倍贴到皮带空白处每张图贴一两个粘贴时避开传送带边缘因为那里本身是黑色阴影模型很容易学会用阴影代替目标。实现时注意只改图片和对应 txt不要动原始 COCO JSON验证流程里避免再次读到这些合成样本。4.4 用 val 结果反推问题训练完不要只看一个 mAPyolo detect val modelruns/detect/train/weights/best.pt dataconveyor.yaml输出里逐类看 mAP50 和 recall。如果铁棍 recall 低而 precision 高多半是标注时漏了被遮挡的半截铁棍模型学到的铁棍全都是完整完好的如果垃圾 precision 低多半是皮带滚轴、螺丝孔和反光点被当成正样本。这时先去看分错样本的图像区域确认是标注漏标还是特征相似再决定改数据还是改阈值。面积分位里小于 32x32 的目标占比越高mAP0.5:0.95 相对 mAP0.5 掉得越多这是正常现象不要因为 0.95 阈值下的分数低就认为模型没训好。5. 用 ByteTrack 连续帧命中做报警去抖5.1 单帧检测为什么不能直接触发单帧模型在真实 NVR 流上会间歇性误报。传送带上的焊渣、反光点在某一帧里的外观和垃圾非常接近但用连续几帧看位置不连贯闪烁一下就消失。报警逻辑放到跟踪层面用连续命中次数过滤比单纯调低置信度阈值更有效也不会牺牲真正的小目标召回。5.2 连续命中计数与重置用 ultralytics 的model.track时persistTrue会跨帧保留目标 idtracker 用 bytetrack.yaml比默认的 botsort 对这种刚体运动的小目标更稳。classes[1]只跟踪垃圾类对应 data.yaml 里 trash 的索引铁棍不会频繁占用跟踪资源。连续命中计数用字典维护from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) hit_counter {} last_seen {} ALARM_HITS 3 for frame_idx, frame in enumerate(video_frames()): results model.track( frame, persistTrue, conf0.35, iou0.5, trackerbytetrack.yaml, classes[1], ) boxes results[0].boxes if boxes is None or boxes.id is None: continue seen set() for tid in boxes.id: tid int(tid) seen.add(tid) last_seen[tid] frame_idx hit_counter[tid] hit_counter.get(tid, 0) 1 if hit_counter[tid] ALARM_HITS: trigger_alarm(frame, boxes.xyxy) hit_counter[tid] 0 expired [t for t in last_seen if frame_idx - last_seen[t] 30] for t in expired: hit_counter.pop(t, None) last_seen.pop(t, None)hit_counter是每个跟踪 id 的累计命中次数达到 3 帧才触发报警触发后立刻重置避免同一堆垃圾持续占用画面时反复报警last_seen用来清理离开画面超过 30 帧的旧 id防止 id 丢失后计数器残留。trigger_alarm内部需要保存当前帧和 bbox 坐标坐标要映射回 NVR 原始画面尺寸因为 track 的输入可能是 resize 后的帧。把触发条件从「一帧高分」改成「三帧连续命中」误报率通常会下降一到两个数量级再加上 60 帧报警冷却这套检测流程才真正具备上线条件。本文还有配套的精品资源点击获取