☰
PPE识别数据集构建与YOLO训练实战:安全帽、反光衣、工作服检测全流程
2026/10/12 4:18:41 网站建设 项目流程

简介:本资源是基于YOLO的安全帽/反光衣/工作服自动识别数据集,面向施工现场、工厂等场景的安全管理人员、算法工程师及AI学习者。数据集可用于训练人员规范着装检测模型,通过OpenCV+YOLO网络对监控画面中的人员穿戴进行实时分析判断,实现对未按要求佩戴安全帽、穿着反光衣或工作服行为的自动告警与抓拍,适用于智慧工地、安全生产信息化管理等落地场景。资源共包含2000个文件,以1999个txt标注文件为主体,配合1个yaml配置文件,压缩包大小约952.16MB。txt文件为YOLO格式的标注数据,可直接用于模型训练与验证,yaml文件则定义了类别与路径等配置信息。已有1603人浏览学习。掌握该数据集,可快速构建人员穿戴合规检测模型,完成数据准备、训练调参与推理部署的完整流程,为降低安全隐患、提升现场管理水平提供数据支撑。

1. 为什么安全帽/反光衣/工作服识别,先卡在数据集上

安全帽、反光衣、工作服的自动识别,在工地安全管理里几乎是视觉方案的第一站。它看着不复杂,真做起来最先卡住的往往不是模型,而是数据集:类别怎么定、框怎么画、白天黑夜怎么覆盖。这些没定清楚,YOLO 再能打,也会在某个逆光摄像头前翻车。

这套数据集方案解决的就是这个前置问题:把三类 PPE 样本整理成 YOLO 可直接训练的结构,把类别定义、标注边界、验证口径一并定死,让训练和上线验收都有统一标尺。适合练手目标检测的开发者,也适合被现场误报反复折腾的实施工程师。

下文按「设计、构建、训练、避坑、验证」五步推进,命令和参数基于常见 YOLO 训练做法,照着改即可。

2. 数据集怎么设计:五类标注、YOLO 格式与转换脚本

做 PPE 自动识别,数据集设计的第一步不是收集图片,而是把类别定义定死。这个环节省下十分钟,后面标注、训练、验收全都要加倍还回去。下面先讲三个目标各自难在哪,再给出一套可直接落地的标注方案和格式规范。

2.1 三个目标为什么难:小目标、遮挡与类内差异

安全帽是最典型的小目标。一个 1080p 的监控画面里,人站在 20 米外时帽子只有 20 像素左右,imgsz=640 下采样后可能只剩 12 像素,已经趋近检测极限。反光衣的核心难点在光照:反光条纹在不同角度、不同光源下呈现完全不同的形态,白天是银白色亮条,夜间补光灯下会过曝成一整块。模型如果只见过白天样本,晚上漏检几乎是必然的。工作服难在类内差异:不同施工单位的工装颜色、款式差异很大,有些还和普通便装撞色,不定义清楚就会把路人误识别成施工人员。

遮挡同样不能忽视。工地里人员交叉走动、机械臂遮挡非常常见,一个人往往只有半身可见。标注规范里必须明确「被遮挡超过 50% 就不标」还是「可见部分照标」,并且全团队只能选一种。混着标,模型学到的目标边界会忽大忽小,验证集上的框波动一眼就能看出来。

2.2 标注方案选型:为什么推荐 helmet + head 双类别

常见的 PPE 标注方案有三种,差别在违规判断的逻辑里。

方案类别构成违规判断方式主要问题
Aperson / helmet / reflective_vest / workwear几何匹配 person 与 helmet 判断是否戴帽遮挡时匹配经常出错
B在 A 基础上加 head,共 5 类数 head 即未戴帽人数,简洁直接标注量多约 20%
C目标框 + 属性标签后处理解析属性工具链复杂,YOLO 原生不支持

方案 A 最直观,但依赖 person 框和 helmet 框的几何匹配,两人靠近时容易把 A 的帽子算到 B 头上。方案 C 把 PPE 状态做成属性,多标签输出需要改造模型头,不划算。我一般直接用方案 B:helmet 和 head 互斥,戴帽标 helmet,不戴标 head,模型输出里 head 的数量就是未戴帽人数,反光衣则用 reflective_vest 与 person 的数量差来判断,省掉大量后处理逻辑。

还要注意,反光衣和工作服是「可叠加」的关系,同一个人可以同时有 vest 和 workwear 两个框,像素上高度重叠也没关系。YOLO 按类别独立预测,不会互相干扰,标注时不用刻意错开。

