简介面向深度学习与计算机视觉开发者这份头盔检测数据集主要服务于以 YOLOv5 为代表的目标检测项目能够有效缓解训练数据不足、人工标注耗时的问题。压缩包共包含 160 个文件其中 80 张 JPG 图片覆盖不同环境与角度下的头盔目标另有 80 个配套 XML 标注文件采用 VOC 格式记录目标类别和边框位置既可转换为 YOLO 所需的文本标注也可直接用于验证与微调免去从零收集图像并手工标框的重复劳动。资源总大小仅 2.86MB轻量易获取适合个人学习、课堂实验或快速原型验证。目前已有 482 人学习下载适合入门目标检测、搭建安全监管应用的开发者。借助这份数据集可完整经历数据整理、模型训练、测试评估等环节为工地或工厂等场景的安全帽佩戴识别提供可靠支撑也有助于深入理解检测模型的数据组织与标注规范。1. 一个头盔检测数据集 ZIP为什么值得你重新“盘”一遍做工地安全帽检测的同行大概率都下载过那种叫用于yolo5头盔目标检测的数据集.zip的资源——解压出来几十个文件夹、几百张 JPEG、配一堆同名 txt第一眼感觉“齐了”扔进 YOLOv5 就能训。但真实情况往往是要么类别名和data.yaml对不上要么标注框里混着大量nan坐标要么验证集里混进了重复图片。数据集 ZIP 不是拿来即用的成品它更像毛坯房能住但水电要自己改。这篇笔记就是围绕这类头盔检测数据集讲清楚怎么把 ZIP 里的原始素材变成一套能稳定训练、能出高 mAP、能上工地的数据集流水线。适合刚拿到数据集准备训第一个模型的新手也适合被脏标注坑过、想系统梳理一遍的熟手。先说结论拿到 ZIP 的第一件事不是训练是校验。2. 先看数据格式再谈训练YOLO 标注、目录结构与类别映射2.1 数据集 ZIP 里到底应该有什么常见的目录组织形式一个规范的 YOLOv5 头盔检测数据集解压后应该包含三类东西图片文件.jpg / .png、同名的标注文件.txt、以及一个dataset.yaml或data.yaml的配置文件。标注文件里每行代表一个目标格式是class_id x_center y_center width height所有坐标都是相对图片宽高的归一化值取值在 01 之间。常见目录结构有两种。第一种是images/和labels/分家下面再按train/val/test拆分helmet_dataset/ ├── dataset.yaml ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/第二种是train/文件夹里同时放图片和标注同一个文件名通过后缀区分。我一般建议改成第一种因为 YOLOv5 的datasets.py会在images的兄弟路径下自动找labels目录规范能省掉后续一堆--cfg的坑。![注意] 正规的 YOLO 数据集 label 文件名必须和图片名完全一致含扩展名前缀比如IMG_001.jpg对应IMG_001.txt。不一致会直接被跳过不报错、不警告这是最容易忽视的“静默坑”。2.2 类别定义安全帽、戴帽人、未戴帽人到底怎么分头盔检测数据集的类别通常有 23 类。最简版本是两类helmet安全帽和head人头模型学的是“戴帽人”和“未戴帽人”的完整躯干框。复杂一点的会拆成三类with_helmet、without_helmet、helmet只框帽子本身。选哪种取决于你的落地场景——如果只判断“这个人有没有戴帽”拆三类更稳如果后续还要数帽子数、联动闸机两类就够了。class_id 的编号顺序必须和 YAML 里的类别列表严格对齐。比如dataset.yaml里写的是train: ./images/train val: ./images/val nc: 2 names: [helmet, head]那么标注文件里class_id0必须是helmetclass_id1必须是head。如果拿来的数据集是 VOC 格式XML 标注转的转换脚本如果没按字母序或原始类别表排序class_id 很容易整体错位——模型训练时 loss 能降推理时框里的名字全是错的。遇到这种情况写一个脚本把所有 txt 的 class_id 按你的目标类别表重映射一遍。# remap_classes.py # 将旧类别索引映射到新类别索引注意这里演示映射关系要按实际数据集调整 old_to_new {0: 1, 1: 0, 2: 0} # 例原0号类(背景帽)合并为新1号类(head的一种) import os from pathlib import Path label_dir Path(labels/train) for txt in label_dir.glob(*.txt): lines txt.read_text().strip().splitlines() out_lines [] for line in lines: parts line.split() if len(parts) ! 5: print(f跳过异常行: {txt} - {line}) continue cls int(parts[0]) new_cls old_to_new.get(cls) if new_cls is None: continue # 或按需报错 out_lines.append(f{new_cls} .join(parts[1:])) txt.write_text(\n.join(out_lines))这段代码的核心逻辑是遍历标注目录逐行解析class_id并替换同时顺手把坐标位不足 5 的脏行丢掉——那句话“跳过异常行”后面可以加计数跑完统计一下清洗量能直观看出数据集的脏程度。2.3 解压后的首轮体检缺文件、空标注、越界框一起查大多数公开 ZIP 数据集从网盘下来都缺零少碎要么某几张 jpg 没有对应 txt要么 txt 是空文件要么 bbox 宽高超过 1.0坐标越界。这些必须一次查清。我常用的做法是写一段体检脚本三个指标同时扫图片和标注是否一一对应、标注是否为空、坐标是否越界。python - EOF from pathlib import Path from PIL import Image for split in [train, val]: img_dir Path(fimages/{split}) lab_dir Path(flabels/{split}) orphan_txt, missing_txt, empty_lab, out_of_range [], [], [], 0 for img_path in img_dir.glob(*.jpg): lab_path lab_dir / (img_path.stem .txt) if not lab_path.exists(): missing_txt.append(str(img_path)) continue for line in lab_path.read_text().splitlines(): parts line.split() if len(parts) ! 5: empty_lab.append(str(lab_path)) continue x, y, w, h map(float, parts[1:]) if not (0 x 1 and 0 y 1 and 0 w 1 and 0 h 1): out_of_range 1 # 反向检查有txt但无图片 for lab_path in lab_dir.glob(*.txt): if not (img_dir / (lab_path.stem .jpg)).exists(): orphan_txt.append(str(lab_path)) print(f[{split}] 缺标注: {len(missing_txt)}, 孤立txt: {len(orphan_txt)}, 空/脏行: {len(empty_lab)}, 越界框: {out_of_range}) EOF跑完如果out_of_range不为 0千万别直接开始训练。越界框在 YOLOv5 训练时通常不会报 fatal error但会影响 anchor 匹配和 loss 计算最终体现在 mAP 上不去。处理方式有两个用albumentations或 OpenCV 做四角裁剪归一化或者干脆把整行删掉。在头盔场景里边界被截半的“半个人头”其实很常见但这类样本对识别帮助有限删掉比硬留更省心。提示Pillow打开损坏图片也会报错可以把体检脚本扩展成“尝试打开每张图失败则记录”能一次性踩掉图片解码翻车的雷。3. 把 ZIP 变成能训的工程data.yaml 配置、数据集划分与首次训练3.1 为什么要手工重写一份 dataset.yamlZIP 里自带的 yaml 文件往往是作者在他本地环境写的里面有train: E:\dataset\helmet\images\train这种绝对路径到了你机器上必然失效。YOLOv5 对路径的处理很直接yaml 里的路径可以是相对路径也可以说绝对路径但绝不允许“文件存在却读不到”。我拿到新数据集的第一件事就是新建一个干净的 yaml路径全部改成./相对形式并把nc和names重新核对一遍。# helmet_local.yaml # 注意这个 yaml 放在项目根目录下运行时相对路径按运行时所在目录解释 train: ./helmet_dataset/images/train val: ./helmet_dataset/images/val test: ./helmet_dataset/images/test # 可选没有可以注释掉 nc: 2 names: 0: helmet 1: head这段配置没有复杂逻辑但有几个隐形的点。一是train和val目录里如果混了不属于对应 split 的图片训练集会污染二是 YOLOv5 在训练时会用val来做每个 epoch 的评估test只在val.py --task test时用日常训练可以完全不配。如果你后面要对比不同数据集的效果yaml 文件名要比内容更值得注意helmet_local.yaml和helmet_site.yaml两个文件名至少能让你看清跑的是哪份数据。3.2 划分训练验证集随机分还是按场景分很多 ZIP 数据集已经分好了 train/val但比例往往不合理比如 9:1甚至 val 只有 20 张。抽样代表性不足训练时验证集的 mAP 会剧烈抖动——这个 epoch 是 0.85下个 epoch 掉到 0.72典型的“规模太小导致的噪声”。建议按 8:1:1 或 7:2:1 重新划分而且划分时要保证同一工地、同一批次拍摄的近似重复图片不能同时落在 train 和 val。头盔检测是强“场景相关”的任务——同一个工地的背景、光线、机位高度几乎一样随机划分会让模型记住背景而不是头盔特征。# split_dataset.py # 按场景拍摄目录/前缀分组后划分而不是逐图随机划分 import random from pathlib import Path import shutil random.seed(42) src_img Path(helmet_dataset/images/train_all) # 把所有图片先并到这里 src_lab Path(helmet_dataset/labels/train_all) out_img Path(helmet_dataset/images) out_lab Path(helmet_dataset/labels) groups {} # 用文件名前缀当场景标识 for img_path in src_img.glob(*.jpg): # 例siteA_001.jpg - siteA scene img_path.stem.split(_)[0] groups.setdefault(scene, []).append(img_path) # 按场景分组每组内按比例拆分配置 for scene, paths in groups.items(): random.shuffle(paths) n_val max(1, int(len(paths) * 0.2)) val_paths paths[:n_val] trn_paths paths[n_val:] for p in trn_paths: shutil.copy2(p, out_img / train / p.name) shutil.copy2(src_lab / (p.stem .txt), out_lab / train / (p.stem .txt)) for p in val_paths: shutil.copy2(p, out_img / val / p.name) shutil.copy2(src_lab / (p.stem .txt), out_lab / val / (p.stem .txt))这段脚本的核心是按文件名前缀理解“场景”如果你的数据集文件名毫无规律全是20230101_154230_001.jpg这种可以用拍摄时间戳的前 8 位当场景。头盔数据集最常见的翻车就是把同一个连续视频抽帧的图片随机打散进 train 和 val导致验证结果虚高——现场换一个工地立刻原形毕露。按场景划分后验证 mAP 会掉几个点但这才是真实水平。3.3 用 YOLOv5 跑通第一次训练命令、超参与资源预算环境配置好后首次训练只需要一条命令。这里用 YOLOv5 官方仓库的train.py预训练权重用yolov5s.pt约 14 MB下载失败就先跑 CPU 版或换源。以下是在单卡 GPU 上收着跑的配置python train.py \ --data helmet_local.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0 \ --workers 4 \ --cache ram \ --project runs/train_helmet \ --name v1_clean参数含义拆开说--img 640是把训练分辨率固定到 640×640头盔检测边界框相对偏小安全帽在整个画面里通常只占几十个像素可以用--img 960或 1280 提高小目标召回但显存占用成平方上涨--cache ram是把图片一次性载入内存训练速度大幅提升前提是你的数据集在 2~3 GB 以内--batch 16在 8 GB 显存的卡上跑 640 分辨率刚好再大就 gradient accumulation 了。第一轮训练不要追求 max mAP重点看三个东西train/box_loss和obj_loss是否持续下降、val/mAP_0.5在 30~50 个 epoch 后是否稳定、显存有没有爆。如果出现 loss 曲线在某个点横盘后突然上涨大概率是学习率过大默认 0.01 在小数据集上偏激进下一轮改成--lr0 0.001再跑。头盔检测数据集通常几百到几千张100 个 epoch 在 RTX 3060 上大约 2~4 小时如果 30 分钟就训完检查一下是不是数据集没读进去常见于 yaml 路径写错YOLOv5 会打印train: ... images0这种信息一眼就能看出来。注意跑第一次训练前确认labels里的 class_id 最大值小于等于nc - 1。如果数据里混着class_id5而 yaml 只声明了 2 类torch会直接报 index 越界这类错误其实很好排查。4. 让模型在小目标与类别不均衡下稳住增广策略与损失调优4.1 头盔目标小、遮挡多为什么默认增广不够用工地监控里安全帽经常只占画面 1%~3% 的面积且被脚手架、塔吊阴影遮挡。YOLOv5 默认开启的 mosaic、copy-paste 在目标偏大的常规数据集上效果好但在“大量小目标 互相重叠”的头盔数据上mosaic 会把多个含帽子的区域拼到一张图上导致目标更小反而让小目标更难学。这是很多头盔检测模型训出来 mAP 不差、到现场却频繁漏检的根因之一——训练分布和现场分布错位了。常见做法是把 mosaic 概率调低同时显式引入随机裁剪缩放增强RandomScale和 HSV 色彩抖动。YOLOv5 的hyp.scratch-low.yaml是官方“保守”配置适合小目标场景可以直接拿来改# hyp.helmet.yaml # 基于官方 hyp.scratch-low.yaml 的关键修改项 lr0: 0.001 # 小数据集用更保守的初始学习率 mosaic: 0.5 # 默认 1.0降一半减少小目标被拼得更小 mixup: 0.1 # mixup 容易让遮挡/重叠目标边界模糊也调低 hsv_h: 0.02 # 工地不同时段色温差异大适度增强 hsv_s: 0.7 # 饱和度增强应对黄昏/逆光 hsv_v: 0.5 # 亮度增强应对夜间低照度 fliplr: 0.5这里有三个参数值得按数据分布微调。mosaic降到 0.5 之后前 50 个 epoch 收敛速度会变慢但最终在验证集上的小目标 recall 更高hsv_h不要超过 0.05否则安全帽的橙色会被偏成紫色模型等于在学一个不存在的颜色规律mixup在头盔场景里最直接的负面效果是“帽子轮廓被混合成半透明”导致回归框偏大降下来比调别的更管用。4.2 类别不均衡戴帽人头多、未戴帽人头少怎么办工地监控里绝大多数工人是戴帽的未戴帽的样本可能只占 10%~20%。训练时模型会把head未戴帽的 loss 贡献压得很低推理时倾向把所有人都预测成戴帽——红线违规行为全部漏报。两个解决方向一是数据层面做重采样让每个 batch 里各类别样本比例更接近二是损失层面给样本量少的类别加大权重。# 统计类别分布并生成重采样权重 from pathlib import Path from collections import Counter import random label_root Path(helmet_dataset/labels) counts Counter() for lab_path in label_root.rglob(*.txt): for line in lab_path.read_text().splitlines(): parts line.split() if len(parts) 5: counts[int(parts[0])] 1 total sum(counts.values()) print(f类别计数: {dict(counts)}) # 给每个 label 文件分配采样权重稀有类别的图片被重复采样 weights [] for lab_path in label_root.rglob(*.txt): local Counter(int(l.split()[0]) for l in lab_path.read_text().splitlines() if len(l.split()) 5) # 权重 每个类别的频率倒数之和再归一化 w sum(total / counts[c] for c in local if c in counts) weights.append(w) # 实际训练时按 weights 做 sample 即可例如用 PyTorch WeightedRandomSampler这段脚本没有跑重采样本身只输出权重向量。原因是我更倾向在 DataLoader 层用WeightedRandomSampler实现而不是物理上复制图片文件——复制图片会让磁盘占用变大而且容易把同一张图同时写进 train 和 val。如果你不想改训练代码最有效率的做法是直接用你的数据管理脚本把未戴帽样本横向翻转、小幅旋转生成 5 份副本加进训练集。提示重采样只解决“数量不均衡”不解决“场景单一”。如果未戴帽样本全部来自同一个工地大门模型学会的是“这个背景未戴帽”换个工地就失效。要根治只能靠补充多场景数据这个没有捷径。4.3 从热力图看“模型在看哪里”诊断小目标漏检的一个实用工具刚训完模型先别急部署。用 YOLOv5 的detect.py加--save-txt跑一遍验证集选出那些漏检、误检的样本做可视化。我常用的可视化方式是原始检测框叠加 Grad-CAM 热力图——能直接看出模型是“看到了帽子但没框对”还是“整个区域都没注意”。# 检查一个漏检样本是否存在对应的低置信度检测低于阈值但存在 import torch from models.experimental import attempt_load from utils.general import non_max_suppression model attempt_load(runs/train_helmet/v1_clean/weights/best.pt, devicecpu) img torch.zeros((1, 3, 640, 640)) # 示意加载真实图片前向推理 # 推理时降低 conf_thres看模型在漏检位置附近是否有“犹豫”输出 results model(img)[0] pred non_max_suppression(results, conf_thres0.05, iou_thres0.5) print(低置信度检测框数量:, len(pred[0]) if pred[0] is not None else 0)这段代码的重点是conf_thres0.05——正常检测阈值 0.25 时没有输出降到 0.05 后能输出几个低分框说明模型其实“看到了”目标只是置信度不够。这种情况可以靠调低部署阈值0.15~0.2解决如果降到 0.05 依然无框那就是特征学习不足只能加数据、加迭代。这一步能帮你确定问题出在“阈值设置”还是“模型能力”避免瞎调参。5. 避坑数据集与训练环节的 5 个高频翻车点5.1 图片尺寸不统一导致训练中断现象训练到一半报Expected 3D or 4D tensor错误或验证集 mAP 为 0。原因数据集中混着 1920×1080 的监控截图和 640×480 的手机照片YOLOv5 在数据加载时按--img 640做 letterbox 适配但极端宽高比如 3:1 的细长条会让缩放后的图严重变形部分层计算时 tensor shape 对不上。解决训练前统一用脚本把所有图片 padding 成 1:1 的 640 或 960 尺寸同时重新计算对应的标注坐标。不要直接改--img参数来硬适应——治标不治本且推理时也会出现同样问题。5.2 多个 ZIP 压缩包解压后路径层级不一致现象A 包的图片在site1/images/B 包的图片在site2/dataset/train/合并时用了cp命令部分文件被覆盖。原因网上找的头盔数据集往往由不同作者分别打包目录结构各有一套合并全靠手动。解决合并前先用tree或 Python 脚本打一张目录清单统一映射到一个中转目录再执行上面的体检脚本。一句话规则任何外部 ZIP 进项目前先过一遍清理脚本禁止直接解压进train/目录。5.3 标注框覆盖全图导致 loss 震荡现象训练时box_loss正常下降但obj_loss波动极大且 mAP_0.5 一直卡在 0.5 附近。原因部分标注框width1.0, height1.0即框住了整个画面。这类错误通常出现在“标注人员想框住所有戴帽的人但偷懒框了整图”的数据上。全图框会让模型对“帽子”的定位信号完全失效。解决体检脚本里加一条规则w * h 0.7的框直接视为垃圾标注删除并统计删除量。如果删除比例超过 5%说明数据集质量可疑需要逐张人工复核或用自动清理工具二次筛。5.4 验证集指标虚高但现场完全不能用现象验证集 mAP_0.5 到 0.9但部署到工地电脑上录了 10 分钟视频漏检一大片。原因最典型的场景分布过拟合——验证集和训练集来自同一个工地、同一机位背景和光线几乎一样。模型把“工地的绿色围挡”当成了特征。解决用手头数据按工地 ID 强制分组划分验证集里只放没见过的工地。更严格的做法是硬性保留一个工地的数据完全不参与训练只做最终测试。这也是前面 split 脚本里按前缀分组的原因——宁可让训练数据少一点也要保证验证集真实。5.5 小模型部署时精度暴跌现象用 YOLOv5s 或 YOLOv5n 训练时 Loss 正常转成 ONNX 后精度掉了 3~5 个百分点。原因YOLOv5 在训练和推理时的预处理不完全一致特别是letterbox的填充值与归一化系数/255.0在导出时配置不对。另外模型蒸馏不足小模型难以学到头盔的细微纹理。解决部署侧使用和训练时完全一致的预处理代码直接从utils/augmentations.py里把letterbox函数引过来不要自己重写。如果精度还是掉了回到第 4 章的增广方案把mosaic调高一点重新训一版。6. 进阶用混淆矩阵和现场回放验证“这个模型真的能上工地”数据集 ZIP 训出来的模型最后要过三道关我才会让它在现场跑。第一道是混淆矩阵YOLOv5 训练结束会在runs/train_helmet/v1_clean/下生成confusion_matrix.png看矩阵右下角——未戴帽人被预测成戴帽人的比例这是头盔检测的红线。如果那一格超过 5%说明数据不均衡问题还没彻底解决先别部署回去重采样。如果戴帽人被预测成未戴帽人影响的是误报率常见但危害小一档。第二道是时序稳定性测试。拿一段现场监控视频逐帧跑检测把每帧的head类置信度按时间画出来。真实的工地画面会有大量“帽子出现在画面边缘、只露出一半、快速移动”的场景单帧检测会频繁跳变连续 10 帧里 5 帧有 3 帧无。一个我常用的补救技巧是轻量级时间平滑连续 3 帧内至少 2 帧检出才判定为“有人未戴帽”能大幅降低抖动误报。你可以在detect.py输出结果后自行做这个后处理不用改模型。第三道是昼夜切换测试。头盔检测的工地现场最不稳的因素是光照正午帽沿投影、黄昏逆光、夜里补光灯。最好的策略不是训练一个“万能模型”而是分场景录回放统计哪些时段漏检率最高再针对性地往训练集加那个时段的真实截图。这个流程听起来朴素但比在训练参数里反复调靠谱得多。我从第一次做安全帽识别到现在最深的体感是——数据集 ZIP 只是起点测试现场的负样本才是真正的调参依据。愿你拿到这个数据集后少走我走过的弯路希望帮到你。本文还有配套的精品资源点击获取