简介:面向施工工地、工厂等安全监管场景的YOLO目标检测数据集,聚焦人员安全帽、反光衣与工作服穿戴状态的自动识别需求,适合计算机视觉学习者、算法工程师及安全信息化项目开发者用于模型训练、验证与场景落地。压缩包共2000个文件,以1999个txt标注文件和1个yaml配置文件为主,txt可用于承载图像标注与样本索引信息,yaml便于组织数据集路径与类别配置,整体约952.16MB,规模适中,便于按目录开展训练与评估。目前已有1622人学习下载,具备一定参考热度。读者可据此构建违规着装检测流程,结合实时视频分析实现告警、抓拍与弹窗提示等应用思路,也可用于工地、工厂的安全着装监测实验,辅助完成数据整理、类别配置和模型迭代,为事前预防与常态检测提供数据基础。
1. 一套 3 类的 YOLO 安全帽数据集,先看它能替你省掉哪两周
多数人拿到安全帽、反光衣数据集,第一件事就是改个 data.yaml 跑 yolo train,看着 mAP 涨到 0.9 就切到现场摄像头,结果站在远处的工人一个都没框出来。这套数据集真正有用的地方,是它把安全帽、反光衣、工作服三类目标在真实作业场景下的判定边界标了出来——帽子被遮挡到什么程度还算、反光条只露出一半怎么框、背心和工作服重叠时归哪一类。它适合两类人:一是要做工地、厂区、园区安全合规检测的算法工程师,二是想拿真实分布数据验证自己训练链路的学生和自学者。下面从数据组织一直讲到训练、验收和踩坑。
2. 拆开压缩包:目录布局、标签格式与三类目标的标注口径
先不要急着跑训练脚本。数据的目录结构和标注口径决定了后面所有的上限,因为训练脚本随时能换,标错的框却改不回来。这一章先把目录、标签格式和三类目标的标注规则逐项过一遍,确认手里的数据和你以为的是同一份东西。
2.1 目录布局与标签文件格式
YOLO 系列的数据组织方式几乎是固定的:图片和 txt 标签分开存,靠文件名 stem 一一对应,目录名 train/val/test 直接对应 data.yaml 里的键。拿到数据集之后,第一件事是用 tree 或文件管理器确认层级,不要用「大概是这样」来假设。
# 常见的目录层级,先确认压缩包解开后没有多套一层文件夹 datasets/ppe/ ├── images/ │ ├── train/ # 训练集图片,jpg/png 混排也能跑,但最好统一 │ ├── val/ # 验证集,用于选模型和调阈值 │ └── test/ # 测试集,只在最后验收时开一次 ├── labels/ │ ├── train/ # 与 images/train 同名的 .txt │ ├── val/ │ └── test/ └── data.yaml # 类别名、路径配置每个 txt 里一行一个目标,格式是class_id x_center y_center width height,后四个值都是相对原图宽高归一化到 0~1 的浮点数。这意味着标签和图片分辨率是解耦的,你把 1920×1080 的原图缩到 640 训练,标签不用动。反过来,如果你用脚本裁图、拼图、做随机裁剪,标签必须同步重算,这一步没做好是后面 mAP 崩掉的最常见原因。
标签内容长这样:
0 0.482031 0.361111 0.039062 0.074074 1 0.501562 0.615741 0.156250 0.231481 2 0.498437 0.618055 0.171875 0.259259第一列是类别 id,对应 data.yaml 里 names 列表的下标,从 0 开始。注意这个 id 是「顺序敏感」的:names 里['helmet','vest','workwear']和['vest','helmet','workwear']是两份完全不同的数据集,标签不会跟着名字走,只会跟着下标走。血泪经验是训练前一定打印一遍 names,和标注时用的映射表逐行对一遍,多花三十秒,省掉一次通宵重训。
归一化坐标还有一个隐性约束:x_center ± width/2必须落在 0~1 之间。有些标注工具在目标贴边时会导出负数或大于 1 的值,YOLO 训练时默认会做裁剪,但裁剪后框的宽高关系就变了,等价于给了模型一个错误的正样本。所以 2.1 这一步有两个动作:确认目录层级没有多套一层,确认标签坐标全部落在合法区间。
2.2 三类目标的标注口径:安全帽、反光衣、工作服怎么划界
数据集的核心难点不在格式,在口径。同样是「一个人」,标成反光衣还是工作服,直接决定模型上线后报警逻辑对不对。下面这张表是我整理的判定规则,可以对着数据抽查几十张看看是否一致。
| id | 类别 | 框什么 | 不框什么 | 典型难点 |
|---|---|---|---|---|
| 0 | 安全帽 helmet | 头顶可见的帽体,包含帽檐 | 手拿的、挂在墙上的、放在地上的 | 侧脸时帽檐只剩一条边,框容易画小 |
| 1 | 反光衣 vest | 躯干上带反光条的外穿背心 | 只露袖子的、搭在肩上的 | 背对镜头时反光条弱,与工作服颜色接近 |
| 2 | 工作服 workwear | 非反光材质的统一工装上衣 | 便服、雨衣、外套 | 深色工装在逆光下和背景粘连 |
几条容易分歧的地方,建议在项目里直接定死,写进标注规范文档:
一是互斥原则。同一个人身上既有反光衣又有工作服时,只标外层可见的那件,不要两个框都打在同一块区域上。重叠框会让模型在推理时对同一目标输出两个类别的框,NMS 只能按 IoU 压掉位置相近的框,压不掉类别冲突,结果就是报警记录里同一个人被记两条。
二是遮挡原则。可见面积低于三成的目标不标。工地场景里人被脚手架挡掉大半是常态,如果强行标,模型会学到一堆残缺特征,反而拉低正常样本的召回。这部分数据更适合放到测试集里当难例,用来评估模型在遮挡下的表现,而不是塞进训练集教模型「残缺也算」。
三是小目标原则。远处的人头在 1920 宽度下可能只有 12×14 像素,这类样本要么整体放大后再标,要么在训练时提高输入尺寸。安全帽检测的漏检大头全在这一档,后面第 6 章会讲切图推理怎么处理。
2.3 data.yaml 与类别映射:训练前最后一道校验
配置文件看着简单,写错一个键就是几小时的浪费。路径建议写绝对路径,尤其是在容器里跑训练的时候,相对路径会相对于 ultralytics 的工作目录解析,很容易出现「训练集读到了、验证集读空了」的情况。
# data.yaml:路径写绝对路径,避免在容器/远端运行时解析错位 path: /data/datasets/ppe train: images/train val: images/val test: images/test nc: 3 names: 0: helmet # 安全帽 1: vest # 反光衣 2: workwear # 工作服nc必须等于 names 的条目数,且 id 从 0 连续。names 既可以写成上面的字典形式,也可以写成列表['helmet','vest','workwear'],两种等价,字典形式可读性更好。改完 yaml 之后跑一段十几行的校验脚本,比直接开训划算得多:
from ultralytics import YOLO from pathlib import Path import yaml cfg = yaml.safe_load(Path("/data/datasets/ppe/data.yaml").read_text()) names = cfg["names"] print("类别映射:", names) # 人工核对这一行,别跳过 # 用 model 的 names 校验:加载官方权重只为读结构,不做训练 m = YOLO("yolov8n.pt") assert cfg["nc"] == len(names), "nc 与 names 数量不一致" # 抽查一个标签文件,确认最大类别 id 没越界 sample = Path("/data/datasets/ppe/labels/val").glob("*.txt") for f in list(sample)[:200]: for line in f.read_text().splitlines(): cid = int(line.split()[0]) assert 0 <= cid < cfg["nc"], f"{f.name} 出现越界类别 id {cid}" print("校验通过")这段脚本做两件事:打印类别映射供人工核对,以及抽查 200 个验证集标签确认类别 id 没有越界。越界 id 在训练时不会报错,会被静默丢弃或当成背景,表现出来就是「某一类怎么都学不会」,排查起来极其费时。抽样数量取 200 是性能和覆盖的折中,验证集通常几千张,抽查两百张足以发现系统性的映射错误。
3. 从数据体检到第一个 baseline:训练链路怎么搭
数据看清楚了就进入训练链路。这一步的目标不是一次训出最好的模型,而是尽快拿到一个能跑通全流程、指标可解释的 baseline。有了 baseline,后面加数据、调增强、换模型才有对比基准,否则所有改动都是玄学。
3.1 数据体检:先筛掉坏图、空标签和重复样本
真实交付的数据集几乎不可能 100% 干净。空标签文件(图里没目标但文件存在)、孤立图片(有图无标签)、重复图片(同一张图改名复制多份)、坐标越界,这四类问题在训练前用脚本扫一遍,比训完再猜便宜太多。
import hashlib from pathlib import Path from collections import Counter ROOT = Path("/data/datasets/ppe") IMG_EXT = {".jpg", ".jpeg", ".png"} def scan(split): stat = Counter() bad = [] seen = {} for img in (ROOT / "images" / split).iterdir(): if img.suffix.lower() not in IMG_EXT: continue stat["images"] += 1 # 内容哈希找重复图:改名/重新压缩过的副本也能抓出来 h = hashlib.md5(img.read_bytes()).hexdigest() if h in seen: bad.append(("dup", img.name, seen[h])) seen[h] = img.name lbl = ROOT / "labels" / split / (img.stem + ".txt") if not lbl.exists(): bad.append(("no_label", img.name, "")) continue lines = [l for l in lbl.read_text().splitlines() if l.strip()] if not lines: stat["empty"] += 1 # 空标签图,在小数据集里最多占 5%~10% for l in lines: p = l.split() if len(p) != 5: bad.append(("bad_cols", img.name, l)); continue cid = int(p[0]); x, y, w, hh = map(float, p[1:]) stat[f"cls_{cid}"] += 1 # 归一化坐标合法性检查,贴边样本最容易在这里炸 if not (0 <= x <= 1 and 0 <= y <= 1) or w <= 0 or hh <= 0 or w > 1 or hh > 1: bad.append(("bad_coord", img.name, l)) return stat, bad for s in ("train", "val", "test"): stat, bad = scan(s) print(s, dict(stat)) print(" 异常数:", len(bad), "前 3 条:", bad[:3])输出里最值得盯的是三类实例数之比。安全帽和反光衣的样本通常远多于工作服,如果比例超过 10:1,少数类基本学不动,混淆矩阵里会发现工作服几乎全被分到反光衣。空标签占比也要看,正常情况在 5% 以内算健康——适当的背景图能压低误检,但比例高了模型会偏向「什么都没有」,召回往下掉。重复图未必是错,但训练集和验证集之间出现重复,就是数据泄漏,验证指标会虚高。
3.2 baseline 训练命令与超参设置
第一版不要上大模型。用 yolov8s 或 yolov8n 先跑通,确认指标方向对,再换 yolov8m 或更大尺寸。工地场景的目标尺寸偏小、类别只有三类,参数量上去收益有限,推理成本却是实打实涨的。
# baseline:先小模型跑通链路,imgsz 用 640 起步 yolo detect train \ model=yolov8s.pt \ data=/data/datasets/ppe/data.yaml \ imgsz=640 \ epochs=150 \ batch=32 \ device=0 \ workers=8 \ patience=30 \ optimizer=SGD \ lr0=0.01 \ cos_lr=True \ project=runs/ppe \ name=baseline_s参数的含义和调整方向:
| 参数 | baseline 取值 | 说明与调整建议 |
|---|---|---|
| imgsz | 640 | 远距离小目标多时提到 960,显存和耗时约涨 2.2 倍 |
| epochs | 150 | 配合 patience=30 早停;数据集小于 3000 张时可降到 100 |
| batch | 32 | 显存不够就降,同时按比例降 lr0,或用 auto batch |
| patience | 30 | 连续 30 轮验证指标不提升就停,避免无意义等待 |
| lr0 | 0.01 | SGD 常规起点;换 AdamW 时降到 0.001~0.002 |
| cos_lr | True | 余弦退火,末段学习率平滑下降,收尾更稳 |
imgsz是这里面最值得反复试的一个。安全帽在 640 输入下经过 32 倍下采样后只剩几个像素的特征图响应,检测头几乎抓不到;提到 960 或者 1280,小目标召回通常能涨几个点,代价是训练时间和显存。我一般会先用 640 跑一轮确认链路没坏,再用 960 跑第二轮作为正式 baseline,两次结果放一起看,才知道提升是来自尺寸还是来自别的改动。
3.3 增强策略:工地固定机位该关掉哪些
默认的增强配置是从通用检测任务调过来的,直接套在工地场景上会造出物理上不存在的样本。摄像头固定在高处俯拍,画面里的工人永远垂直站立,你要是开了大角度旋转,模型就得学一堆倒挂的人,纯属浪费容量。
# hyp_ppe.yaml:固定机位场景的增强配置,配合 yolo train 的 cfg 参数使用 lr0: 0.01 lrf: 0.01 mosaic: 1.0 # 拼接增强,小目标多的时候保留 close_mosaic: 10 # 最后 10 轮关掉,让模型收敛到真实分布 hsv_h: 0.015 # 色调扰动小一点,反光衣靠颜色区分 hsv_s: 0.7 hsv_v: 0.4 # 亮度扰动保留,工地昼夜/逆光差异大 degrees: 0.0 # 关掉旋转,俯拍场景没有倒置的人 translate: 0.1 scale: 0.5 # 缩放保留,模拟不同距离的工人 fliplr: 0.5 # 左右翻转安全,类别语义不变 flipud: 0.0 # 关掉上下翻转,避免出现倒挂的人 mixup: 0.0 # 混叠会糊掉反光条纹理,先关 copy_paste: 0.0 # 少数类过采样时再打开三个点值得展开。close_mosaic=10经常被忽略,mosaic 把四张图拼成一张,目标尺度分布和真实画面差异很大,如果一直开到最后一轮,模型收敛时会带着这个偏差,验证指标和现场表现对不上。degrees=0.0和flipud=0.0是场景强约束,不是通用建议——如果你的数据来自手持设备或无人机,这两项另说。copy_paste默认关着,等确认了工作服这一类样本严重不足,再打开做定向过采样,比一上来就开要可控。
4. 验收不只看 mAP:按现场工况回放验证集
训练日志里 mAP50 到 0.9 就认为做完了,是这套流程里最容易翻车的地方。mAP 是在整个验证集上按类别平均出来的,它不区分「远处的小目标」和「近处的大目标」,也不区分「漏检」和「误检」,而工地报警系统对这两类错误的容忍度完全不同。
4.1 每类 AP 与混淆矩阵怎么读
训练结束先看两个东西:每类 AP 明细和归一化混淆矩阵。前者告诉你哪一类拖后腿,后者告诉你它被错分成了什么。这两份信息合起来才能定位问题。
# 单独跑验证,输出 per-class 指标和混淆矩阵 yolo detect val \ model=runs/ppe/baseline_s/weights/best.pt \ data=/data/datasets/ppe/data.yaml \ imgsz=960 \ conf=0.001 \ iou=0.6 \ plots=Trueconf=0.001是验证时的标准做法:算 mAP 需要完整的 PR 曲线,阈值压到极低才能拿到召回的上界,别用 0.25 去跑 val,那样算出来的 mAP 偏低且不可比。iou=0.6是 NMS 的 IoU 阈值,验证阶段用它来生成预测框,和训练时的默认值保持一致即可。
结果解读有几个经验判断:三类里安全帽的 AP 通常最高,因为特征最显著;工作服最低,因为它和便服、反光衣的边界模糊;如果发现反光衣的 AP 明显低于安全帽,但混淆矩阵显示它大量被分到工作服,那多半不是模型容量问题,而是标注口径不一致,得回去查 2.2 节定的规则有没有被严格遵守。
4.2 用现场视频抽帧做工况回放
验证集指标只是第一关。真正决定能不能交付的,是拿一段现场视频抽帧跑推理,人工看几十张带框图。这一步花不了多少时间,但能暴露验证集覆盖不到的工况。
# 每 5 帧抽一张,避免相邻帧高度重复导致统计失真 ffmpeg -i cam01.mp4 -vf "select='not(mod(n\,5))',setpts=N/FRAME_RATE/TB" -vsync vfr frames/cam01_%05d.jpg # 对抽出的帧批量推理,保存带框图与 txt 结果 yolo detect predict \ model=runs/ppe/baseline_s/weights/best.pt \ source=frames/ \ imgsz=960 \ conf=0.35 \ iou=0.6 \ save_txt=True \ save_conf=True \ project=runs/replay \ name=cam01然后在runs/replay/cam01/labels/下统计每帧的类别计数,重点看两个数:有人的帧里安全帽框为 0 的比例,以及同一帧里反光衣和工作服同时出现的帧数。前者就是漏检率的一个粗估,后者说明互斥原则在推理侧没生效,需要回去检查训练数据里的重叠标注。抽样人工核对三十到五十帧,一般就能判断模型是「真的能用」还是「指标好看」。
4.3 conf、IoU 与 NMS 的取值权衡
部署时的阈值和验证时的阈值不是一回事。安全帽漏检意味着没监测到违章,代价远高于误报,所以两个类可以用不同策略:安全帽放宽阈值、保召回,反光衣稍微收紧、压误报。ultralytics 支持按类别覆盖阈值,写在推理配置里就行。
| 参数 | 验证取值 | 部署取值 | 调整逻辑 |
|---|---|---|---|
| conf | 0.001 | 0.30~0.40 | 越低召回越高、误报越多;安全帽类可单独降到 0.20 |
| iou (NMS) | 0.6 | 0.5~0.7 | 人群密集时调高,避免重叠目标被误压 |
| imgsz | 960 | 960~1280 | 推理尺寸不小于训练尺寸,降尺寸会明显掉小目标 |
| max_det | 300 | 100~300 | 单帧人数上限,过大会拖慢后处理 |
iou这一项在密集场景里特别关键。NMS 的规则是「两个同类框 IoU 超过阈值就压掉分低的那一个」,工地上三五个人挤在一起的时候,IOU 0.5 很容易把相邻的人框当成重复框压掉,表现就是画面里五个人只报了三顶帽子。把 iou 提到 0.65~0.7,能缓解这个问题,代价是偶尔出现重复框,需要下游逻辑按位置做二次去重。
5. 五个真实翻车现场:数据与训练侧的排查清单
这一章集中说问题。下面五条都是我在不同项目里真实遇到过的,每条按「现象 → 原因 → 解决」写,遇到对应现象可以直接对号入座。
5.1 数据侧的三类问题
第一条:模型指标正常,上线后把反光衣认成安全帽。
现象是验证集 mAP 分布看着合理,但现场回放时同一件反光衣被框成了 helmet。原因是类别 id 与 names 的顺序错位。标注阶段用的映射表是 0-vest、1-helmet,而 data.yaml 写成了 0-helmet、1-vest,标签文件本身没动,索引到哪一类就由 names 决定,训练出来的模型自然把颜色特征学反了。解决办法是固定一份classes.txt作为唯一真源,标注导出和 data.yaml 都从它生成,训练前执行 2.3 节的校验脚本,把映射打印出来人工确认一遍。从那以后我把这一步设成了流程里的强制检查点。
第二条:召回怎么都上不去,loss 却在正常下降。
现象是训练 loss 曲线平滑下降,验证 mAP 卡在 0.5 左右不动,混淆矩阵里大量真实目标被预测成背景。原因是标签路径错位导致部分标签没读进来。YOLO 对「图片存在但标签缺失」的处理是静默跳过,不报错,于是这部分图变成了纯背景样本,模型学到的是「这些位置什么都没有」。解决办法是跑 3.1 的体检脚本,看no_label计数;同时看 train 和 val 的图片数是否和 labels 数量对得上。另一个高发场景是路径里有多层同名目录,标签实际在labels/labels/train下。
第三条:验证集 mAP 0.95,现场只有 0.6。
现象是验证指标高得离谱,回放一段新视频却大面积漏检。原因是训练集和验证集来自同一段视频的相邻帧,帧间差异只有几帧,几乎等于把训练样本复制了一份给验证集。这是典型的数据泄漏,指标衡量的是「记住」而不是「泛化」。解决办法是按摄像头编号、按拍摄日期、按场景切分,不按帧随机切。切完之后验证集指标通常会掉几个点,那个更低的数才是真话。
5.2 训练与评估侧的两个问题
第四条:工作服这一类的 AP 接近 0,全被分到反光衣。
现象是混淆矩阵里工作服那一行几乎全落在反光衣列上,自己的对角线接近空白。原因有两层:一是样本量差距过大,三类实例数可能到 20:1;二是这两类在视觉上本来就接近,深色反光衣和深色工装在低照度下几乎同色。解决分三步走:先把语义边界重新核一遍,确认标注规则里互斥原则确实被执行了;再针对工作服补采样本,或者用copy_paste做定向过采样;如果补完还是分不开,就把两类合并成一个「工装」大类,下游用颜色或反光区域检测做二次判定,比硬训一个分不开的分类头实用。
第五条:远处的小目标全漏,近处的大目标很准。
现象是回放视频里三米内的人框得很稳,十米外的工人一个都不报。原因是输入尺寸不够,安全帽在 640 输入下经过下采样后特征响应太弱。解决有两条路,代价不同:把训练和推理的 imgsz 一起提到 960 或 1280,简单直接,但显存和延迟都涨;或者保持 640,改用切图推理(把大图切成重叠小块分别推理再合并),显存占用低但后处理复杂。我一般先用提尺寸验证收益,确认小目标确实是瓶颈之后,再决定要不要上切图。
6. 难例回流与切图推理:把数据集用出第二层价值
数据集用一遍就放着了,是最浪费的做法。真正拉开差距的是把它变成一个可持续迭代的闭环:现场视频回放出来的错检帧,人工修一遍标签,回流进训练集,下一版模型专门补这批短板。工具链上,用训练好的模型对未标注帧做预标注(yolo detect predict ... save_txt=True),再导入标注工具只做修正,效率比从零画框高一个数量级。回流的样本优先挑三类:漏检的小目标、被遮挡的目标、模型置信度在 0.2~0.4 之间的模糊样本。每轮回流控制在几百张,训一轮看验证集上那一批难例的子集指标有没有涨,涨了才继续加。
切图推理是另一半。做法是把 1920×1080 的帧切成 640×640、重叠 20% 的若干块,逐块推理后把框映射回原图坐标,再做一次全局 NMS。重叠率取 20% 是为了避免目标正好被切在边界上,被切成两半的目标在任意一块里都不完整,反而更难检。这套流程对小目标提升明显,代价是单帧推理耗时涨三到四倍,实时性要求高的场景要慎重。
| 迭代动作 | 触发条件 | 观察指标 |
|---|---|---|
| 难例回流 | 回放漏检率高于 15% | 难例子集的 recall |
| 提高输入尺寸 | 小目标(短边小于 20px)漏检集中 | 小目标分桶的 AP |
| 切图推理 | 已提尺寸仍不够,且允许延迟上升 | 端到端漏检率、单帧耗时 |
| 类别合并 | 两类混淆矩阵互错超过 30% | 合并后类别 AP |
有个习惯我保持了挺久:每次改完数据或增强配置,都先把上一版的 best.pt 在同一份固定难例集上跑一遍,拿到对照数,再跑新版。没有对照就没有判断力,只看总 mAP 涨了 0.3 个点就下结论,十有八九是随机波动。另外,切图推理的重叠率和 NMS 阈值要一起调,单独调一个经常是这边压下去那边冒上来。希望帮到你。
本文还有配套的精品资源,点击获取