2.3 YOLO 标注格式与目录组织

YOLO 的标签是纯文本,每行一个目标:class x_center y_center width height,中心点和宽高都归一化到 0~1。下面是一个真实标注文件的内容,三行分别代表一个安全帽、一件反光衣和一个裸露头部。

0 0.521 0.384 0.062 0.055 2 0.510 0.402 0.180 0.240 1 0.488 0.379 0.041 0.038

第二行是反光衣,第一行是它对应人员的安全帽,第三行 head 表示另一个没戴帽的人。width 只有 0.04~0.06 很常见,这正好对应了「小目标」的现实;写文件时保留 6 位小数,否则小框的位置精度会丢失。

目录结构按 images/labels 分开镜像存放,训练、验证、测试三份都要独立:

ppe_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── ppe.yaml

图像和标签同名,img_000123.jpg 对应 img_000123.txt。没有目标的图可以留空 txt,YOLO 认空文件;但空 txt 一多就要警惕是不是漏标了,后面第 5 章会专门讲校验。

2.4 COCO 转 YOLO 的脚本和四个隐藏坑

很多标注工具导出的是 COCO json 而不是 YOLO txt,转换脚本不难,但坑都在细节里。下面这份脚本按图片聚合标注,逐条转成归一化坐标。

# coco_to_yolo.py import json from pathlib import Path def coco_to_yolo(coco_file: str, label_dir: str, class_map: dict): """把 COCO json 标注转成 YOLO txt。 class_map 示例: {1: 0, 2: 1, 3: 2, 4: 3, 5: 4},含义是 COCO 类别 id 到 YOLO 类别 id 的映射。 """ with open(coco_file, encoding='utf-8') as f: data = json.load(f) images = {img['id']: img for img in data['images']} anns_by_img = {} for ann in data['annotations']: anns_by_img.setdefault(ann['image_id'], []).append(ann) label_dir = Path(label_dir) label_dir.mkdir(parents=True, exist_ok=True) for img_id, anns in anns_by_img.items(): img = images[img_id] w, h = img['width'], img['height'] if w <= 0 or h <= 0: continue lines = [] for ann in anns: cls = class_map.get(ann['category_id']) if cls is None: continue x, y, bw, bh = ann['bbox'] # COCO: 左上角 x,y + 宽高 cx = (x + bw / 2) / w # 转为中心点 x cy = (y + bh / 2) / h # 转为中心点 y lines.append(f"{cls} {cx:.6f} {cy:.6f} {bw / w:.6f} {bh / h:.6f}") out = label_dir / (Path(img['file_name']).stem + '.txt') out.write_text('\n'.join(lines), encoding='utf-8')

这段脚本的逻辑很直白,但四个坑要记住。第一,COCO 的 bbox 是左上角坐标加宽高,不是中心点,直接除宽高就错了。第二,归一化必须用原图的 width/height,不能用训练时缩放的尺寸,否则框的位置整体偏移。第三,category_id 和 names 的映射不能靠猜,要从 json 的 categories 字段读出来人工确认一遍。第四,转换完必须回读画框抽查,肉眼过 50 张再开训,这一步省不掉。

3. 从零构建 PPE 数据集:采集清单、标注规范与分组划分

如果手里还没有数据,不要急着去拼凑公开图片,现场采集的样本和你部署的摄像头视角一定更匹配。构建过程四件事:采集、标注、增强、划分。每一步都直接影响上线效果。

3.1 现场采集:机位、距离与时段覆盖清单

采集阶段的核心目标是让「现场会出现的情况」都有样本,而不是追求总量。这个阶段省下的时间,会在标注和训练阶段十倍还回去。

注意:采集时对着清单打钩,比盲目多拍 1000 张更有效。

  • 机位:平视、俯视(球机/塔吊)、仰视各占三分之一。只收平视样本,俯视摄像头一上就会漏检。
  • 距离:5 米、10 米、20 米三档。数据里必须真的有「小目标」,不能全是近景大头照。
  • 时段:上午逆光、正午顶光、傍晚、夜间补光每个时段都留。只收白天会导致夜间全灭。
  • 姿态:站立、走动、弯腰、背对。背对时安全帽和反光衣仍然可见,是着装合规判断的重要角度。
  • 着装:不同颜色的工装、新旧反光衣、敞开或半扣的反光衣。如果现场允许,还要多拍「没戴帽、没穿反光衣」的负例。

