
简介面向矿山安全监控与工业视觉场景的COCO格式标注数据集覆盖安全帽、指示器、人员、自救器四类目标可用于目标检测、实例分割模型训练与算法验证实测识别率达96.3%。包体共2000个文件包含1997张矿井现场JPG图像与3个JSON标注文件压缩包大小约294.93MBJSON为标准COCO格式包含类别信息、边界框与分割标注可直接对接YOLO、MMDetection等常用框架。目前已有492人学习下载。数据集以真实井下环境图像为主兼顾不同光照、角度和遮挡条件既可作为算法落地的训练样本也可用于模型鲁棒性测试3个JSON文件已按典型划分组织便于快速划分训练/验证集配套博客对标注细节和文件组织进行了说明能帮助使用者高效开展安全帽佩戴检测、人员定位及救援设备监测等任务。1. 矿井下带 COCO JSON 标注的数据集为什么安全帽检测要拿它起步做矿山安全行为识别第一道坎永远是数据井下低照度、粉尘多、设备遮挡密通用场景训出来的安全帽模型下井就翻车。这套以 COCO JSON 格式带标注发布的矿井下数据集覆盖安全帽、指示器、人、自救器四类目标报告识别率 96.3%。对正在做矿山智能巡检、安全帽佩戴检测、井下人员管理的算法工程师和研究者来说它解决的是两件事一是拿一套真实井下分布的数据做基线二是先跑通从 JSON 标注到训练验证的完整流程省掉从截帧清洗到人工标注的原始积累。下面按我实际做这个方向的顺序把格式、转换、训练和踩坑一次讲透。2. 拆解 COCO JSON 标注五个顶层字段与一段类别统计代码2.1 COCO 标注的五个顶层字段井下数据集里怎么存COCO JSON 不是一种固定 schema而是一个结构约定。绝大多数目标检测数据集只要挂在 COCO 名下顶层就会包含五个字段info、licenses、images、annotations、categories。矿井这份数据集也不例外只是把categories换成了安全帽、人、自救器和指示器这四类。info里一般写数据集的描述、版本和 contributor拿到数据集第一件事就是看这里确认是不是你要的井下场景别等训练完才发现混了地面矿区的帧。licenses声明的授权方式决定你能不能拿它做商用项目如果是内部标注没公开授权落地前要找数据提供方补手续。images是图像表每一条记录id、file_name、width、height后面做转换脚本全靠它做图片尺寸映射。annotations是核心的框表每个目标一条记录image_id指向图像category_id指向类别bbox是[x, y, width, height]四元组左上角坐标加宽高area在过滤小目标时有用iscrowd标记重叠目标。categories是类别表和supercategory的映射。打开这种 JSON 不要用记事本几十兆的文件记事本直接卡死VSCode 可以预览但真要统计或者清洗老老实实写 Python。习惯用命令行的jq .categories instances_mining.json也能快速看类别结构。记住一个原则COCO JSON 是给程序读的不是给人读的。2.2 读 COCO JSON类别统计脚本与四个可调点假设解压后的目录结构长这样annotations/instances_mining.json是标注文件images/下是全部井下监控帧。我先跑一段最基础的统计脚本确认四类目标的数量分布和有无空图。import json from collections import Counter from pathlib import Path coco_path Path(annotations/instances_mining.json) with open(coco_path, encodingutf-8) as f: coco json.load(f) # categories 的 id 在不同工具里可能从 0 或 1 开始先打印出来看 cat_map {c[id]: c[name] for c in coco[categories]} print(类别映射:, cat_map) # 统计每个类别的标注框数量 ann_counter Counter() per_image Counter() for ann in coco[annotations]: ann_counter[cat_map[ann[category_id]]] 1 per_image[ann[image_id]] 1 for name, count in ann_counter.most_common(): print(f{name}: {count} 个框) # 找出没有标注的图片 img_ids {img[id] for img in coco[images]} ann_img_ids {ann[image_id] for ann in coco[annotations]} empty_imgs img_ids - ann_img_ids print(无标注图片数:, len(empty_imgs))这段脚本的逻辑不复杂先建立category_id到类别名的映射再遍历所有标注按类别计数同时统计每张图的目标数量。参数上要注意两点。第一encodingutf-8必须带Windows 下默认编码可能不是 UTF-8json 里如果带中文类别名不加这个参数直接报错。第二Counter统计完以后重点看两个数类别分布是否极端不平衡、空图数量是否过大。安全帽和人在井下监控里通常每个视频都有但自救器和指示器可能只在特定区域出现数量会少一个量级这是后面训练类别不平衡的根源。顺带看一眼per_image的平均目标密度。我遇到过一份井下数据集平均每帧只有 1.2 个目标这种稀疏数据训练出来的模型泛化能力很差因为模型没见过密集遮挡场景。如果统计出来平均目标数小于 2后续要考虑把多帧拼接成一张图再训练或者干脆补标注。2.3 标注质量自检bbox 越界、面积为 0、类别 ID 从 0 开始标注工具导出的 COCO JSON 经常带着三个隐蔽问题bbox 越界、面积字段和实际框不匹配、类别 ID 不从 1 开始。先解释类别 IDCOCO 官方规定id从 1 开始但很多标注工具导出时从 0 开始。你的框架如果按 0 处理没问题但有些老版本解析器默认从 1 开始就会出现安全帽这一类整个消失的诡异现象。拿到数据集先打印categories就为了确认这件事。bbox 越界更常见。井下监控画面里人刚从画面边缘走进来时标注员经常把框画到图像外面于是 bbox 出现负坐标或者超出宽高。训练时归一化到 0~1 之后负坐标变成负数loss 直接 NaN。最稳妥的做法是转换时做一次越界检查bad_boxes 0 for ann in coco[annotations]: x, y, w, h ann[bbox] img next(i for i in coco[images] if i[id] ann[image_id]) if x 0 or y 0 or w 0 or h 0: bad_boxes 1 continue if x w img[width] or y h img[height]: bad_boxes 1 print(越界或非法框数量:, bad_boxes)逻辑是按image_id找到对应图像尺寸然后逐项判断 bbox 是否合法。这个检查和面积无关area字段经常不准尤其是标注工具自动计算和人工调整框之后不重算的情况。筛选小目标时直接用w * h重新算面积不要信 JSON 里的area。3. 把 COCO JSON 转成 YOLO 可训练格式转换脚本与划分策略3.1 为什么检测训练器更喜欢 YOLO txt 而不是直接用 COCOultralytics 这类框架能直接吃 COCO JSON但内部还是会先把它拆成每张图一个 label txt 再进 dataloader。既然早晚要拆我一般选择在转换阶段自己拆而不是依赖框架内置的解析器。原因有三个第一内置解析器不支持类别重映射如果标注文件的category_id不连续转出来类别就乱了第二转换时可以做框过滤和越界处理框架内置的逻辑不会帮你检查这些第三拆出来的 txt 标签是纯文本换框架、做数据增强、人工抽检都方便不用每次都被 COCO 解析器版本卡住。YOLO 的 txt 格式每行对应一个目标字段是class x_center y_center width height全部归一化到 0~1 之间。归一化坐标系是图像宽高比例不是像素坐标很多新手在这里翻车把像素坐标直接写进 txt训练时模型永远收敛不了。记住一句口诀COCO 是像素绝对坐标YOLO 是归一化相对坐标。3.2 COCO JSON 转 YOLO txt转换脚本与归一化边界下面这段脚本是我常用的转换逻辑可以直接复制改路径用。import json from pathlib import Path def coco_to_yolo(coco_path, out_dir): with open(coco_path, encodingutf-8) as f: coco json.load(f) # 按 categories 出现顺序建映射保证类别 ID 从 0 开始且连续 cat_ids sorted({c[id] for c in coco[categories]}) cat_map {cid: i for i, cid in enumerate(cat_ids)} print(重映射后的类别:, {i: c[name] for c in coco[categories] for i in [cat_map[c[id]]]}) out_dir Path(out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) for img in coco[images]: w, h img[width], img[height] anns [a for a in coco[annotations] if a[image_id] img[id]] if not anns: continue # 空图不生成 txt后续训练会跳过 txt_path out_dir / (Path(img[file_name]).stem .txt) lines [] for a in anns: cls_id cat_map[a[category_id]] x, y, bw, bh a[bbox] # 像素坐标转归一化坐标 cx, cy (x bw / 2) / w, (y bh / 2) / h nw, nh bw / w, bh / h # 越界裁剪到 [0, 1]防止 loss 异常 cx, cy max(0, min(1, cx)), max(0, min(1, cy)) nw, nh max(0, min(1, nw)), max(0, min(1, nh)) if nw 1e-6 or nh 1e-6: continue lines.append(f{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) if lines: txt_path.write_text(\n.join(lines), encodingutf-8) print(f处理完成共 {len(coco[images])} 张图)参数说明cat_map用排序后的category_id重新编号是为了让类别 ID 从 0 开始连续递增data.yaml里的names列表要按这个顺序写。cx和cy是中心点坐标由x bw / 2算出/w和/h是归一化。最后一段max(0, min(1, ...))做边界裁剪目的就是处理 2.3 里提到的越界框。裁剪而不是丢弃是因为多数越界框只是边缘目标的一部分丢掉会损失标注信息。执行完检查一下输出目录抽几张图看看 txt 里的坐标是否大致落在对应目标上。这一步不要省我就见过转换脚本把x bw / 2写成x - bw / 2的所有框偏移了整整一个框宽训练出来 mAP 直接掉 20 个点。3.3 训练集/验证集划分按视频源分组而不是随机打散划分数据集是决定模型真实水平的关键步骤也是最容易被忽视的一步。井下视频是连续帧同一场景的相邻帧非常相似。如果随机划分训练集和验证集会混入同一个视频的相似帧验证集 mAP 虚高部署时换个机位立刻现原形。正确做法是按视频源分组。假设文件名形如cam01_000123.jpgcam01是摄像头编号000123是帧号那按前缀划分就能保证同一摄像头的帧全部进同一个集合。import random from pathlib import Path random.seed(42) frames list(Path(images).glob(*.jpg)) video_groups {} for p in frames: video_id p.stem.split(_)[0] video_groups.setdefault(video_id, []).append(p) video_ids list(video_groups.keys()) random.shuffle(video_ids) split_idx int(len(video_ids) * 0.8) train_videos set(video_ids[:split_idx]) val_videos set(video_ids[split_idx:]) train_txt Path(train.txt) val_txt Path(val.txt) with train_txt.open(w, encodingutf-8) as ft, val_txt.open(w, encodingutf-8) as fv: for vid, paths in video_groups.items(): target ft if vid in train_videos else fv for p in paths: target.write(str(p.resolve()) \n)逻辑是把视频 ID 作为分组键先对视频列表做 shuffle再按 8:2 切分最后把所有帧路径写入对应的 txt。参数上random.seed(42)保证可复现换 seed 划分结果会变。如果数据集本身是按井下不同巷道采集的建议在视频 ID 之外再把巷道 ID 也作为分组依据否则不同巷道的光照条件差异会直接泄漏到验证集里。3.4 面向矿井场景的增强参数低照度、粉尘与贴脸遮挡井下数据集的典型问题是光照分布不均匀有的摄像头对着采掘面亮度尚可有的对着巷道深处基本是暗光。模型要在这种环境下稳定工作数据增强不能照搬 COCO 预训练那套。我通常用 ultralytics 训练时这样调整增强参数关闭mosaic到 0.5而不是用默认的 1.0。井下摄像头安装高度固定安全帽这类小目标的尺度变化没有自然场景那么剧烈过强的 mosaic 会把目标切成碎片反而让模型学到残缺目标。degrees调到 5因为摄像头几乎不旋转旋转增强幅度太大产生的样本不符合真实分布。hsv_h设 0.02、hsv_s设 0.4、hsv_v设 0.2给模型一点颜色扰动空间应对不同巷道的色温差异但又不能太大否则暗光下安全帽的黄色会被扭曲成别的颜色模型把黄色当核心特征就麻烦了。需要说明的是mosaic这类增强开关是在训练配置里传的不同版本的 ultralytics 参数名略有差异但hsv_h、hsv_s、mosaic、degrees这些是通用参数改数值即可。4. 训练到 96.3% 的配置参数从 YOLOv8 到矿井场景调优4.1 先确认指标口径96.3% 是 mAP0.5 还是准确率目标检测领域说的识别率是个模糊说法落到指标上有三个常见口径分类准确率、mAP0.5、mAP0.5:0.95。报告里的 96.3%大概率是mAP0.5也就是 IoU 阈值取 0.5 时所有类别的平均精度。这个指标对定位精度要求不苛刻框稍微偏一点也能判对所以看起来数值很高。自己复现时不要拿验证集里挑几张图数对错要跑完整的验证流程。用 ultralytics 执行yolo detect val modelruns/detect/train/weights/best.pt datamining.yaml命令行会输出mAP50(B)和mAP50-95(B)两个关键值。mAP50对齐报告的 96.3%mAP50-95是 IoU 从 0.5 到 0.95 步进 0.05 的平均这个数通常在 0.6~0.75 之间。如果mAP50能到 0.96 而mAP50-95只有 0.6说明框的位置还行但精度不足部署时如果业务方要求框贴合目标需要调高 IoU 阈值或者换更大模型。验证集本身也要干净如果验证集里混了没有标注的帧或者标注框质量差mAP50会失真。这也是为什么前面强调空图过滤和越界检查要在转换时一并处理。4.2 GPU 训练配置YOLOv8 在矿井数据上的训练参数井下数据集规模通常不会太大几百到两三千张图是常态。模型方面yolov8s 起步就够用数据量少于一千张时用 yolov8m 反而容易过拟合。输入尺寸建议直接上 1280不要用默认的 640。井下监控普遍是 1080p一个戴帽人头在画面里大约 40×50 像素缩到 640 就只有 20×25已经进入小目标范畴检测难度陡增。yolo detect train \ datamining.yaml \ modelyolov8s.pt \ epochs100 \ imgsz1280 \ batch16 \ lr00.005 \ mosaic0.5 \ degrees5 \ hsv_h0.02 \ hsv_s0.4 \ hsv_v0.2 \ patience20mining.yaml的内容是数据路径和类别名注意类别顺序必须和转换脚本里的cat_map重映射结果一致path: /path/to/mining_dataset train: images/train val: images/val names: 0: helmet 1: person 2: self_rescuer 3: indicator参数说明batch16是在 24GB 显存下的常见选择显存小就降到 8但不要低于 4否则 BatchNorm 统计不稳定井下暗光帧尤其明显。lr00.005配合默认的 warmup 3 epochs预训练权重不容易被冲坏。patience20表示 20 个 epoch 验证集 mAP 不涨就早停井下数据噪声大早停阈值比通用场景放宽一点避免在第 30 个 epoch 因为指标波动被误停。4.3 类别不平衡与置信度阈值小目标指示器的处理四类目标里安全帽和人的数量通常最多自救器和指示器少。类别不平衡直接表现为两个类 AP 很高另外两个 AP 很低。先看results.csv里每个类别的 AP如果indicator的 AP 明显低于其他类需要针对性处理。一个常见做法是给少数类做复制粘贴增强从训练集中把指示器目标连同背景 patch 裁下来随机粘贴到其他图上。这种增强对静止安装在设备上的指示器有效因为它在真实场景里的姿态变化本来就小。另一招是提升这类目标的置信度阈值权重让模型对指示器的召回更激进。推理时通过conf参数控制yolo detect predict modelbest.pt sourceval_images conf0.25 iou0.5 imgsz1280conf0.25是全局阈值如果指示器类别漏检严重可以把阈值降到 0.2代价是安全帽类会多出一些误检。更精细的做法是分两类跑推理安全帽和人的conf用 0.3指示器单独跑一次conf0.15再把结果合并。虽然麻烦一点但四类目标的语义差异太大一个全局阈值很难同时满足小目标和常规目标。5. 矿井数据训练避坑四条从标注到收敛的血泪经验5.1 现象json 解析正常训练完 helmet 类 AP 为 0原因标注文件里的category_id从 0 开始编号训练框架内部解析时把它当成无效类别直接过滤。矿井数据集来源复杂不同标注工具导出的 COCO 格式在类别起始编号上不统一。解决训练前先在转换脚本里打印categories的完整映射确认id起始值。转换到 YOLO 格式时用sorted重新编号让类别 ID 从 0 开始连续排布同时在data.yaml里按同样顺序写names。我碰到过最隐蔽的情况是categories列表里的id是[0, 2, 5, 8]直接映射到 YOLO 后类别变成[0, 1, 2, 3]中间的类别名全错位了所以重映射那一步不能省。5.2 现象训练时 loss 出现 NaN或验证时检测框整体偏移原因标注框里有负坐标或者超宽超高的越界框转换时没有做归一化裁剪。COCO 格式里 bbox 是像素坐标负值在除以图像宽高后会变成负数损失函数计算时直接异常。解决在转换脚本的归一化之后显式做max(0, min(1, ...))裁剪。注意不要只裁剪中心坐标宽度和高度同样要裁剪。另外检查有没有w或h为 0 的空框这类框在标注工具里通常是被误拖拽产生的直接丢弃。我在 2.3 里的越界检查脚本就是为这个问题准备的每次拿到新数据集先跑一遍再转换。5.3 现象验证集 mAP 很高但部署到新摄像头上漏检严重原因划分数据集时随机打散同一视频的相似帧同时出现在训练集和验证集里验证结果虚高。井下视频帧间差异小随机划分的数据泄漏在数值上可以轻松拉高 5 个点的 mAP。解决按视频源分组划分训练集和验证集不共享任何摄像头或巷道的数据。更进一步把不同光照条件的巷道编号作为分组键下的二级分类确保验证集覆盖暗光、逆光、粉尘多种场景而不是只有和训练集相近的亮度分布。部署前用一段全新视频做测试这段视频的帧绝不能进过训练集或验证集。5.4 现象指示器类别 AP 只有 0.2其余三类都在 0.95 以上原因指示器目标体积极小只占画面的 0.1%~0.5%且数量远少于安全帽训练时对 loss 的贡献被淹没。更麻烦的是它经常和设备边缘重叠标注框画得参差不齐。解决不要只调置信度阈值先从数据下手。把指示器的训练样本做复制粘贴增强数量翻到超过安全帽的 1/3 再训练。如果增强后 AP 还是上不去检查标注框是否包含太多背景——指示器本身可能是圆形或方形指示灯标注员用矩形框标注时习惯留一圈余量导致框内目标占比过低。用标注的segmentation掩码重新生成紧致外接框比手工调整快得多。6. 用混淆矩阵定位漏洞硬帧回归清单比总 mAP 更可靠6.1 先读 confusion_matrix.png 再看 mAP模型训练完很多人只看 mAP 数字就收工了。mAP 是四类目标的平均值掩盖了类别间差异。训练目录下会生成confusion_matrix.png这个图比 mAP 有用得多。重点看两个位置一是主对角线上的颜色深度二是安全帽行里哪一列有亮色块。如果安全帽大量被预测成 person说明模型学到的是头顶有人形轮廓这个特征而不是安全帽本身的颜色和形态暗光下大概率翻车。井下数据的混淆矩阵还有一个特征self_rescuer 和 person 容易互相串。自救器通常挂在人腰侧框比较小模型经常把它当做人身上的一个无关 patch。这类误检靠调 NMS 很难解决正确的做法是回到数据层面补一些自救器在腰部、背在身上的不同角度样本。6.2 一组固定暗光帧的回归脚本跑完主实验以后我会固定一组难帧做回归从全量数据里挑 10 帧覆盖暗光、逆光、粉尘、密集遮挡每帧必须是模型曾经漏检或误检过的。这组帧不进训练集每次调参都跑一遍看改进方向对不对。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) hard_frames [ hard/dark_01.jpg, hard/overlap_02.jpg, hard/dust_03.jpg, hard/backlight_04.jpg, ] for p in hard_frames: res model.predict(p, conf0.25, iou0.5, imgsz1280) for box in res[0].boxes: cls_id int(box.cls[0]) print(f{p}: {model.names[cls_id]} {box.conf[0]:.2f})这段代码只做一件事逐帧预测把类别名和置信度打出来。参数上conf0.25是主实验的固定阈值跑回归时不要随意改改了结果没法对比。判断标准很简单——上次漏检的这帧这次检出没有上次误检的这次还在不在。如果总 mAP 涨了但硬帧回归变差了说明模型是在用整体分布的提升掩盖局部场景的退化这轮调参就是失败的。我现在做井下检测项目已经养成先跑硬帧回归再看 mAP 的习惯。96.3% 这个数字可以作为项目起步的基线但要拿到现场真正稳定可用靠的还是反复在一个固定难帧集上去验证和打磨。这个框架搭好后面无论是换模型、调增强还是补数据每一步都有据可依。希望帮到你。本文还有配套的精品资源点击获取