传送带异物检测数据集:COCO JSON格式从标注到验证的工程实践
简介传送带异物检测场景下的小型目标检测数据集面向工业质检、智能安防等方向的算法工程师与学习者可用于训练识别铁棍、垃圾等异物的深度学习模型。数据来自NVR监控视频截帧包含不同时间段的传送带真实画面共105张JPG图像背景与光照变化有助于提升模型泛化能力同时提供3个JSON文件采用COCO JSON格式标注包含目标类别与边界框坐标可被MMDetection、Detectron2等主流框架直接读取也便于转换为YOLO等格式。整个压缩包仅4.65MB体量轻便文件结构清晰图片与标注一一对应方便快速完成训练/验证集划分。已有412人学习下载适合想通过真实工业场景数据熟悉COCO标注流程、开展检测模型调试与效果评估的开发者。1. 传送带异物检测数据集最容易被低估的是标注格式而不是算法传送带上混入铁棍、垃圾在冶金、煤炭、垃圾分拣线上是事故高发源设备损坏、安全事故往往就出在这几秒。做这类视觉检测大多数人第一步想到的是先凑样本、训练个YOLO出来跑通但真正做到能部署、能交付最卡脖子的反而是数据组织方式你用txt标注跑出来的模型换一个检测框架就得重标一遍而COCO JSON格式几乎被所有主流框架原生支持还天然附带类别、面积、iscrowd这些训练和评估需要的元信息。这篇东西我按一线工程的做法把从原始视频到可训练COCO数据集的完整路径写出来包括标注字段怎么填、AI辅助怎么省力、类别不均衡怎么办以及最后用官方指标验证时最容易踩的几个坑。2. COCO JSON格式字段拆解先会解析再标注避免框架之间反复迁移2.1 五个顶层字段里只有三个会被训练框架读取COCO数据集格式顶层固定5个字段info、licenses、images、annotations、categories。info和licenses只记录版本号、数据来源和版权信息PyTorch的数据加载器不会去读它们。真正被训练循环消费的是images、annotations和categories这三个images是样本清单每条包含id、width、height、file_nameannotations是标注本体每条标注由id、image_id、category_id、bbox、area和iscrowd组成categories是类别映射表给定每个类别的id和name行业里常见的一种翻车情况是从网上直接下载的COCO数据集只有annotations和images缺categories或字段类型不符合json规范。json格式化工具能帮你看到缩进但不可能帮你校正字段类型——把area写成字符串的比比皆是。我一般拿到任何标注文件第一件事是用json.load解析然后立即按下面的维度统计一遍这张表的质量。import json from collections import Counter with open(conveyor_anomaly.json, r) as f: coco json.load(f) print(图片总数:, len(coco[images])) print(标注实例数:, len(coco[annotations])) print(类别定义:, [(c[id], c[name]) for c in coco[categories]]) ann_stats Counter(a[category_id] for a in coco[annotations]) for cat_id in ann_stats: name next(c[name] for c in coco[categories] if c[id] cat_id) print(f{name} (id{cat_id}): {ann_stats[cat_id]} 个实例) img_ids set(i[id] for i in coco[images]) annotated_ids set(a[image_id] for a in coco[annotations]) print(f无标注图片数: {len(img_ids - annotated_ids)}) areas [a[area] for a in coco[annotations]] if areas: print(f面积均值: {sum(areas)/len(areas):.2f}, 最小: {min(areas):.2f})这段脚本在三处地方尤其有用一是标注团队交回数据后的验收二是从公开数据集裁剪类别时快速确认各类别的可用实例数三是做类别不均衡分析前先搞清楚每类真实数量。铁棍这类常见异物可能标注了上千个垃圾袋、塑料瓶可能只有几十个不跑这段脚本后面训练时类别权重怎么配完全靠猜。2.2 annotations字段有严格约定bbox坐标和area必须一致字段类型说明常见错误idint实例唯一编号全文件不重复重复id导致加载器丢标注image_idint必须指向images中的id写错则标签挂到错误图片上category_idint指向categories.idCOCO约定从1开始使用0会被框架抛异常bbox[x, y, w, h]左上角坐标加宽高类型为float误写成中心点坐标或intareafloat数值上等于w乘h与bbox不一致会让评估发生偏移iscrowd0/11表示密集物体区域训练时通常忽略普通异物必须为0area在COCO官方数据集里对分割掩码是计算掩码面积不是bbox的w乘h对纯bbox标注的数据集area应等于w乘h。很多人从LabelMe或某图像标注平台导出的COCO格式里area字段是错的或缺失的pycocotools的评估阶段按区域大小分桶统计AP时结果会出现明显偏差尤其大目标的结果异常。iscrowd的处理也值得单独看一眼。实际传送带异物检测里料堆、碎渣这类不是目标的东西不要标成iscrowd1更不要不标——标了iscrowd1的检测器会学到“看到密集区就忽略”漏检率反而上升。正解是把它们留在背景里不参与任何标注。2.3 格式校验函数一份能用五年的通用检查脚本把上一节的约定落成代码我给的检查脚本覆盖类别不存在、image_id无对应图片、bbox越界、宽高非正、面积与bbox不匹配。校验通过后再进训练能省掉大量排查时间。def validate_coco(path: str) - list: with open(path, r) as f: coco json.load(f) cat_ids {c[id] for c in coco[categories]} img_map {i[id]: i for i in coco[images]} errors [] for ann in coco[annotations]: cid ann[category_id] if cid not in cat_ids: errors.append(fcategory_id {cid} 未在categories中定义) continue iid ann[image_id] if iid not in img_map: errors.append(fimage_id {iid} 未在images中存在) continue x, y, w, h ann[bbox] if w 0 or h 0: errors.append(fbbox宽高非正: {ann[bbox]}) img img_map[iid] if x w img[width] 1 or y h img[height] 1: errors.append(fbbox越界: image_id{iid}, bbox{ann[bbox]}) if abs(ann[area] - w * h) / (w * h) 0.01: errors.append(farea与bbox不一致: {ann[area]} vs {w*h}) return errors errs validate_coco(conveyor_anomaly.json) print(问题数:, len(errs)) for e in errs[:10]: print(e)validate函数接收COCO json路径返回按顺序发现的错误列表。实际工程里我还会加一个阈值参数来控制越界容忍度图像边缘的标注框越界应该修而不是一律忽略。这份脚本可以挂在CI上每当标注团队推新版本数据先过校验再合并避免脏数据悄悄混进训练集。校验函数对宽高加1像素的容忍是因为少数图像经过缩放后整数化时会多出1像素的舍入误差这个细节来自pycocotools掩码评估的实际误差特征。3. 从视频到COCO JSON抽帧、清洗与CVAT标注的完整流程3.1 抽帧策略传送带场景下按秒采样不如按目标位移采样传送带视频里异物出现的时机完全随机摄像头帧率通常在25到60fps。如果每秒抽1帧传送带速度每分钟30米的话相邻抽帧间物体位移约0.5米铁棍这种细长目标在中远距离下可能只出现一次就被漏掉。我一般看传送带速度和目标尺寸两个参数决定采样间隔而不是跟着视频长度走传送带速度0.5m/s目标最小直径5cm每5帧抽1帧能保证同一目标出现在3到4帧中目标在画面里停留时间不足0.5秒优先连续抽帧宁可先多抽再人工清洗用ffmpeg按间隔抽帧最常见的命令如下ffmpeg -i conveyor_raw_01.mp4 -vf fps5 -q:v 2 sampled_%04d.jpgfps5表示每秒抽5帧q:v控制JPEG质量数值越小质量越高2是清晰度和文件体积的平衡点。抽帧后先把画面角度异常的、镜头脏污的、传送带空载过久的帧直接筛掉。空载帧在最终数据里占比过高会让模型把“没有异物”当成最强的先验训练时负样本的比例会压不住推理时误报率升高。3.2 CVAT标注并导出COCO 1.0格式一个平台解决从画框到导出的全部问题数据标注行业的技术栈基本围绕CVAT、LabelMe、X-AnyLabeling在转CVAT是目前对COCO兼容性最好、导出字段最完整的工具尤其它支持多人在线协作避免团队数据标注时标签命名不一致这种老问题。用CVAT不光是画框在项目管理后台里就要定义好category列表铁棍iron_rod、垃圾袋trash_bag、塑料瓶plastic_bottle、纸团paper_cluster。不要用中文名类别名进JSON后大概率被框架当字符串处理中文名在后续日志和特征分析里还要再接一层映射没必要。标注规范是每条bbox要贴住目标外边缘铁棍这类长条目标允许分段标注分成两段后模型对遮挡场景的鲁棒性会明显提升。CVAT导出的路径是任务页进入Export job dataset选择COCO 1.0。导出结果是一个zip包里面包含annotations/instances_default.json和对应图片正是COCO兼容的JSON格式。导出来之后注意观察类别id的排列顺序——CVAT是按创建顺序分配的和你在YOLO里习惯的0、1、2不对齐需要重新映射。3.3 从CVAT的COCO导出到可训练数据按场景划分train/val并转YOLO格式训练前要划分train/val划分策略按场景而不是按图片随机分。同一条传送带可能有多段录像是不同日期拍的划分时以视频片段为单位放入train或val理由在后面第5章展开这里先给分组划分的脚本import json, random random.seed(2024) with open(instances_default.json) as f: coco json.load(f) images coco[images] # 假设每张图的file_name前缀是视频片段ID videos {} for img in images: vid img[file_name].split(_)[0] videos.setdefault(vid, []).append(img[id]) vid_keys list(videos.keys()) random.shuffle(vid_keys) val_vids set(vid_keys[:int(len(vid_keys) * 0.2)]) val_img_ids set() for vid in val_vids: val_img_ids.update(videos[vid]) val_coco { images: [i for i in images if i[id] in val_img_ids], annotations: [a for a in coco[annotations] if a[image_id] in val_img_ids], categories: coco[categories], } train_coco { images: [i for i in images if i[id] not in val_img_ids], annotations: [a for a in coco[annotations] if a[image_id] not in val_img_ids], categories: coco[categories], } with open(instances_train.json, w) as f: json.dump(train_coco, f) with open(instances_val.json, w) as f: json.dump(val_coco, f)这段脚本按file_name前缀分组确保同一视频片段的所有帧要么全在训练集要么全在验证集避免前后帧数据泄漏导致验证指标虚高。分割验证集后如果发现某一天的光照图片比例失衡回到这个脚本手动指定val_vids即可。如果后续要跑YOLO系列模型还需要把COCO JSON转换成YOLO的txt格式。yolov8训练自己的数据集时原生接口读的是txt格式转换脚本如下import json from pathlib import Path def coco_to_yolo(coco_path, out_dir): with open(coco_path) as f: coco json.load(f) img_map {i[id]: i[file_name] for i in coco[images]} with open(coco_classes.txt, w) as f: for c in coco[categories]: f.write(c[name] \n) ann_by_img {} for a in coco[annotations]: ann_by_img.setdefault(a[image_id], []).append(a) for img_id, fname in img_map.items(): fname Path(fname) txt_path Path(out_dir) / (fname.stem .txt) with open(txt_path, w) as wf: for a in ann_by_img.get(img_id, []): x, y, w, h a[bbox] img next(i for i in coco[images] if i[id] img_id) cx (x w / 2) / img[width] cy (y h / 2) / img[height] nw w / img[width] nh h / img[height] wf.write(f{a[category_id]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n) coco_to_yolo(instances_train.json, yolo_labels/train)转换时注意COCO的category_id从1开始YOLO需要的类别索引从0开始脚本里没有减1需要在写txt的时候将category_id减1这一步常被忽略。正确写法是a[category_id] - 1否则训练后类别名和预期对不上。4. 半自动辅助标注与数据增强样本少不是借口4.1 AI预标注先训一个弱模型人工只修正边界对于铁棍、钢筋这类形状规整的规则异物半自动辅助标注的可行度很高。常见做法是先用小样本手标200到300张训一个YOLOv8n或YOLOv8s作为预标注模型跑在未标注帧上生成伪标签再由人工修正确认没被标出的极端角度、修正被背景干扰误框出来的区域。修正后补充回训练集迭代两轮3000张图的人工工作量大概能压缩到500张的真实标注量。CVAT本身就支持在平台里接模型输出。一种接法是训练好后用ultralytics的predict导出标注结果再通过CVAT的API创建任务、导入标签。另一种更轻量本地跑推理后用脚本把结果与原始帧对齐人工在标注平台里改错。预标注推理脚本from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict(sourceframes/raw, conf0.35, imgsz1280, classes[0, 1, 2]) annotations [] for res in results: image_id res.path for box in res.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf box.conf[0].item() cls int(box.cls[0].item()) annotations.append({ image_id: image_id, category_id: cls 1, bbox: [x1, y1, x2 - x1, y2 - y1], score: conf, area: (x2 - x1) * (y2 - y1), iscrowd: 0, })conf设0.35是偏低阈值宁多标不漏标人工修正删掉假阳性的成本远低于重新补漏标的成本。classes参数限定只对铁棍、垃圾和塑料瓶这三类做推理要和你训练模型时定义的类别索引严格对应。score保留下来导入标注平台后作为筛选维度优先处理低分框那些大概率需要人工修正。4.2 albumentations增强对传送带场景最有用的三种变换数据增强在整个pipeline里能和标注质量并列直接决定收敛速度和最终mAP。传送带场景的特殊性在于背景高度重复、光照随车间时段大幅变化、异物品种少但形态差异大因此增强策略与通用目标检测任务不一样。我常用的增强组合import albumentations as A train_transform A.Compose([ A.RandomBrightnessContrast(brightness_limit0.3, contrast_limit0.3, p0.8), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit30, p0.6), A.RandomGamma(gamma_limit(80, 120), p0.4), A.MotionBlur(blur_limit(5, 15), p0.3), A.RandomScale(scale_limit(-0.3, 0.2), p0.5), A.PadIfNeeded(min_height640, min_width640, border_mode0, value128), A.Resize(640, 640), ], bbox_paramsA.BboxParams(formatcoco, label_fields[category_ids]))RandomBrightnessContrast模拟不同时段光照brightness_limit为0.3表示亮度在±30%范围内随机扰动MotionBlur对应传送带高速运动时的动态模糊RandomScale改变目标尺度分布让模型对不同距离的异物都有响应PadIfNeeded在随机缩放产生小图时用128灰填充补齐到640x640避免黑色边框造成误判。整套增强不要加水平翻转——垃圾袋和铁棍不具有左右对称性水平翻转后形态特征与真实数据分布不一致推理时会出现翻转误差。albumentations的bbox_params里formatcoco即[x,y,w,h]格式如果已经转成了YOLO相对坐标格式可以填formatyolo两者不需要手动换算。label_fields里的category_ids和bbox列表要保持一一对应。4.3 类别不均衡的三个处理手段异物检测数据集的类别分布天然不均衡铁棍在金属加工线上出现的频次远高于生活垃圾但部署时必须两类都要检出。三个顺序递进的手段欠采样或过采样整图对少数类垃圾袋、纸团按倍数复制它们所在的整张图而不是单独复制标注框避免过拟合到少数类的重复特征上增强少数类把少数类所在图片的增强概率提高让同一个样本生成多个语义变体重加权损失函数在检测头的分类损失上加per-class权重把少数类的权重调到2到3倍PyTorch的BCEWithLogitsLoss自带pos_weight参数这是多数检测头分类损失的标准做法import torch.nn as nn class_weights torch.tensor([1.0, 2.5, 2.0]) cls_loss nn.BCEWithLogitsLoss(pos_weightclass_weights)pos_weight会对正样本的loss放大对应倍数。注意这个参数要在损失函数初始化时传入而不是训练循环里再乘否则梯度计算会被重复放大。经验上从2倍开始调不要一步拉到10倍——太高的权重会让少数类精度上升而多数类精度急速下降整体mAP反而掉点。5. 用pycocotools验证mAPCOCO评估指标的边界与陷阱5.1 用pycocotools跑验证三行代码输出逐类AP验证集准备成COCO格式后用pycocotools在验证集上计算mAP是比“歪着头看loss曲线”可靠得多的验收方式from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval coco_gt COCO(instances_val.json) coco_dt coco_gt.loadRes(results_val.json) evaluator COCOeval(coco_gt, coco_dt, bbox) evaluator.evaluate() evaluator.accumulate() evaluator.summarize()results_val.json里没一行预测记录字段包含image_id、category_id、bbox、score。COCO的mAP是在IoU从0.5到0.95步长为0.05的10个阈值上分别计算AP再取平均。铁棍这类细长物体对定位误差非常敏感IoU0.5时框偏一点还算命中到0.75以上长条目标的宽高比细微偏移就会让IoU骤降因此对铁棍这类异物mAP0.5:0.95比mAP0.5更能反映部署后的真实表现。逐类看AP时可以直接从evaluator对象里提取per-class结果import numpy as np stats evaluator.stats print(fmAP0.5:0.95 {stats[0]:.3f}) print(fmAP0.5 {stats[1]:.3f}) for idx, cat_id in enumerate(evaluator.params.catIds): precision evaluator.eval[precision][:, :, idx, 0, -1] ap precision[precision -1].mean() if (precision -1).any() else 0.0 name next(c[name] for c in coco_gt.dataset[categories] if c[id] cat_id) print(f{name}: AP {ap:.3f})输出结果里如果某一类的AP明显低于其他类优先去查那一类的训练样本里是否存在标注框过小、遮挡严重或者类别间特征混淆的问题不要先调训练参数。5.2 验证集划分的一个隐蔽坑同一传送带的形态漂移按时间顺序切数据集把前70%放训练集、后30%放验证集是常见做法对传送带场景却直接失效。传送带吃料状态随时间变化早班空载、中班满料、夜班灯光不同不同班次的画面分布差异巨大。按第3章的分组划分脚本按视频片段切分是正解。工程上更稳妥的做法是按形态分层把包含金属碎屑的图像划一组、包含生活碎片的划一组、干净传送带划一组三个组内各自随机抽20%进验证集保证每一种形态在训练和验证中都有代表。5.3 部署前最后的连帧稳定性测试COCO验证指标计算的是逐帧独立评估部署时模型面对的是视频流。跑完验证后真正该做的是连帧稳定性测试统计检出帧占比和人工标注的异物出现频率差多少import cv2 from ultralytics import YOLO model YOLO(best.pt) cap cv2.VideoCapture(test_conveyor.mp4) frame_skip 3 frame_idx 0 detected_ratio 0.0 hit_count 0 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_idx % frame_skip ! 0: frame_idx 1 continue res model.predict(frame, conf0.25) if len(res[0].boxes) 0: hit_count 1 frame_idx 1 detected_ratio hit_count / (frame_idx // frame_skip) print(f异物检出帧占比: {detected_ratio * 100:.2f}%)如果这个占比和人工抽样统计的异物出现频率差距超过5个百分点优先检查数据分布再检查置信度阈值。这个脚本不检测闪烁一个铁棍在帧1检出、帧4没检出、帧5又检出这种抖动在逐帧指标里完全看不出来需要加时序滤波连续N帧有检出且位置重叠度足够时才输出报警信号。这部分是部署侧的工程取舍但它暴露的是数据集里检测难度分布不均衡——中间丢帧的样本在训练集里太少。本文还有配套的精品资源点击获取