现场条件受限时,优先保证时段和距离的覆盖,这两项直接决定模型在小目标、弱光两个最脆弱维度上的表现。

3.2 标注规范:每个类别的边界都写死

标注一致性比标注精确性更重要。边界不统一,模型学到的框就会漂。下面这套规则可以直接抄进标注文档:

  • helmet:只框帽壳,帽檐可含在框内,不框脸。安全帽拿在手里、挂在墙上都不算。
  • head:裸露的头部,含头发及额头区域。戴了帽子但只露出帽檐时,标 helmet 不标 head。
  • reflective_vest:框躯干上反光衣的可见范围,不含手臂和腿。反光衣敞开时按可见布料标注。
  • workwear:统一工装上衣的可见部分。与便装无法区分时不标 workwear,只保留 person。
  • person:全身,包含头部。遮挡超过一半不标;多人重叠且框无法分清时,只标完整可见的那个。

标注工具用常见的开源标注软件即可,导出 YOLO 格式;团队协作时把标签顺序固定成和 ppe.yaml 一致,导出后立刻跑一遍第 5 章里的校验脚本,不要等到训练时才发现类别对不上。

3.3 数据增强:哪些交给训练时自动做,哪些必须离线补

YOLO 训练时自带 mosaic、HSV 扰动、翻转、缩放等在线增强,常规场景不需要离线处理。但两类情况必须离线补:一是稀缺时段,二是相似色背景导致的误检。

夜间反光衣的特征不能靠压暗模拟,因为真实的过曝来自补光灯,必须实拍;而「暗光下的人形和头盔」可以用压暗来扩充。下面这段脚本做低照度增强,并同步复制标注文件。

# offline_augment.py import cv2 import numpy as np from pathlib import Path def darken_images(src_dir: str, out_dir: str, factors=(0.4, 0.6)): """把图片压暗,模拟傍晚/夜间低照度,扩充稀缺时段样本。""" src_dir, out_dir = Path(src_dir), Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) for img_path in src_dir.glob('*.jpg'): img = cv2.imread(str(img_path)) if img is None: continue factor = np.random.uniform(*factors) # 亮度缩放系数,随机避免增强图彼此太像 dark = np.clip(img.astype(np.float32) * factor, 0, 255).astype(np.uint8) cv2.imwrite(str(out_dir / img_path.name), dark) # 标注框坐标不变,直接复制同名 txt src_txt = img_path.with_suffix('.txt') if src_txt.exists(): (out_dir / src_txt.name).write_text( src_txt.read_text(encoding='utf-8'), encoding='utf-8')

逻辑说明:factor 在 0.4~0.6 之间随机,避免增强后的样本彼此过度相似;如果原图本身就偏暗,把 factors 抬高到 (0.6, 0.8)。增强图和原图不要同时留在训练集里,保留增强版即可,防止验证时出现近重复导致 mAP 虚高。反光衣的过曝、眩光这类真实镜头效应,离线模拟效果很差,只能去现场夜间补拍。

3.4 分组划分脚本:按场景切分,别按帧随机切

随机划分在视频数据上是典型的坑。连续帧几乎一模一样,随机分的话训练集和验证集里会出现同一画面的两个邻居,验证 mAP 虚高到 0.95 都没意义,上线当场现原形。正确做法是按场景分组,整组划入 train/val/test。

# split_by_group.py import random import shutil from pathlib import Path def split_by_scene(img_dir: str, label_dir: str, out_dir: str, ratios=(0.8, 0.1, 0.1), seed=2024): """按文件名中的场景前缀分组切分,防止相邻帧泄漏。""" img_dir, label_dir, out_dir = Path(img_dir), Path(label_dir), Path(out_dir) # 文件名形如 scene01_000120.jpg,取前 6 个字符作为分组键 groups = {} for img in img_dir.glob('*.jpg'): key = img.name[:6] groups.setdefault(key, []).append(img) scenes = list(groups.keys()) random.seed(seed) random.shuffle(scenes) n1 = int(len(scenes) * ratios[0]) n2 = n1 + int(len(scenes) * ratios[1]) parts = {} for i, scene in enumerate(scenes): parts[scene] = 'train' if i < n1 else ('val' if i < n2 else 'test') for scene, imgs in groups.items(): part = parts[scene] for img in imgs: dst_img = out_dir / 'images' / part / img.name dst_img.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(img, dst_img) src_txt = label_dir / (img.stem + '.txt') if src_txt.exists(): dst_txt = out_dir / 'labels' / part / src_txt.name dst_txt.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src_txt, dst_txt) if __name__ == '__main__': split_by_scene('raw_images', 'raw_labels', 'ppe_dataset')

