矿井里装摄像头容易,让摄像头真正“看懂”矿工在干什么,难。之前做过一个井下人员行为识别相关的项目,用的就是一套8类别、4847张图的矿井行为数据集,格式是VOC+ YOLO双份,覆盖爬、走、靠、站、跌倒这些基础姿态。这篇文章就把这套数据的来龙去脉、格式转换、标注规范、训练调优和踩坑记录完整拆一遍,给后面要做煤矿行为检测的同行留个参考。
先把这个东西是什么说清楚。所谓的8类别,除了标题里点明的爬、走、靠、站、跌倒,通常还包含躺卧、蹲伏、挥手求助这3类。前5个是姿态识别的基础类别,后3个是异常状态和主动求救信号。4847张图全部是井下监控视角的实拍和仿真合成图,标注格式同时给了Pascal VOC的XML和YOLO的TXT,这意味着你拿过来可以直接喂给YOLOv5、YOLOv8、YOLOv9甚至RT-DETR,也可以反向转成COCO格式做Mask R-CNN、SlowFast这类模型。
合适的人看这篇东西会很有收获:正在做智慧矿山、井下安监算法选型的算法工程师,准备自己采集标注数据搞行为识别的团队,还有刚入门目标检测、想搞清楚VOC和YOLO格式到底怎么互转的学习者。对纯业务侧的管理人员,这篇文章也能帮你理解为什么算法在井下行不通,问题往往不在模型,而在数据和场景本身。
1. 项目整体设计与需求拆解
1.1 为什么矿井场景需要行为状态检测
井下的安全管理和地面完全两个逻辑。地面工厂有固定的工位、确定的动线,摄像头角度相对规整。井下巷道是狭长空间,采煤工作面设备密集,粉尘大、照度低,人员穿着矿工服、戴着安全帽,有时候还在电缆支架旁边弯腰作业。传统的人工盯监控根本不现实,一个矿的摄像头少说几十路,多的几百路,靠人眼根本看不过来。
行为检测要解决的核心问题就三个:一是人员是否处于异常姿态,比如跌倒、躺卧,这是伤亡事故的直接信号;二是人员是否在做危险动作,比如在非作业区域攀爬、跨越;三是人员是否在正确的位置以正确的姿态工作,比如该站的站了、该蹲的蹲了,有没有长时间倚靠设备摸鱼——这个后者其实是管理需求,但在算法层面和安全性是同一套特征。
从技术选型来说,这类项目优先考虑2D目标检测加姿态分类,而不是3D骨骼点估计。原因很直接:井下摄像头基本都是单目,没有深度信息,部署端算力也有限,3D方案成本高且不稳定。2D检测框架成熟,YOLO系列在边缘设备上跑得动,标注成本也可控。这套数据集就是基于这一思路设计的,每个目标给一个边界框和一个行为类别标签,训练出来的模型可以直接对接现有的矿下视频流。
1.2 8个类别构成与标注逻辑
8个类别不是随便拍的脑袋,每一类都要回答“这个姿态发生的典型场景是什么”和“模型应该把注意力放在哪里”两个问题。
- 站立:最常见类别,也是其他短时动作的过渡态。标注时框住整个人体,头和脚都要完整包含。
- 行走:侧向行走、正向行走都算,在巷道里占绝大多数。难点是矿工服颜色和巷道背景接近时,模型容易跟丢。
- 爬行:包括地面爬行和攀爬,后者比如爬电缆沟、爬支架。这个类别对安全检测很关键,出现频率低但风险极高。
- 倚靠:身体斜靠在巷道壁、设备、支架上,重心偏移。矿工休息时的典型姿态,需要和站立严格区分。
- 跌倒:身体从直立状态快速倒地,最终姿态是躺卧或侧卧。这是安全告警最核心的类别,误报率一定要压到最低。
- 躺卧:跌倒后的持续状态,也可能是晕倒、睡觉。模型层面容易和蹲伏混淆,标注时要注意人体长轴方向。
- 蹲伏:下蹲、弯腰,检修设备时常见。躯干压缩,头和膝盖的高度差缩小,跟跌倒的中间态很像。
- 挥手求助:这个类别是给紧急呼救用的,人员高举手臂挥动。井下噪声大、照明差,语音呼救不可靠,视觉求救信号是很好的补充。
标注时有一条硬性规则:一个目标只有一个行为标签,多目标重叠时,小目标不被大目标完全遮盖就可以独立标注。如果两个类别同时成立,比如人跌倒后还在挥手,统一归为挥手求助,因为这是更紧急的信号。
2. VOC与YOLO双格式的底层实现
2.1 VOC格式的XML结构与标注细节
VOC格式其实就是每张图片对应一个XML文件,文件名和图片名保持一致,内容记录图片尺寸、来源和每个目标的类别、边界框坐标。
<annotation> <folder>mine_behavior</folder> <filename>img_1042.jpg</filename> <path>/data/mine_behavior/images/img_1042.jpg</path> <source> <database>mine_behavior_dataset</database> </source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>fall_down</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>823</xmin> <ymin>641</ymin> <xmax>1219</xmax> <ymax>1044</ymax> </bndbox> </object> </annotation>XML里的truncated字段值得多说一句。如果目标在画面边缘被截断,这个字段要标1。为什么重要?因为训练时如果大量目标都是被截断的,模型会学到“人只出现在画面中间”这种隐性偏见。但反过来,如果完全剔除截断目标,测试时边缘出现的人就检测不到。正确做法是保留大部分截断目标,把truncated置1,在训练时通过mosaic增强让模型见过各种位置的目标。
2.2 YOLO格式转换与坐标归一化
YOLO格式的标注存储在TXT文件里,每行一个目标,格式是:
类别id center_x center_y width height注意这里的坐标全部是相对于图片宽高的归一化值,范围0到1。转换公式很简单:
import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, class_names): tree = ET.parse(xml_file) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) # 边界检查,防止越界坐标 xmin = max(0, min(xmin, img_w - 1)) xmax = max(0, min(xmax, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) ymax = max(0, min(ymax, img_h - 1)) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") return lines这段代码里最容易踩坑的是边界框越界。人工标注时,框的坐标偶尔会超出图片范围几个像素,比如xmax标到1921,而图片宽度是1920。直接用这个值去归一化,轻则训练时报错,重则导致loss变成NaN。所以转换脚本里一定要加边界检查,超出部分clip回图片有效范围。
还要注意类别id的映射顺序。VOC的XML里存的是类别名,YOLO的TXT里存的是数字id。这个id不是随便定的,要跟训练配置里的data.yaml保持一致。比如:
names: 0: standing 1: walking 2: climbing 3: leaning 4: fall_down 5: lying 6: squatting 7: waving_help转换脚本里的class_names列表顺序必须和这个yaml完全一致,否则训练出来的模型预测结果会张冠李戴。这个错误特别隐蔽,因为训练不会报错,loss也正常下降,但测试时发现检测框是对的、类别全乱了。
3. 数据采集、清洗与标注实操
3.1 井下实拍数据的采集方案
4847张图听着不多,但井下数据采集的成本远比地面高。实拍部分要解决三个问题:谁去拍、在哪儿拍、怎么拍出多样性。
我当时对接的是某矿业集团下的一个试点矿井,下井拍摄需要提前申请,穿全套矿工装备,戴矿灯、自救器,跟着安全员走指定的巡检路线。拍摄设备用的是防爆工业相机,固定在支架上模拟实际监控视角。关键经验是:一定要在真实工况下拍,不要刻意清场。让工人正常干活,该走就走,该蹲就蹲,该靠着歇一会儿就歇一会儿。摆拍出来的数据动作太“标准”,模型学出来很理想化,一到真实场景就废。
机位设置也很有讲究。井下摄像头一般架在巷道顶部侧方,俯视角度大概30度到45度之间。有些人做数据采集图省事,相机平视拍,看着爽,但跟实际部署的摄像头角度完全对不上。角度不匹配是模型效果差的一个隐形杀手。我记得有组对照实验,同一套模型,用平视数据训练的,在俯瞰图上mAP直接掉了11个点。
另一个多样性来源是时段。井下照明虽然常年开着,但不同的时段,矿灯光照范围不同,巷道里的粉尘浓度也不同。早班刚开完采煤机的时候,空气里悬浮煤尘多,画面灰蒙蒙的,跟停机检修时完全是两种画质。这些都要拍进数据集里,否则模型遇到粉尘大的场景就失灵。
3.2 合成数据的生成与配比
实拍数据永远不够,尤其是跌倒、爬行这类异常行为,正常生产工况下根本不会发生,或者极少发生。你总不能安排工人天天在地板上摔跤吧,既危险又不现实。所以数据集里必须混入仿真合成数据。
合成数据生成有两类路子:一类是用3D人体模型绑定动作捕捉数据,在Unity或者Unreal里搭建井下巷道场景,渲染出逼真的监控画面;另一类是用图像合成的方式,把动作姿态的人体抠图贴到井下背景图上,做透视变换和光照匹配。后者成本低、速度快,但真实感差一些;前者真实感好,但需要美术资源和动捕数据,周期长。
我这次的实践是偏重3D仿真渲染。井下巷道场景用简单的长方体加管线贴图搭就行,关键是把灯光效果做出来。井下的光源是有数的几个矿灯和巷道照明灯,光照衰减、阴影投射,这些东西越真实,合成数据迁移到实拍数据的效果就越好。
配比上,合成数据占总量的三到四成比较合适。建议每个类别单独控制数量,像跌倒、爬行、挥手求助这3类,合成数据占比可以拉到50%甚至更高,因为实拍压根拍不到。但合成数据不能是清一色的完美姿态,要故意加入一些“错误”样本,比如跌倒的人被架子挡住一半、爬行的人只露出上半身——这些遮挡在真实场景里极其常见,模型提前见过类似情况就不容易漏检。
3.3 标注流程与质检标准
标注工具我用的是X-AnyLabeling,支持自动标注加人工修正的混合模式。先让一个弱模型跑一遍预标注,人工只负责拖拽修正边界框和改类别标签,效率能提高一倍多。但要注意,预标注模型会把同样的错误重复几百张图,所以人工修正阶段不能只盯着框准不准,还要整体扫一遍同类目标有没有系统性错漏。
标注规范的几条细则:
- 穿厚棉服的矿工在井下会显得比实际体型大一圈,边界框要贴合轮廓,不能只框躯干主体,把安全帽露在外面不算完整目标。
- 跌倒的判断标准以躯干与地面夹角为准,大于45度且身体处于失去平衡的状态就算,短暂弯腰不算。
- 几个人站在一起开小组会这种场景,目标密集、互相遮挡,遮挡超过50%的小目标要标注,超过80%的可以不标。
- 所有图片在标注完成后要做两轮交叉质检,A标注、B质检,不一致的地方回到A处修改。
质检完还要跑一个自动化脚本,统计每个类别的标注框数量、平均宽高比、目标面积分布。如果发现哪个类别的中小目标占比过高,说明采集视角里有大量远距离画面,需要补充近镜头数据。目标尺度的多样性直接影响模型在小目标上的表现,这一步不能省。
3.4 数据增强策略的取舍
4847张图对深度学习模型来说是小规模数据集,不增强根本不够用。但增强策略不是越多越好,用得过于激进反而会把模型搞坏。
合理的增强组合:
- Mosaic增强:开启,batch里4张图拼成一张,提高目标密度,增强小目标检测能力。
- 随机透视和仿射变换:开启,但幅度要保守,因为井下摄像头是固定视角的,过大的旋转和形变不符合实际。
- HSV颜色扰动:开启,整体饱和度偏移控制在0.2以内,色相偏转不要超过0.03,井下颜色本来就单调,大幅调色会产生现实中不存在的颜色组合。
- 随机遮挡和Cutout:可以少量用,模拟人员被设备遮挡的情况。
- MixUp:建议关掉或者权重调低,因为两个矿工重叠的合成图在语义上非常诡异,容易干扰类别判断。
需要特别提醒的是亮度扰动。井下图像本身偏暗,如果增强时把亮度调得更暗,模型可能学到“暗处的都不是人”这种错误关联。实际处理时我都做了限制,亮度下界设在0.7倍原始亮度,低于这个值的增强结果直接丢弃。
4. 模型训练与关键参数调优
4.1 模型选型与预训练权重的选择
数据集准备到这个程度,训练模型其实是个偏工程化的活。我试过YOLOv5、YOLOv8和YOLOv9,最后部署选的是YOLOv8s,主要看中它的推理速度和部署生态。
选型逻辑是这样的:井下边缘计算盒子(比如某厂家的AI盒子)算力有限,单张图像推理耗时要求控制在20毫秒以内,太重的模型跑不动;但行为识别又是安全场景,精度不能妥协太多。YOLOv8s在两者之间平衡得比较好,实测下来比YOLOv5s精度高大约2个点,比YOLOv8m速度快30%以上,属于性价比最高的档位。
预训练权重一定要用COCO预训练而不是从零开始训练。COCO上有80类日常物体,其中person类跟我们的目标很接近,相当于模型已经学会提取人的特征了。从这个基础上迁移学习,收敛速度快得多,最终精度也高。千万别图省事从头训,小数据集从头训的效果大概率不如直接用预训练权重微调。
4.2 训练配置与超参数参考
贴一份我在这个数据集上调出来的效果还不错的配置,供参考:
| 参数 | 设置值 | 说明 |
|---|---|---|
| 输入尺寸 | 640x640 | 兼顾小目标召回和推理速度 |
| batch_size | 16 | 单卡显存不够就开梯度累积 |
| epochs | 150 | 数据集小,训练轮数可以多一点 |
| optimizer | SGD, lr=0.01, momentum=0.937 | 比AdamW在YOLO上更稳 |
| lrf | 0.01 | 学习率衰减到初始的1% |
| warmup_epochs | 3.0 | 前3轮预热,防止初期震荡 |
| weight_decay | 0.0005 | L2正则,防止过拟合 |
| mosaic | 1.0 | 全程开启 |
| close_mosaic | 10 | 最后10轮关闭mosaic,让模型适应真实构图 |
| hsv_h | 0.015 | 色相增强幅度 |
| hsv_s | 0.2 | 饱和度增强幅度 |
| hsv_v | 0.3 | 亮度增强幅度 |
最重要的一条经验是close_mosaic。mosaic增强出来的图像包含4张图片的内容,目标分布和真实单图完全不同。如果训练全程都开mosaic,模型从来没适应过正常构图,测试时效果反而差。YOLOv8提供了训练最后N轮自动关闭mosaic的功能,这个不要省,设置成10到15轮比较合适。
类别不平衡的问题也要靠配置解决。爬行、跌倒、挥手求助这几个类别的样本数明显少于站立、行走,训练时会出现大类别压制小类别的情况。解决方法是给loss里各类别加上权重,权重跟样本数量的平方根成反比:
cls_weights = {cls: 1.0 / sqrt(class_count[cls]) for cls in class_ids}样本少的类别权重加大,但不要直接线性反比,平方根反比更平滑,不会让少数类过拟合。
4.3 评估指标的细节解读
训练完之后,不要只看mAP@0.5一个数字,那个不够用。安全场景要看的是每个类别单独的表现,尤其是跌倒和躺卧。
mAP@0.5是IoU阈值0.5下的平均精度,这个指标比较宽松,框的位置差不多就算对。对于行为检测,框的位置精度要求没有普通目标检测那么苛刻,因为判断依据是姿态和位置关系而非精确定位,所以mAP@0.5反而是一个很合适的核心指标。mAP@0.5:0.95是更严格的指标,IoU从0.5加到0.95取平均,主要用于对比不同模型的综合能力,但真实场景不必追求这个指标过高。
另一个容易忽略的指标是各类别的F1值。跌倒检测最怕漏报,漏了就是事故,所以跌倒类别的recall(召回率)要优先保,宁可误报多一点,不能漏。而倚靠和蹲伏这两个类之间经常混淆,如果看混淆矩阵发现这两个互相串,可以在训练时把这两类的分类损失权重加高,或者干脆合并成一个类别“非标准站姿”——有些项目里这样做,看需求了。
5. 部署环节的典型问题与实践总结
5.1 井下环境对推理的干扰
模型训好只是第一步,真正让它在井下稳定工作是另一个战场。以下问题我在项目里全踩过。
镜头起雾和粉尘附着。井下湿气大,镜头玻璃上很快会积一层水汽和煤尘。图像变得模糊,对比度下降,检测率明显掉。这不是算法能解决的,要在部署方案里加定时清洁机制,或者给摄像头加防尘罩。训练时可以给图像加一点高斯模糊做数据增强,让模型对轻微模糊鲁棒一些。
红外补光导致色调偏移。井下低照度区域需要红外补光,红外图像是黑白的,或者严重偏色。这套数据集如果主要是可见光图像,在红外摄像头上效果会打折。有条件的项目应该单独采集一部分红外图像做微调训练,或者在训练时对图像做灰度化增强,模拟红外效果。
俯视角度的目标压缩。摄像头装在巷道顶部俯拍,人在地面上,身体被压缩成比较扁的形状,跟平视图里人的长宽比差别很大。我在标注时特别注意了这一点,俯视图里的框更宽更扁,要让训练模型见过足够多的俯视样本。
5.2 行为类别混淆与场景误报
单一帧检测在行为识别上有个天然缺陷:它只看空间特征,不看时间上下文。一个人蹲下来系鞋带,单帧看起来跟蹲伏一模一样,但正常场景里这就是个无关动作。这时候就要在后处理里加时序逻辑了。
我做的处理方式是基于帧级别的检测结果做一个滑动窗口投票。窗口长度设成30帧,对每个目标的类别置信度做累积平均,只有连续N帧都判定为同一类别才输出最终结果。跌倒检测更特殊,它本质是一个事件而非一个姿态,更好的策略是在判断出跌倒姿势后,再看后续若干帧里目标是否长时间没有移动和站立,如果躺着不动超过一定时间才触发告警。简单说,用时间换置信度,可以有效压掉大部分误报。
这类后处理逻辑需要写一个推理管线,检测模型输出结果进队列,状态机做时序判断。具体实现如下:
class BehaviorStateTracker: def __init__(self, window_size=30, hold_frames=45): self.window = deque(maxlen=window_size) self.fall_hold_count = 0 def update(self, detections): # detections: list of (cls_id, conf, bbox) current_states = {} for cls_id, conf, bbox in detections: label = class_names[cls_id] current_states[label] = current_states.get(label, 0) + 1 self.window.append(current_states) # 统计最近30帧各类别出现频次 vote_result = {} for frame in self.window: for k, v in frame.items(): vote_result[k] = vote_result.get(k, 0) + 1 # 取频次最高的类别作为本轮状态 return max(vote_result.items(), key=lambda x: x[1])[0]这只是最基本的思路,实际生产还要做目标跟踪,用IoU或deep sort把前后帧的同一目标关联起来,不然两个工人互相走过,目标id一换,时序统计就全乱了。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 跌倒类召回率低 | 跌倒样本太少、俯视角度样本不足 | 补合成数据,重点增加遮挡场景;调整类别权重 |
| 站姿和倚靠混淆严重 | 边界框没有覆盖到靠墙重心偏移 | 重新检查这两类的标注标准,必要时加判据 |
| 小目标检测不到 | 输入分辨率低、目标像素少 | 试试1280输入,或使用SAHI切片推理 |
| 误报率居高不下 | 单帧判断没有时序过滤 | 加滑动窗口投票和状态机 |
| 红外摄像头效果差 | 训练数据缺乏红外样本 | 补充灰度化增强或单独采集红外微调集 |
| 模型在粉尘场景失灵 | 对比度下降 | 训练数据里增加粉尘干扰合成图 |
5.4 个人实操体会
做完整套数据构建和训练的流程,最大的感悟是数据集的工程量比模型训练大得多,也重要得多。很多人拿着公开数据集,跑通一个YOLO训练流程就觉得自己完成了任务,但真实项目里数据决定了性能天花板,模型只是在逼近这个天花板。
以这套8类别4847张的数据为例,它足够支撑一个行为检测Demo或者小规模试点项目,但要真正铺开到几个矿的上百路摄像头,还得继续扩充数据——尤其是跌倒、爬行这些长尾类别,以及更多井下分布、更多季节、更多班组作业风格的数据。模型可以在现有数据上做到95%的mAP,但真实场景里遇到没见过的姿态组合、没见过的光照条件,照样会翻车。
我的建议是:数据集做出来之后,先花时间把训练脚本、评测脚本、可视化脚本、错误分析脚本全部固化下来,然后持续用线上数据回流迭代。第一版模型上线之后,每周从前端设备抽几小时的视频,人工标注里面的漏检和误检,合并到训练集里做增量训练。模型会越用越聪明,这个过程中数据集本身才是最宝贵的资产。
真正做智慧矿山的人应该都有体会,这个行业不像互联网产品,没有那么多炫酷的技术噱头,考验的全是对脏活累活的耐心。能把数据整理干净、把标注规范定清楚、把评估口径统一,就已经跑赢了大多数团队。后面再谈算法优化,才有底气。