简介面向施工工地、工厂等安全监管场景的YOLO目标检测数据集聚焦人员是否规范佩戴安全帽、穿着反光衣与工作服这一实际需求可用于训练与验证实时着装检测模型适合计算机视觉入门者、算法工程师及安全信息化项目开发者参考使用。压缩包共收录2000个文件其中1999个txt为逐图标注文件记录目标类别与边界框坐标另有1个yaml配置文件用于声明数据集路径与类别名称整包约952.16MB规模足以支撑常规训练与精度验证。目前已有1622人学习下载。借助这批标注数据读者可快速搭建检测训练流程复现违规着装识别与告警抓拍的完整链路并结合自有监控画面做迁移微调配套的类别配置也便于直接接入OpenCVYOLO推理脚本减少数据整理与格式转换的重复工作。1. 一份 YOLO 安全帽/反光衣数据集值不值得拆开先看一眼我见过太多人拿到安全帽/反光衣数据集的第一反应是解压、改个 data.yaml、直接开训跑完一看 mAP50 有 0.9兴冲冲拿到现场一测逆光下漏掉一半远处塔吊上的人直接识别成未戴帽。问题八成不在模型而在数据。这份按 YOLO 标签格式整理的安全帽、反光衣、工作服识别数据集价值不在图片数量而在于它把戴没戴这件事拆成了可训练的框级标注——安全帽、人头、反光衣、人体几类各自独立成框边界清楚才能让模型学到帽子在头上和帽子拿在手里的区别。适合做厂区、工地、电力巡检、仓储作业这类佩戴合规检测的团队也适合拿它当 YOLO 检测链路练手的数据工程样本。但前提是你得先把它拆开体检而不是当黑匣子喂进去。2. 拆包与标签体检先把这本账对平再谈训练数据集落地第一步永远不是训练是核对。YOLO 检测的标注是一图一 txt、一行一目标格式看着简单错起来却悄无声息txt 少一个、图片多了几张、坐标归一化除错了宽高训练过程不会报错只会让指标莫名其妙地低。下面三个动作我一般会在一小时内走完后面省下的调试时间远不止这些。2.1 目录结构与标注文件的对齐关系标准布局是 images 和 labels 平行split 名一一对应dataset/ ├── images/ │ ├── train/ val/ test/ └── labels/ ├── train/ val/ test/关键约束只有一条images/train/a.jpg必须对应labels/train/a.txt同名不同后缀。txt 里每行五个字段——class_id x_center y_center width height后四个都是除以图宽图高之后的 0~1 浮点数不是像素。这一点搞错的人不少尤其是从 VOC 转过来的很容易把像素坐标直接塞进去模型照样能训但框会全部挤在左上角一小块。提示空 txt 文件不是错误是负样本背景图对抑制见人就报未戴帽的误检很有用体检时单独计数别当垃圾删掉。另外要确认 data.yaml 里的names顺序和 txt 里的 class_id 严格对齐。我见过一次 0 和 1 写反的helmet 和 head 互换训练曲线一切正常结果模型把安全帽判成未戴白白烧了两天卡。2.2 标签体检脚本类别分布、越界与过小框下面这段脚本做四件事类别计数、缺失标签、坐标越界、过小框筛查。放在数据集根目录跑即可。# check_labels.py —— YOLO 数据集标签体检 from pathlib import Path from collections import Counter import math ROOT Path(dataset) IMG_EXTS {.jpg, .jpeg, .png, .bmp, .webp} # 必须和 data.yaml 的 names 顺序一致 NAMES [helmet, head, vest, person] def to_label(img: Path) - Path: parts list(img.parts) parts[parts.index(images)] labels # images - labels return Path(*parts).with_suffix(.txt) cls_cnt Counter() missing, bad, tiny [], [], [] empty_num 0 for split in [train, val, test]: for img in (ROOT / images / split).rglob(*): if img.suffix.lower() not in IMG_EXTS: continue lp to_label(img) if not lp.exists(): missing.append(str(img)) continue rows [r.strip() for r in lp.read_text(encodingutf-8).splitlines() if r.strip()] if not rows: empty_num 1 # 负样本合法 continue for r in rows: f r.split() if len(f) ! 5: bad.append((str(lp), 字段数!5)); break cid int(float(f[0])) if cid 0 or cid len(NAMES): bad.append((str(lp), f类别越界:{cid})); break x, y, w, h map(float, f[1:]) if not all(0.0 v 1.0 for v in (x, y, w, h)): bad.append((str(lp), f坐标越界:{r})); break if w * h 1e-4: # 面积占比 0.01% tiny.append((str(lp), r)) cls_cnt[NAMES[cid]] 1 print(类别分布:, dict(cls_cnt)) print(负样本(空txt):, empty_num, | 缺标签图:, len(missing)) print(格式异常:, len(bad), | 过小框:, len(tiny)) for item in bad[:10]: print( BAD, item)几个参数和经验值1e-4这个面积阈值对应 640 图上大约 6×6 像素比它还小的框基本是标注噪声肉眼都难判断戴没戴留着只会拉低回归质量。bad只要非零就必须处理因为 Ultralytics 一脉的训练链路遇到坏行通常静默跳过你不会收到任何报错。拿到cls_cnt后看两件事一是 head 和 helmet 的数量比正常场景下未戴帽的人远少于戴帽的人比例到 1:5 以上就得考虑不平衡处理二是 vest 类的占比反光衣标注经常只在正面清晰时才有侧面和背面缺失会训练出只认正面的模型。2.3 从 VOC / COCO 转 YOLO 的坐标换算手里如果混着 XML 或 COCO json先统一成 YOLO 再合并别在训练时做多格式适配。VOC 转 YOLO 的核心是先裁到图内再归一化import xml.etree.ElementTree as ET from pathlib import Path NAMES [helmet, head, vest, person] def voc_to_yolo(xml_path: Path): root ET.parse(xml_path).getroot() W float(root.findtext(size/width)) H float(root.findtext(size/height)) lines [] for obj in root.iter(object): name obj.findtext(name) if name not in NAMES: continue # 未在映射表里的类直接丢弃 cid NAMES.index(name) b obj.find(bndbox) x1, y1 float(b.findtext(xmin)), float(b.findtext(ymin)) x2, y2 float(b.findtext(xmax)), float(b.findtext(ymax)) # 关键裁剪到图像范围负坐标会让归一化后的值变成负数 x1, x2 max(0.0, x1), min(W, x2) y1, y2 max(0.0, y1), min(H, y2) if x2 - x1 1 or y2 - y1 1: continue cx, cy (x1 x2) / 2 / W, (y1 y2) / 2 / H w, h (x2 - x1) / W, (y2 - y1) / H lines.append(f{cid} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return linesCOCO 的换算同理只是bbox存的是左上角加宽高需要cx x w/2再除图宽。转换完一定要用 2.2 的脚本再跑一遍因为转换脚本最常翻车的地方是图片尺寸读错——有些 XML 头里写的 size 和实际图片不一致稳妥做法是直接用 PIL 读图片真实尺寸而不是信标注文件里的字段。3. 训练前的数据工程划分、增强与类别不平衡体检过了只是证明数据没错接下来要让它有用。这一步是在数据层面做调整怎么划、怎么增强、怎么补负样本。三个决定做对同样的模型能多出好几个点的实际召回。3.1 训练/验证/测试划分别让相邻帧泄漏进去如果数据集已经给了 split先别急着信。工地数据很多是从监控视频抽帧来的同一段视频里相隔几秒的两帧几乎一样。随机打乱划分的话训练集里有第 100 帧、验证集里有第 101 帧模型等于提前看过答案验证 mAP 能虚高十几个点上线一测原形毕露。判断方法很简单看文件名。cam01_000123.jpg、cam01_000124.jpg这种带连续编号的就是抽帧。正确做法是按组划分把同一段视频、同一个源目录的所有帧绑在一起走import hashlib, shutil from pathlib import Path SRC Path(dataset/raw) DST Path(dataset/cleaned) EXTS {.jpg, .jpeg, .png, .webp} def group_key(p: Path) - str: # cam01_000123 - cam01若无下划线则整名归为一组 return p.stem.rsplit(_, 1)[0] if _ in p.stem else p.stem def split_of(g: str) - str: h int(hashlib.md5(g.encode()).hexdigest()[:8], 16) % 10 return train if h 8 else (val if h 9 else test) for img in SRC.rglob(*): if img.suffix.lower() not in EXTS: continue g group_key(img) s split_of(g) # 同组必定进同一个 split for sub in (images, labels): tgt DST / sub / s / img.name tgt.parent.mkdir(parentsTrue, exist_okTrue) if sub images: shutil.copy2(img, tgt) else: lp Path(*[(labels if p images else p) for p in img.parts]).with_suffix(.txt) if lp.exists(): shutil.copy2(lp, tgt) else: tgt.write_text() # 缺标签按负样本补空文件用 md5 取模而不是random.shuffle是为了可复现——同一份数据重复跑划分结果完全一致团队里两个人的实验才能对比。比例 8:1:1 是常见起点数据量小于两千张时改成 7:2:1或者直接做 5 折交叉验证看指标波动区间比单次划分的绝对值更有参考意义。3.2 针对小目标与逆光场景的增强策略安全帽在画面里通常很小。一个 1080p 的工地远景几十米外的人头可能就 12×12 像素。这时候首要动作不是增强而是提分辨率imgsz从 640 提到 960 或 1280小目标召回往往能涨 5~10 个点代价是显存和推理耗时翻倍。这一步比调任何增强参数都划算。增强里最容易翻车的是颜色。反光衣的识别线索很大程度就是荧光黄、荧光橙这类高饱和颜色hsv_h开大到 0.05 以上模型可能学会把任意亮色块当反光衣。我一般这样配参数建议值理由hsv_h0.010~0.015色相扰动小保住黄/橙的颜色语义hsv_s0.4~0.7饱和度扰动可大兼顾阴天/背光褪色hsv_v0.4~0.6亮度扰动大模拟逆光和夜间补光mosaic0.8最后 15 轮关掉提升小目标出现频率但会缩小目标mixup0~0.1检测任务慎用容易造出物理上不合理的重叠scale0.5缩放幅度大一点覆盖远近不同景别degrees0~5监控视角基本水平大角度旋转不真实mosaic 是把四张图拼成一张能让小目标在拼接后被放大出现在画面中间对安全帽这类小框很有效。但它同时会让单张图里的目标尺度变小所以训练末段必须close_mosaic关掉让模型回归真实分布——这条几乎是标配不关的话最后几轮指标会突然抖一下。还有一种情况是夜间和逆光样本少。与其硬增强不如在数据层面补同一场景的白天帧做 gamma 变换生成伪夜间图v通道压暗、加轻微噪声比让模型从零学光照不变性省事得多。3.3 类别不平衡与负样本的补法工地场景的天然分布是戴帽的人多、未戴帽的少、穿反光衣的更少。直接训出来的模型倾向于看见人就报戴帽因为这样损失最低。处理手段有三种按性价比排序第一是补负样本。把有人的场景但没有任何违规目标的图片做成空 txt 放进去比例控制在正样本的 5%~10%。这类图专门教模型这场面没问题别报警对压误检立竿见影。很多人忽略这一点结果模型在空场景里也能框出一堆不存在的帽子。第二是重采样。把 no_helmet / head 这类稀有类的图片复制一份参与训练注意要复制整图整标注不能只复制框否则同一张图里的其他目标会被重复计数。采样倍率控制在 2~3 倍再多就是过拟合这几张图。第三才是损失层面加权Ultralytics 一脉没有直接暴露类别权重参数常规做法是自定义损失或在 dataloader 里做加权采样。我的经验是前两招做到位第三招很少需要动。顺带说一个类别设计的坑helmet安全帽和head人头这两类在空间上高度重叠IoU 常年大于 0.5。如果你的业务只需要判断戴没戴其实可以把 head 当成未戴帽的唯一类把 helmet 单独一类而不要同时标注 person 和 head否则 NMS 阶段会互相压制出现莫名其妙的漏框。4. YOLO 训练链路落地配置、超参与日志判读数据理顺之后训练本身反而是最短的一段。但配置写错、显存爆掉、看不懂日志这三件事足以让一个新手在第一天就放弃。这一章把配置拆到字段级把显存降级路径列清楚。4.1 data.yaml 与训练命令的写法data.yaml 只有六个字段但有两个容易写错# configs/ppe.yaml path: /data/ppe # 数据集根目录绝对路径最稳 train: images/train # 相对 path不要写 ./images/train val: images/val test: images/test names: 0: helmet 1: head 2: vest 3: personpath用绝对路径是因为训练脚本可能在别的目录启动相对路径一错就是找不到图片而报错信息往往只写数据集为空排查半天。train和val写相对路径时不要带./部分版本解析会出问题。names的键必须是整数且从 0 连续中间断号会让类别 id 错位。训练命令yolo detect train \ dataconfigs/ppe.yaml \ modelyolov8s.pt \ imgsz960 \ epochs120 \ batch16 \ workers8 \ device0 \ patience30 \ optimizerSGD lr00.01 lrf0.01 \ hsv_h0.012 hsv_s0.5 hsv_v0.5 \ mosaic0.8 close_mosaic15 \ scale0.5 degrees3 \ cacheram \ projectruns/ppe namev1 exist_okFalse逐项说imgsz960是为小目标让路显存不够时优先降 batch 而不是降分辨率。patience30是早停轮数验证指标 30 轮不涨就停避免无意义烧卡。lr00.01配合 SGD 是检测任务的稳妥起点换成 AdamW 时降到 0.001 量级。cacheram把图片预加载进内存数据量在几千张时能明显提速内存小于 32G 就改cachedisk。exist_okFalse防止两次实验写进同一个目录互相覆盖权重。4.2 超参与显存不够时的降级路径显存爆掉是最高频的报错CUDA out of memory一出现很多人的第一反应是把 batch 砍到 4结果 BN 统计不稳指标掉得很难看。正确的降级顺序是先降batch从 16 到 8再不行到 4再开ampTrue混合精度默认开如果手动关了要打开再降imgsz960 到 768 再到 640这是最后手段因为直接影响小目标最后才动模型规模yolov8s 换 yolov8n。batch 小于 8 时把optimizer换成 AdamW 并降低学习率小 batch 下 SGD 的梯度噪声太大。另外workers设成 CPU 核数的 0.75 倍左右设太高反而因为进程切换变慢Windows 上还要注意多进程启动的保护问题一般设 0 或 2。注意close_mosaic的值必须小于epochs设成 15 表示最后 15 轮关闭 mosaic。如果 epochs 只有 30最后一半时间都在真实分布上训前面的增强收益会被冲淡这时候把 epochs 提到 100 以上更合理。4.3 训练日志怎么看、什么时候该停日志里真正要看的是四组数box_loss、cls_loss、dfl_loss分布式焦点损失管框回归质量、metrics/mAP50、metrics/mAP50-95。判断标准不是绝对值是两条曲线的相对走势。正常收敛是三条 loss 都单调下降并逐渐平缓mAP50 上升后进入平台。如果cls_loss还在降而box_loss开始往上翘说明分类在过拟合、定位在退化通常是标注框不一致导致的——同一批图里有人框得紧、有人框得松回归目标自相矛盾。这时候该回去复查标注规范而不是调学习率。如果train/box_loss持续下降而val/box_loss从第 N 轮开始上升就是标准过拟合处理顺序是先加数据补负样本、补稀有场景再加大增强最后才考虑减模型容量或加 weight_decay。还有一个隐蔽信号mAP50 很高但 mAP50-95 很低。说明框大概位置对了但边界不准这在安全帽这种小目标上很常见本质是标注框尺度不统一。可以统计一下每类框的宽高分布如果 helmet 类的宽度分布出现明显的双峰那基本就是两批人标的。断点续训用resumeTrue指向last.pt所在目录即可它会连优化器状态和学习率调度一起恢复。别用modellast.pt重开训练那样只加载权重不恢复调度学习率会从头开始曲线会突然掉一截很多人以为是模型坏了。5. 避坑与排查标注、增强、评估里最常见的五个翻车点这一章是我自己在不同项目里反复踩过的按现象 → 原因 → 解决记录遇到对应症状直接对照查。5.1 数据与标注类问题现象一训练能跑但 mAP 长期卡在 0.3 以下且各类指标都很差。原因坐标没有归一化或者归一化时用了错误的宽高比如把图片的 EXIF 旋转后的尺寸当成了原始尺寸。YOLO 只接受 0~1 的值超出范围不会报错框会跑到图像外。 解决跑 2.2 的体检脚本看bad里的坐标越界项。同时用 PIL 读一遍图片真实尺寸和转换时用的宽高比对尤其是手机拍摄的竖图EXIF 里带旋转标记OpenCV 读出来是横的、PIL 读出来是竖的两边不一致就全错。现象二模型对戴帽的人框得很准但几乎从不输出未戴帽类。原因类别极度不平衡加上 helmet 和 head 空间重叠NMS 阶段 head 框被 helmet 框压制掉了。 解决先看cls_cnt里 head 类的绝对数量少于 helmet 的 1/8 就要重采样再检查 NMS 的 IoU 阈值默认 0.7 在高度重叠的类别上偏严可以提到 0.75~0.8 观察变化如果业务只需要是否违规考虑合并成帽子/人头两类去掉 person 类减少干扰。现象三验证集指标漂亮换一个摄像头画面全崩。原因数据划分时相邻帧泄漏或者整个数据集只来自一两个机位模型学到了机位特征而不是目标特征。 解决按 3.1 的分组方式重划确认验证集里没有和训练集同源的帧再从新机位的视频里抽 200 帧做独立测试集这个数字才是真实水平。5.2 训练与部署类问题现象四训练到一半 loss 变 NaN或者框突然全部塌缩到一个点。原因学习率过高或者标注里存在宽或高为 0 的退化框。退化框会让宽高回归的 log 计算出负无穷。 解决体检脚本里加一条w 0 or h 0的过滤这类框直接丢弃学习率降到 0.005 重跑配合warmup_epochs3让前几轮平滑起步。已经出现 NaN 的权重没法救只能从头训。现象五训练指标 0.9部署到边缘设备上推理漏检翻倍。原因训练用了 letterbox 填充到 960×960部署时预处理直接 resize 成 960×960 没保持长宽比目标被拉伸变形或者训练开了 TTA、部署没开再或者量化成 INT8 后小目标的激活值被截断。 解决核对训练和推理的预处理必须完全一致重点是 letterbox 的灰边填充量化时保留检测头的输出层为 FP16只量化主干上线前用同一批图分别跑 PyTorch 和部署引擎逐图对比框数和类别差异超过 3% 就回去查预处理。6. 上线前的验证mAP 之外的两个数字mAP 是排序指标回答的是整体上框得准不准但业务方问的是漏了几个、误报几次。这两个问题 mAP 答不了得自己做阈值定标和分场景切片。具体做法是把测试集按业务关心的维度切成几个子集——白天/夜间、近景/远景、正面/背面、单人/密集——每个子集单独跑一遍输出不同置信度下的召回和误报数。下面这段扫描能直接给出拐点from ultralytics import YOLO import json model YOLO(runs/ppe/v1/weights/best.pt) rows [] for conf in [0.15, 0.25, 0.35, 0.45, 0.55, 0.65]: m model.val(dataconfigs/ppe.yaml, splittest, confconf, iou0.6, verboseFalse) rows.append({ conf: conf, precision: round(float(m.box.mp), 4), recall: round(float(m.box.mr), 4), mAP50: round(float(m.box.map50), 4), }) print(json.dumps(rows, ensure_asciiFalse, indent2))这段脚本扫的是全局阈值重点看召回从哪个点开始掉崖。合规检测类业务通常宁可误报不可漏报所以阈值要选在召回还没明显下滑的位置比如 0.25 而不是 0.5。选定之后再到各子集上单独验证一遍夜间子集的召回如果比白天低 20 个点以上说明数据里夜间样本不足得回去补数据而不是继续调参。还有两个容易被忽略的细节。一是推理尺寸训练用 960 部署就尽量保持 960改成 640 会直接损失小目标召回省下的那点算力不值。二是 NMS 的 IoU 阈值密集工位场景下人头挨得近0.7 会把相邻的两个头合并成一个可以适当提到 0.75同时观察误检是否上升。血泪经验是我曾经拿着 0.92 的 mAP 去汇报现场演示时一个逆光工位连续漏了三帧场面很难看。从那以后我每次交付前都强制走一遍——分场景切片、逐子集定阈值、PyTorch 与部署引擎逐图对比三样缺一不可宁可多花半天也别在现场赌运气。这份数据集的用法也是同理把它当起点而不是终点补上你自己场景的那部分样本模型才真正属于你。希望帮到你。本文还有配套的精品资源点击获取