关键在分组键。文件名前面是场景号,同一场景所有帧都进同一个集合,验证集才可信。切分数量按场景数算而不是按图片数算,避免一个长视频独占训练集。如果数据来自多个摄像头,把摄像头编号也拼进 key,比如 cam01_scene02_000120.jpg,按前 10 位分组。test 集最好单独留一个训练时完全不看的工地,那才是真正的考试题。

4. 用 YOLO 训练 PPE 识别模型:命令、超参与监控

数据就绪后,训练本身不难,难在让参数贴合 PPE 的小目标特点。下面按配置文件、训练命令、监控三个环节讲,命令基于常见的 YOLO 训练方式,直接抄就能跑。

4.1 数据配置文件与一条训练命令

先写数据配置文件,让 YOLO 能找到图片和标签:

# ppe.yaml path: /data/ppe_dataset train: images/train val: images/val test: images/test names: 0: helmet 1: head 2: reflective_vest 3: workwear 4: person

path 用绝对路径,train/val/test 是相对 path 的目录。names 的序号必须和 txt 里的 class id 一一对应,这是训练前最容易出错的地方,错一个就全乱。

然后是训练命令:

# 训练安全帽/反光衣/工作服识别模型 yolo detect train \ data=ppe.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=640 \ batch=16 \ device=0 \ patience=40 \ project=./runs_ppe \ name=ppe_v1

model 用预训练权重而不是从零开始,收敛快且稳定;patience=40 表示验证指标连续 40 个 epoch 不涨就自动停,防止傻跑浪费算力;project 和 name 指定输出目录,方便多个实验对比。习惯 YOLOv5 风格的话,换成 train.py 加 --img、--batch 参数,含义一致。

4.2 三个最值得调的参数:imgsz、batch 与 epochs

参数默认PPE 场景建议理由
imgsz640先 640 跑通,再试 960目标小,加大输入直接提升召回
batch168G 显存用 8,16G 用 16显存不足优先降 batch,别降 imgsz
epochs100150~300小目标收敛慢,靠 patience 停

imgsz 是 PPE 场景最重要的旋钮。安全帽在 640 下只有十几个像素,提到 960 后特征明显更丰富,代价是训练时间和显存大约按平方增长。一般流程是 640 把数据链路跑通,再用 960 精调一轮。batch 受显存限制,8G 单卡跑 s 级模型用 8 比较稳,不够就开 AMP 混合精度。

类别不平衡时,补样本比调损失权重有效。head 这类样本少,先回现场补标注,实在补不动再考虑调 cls 损失。调损失权重属于「玄学」,数据不够时收益很有限,我一般只在数据补完后还差一口气时才碰。

4.3 训练监控:看三样东西

第一看曲线。训练集 loss 持续降、验证集 loss 反弹,是过拟合信号;验证 mAP50 先升后掉也一样。第二看混淆矩阵。helmet 和 head 互相误检,先查标注边界是不是不统一;黄色机械被识别成头盔,重点查颜色增强和难例。第三看权重。默认取验证 mAP 最高的 best.pt,如果最后 20 个 epoch 验证集已经过拟合,last.pt 往往不如 best.pt。

训练完立刻在预留测试集上做一次完整评测:

# 在测试集上评测,输出各类别 mAP yolo detect val data=ppe.yaml \ model=runs_ppe/ppe_v1/weights/best.pt \ imgsz=640

输出里逐类的 mAP50 比总的指标更关键。head 这类样本少的类别通常是最低的那个,它低说明违规人数会被漏报,上线前就要针对它补数据、重训。

5. 踩坑与排查:PPE 识别数据集训练的 5 个常见问题

这一章是反复折腾出来的记录,每一条都对应一个具体的翻车场景。按「现象 → 原因 → 解决」写,方便对照排查。

5.1 验证 mAP 虚高,上线就翻车

现象:验证集 mAP50 高达 0.95,换到新工地摄像头,检测率掉到 60% 以下。原因:随机划分把同一段视频的连续帧同时分进 train 和 val,模型等于提前看了答案。解决:用 3.4 的分组脚本按场景切分,test 集单独留一个完全没参与采集的工地画面。划分方式不修,后面调什么都白调。

5.2 反光衣白天正常、夜间大量漏检

