ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

传送带异物检测数据集:COCO JSON标注格式与构建全流程解析

传送带异物检测数据集:COCO JSON标注格式与构建全流程解析 简介这份数据集面向工业视觉与目标检测方向的开发者聚焦传送带场景中的金属异物与垃圾识别可服务于产线物料的实时监测与异常告警。包内共108个文件包含105张JPG图像样本与3个JSON标注文件整体体积仅4.65MB结构简洁、便于快速验证算法。图像素材取自NVR监控视频的连续帧覆盖不同时间段的传送带画面贴近真实生产环境标注采用COCO格式划分有铁棍、垃圾两类目标提供边界框与类别信息可直接用于YOLO、MMDetection、Detectron2等主流检测框架的训练与评估。除了原图与标注文件就无需额外预处理方便研究者将注意力集中在模型调优与检测精度提升上。目前已有413人学习/下载该资源适合需要快速获取真实场景数据、开展传送带异物检测实验的算法工程师与学生。1. 传送带中异物检测识别数据集为什么说 COCO JSON 格式是这条产线的第一道闸门一条传送带每小时过几万件物料混进去一根铁棍或者一块包装残片轻则划伤设备重则整线停机。想把视觉异物检测识别做起来几乎所有团队的起手式都是先找数据集、再调模型但真正卡住进度的往往不是模型结构而是最开始那套标注格式。异物检测识别数据集最常见的载体就是 COCO JSON 格式——一个只在 JSON 文件里约定图片路径、标注框和类别名的标准化结构。它门槛不高但字段版本多、坑也多类别 id 从 0 还是 1 开始、bbox 是 (x,y,w,h) 还是 (x1,y1,x2,y2)、分割点和框要不要共存。这篇笔记就把传送带场景下铁棍、垃圾这类异物的数据集从采集、标注、转格式、训练验证到排错讲一遍适合正在搭工业质检数据的算法工程师和机器视觉方案商参考。2. COCO JSON 标注格式拆解把铁棍和垃圾填进 JSON 的正确姿势2.1 三个顶层字段images、annotations、categories 的职责与关系COCO JSON 顶层是三个数组字段images 负责建立图像索引annotations 负责挂载每一个标注框categories 负责声明类别清单。在传送带异物检测项目里铁棍和垃圾各占一个 category后续如果新增螺丝、易拉罐只需要向 categories 数组追加 id 即可不需要改动标注结构。这一点是这类数据集相比自定义 txt 格式最大的优势——扩展类别不动底层。字段内容在本项目中的作用imagesid, file_name, width, height记录每一帧图像来源与尺寸annotationsid, image_id, category_id, bbox, area, segmentation某个异物在该帧的位置与形状categoriesid, name, supercategory类别清单铁棍/垃圾需要注意的细节集中在 annotations。bbox 的格式是 [x, y, width, height]单位是像素原点在图像左上角segmentation 是或多边形顶点列表检测任务里可以留空数组area 是标注面积评估 mAP 时会用到。image_id 必须与 images 数组里的 id 对上category_id 必须与 categories 数组里的 id 对上这三者的关联就是整个 COCO 格式的核心。另一个关键点是 category_id 从 1 开始COCO 官方数据集没有 id0 的类这一点很多人第一次转换格式就会翻车后面避坑章节会专门讲。2.2 从 labelme 标注到 COCO JSON转换脚本与参数说明实际项目里最常见的流程是工位架一台相机录制视频抽帧后用 labelme 手工框出铁棍和垃圾。labelme 的产物是一张图一个 JSON里面存的是多边形顶点而训练框架要的是整个数据集的 COCO JSON。所以中间必须有一个合并转换脚本。下面这个脚本是我常用的精简版覆盖从 labelme 目录到 COCO JSON 的完整转换import json import os from PIL import Image def labelme_to_coco(labelme_dir, image_dir, output_path, category_map): images, annotations [], [] ann_id 1 for idx, fname in enumerate(sorted(os.listdir(labelme_dir))): if not fname.endswith(.json): continue with open(os.path.join(labelme_dir, fname), r, encodingutf-8) as f: data json.load(f) img_path os.path.join(image_dir, data[imagePath]) with Image.open(img_path) as im: w, h im.size image_id idx 1 images.append({ id: image_id, file_name: data[imagePath], width: w, height: h }) for shape in data[shapes]: label shape[label] if label not in category_map: continue points shape[points] # [[x1,y1], [x2,y2], ...] xs [p[0] for p in points] ys [p[1] for p in points] x_min, y_min min(xs), min(ys) box_w max(xs) - x_min box_h max(ys) - y_min area box_w * box_h segmentation [[round(v, 1) for p in points for v in p]] annotations.append({ id: ann_id, image_id: image_id, category_id: category_map[label], bbox: [x_min, y_min, box_w, box_h], area: area, segmentation: segmentation, iscrowd: 0 }) ann_id 1 categories [{id: v, name: k, supercategory: foreign_body} for k, v in category_map.items()] with open(output_path, w, encodingutf-8) as f: json.dump({images: images, annotations: annotations, categories: categories}, f) if __name__ __main__: labelme_to_coco( labelme_dir./labelme_jsons, image_dir./images, output_path./dataset/annotations/train.json, category_map{iron_bar: 1, trash: 2} )逻辑说明labelme 的 shapes 里每个 shape 的 points 是多边形顶点代码里用 min/max 把多边形转成外接矩形框这样检测任务只需要框信息。如果之后需要做分割任务segmentation 里保留的 vertices 可以直接用。转换脚本的核心是维护三个 id 的对应关系image_id 从 1 递增ann_id 从 1 递增category_id 由传入的 category_map 决定。img_path 拼接用的是 labelme JSON 里记录的 imagePath 字段建议标注前把所有图片统一放到同一个 image_dir避免相对路径错乱。参数说明category_map 的值必须从 1 开始不要用 0labelme 里标注的 label 名称要和 map 的 key 严格一致比如统一用 iron_bar 和 trash不要一会写“铁棍”一会写“iron bar”。area 这里用 bbox 面积代替 polygon 面积检测任务评估时误差可忽略但如果数据集将来要复用给分割任务就需要用多边形面积。转换完成后用 json 工具打印一次 categories 字段确认 id 正确这一步我每次都会做成本几乎为零但能避免后面训练时出现“类别全乱”的玄学问题。2.3 矩形框还是多边形传送带场景的标注粒度怎么选铁棍是规则的长条形外接矩形框能干净地套住垃圾形状不规则多边形更贴合边缘。但传送带场景里有两件事让多边形标注不划算一是物料堆叠时垃圾只有局部露出来人工判断边界已经很困难再多点几个顶点只会增加主观偏差二是主流的 YOLO 系列检测器输出本身就是矩形框多边形标注最终也要转成框参与训练。所以我的做法是最终交给训练的数据一律用矩形框标注只有目标特别小且密集易漏检时才用多边形辅助比如零散螺丝、小铁屑。多边形标注成本大概是矩形框的三倍一套几百张的数据集差异就是几个工时。还有一个比框型更值得花时间的点标注规范。两个标注员对同一个垃圾一个框住整块区域一个只框住露出部分模型会学到两种“正确答案”结果就是验证时 mAP 忽高忽低。传送带项目的标注规范里至少要写三条标注到可见部分的外边缘、不跨物体、不把背景阴影圈进去。这个规范建议直接写进标注工具目录下的 README 里防止换人后标准漂移。3. 采集与预处理让传感器数据变成有效训练样本3.1 传送带场景的三大采集变量光照、运动模糊、遮挡传送带数据和公开数据集的本质区别环境可控但干扰多。光照方面车间常有频闪的钠灯相机帧率不够会出现明暗条纹传送带上方若没有遮光帘阳光直射会造成局部过曝。最简单的做法是工业面阵相机加恒定 LED 补光曝光时间控制在 1/1000 秒以内避免运动模糊。运动模糊是影响最大的一环传送带线速度 1 m/s 时曝光 10ms 会让目标在图像上拖出约 10 个像素的尾巴小目标直接变成一条线。可以先用“移动像素数 线速度 × 曝光时间 / 像素分辨率”算一遍目标移动超过 3 个像素就降曝光或提高帧率。遮挡来自物料堆叠。预处理阶段建议把遮挡程度分成三档完整可见、部分遮挡、严重遮挡。训练时三档都要有比例控制在 6:3:1。如果严重遮挡样本过多模型会学到看到一小块像垃圾的区域就报警误检率飙升反过来这一档完全没有实际生产中遮挡目标就会漏检。采集时多拍不同角度、不同料流高度的视频抽帧时自然就能覆盖这几档。3.2 类别边界铁棍、垃圾、背景怎么划清类目定义不清晰是数据集质量的第一杀手。铁棍要定义到“什么材质、什么颜色、生锈算不算”垃圾要定义到“纸屑、塑料片、包装残片、线头算不算”。我的经验是类别宁细勿粗给铁棍按长度分 iron_bar_long、iron_bar_short给垃圾按材质分 plastic_waste、paper_waste。类别数量上去了但每个类的特征更一致。异物检测识别模型学的是类内共性把形状差异巨大的目标塞进同一个 class模型只能学到一半的共性表现就是 mAP 看着还行实际产线上漏检不断。如果项目对算力或标注时间敏感稳妥做法是做成二分类iron_bar 和 trash同时用“背景”概念约束。背景不需要标注但 images 里必须有一部分完全不含异物的负样本图。负样本比例建议不低于正样本的三分之一不然模型会倾向于把所有矩形输出都判定成异物。这里还有一层负样本不是随便找空图而是采集传送带正常物料通过时的画面让模型学会区分“正常物料”和“异物”否则模型会把所有不规则物料都当垃圾。3.3 视频抽帧去重用帧差法剔除相似帧的脚本传送带视频每秒几十帧相邻帧内容几乎一样。直接把全部帧放进数据集带来两个问题一是训练集和验证集之间容易泄漏二是标注工作量翻倍但信息量不增加。常见做法是按时间间隔抽帧比如每秒抽 2 帧再叠加一个感知去重把画面变化小于阈值的帧丢掉。下面是一个基于帧差的脚本我一般放在采集流程的第一步import cv2 import numpy as np from pathlib import Path def dedup_frames(video_path, out_dir, min_gap4.0): cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) frame_interval int(round(fps / 2)) # 每秒抽 2 帧 last_frame None saved 0 idx 0 while True: ret, frame cap.read() if not ret: break idx 1 if idx % frame_interval ! 0: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, (256, 256)) if last_frame is None: diff 999.0 else: diff float(np.mean(np.abs(gray.astype(np.int16) - last_frame.astype(np.int16)))) if diff min_gap: continue last_frame gray out_name Path(out_dir) / fframe_{saved:06d}.jpg cv2.imwrite(str(out_name), frame, [cv2.IMWRITE_JPEG_QUALITY, 95]) saved 1 cap.release() if __name__ __main__: dedup_frames(conveyor_01.mp4, ./raw_frames, min_gap4.0)逻辑说明先把每一帧缩到 256x256 灰度图再计算当前帧与上一个保留帧之间的平均像素差。diff 小于 min_gap 就跳过否则存盘。这个机制同时实现了时间均匀采样和视觉去重传送带上物料连续移动时帧差大全部保留物料静止或画面几乎不变时连续几帧被合并成一张。min_gap 是平均灰度差值默认 4.0光照稳定的车间可以设 2~3环境震动大的产线要提高到 5~6否则许多帧会因为震动被误判为“变化大”而全部留下。参数说明fps 通过 cap.get 读取这里按每秒 2 帧采样。如果传送带带速在 2 m/s 以上建议改成每秒 4 帧否则小目标异物只在画面里停留一两帧抽帧时很容易漏掉。保存为 JPG 质量 95足够标注和训练使用。抽帧后还是要过一遍人工删除模糊帧、异物完全被遮挡的帧这类帧让标注的人盯着看几分钟都不知道该框什么不如直接删掉。4. 用 COCO 数据集驱动检测训练从 JSON 到损失曲线的验证闭环4.1 把 COCO JSON 转成 YOLO 训练格式脚本与归一化细节YOLOv8 训练自己的数据集时ultralytics 默认不直接吃 COCO JSON而是要求每张图像配一个同名 txt 文件每行是“class_id cx cy w h”坐标归一化到 0~1。所以分发数据集用 COCO JSON训练时再转一次是这套方案里最常见的做法。转换脚本本身不复杂但归一化细节决定了模型能不能正常收敛import json import os from pathlib import Path def coco_to_yolo(coco_json_path, image_dir, label_dir): with open(coco_json_path, r, encodingutf-8) as f: coco json.load(f) img_id_to_name {img[id]: img[file_name] for img in coco[images]} img_id_to_size {img[id]: (img[width], img[height]) for img in coco[images]} annotations {} for ann in coco[annotations]: annotations.setdefault(ann[image_id], []).append(ann) for img_id, anns in annotations.items(): file_name img_id_to_name[img_id] w_img, h_img img_id_to_size[img_id] label_name Path(file_name).with_suffix(.txt) lines [] for ann in anns: cat_id ann[category_id] x, y, w, h ann[bbox] cx (x w / 2) / w_img cy (y h / 2) / h_img nw w / w_img nh h / h_img lines.append(f{cat_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n) with open(os.path.join(label_dir, label_name), w) as f: f.writelines(lines) if __name__ __main__: coco_to_yolo( coco_json_path./dataset/annotations/train.json, image_dir./dataset/images/train, label_dir./dataset/labels/train )逻辑说明脚本把 COCO 的 bbox 从左上角坐标转成中心点坐标再除以图像宽高完成归一化。关键是 category_id 的处理YOLO 的 class id 从 0 开始而 COCO 的 category id 从 1 开始。上面代码里没有做减 1 操作实际使用时如果 COCO 的类别是 1 和 2训练前要在读入 categories 后建一个映射比如 new_id old_id - 1。我建议在脚本里先打印一次 categories 数组确认编号规则后再决定是否偏移不要盲目减 1。参数说明label txt 文件名必须和图像文件名一致只是扩展名从 .jpg 换成 .txt。YOLO 训练时由 data.yaml 指定图像路径框架自动在同级 labels 目录找同名 txt。训练前强烈建议随机挑几张图把 txt 里的坐标画回去对比原图确认转换没有把框画偏。这一步能过滤掉因为归一化顺序写错导致的整批数据异常这类错误训练时不会报错但结果完全不能用。4.2 data.yaml 与训练配置YOLOv8 的最小可用参数训练自定义 COCO 导出的数据data.yaml 只需要四块内容train 路径、val 路径、nc 和 names。示例train: ./dataset/images/train val: ./dataset/images/val nc: 2 names: [iron_bar, trash]val 必须和 train 完全分开不能同一批图。如果数据量小比如 500 张以内建议按视频来源划分同一段视频的帧全部进 train 或全部进 val而不是随机打散否则相邻帧会造成信息泄漏指标虚高。训练命令用yolo detect train data./dataset/data.yaml modelyolov8s.pt epochs100 imgsz640 batch16 lr00.01参数推荐值说明epochs100~200异物样本少100 轮基本收敛看验证损失不再下降就停imgsz640 或 1280目标小用 1280但训练时间约翻 3 倍batch16~32显存 16G 用 16更大显卡往上加lr00.01预训练微调从头训练用 0.001 更稳yolov8s.pt 是 COCO 预训练权重虽然预训练类别里没有铁棍和垃圾但能提供基础特征提取能力迁移学习后收敛更快。传送带场景背景相对简单用 s 或 n 就够如果背景杂乱再换 m 或 l。训练过程中主要看 val 的 mAP50 和 mAP50-95。mAP50 是判断“有没有检测到异物”的主要指标mAP50-95 反映框的贴合程度。标注框画得粗的话mAP50-95 不会太高不用强求。4.3 训练后反查用验证集可视化确认数据集质量训练完不是看 loss 低就完事关键是把验证集的预测结果画回原图人工检查 false negative 和 false positive。常见做法是把 yolo predict 的输出目录翻一遍统计哪些帧出现漏检。这里漏检的原因经常不是模型而是标注比如铁棍在图上占 8x80 像素标注框却只框住一小段训练时模型学的是局部特征推断时看到完整铁棍反而不敢判。我用下面这条命令快速出一批可视化结果yolo detect predict modelruns/detect/train/weights/best.pt \ source./dataset/images/val \ save_txtTrue save_confTrue project./runs/badcasesave_txt 会输出每个框的类别和置信度save_conf 把置信度写进文件名。人工翻一遍结果重点看两类置信度很低但确实是异物的图以及置信度很高但其实是背景块的图。前者说明训练样本里该形态太少需要补采后者说明背景误检需要加负样本。这一步做完再回头改数据集比反复调学习率有效得多。5. 异物检测数据集项目中的高频坑现象、原因与解法5.1 三个把训练直接打断的 JSON 格式坑坑一category_id 从 0 开始导致 mAP 全 0。现象训练不报错日志里 loss 正常下降但验证集 mAP 一直为 0预测结果全是空。原因COCO JSON 里 category id 写了 0框架把 id0 当背景所有目标都被当成背景处理。解决把 categories 的 id 从 1 开始编号转换时打印一次 json.dumps(categories) 确认。这个坑我把 labelme 转出来的数据直接喂给框架时踩过反复调了一天参数才定位到是 id 的问题。坑二segmentation 字段为 null 导致后处理报错。现象有的标注工具输出 polygons 为空时写 nullCOCO 检测评估时读到 segmentation 直接抛出 TypeError。原因COCO 规范里 segmentation 必须是 list可以为空但不能是 None。解决转换脚本里统一兜底segmentation 取原值如果是 None 就替换成空列表。检测任务用不到分割信息但这个字段规范必须满足否则评估阶段的代码会崩。坑三bbox 坐标和图片分辨率不匹配。现象训练中途出现 InvalidBoxError提示 bbox 坐标超过图片边界。原因标注时基于放大预览画面的坐标直接写入或者抽帧后图像被 cv2 改变了尺寸但标注还是按原始视频尺寸做的两个尺寸不一致框自然偏掉。解决在转格式脚本里加保护x_min 取 max(0, x_min)x_max 取 min(w, x_max)并且打印越界次数。根子还是在抽帧脚本里保存帧时不要隐式改尺寸保持和标注时一致的图像分辨率。5.2 两个让指标虚高的数据划分坑坑四训练集和验证集来自同一段视频的相邻帧。现象训练 mAP 95% 以上部署到产线完全不达标。原因传送带视频帧高度相似按文件名随机划分时相邻帧可能一个在 train 一个在 val模型相当于直接背答案。解决按视频片段划分而不是按帧划分一段视频的帧全部进 train 或全部进 val更严格一点按采集日期分不同天的数据互相验证。划分完成后用脚本检查一遍 train 和 val 的文件名前缀确认没有同源帧泄漏。坑五负样本缺失导致铁棍漏检。现象正样本标注得很准但实际产线频繁漏检铁棍漏检的都是比较大的目标。原因数据集里几乎全是“有异物的图”没有刻意加“没有异物的图”模型被训练成看到任何感兴趣区域都报警或者反过来因为背景太干净而不敢输出。解决把 20%~30% 的空物流图像放进训练集这些图只在 images 数组里占位不产生标注。如果框架训练时跳过无标注图就单独用一个空图文件夹在预处理阶段混入。这个比例在传送带项目里基本是硬指标我一般按正负样本 7:3 来凑。另外还有一个隐蔽的错觉类别不平衡时看总 mAP 会骗人。铁棍 2000 个、垃圾 200 个mAP 可能 0.9但垃圾的漏检很惨。解法是按类别分别打印 APYOLOv8 训练结束后的结果文件里有 per-class AP铁棍 AP 和垃圾 AP 分开看别只看汇总值。6. 给数据集做一次“后悔药”体检用可视化脚本校验 COCO JSON 与图像一致性6.1 一键画出所有标注框并输出异常警告无论数据集是买来的、同事给的还是自己刚转出来的第一件事永远是把标注画回原图人工过一遍。我用一个脚本把所有图按 JSON 画框同时检查文件是否存在、坐标是否越界import json import cv2 from pathlib import Path def inspect_coco(coco_json, image_dir, out_dir): with open(coco_json, r, encodingutf-8) as f: coco json.load(f) cat_name {c[id]: c[name] for c in coco[categories]} id_to_img {img[id]: img for img in coco[images]} anns_by_img {} for ann in coco[annotations]: anns_by_img.setdefault(ann[image_id], []).append(ann) for img_id, img in id_to_img.items(): img_path Path(image_dir) / img[file_name] if not img_path.exists(): print(f[WARN] missing image: {img_path}) continue im cv2.imread(str(img_path)) h, w im.shape[:2] for ann in anns_by_img.get(img_id, []): x, y, bw, bh [int(v) for v in ann[bbox]] if x 0 or y 0 or x bw w or y bh h: print(f[WARN] bbox out of range: img{img_id} ann{ann[id]}) continue cv2.rectangle(im, (x, y), (x bw, y bh), (0, 0, 255), 2) cv2.putText(im, cat_name.get(ann[category_id], ?), (x, max(0, y - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) Path(out_dir).mkdir(parentsTrue, exist_okTrue) cv2.imwrite(str(Path(out_dir) / img[file_name]), im) if __name__ __main__: inspect_coco(./dataset/annotations/val.json, ./dataset/images/val, ./inspection)逻辑说明脚本按 image_id 把标注聚到对应图下画红框并写类别名。它同时在两个关键点报警图不存在、框越界。漏检类问题只能靠人眼看图发现比如框没框住目标、框把相邻目标一起圈进去这类错误模型学不会因为标注本身就是错的。标注质量问题在训练曲线上很难直接看出来所以这个可视化步骤是数据集交付前的必需环节。参数说明边框颜色、字体大小按需调整如果一张图上异物很多建议把每张图的标注数量也打印出来数量异常偏高往往是漏标或重复框。这个脚本我会在每次清洗完数据集后跑一遍生成的 inspection 目录不看完不开始训练。后面训练出来的 bad case我也会回到这个可视化流程里做对比——标注错和模型笨在图上是一眼就能分开的。做数据集这件事大半功夫不在采集和标框而在这种反复校验。我的习惯是把检查脚本放在项目根目录的 tools/ 下每次换数据源就跑一次就当给标注出错上道后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表