现象:白天 mAP50 0.9,夜间只有 0.3。原因:训练集里 90% 是白天样本,反光条在夜间补光灯下过曝成白色团块,和白天特征差异巨大。解决:夜间加黄昏样本至少补到 20%;离线增强压暗到 0.4~0.7 倍并加少量高斯模糊;验收时按时段分开统计,别拿白天指标糊弄夜间场景。

5.3 黄色安全帽误检黄色机械和护栏

现象:挖掘机、黄色脚手架频繁被框成 helmet,误报率下不来。原因:模型学到的主导特征是颜色,不是帽子的形状;黄色工装、黄色机械在画面里到处都是,颜色通道一激活就崩。解决:训练增强里把饱和度扰动范围收窄,补一批逆光样本;把误检图片收进训练集,只标注真正的目标,模型靠背景反例学会区分。这就是最简单的难例挖掘,比调阈值有用得多。

5.4 标注框过大或过小,小目标评测失真

现象:安全帽框把整个头和脸都圈进去,验证集 mAP50 不错,mAP50-95 却低得离谱;或者帽子框太小,训练时 loss 波动大。原因:「帽子框哪里」没有写死,每个标注人员理解不同,框的尺度标准不统一。解决:规范里明确写「只框帽壳,不框脸」;训练前跑统计脚本,看各类 bbox 的宽高比分布——安全帽应该接近 1:1,反光衣大约在 0.5~0.8 之间,超出范围的人工复查。

5.5 训练报错:类别 id 超出 names 数量

现象:训练一开始就报 class index out of range,或者验证集混淆矩阵里多出陌生行。原因:标注工具导出的类别顺序和 ppe.yaml 的 names 对不上,某个 txt 里写了 5,但 names 只有 5 个(0~4)。解决:训练前跑一遍标签校验脚本:

# check_labels.py from pathlib import Path def check_labels(label_dir: str, num_classes: int = 5): """统计类别分布,并检查 class id 是否越界。""" label_dir = Path(label_dir) class_counts = {} max_id = -1 empty_files = 0 for txt in label_dir.glob('*.txt'): if txt.stat().st_size == 0: empty_files += 1 continue for line in txt.read_text().splitlines(): cls = int(line.split()[0]) # 每行第一个字段是类别 id class_counts[cls] = class_counts.get(cls, 0) + 1 max_id = max(max_id, cls) print('类别分布:', class_counts) print('最大类别 id:', max_id) print('空标签文件数:', empty_files) assert max_id < num_classes, f'类别 id {max_id} 越界,请检查 names 配置'

注意:空 txt 在 YOLO 里合法,表示该图无目标;但空文件太多就要怀疑漏标注,最好把对应图像一并删掉,别让空标签污染训练分布。

6. 上线前最后一道关:按场景验收与黑匣子回归

模型训练完不等于能上线。工地现场几十路摄像头,机位各不相同,全局 mAP 好看骗不了人。

6.1 按机位和时段分桶统计

把测试集按「拍摄机位加时段」打标,分别算 mAP。常见的现象是:平视摄像头 mAP 0.9,俯视球机只有 0.4。球机视角下人是头顶的圆形,头盔和 head 的轮廓与训练数据里的平视视角差异巨大。不打这个分桶,你只会得到一个「平均值还不错」的错觉。具体做法是在评测时给每张测试图加机位前缀,用脚本按前缀分组输出 mAP50,夜间样本单独看 reflective_vest 的召回率。

6.2 做一个固定黑匣子集

不管模型怎么迭代,都先过一套固定的「黑匣子」:大约 200 张从各工地收集的硬样本,覆盖逆光、夜间、相似色机械、遮挡、俯视机位。每次重训后先跑黑匣子,mAP 不低于上一次才允许出门。这个集子平时不动,只有当某个类别系统性改了标注才重审。它是防止「优化了夜间,白天又反弹」的最后防线。

6.3 试跑时加时序过滤再验收

单帧检测直接接视频流,报警会被反复触发。常见做法是接跟踪器做时序确认:head 连续出现 3 帧且附近无 helmet 才报警,反光衣同理。先在录好的 10 分钟视频上数报警次数和延迟,符合现场预期再联调告警,别一上来就推真机。

最后说个我自己的教训。第一次搭 PPE 识别方案时,我图省事用了随机划分,验证集漂亮得不行,结果换到新工地第一天就被大门摄像头打脸。后来把所有数据按场景重分,又封了一个黑匣子集,之后每次改模型都先过它再出门。数据集不只是拿来训练的,它更是你验收的尺子。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询