ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零构建YOLO安全帽反光衣工作服检测数据集实战指南

从零构建YOLO安全帽反光衣工作服检测数据集实战指南 简介这是基于YOLO的安全帽、反光衣与工作服自动识别数据集面向施工现场、化工园区、工厂车间等需要人员规范着装监管的场所解决人工巡检效率低、难以及时发现隐患的问题。包内共2000个文件含1999个txt格式的YOLO标注文件和1个yaml配置文件压缩包约952MB标注内容覆盖不同角度、光照、距离及作业环境下的工人图像可直接用于YOLOv5、YOLOv8等系列模型训练。配合OpenCV等工具可将模型接入监控视频流实时判断人员是否正确佩戴安全帽、穿戴反光衣或工作服一旦发现违规立即触发告警、抓拍与弹窗提示帮助管理人员快速响应实现事前预防、事中常态检测、事后规范管理。当前已有1599人学习浏览适合计算机视觉初学者快速上手也适合工业安全项目开发者直接获取规范数据省去自行采集标注的时间成本是落地人员安全检测应用的实用基础资源。1. 安全帽/反光衣/工作服自动识别数据集先搞清楚它解决什么问题施工工地装了十几个摄像头画面里有人没戴安全帽、有人反光衣穿了一半靠安全员盯屏根本盯不过来。做自动识别第一反应是去下载一个现成的 yolo 预训练模型跑通之后却发现它在你这个工地上的误检率高得吓人问题几乎都出在数据集和现场场景对不上。所谓“基于YOLO的安全帽/反光衣/工作服自动识别数据集”不是简单地把网图堆在一起而是围绕真实机位、真实光照、真实作业状态组织起来的一组带标签图像。它直接决定模型能不能收敛、部署后误报多不多也决定了整个项目的成本上限。这篇文章就讲清楚怎么从零把这份数据做出来并喂给 YOLO 训练适合正在做工地智慧监管、园区 EHS 或相关毕设的工程师。2. 现场数据采集与场景分布7 个决定数据集质量的现实问题2.1 抽帧策略为什么连续视频帧不能直接进数据集常见做法是把工地录像拿回来抽帧。但很多人图省事用 ffmpeg 每秒抽几帧结果几千张图里全是同一批工人同一个角度在同一个位置干活模型很快就背下这段视频而不是学会“安全帽”这个概念。我一般会按以下方式抽每个摄像头取 5 到 10 分钟的有效作业片段每 3 到 5 秒抽一帧并且保证不同片段覆盖不同机位、不同人脸朝向和历史事件。抽完还要按画面内容再做一次相似度粗筛连续两帧 IoU 过高的直接丢弃宁可数量少一点也不要让重复样本占据主导。抽帧的另一个关键点是画面比例。YOLO 训练时会把图像 resize 到固定尺寸如果原图大部分是围墙、天空、堆料区而目标只占很小一块模型学到的背景特征会把分类带偏。所以抽帧之外还要做一次“有效区域裁剪”把每条枪真正覆盖的作业面保留下来。对 1080p 的画面我的经验是目标高度低于 40 像素的帧基本没有标注价值删掉比硬标更划算。这步做得好后续标注工作量能少三分之一。还要提一个容易忽略的事不要把系统时间抽掉。工地的光照在早上、中午、傍晚差别极大抽帧脚本里最好按时间分桶保证每个时段都有样本入库。夜间帧如果太少后面会发现模型对“亮斑”极其敏感这属于数据分布问题不是模型问题。按摄像头编号保存目录是最基本的操作因为后面划分 train/val/test 时要按视频或摄像头切不能按帧随机切否则评估结果会虚高。2.2 标签体系安全帽、反光衣、工作服是“单框”还是“双框”标注之前必须先定标签体系这比框得准不准更影响模型行为。安全帽检测在业界有两套主流口径一是“头帽”双框即分别标戴帽头和不戴帽头二是“人属性”整体框用类别区分戴帽与未戴帽。双框的优点是目标小、定位准缺点是标注量几乎翻倍整体框标注快但人挨着人的时候边界会串。做反光衣和工作服又有另一层逻辑这两种穿戴是附着在躯干上的没有独立行为逻辑我一般直接打在人身上或躯干框上。我比较推荐的类别结构是三到四类起步0: helmet_head # 戴安全帽的头 1: head_no_helmet # 未戴安全帽的头 2: reflective_clothes # 穿反光衣的人/躯干 3: work_uniform # 穿工作服的人/躯干这个结构把“头”和“躯干”分开是考虑到反光衣和工作服可能同时穿。如果一个人既穿反光衣又穿工作服躯干区域会同时出现两个标签用单标签体系无法表达而 YOLO 的标签文件支持同一目标重复出现在不同类别里。另外“安全帽拿在手上”不算戴帽“放在旁边椅子上”更不算这些边界必须在标注规范里写明不然不同标注员的口径差异会让训练集自带噪声。还有一个值得提前做的调整如果部署设备是 rk3588 之类的边缘盒子分辨率通常只有 640 或更低躯干类目可以只标“反光衣”而不标“普通衣服”因为普通衣服类别会让类别数量变多而边缘部署时小目标分类压力更大。等到模型能跑稳再加“无防护”这种负类也不迟。2.3 场景配比时段、机位高度、距离、遮挡和天气怎么搭数据集的场景配比应该跟着部署点位走。你在哪个工地部署就用那个工地的录像做主料再从公开数据或其它工地补齐变量。我一般把场景维度拆成五张表来统计缺哪补哪。这里的“缺”不是靠肉眼感觉而是用标签分布统计脚本去数维度设计目标过少会出现的问题时段白天 60%、傍晚 20%、夜间 20%夜间反光衣漏检或高亮误检机位高度1.5m 手持、3m 立柱、6m 杆顶俯视角度下人头重叠AP 虚高后部署崩目标距离近景 5m、中景 5-15m、远景 15m只学到近景大目标小目标全丢遮挡无遮挡 60%、轻度遮挡 30%、严重遮挡 10%人群密集处互相遮挡时模型输出抖动天气晴、阴、雨、雾各若干对亮度变化不鲁棒雨天误检明显上升配比不是均匀的而是“尽量贴近现场”。如果现场有两台球机、八台枪机数据就按 2:8 的比例来配如果晚上灯火通明夜间样本甚至可以提到 30%。为了让这套配比能落地我习惯在标注前先建一个 scene.csv每个文件记录 camera_id、time_bucket、distance_level 三个字段标注完成后统计维度分布缺哪个补哪个这样不会在训练完才发现某个场景是空的。“目标过少”这个坑也值得多说几句。工地场景里真正的难点不是戴帽和不戴帽而是“不戴帽”这个负样本天然稀少因为现场管理严格的时候大家都会戴。怎么办我会主动制造困难样本让工人配合摘掉帽子走一段或者把帽子拿在手上、挂在护栏上标成负例。数据集里负例太少模型会倾向于把所有头都判成戴帽mAP 数字很漂亮实际安全检查等于没做。3. 标注规范与格式转换从标注软件到 YOLO 训练集要过的四道关3.1 目录结构与 data.yaml 的最小约定VOC 时代的数据集习惯是 JPEGImages、Annotations、ImageSets 三个目录并列而 YOLO 训练自己的数据集更常用 images 与 labels 对等目录。一个最少可用结构如下dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/每个标注文件与图像同名扩展名不同。比如图像是cam03_063000_071.jpg对应标签就是cam03_063000_071.txt。这个同名约定看着简单却是后面所有脚本的根基一旦破坏了随机划分、格式转换、分布统计全要重写。data.yaml 是最容易被新手漏掉的一环它不复杂但路径必须写绝对路径或相对项目根目录的路径path: /data/ppe_dataset train: images/train val: images/val # test: images/test # 有测试集再开 nc: 4 names: 0: helmet_head 1: head_no_helmet 2: reflective_clothes 3: work_uniformnc 和 names 的顺序就是标注文件里 class_id 的顺序训练时改了 names 顺序就要重新生成标签否则模型学到的语义全是乱的。有一种常见情况是标注平台导出的类别列表带中文或类别顺序按字母排序这都会造成 id 错位我每次都会在转格式后跑一次计数脚本核对每类数量是否符合预期。3.2 VOC 转 YOLO 的归一化脚本与边界处理很多标注软件默认导出 VOC 格式的 XML而 YOLO 要求每行一个目标的class_id center_x center_y width height中心点坐标和宽高都是除以图像宽高后的归一化值。转换脚本并不难但边界处理是血泪经验密集区。直接上我常用的脚本import os import xml.etree.ElementTree as ET from pathlib import Path def convert_voc_to_yolo(xml_path: str, out_dir: str, class_map: dict) - None: tree ET.parse(xml_path) root tree.getroot() img_name root.findtext(filename) img_w int(root.find(size).findtext(width)) img_h int(root.find(size).findtext(height)) if img_w 0 or img_h 0: print(f[skip] {xml_path}: image size zero) return txt_name Path(img_name).stem .txt out_path os.path.join(out_dir, txt_name) lines [] for obj in root.iter(object): cls_name obj.findtext(name) if cls_name not in class_map: print(f[warn] {xml_path}: unknown class {cls_name}, skip) continue box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 关键越界修正不是直接丢弃 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)) if xmax xmin or ymax ymin: print(f[warn] {xml_path}: invalid box after clip, skip) continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_map[cls_name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines) \n) # class_map 必须和 data.yaml 中的 names 顺序保持一致 class_map { helmet_head: 0, head_no_helmet: 1, reflective_clothes: 2, work_uniform: 3, }脚本逻辑有四个关键点。第一未知类名选择跳过并打印告警而不是抛异常中断这样能一次性看完整个数据集的脏数据分布第二坐标越界会修正到图像边缘而不是直接丢弃人工标注时鼠标拖拽很容易让框超出画面几个像素第三xmax xmin这种反框必须丢弃这种框来自标注软件里误操作拖反方向第四归一化保留 6 位小数不要用整数除法也不要四舍五入到 3 位否则训练时 anchor 匹配会受影响。转换完成后我会顺手做一个标签分布统计输出每个类别多少框、多大面积、长宽比分布。这一步能发现两种问题一是某个类别框数明显异常二是所有框的长宽比都集中在某个奇怪区间说明标注框的语义不对。这些检查脚本加起来不到 50 行但能避免模型训到一半才发现数据是脏的。3.3 val/test 的划分原则按摄像头编号切不按帧随机切划分看似只是一行 shuf实际上是最容易“不吃教训”的地方。如果直接把所有样本随机分成 train 90%、val 10%那么同一个摄像头同一条视频里第 5 帧在训练集、第 8 帧在验证集评估指标 mAP 会好看很多因为模型已经“见过”几乎相同的画面。但现场部署时是全新的画面效果立刻打回原形。常见的正确做法是“按视频片段”划分。每个片段来自某个摄像头的某段时间先把片段列表按摄像头编号分组然后整个片段进入 train 或 val。代码上就是把一个video_id字段作为划分单位# 先按 video_id 分组再随机抽样 python - EOF import random, os from collections import defaultdict groups defaultdict(list) for name in os.listdir(images/train_all): video_id name.split(_)[0] # 文件名假设为 cam03_063000_071.jpg groups[video_id].append(name) all_videos list(groups.keys()) random.seed(42) random.shuffle(all_videos) val_videos set(all_videos[: max(1, len(all_videos) // 10)]) print(ftrain videos: {len(all_videos) - len(val_videos)}, val videos: {len(val_videos)}) EOF这里只做演示实际划分时还要再保证 train 和 val 里的时段分布大致一致比如不能把所有夜间样本都分到 val否则训练时完全看不到夜间模式验证时又全是夜间指标自然上不去。划分粒度与部署场景相关如果你要在新工地做测试甚至可以专门留一个完整工地作为 val这样的指标才有部署参考价值。4. 样本平衡与数据增强要把误检率压下来只剩这两条路4.1 类别极不平衡先加类别权重还是先过采样安全帽数据集最常见的不平衡是“戴帽头”远多于“未戴帽头”因为工地上大部分人都规范作业。反光衣和工作服也可能出现某种颜色、某种款式占绝对主导。面对不平衡我一般先看正负样本比例如果差一个数量级以内优先用过采样补齐差两个数量级以上再加 loss 权重。YOLOv8 的 loss 由 bbox loss、cls loss 和 dfl loss 三部分组成类别权重主要作用在 cls loss 上。在训练配置里给少数类更高的 loss weight通常是多数类权重设为 0.5 到 1.0少数类设为 2.0 到 4.0。但要注意过高的类别权重会让模型对少数类过拟合见过很多人把未戴帽权重拉到 10结果戴帽的头反而被大量误判为未戴帽。所以权重只做辅助首要手段还是回灌数据。人工采集困难时可以借助伪标签先用当前模型在未标注视频上跑推理凡是未戴帽且置信度高于 0.85 的框人工复核后进数据集。类别不平衡还要看“误检方向”。工地安全帽系统的痛点往往不是漏检而是把安全帽错判成普通头、或把普通头误判成安全帽。这类错误在混淆矩阵里表现为两个类串原因通常是样本里戴帽头的环境和角度太少。面向现场复现时我会专门从相似背景里多采负样本而不是简单复制已有正样本因为复制不会增加背景多样性。4.2 增强参数档位Mosaic、HSV、翻转的推荐边界YOLO 自带增强配置但很多人的第一版数据集增强参数是默认值直接训这样不是不行只是容易踩到“过增强”和“欠增强”两个极端。我用的起点参数如下参数推荐值作用调参边界mosaic1.0四图拼接提升小目标关掉后 mAP 可能掉 1-3 个点mixup0.0-0.2图像叠加正则化数据本身杂时开高会糊语义hsv_h0.015色调扰动反光衣颜色敏感超过 0.02 可能串色hsv_s0.7饱和度扰动过低会让彩色反光衣欠拟合hsv_v0.4明度扰动对夜间样本影响大fliplr0.5水平翻转安全帽左右对称可以开fliplr_ud(flipud)0.0垂直翻转地面视角下不要开语义异常scale0.5随机缩放与 mosaic 搭配时不宜过大translate0.1平移超过 0.2 会让目标频繁出界这里最值得关注的是 hsv 三兄弟。安全帽有黄色、红色、白色、蓝色等多种颜色反光衣通常也是荧光黄或荧光橙饱和度是区分它们的重要线索但工地水泥地偏灰、夜晚灯光偏黄过大的 hsv 扰动会让模型把“颜色”当唯一特征忽略形状和纹理。我的习惯是 hsv_h 从 0.01 起逐步加到 0.02 并每轮看 val loss 是否下降如果某个类别误检明显增加就退回上一档。还有一个容易踩的坑mosaic 会改变目标的像素尺寸四张图拼在一起后每个目标只有原来的 1/2 甚至 1/4 大小。对安全帽这种小目标反而有帮助因为迫使模型学会看小目标但反光衣这种大面积目标在 mosaic 下容易丢框标签里大目标的边缘会被切掉。此时可以配合copy_paste增强把剪裁出来的目标粘贴到别处而不是无脑用 mosaic。4.3 小目标增强与自动锚框别让增强效果过拟合AutoAnchor 是 YOLO 里一个只在训练开始时运行的功能它根据当前标签框的尺寸分布重新聚类锚框。数据集扩充或引入新视角后框的尺寸分布会变如果又改回了默认锚框前几十轮收敛会很慢。常见做法是训练参数里写python train.py --data data.yaml --epochs 200 --batch 16 --img 640 --patience 30当出现损失下降很慢但没跑崩时查一下runs/train/exp*/anchors.png看锚框聚类是否覆盖了大部分框的面积区间。反光衣从远处看是一个竖长条形从近处看是一个大矩形两者面积差可能超过 10 倍。如果你发现锚框集中在中等尺寸说明近景目标被模型当成“半身照”来学部署后人物走近画面时检测框会忽大忽小。“小目标增强”也不是越多越好。有些项目会强行把输入分辨率从 640 提到 1280结果小目标确实更强但 rk3588 这类边缘设备推理速度直接砍半部署时还得降回来等于白训。所以训练分辨率的设定要从部署设备的算力反推先定部署分辨率再回去配增强策略。显存不够的时候很多人会用 v100 或开源训练平台跑本地和云端的分辨率、增强版本必须保持一致否则验收时总是对不上。5. 避坑安全帽/反光衣数据集常见的 5 个翻车现场5.1 夜间反光衣被误检成“灯”或“人”现象白天模型表现正常夜间视频里车前灯、路灯、甚至反光标识都被框成反光衣误检率翻倍反过来真正穿反光衣的工人站在灯光下框却一直在抖。原因训练集夜间样本太少模型把“高亮区域”当成了反光衣的特征施工现场的灯具和反光衣亮度分布相似模型分不清。解决把夜间样本补齐到 20% 以上并且重点加入“有亮斑但没穿反光衣”的负样本标注时把反光衣的“非反光面料”区域也框进来让模型学会看整体轮廓。还有一个小技巧夜间样本用同一个场景多拍一段但人为调节曝光让模型不要依赖单一亮度。5.2 val 和 train 同源mAP 高但现场全崩现象训练完查看 val mAP0.9 以上部署到现场却几乎不可用漏检一抓一大把。原因最常见的还是划分方式错了。随机按帧划分让 val 里混入了同一视频的相邻帧模型相当于“提前看过答案”另外还有一个隐蔽问题就是标注样本本身过度集中在一个工地而现场是另一工地。解决严格按视频片段划分val 里的画面在训练阶段完全不可见如果条件允许留一个完整工地做 external val。评估时把 val 换成一组完全没参与过训练的现场视频重跑yolo val出来的 mAP 才是真实水平。5.3 训练中 loss 突变或者 BN 崩溃现象训练到第 80 轮左右loss 突然从 0.05 跳到数十甚至 nan权重文件再往下训就坏了。原因这通常是学习率过大或 batch size 过小导致 BatchNorm 统计量震荡yolo 训练中 bn 崩溃在单卡小 batch 下尤其常见有些云平台的混合精度策略也会放大数值问题。解决把 batch size 提到 16 以上学习率按 0.01 乘以 batch/64 折算如果显存不够就降输入分辨率而不是降 batch。训练脚本里开启 warmup 并限制前 5 轮不跑增强给 BN 一个稳定统计的机会。出现 nan 时不要继续训直接回到上一个正常 checkpoint 重启。5.4 边缘设备压输入尺寸后误检率变高现象rk3588 上用 int8 模型跑实时推理帧率上去了但小目标和密集目标大量漏掉。原因训练用 640 输入部署时为了省算力压到 320安全帽在 320 分辨率下只有十几个像素特征已经丢了int8 量化又把一部分敏感特征压没了。解决如果目标设备是边缘盒子一开始就按 416 或 480 分辨率训练验证在当前分辨率下的 mAP 再谈优化量化后用几百张真实场景图做校准集重新跑一次量化不要拿训练集替代校准集因为两者的特征分布并不一致。yolo 边缘部署监控误检率高的问题多数出现在这一步。5.5 标注框越界与归一化精度丢失现象训练开始前检查 label 发现某些中心点坐标大于 1或宽高为 0训练时 loss 偶尔跳高还有的框整体向右偏移。原因标注软件导出坐标时按原图算而脚本里图像宽高取错了或者归一化时用整数除法结果全部变成 0。解决转格式脚本里对每条标注做范围检查越界修正到 0-1 区间并打印日志再用一个检查脚本扫描所有 txt 标签任何一行出现负值、大于 1、宽高小于 0.001 都直接报错。这类检查要放到训练前而不是训练跑到一半才发现。6. 用验证结果反过来迭代数据集先看混淆矩阵再回灌难样本6.1 混淆矩阵里的“总和不唯一”和串类信号训练结束后第一件事不是看 mAP而是打开runs/val/exp*/confusion_matrix.png。有人会发现这个矩阵每一行的值加起来不等于 1于是怀疑是 bug。其实 YOLO 的 val 流程里混淆矩阵默认按行归一化后再画图四舍五入到小数点后两位行和是 0.99 或 1.01 都正常。真正要看的是对角线以外的串类格子。比如head_no_helmet与helmet_head互相混多半是安全帽样本中帽檐和头发遮挡太多如果reflective_clothes被大量认成背景说明反光衣目标太小或标注框太松。6.2 用伪标签回灌难样本数据集滚起来的闭环一个可以让数据集持续变好的技巧是把部署当天的误检帧回灌。具体做法是跑一段真实视频的推理把模型置信度低于 0.6 但人工确认确实是目标、或置信度高于阈值但明显是误检的帧保存下来先用上一版模型生成初始框再人工修正最后转成 YOLO 标签。这样每一轮部署都在补充当前模型最缺失的“困难样本”第二轮训练后误检率通常能下降三到五成。这个过程优势在于成本可控我做的每个工地项目数据集构建都占七成精力训练只占剩下的零头但收益也恰恰来自这里。迭代时要注意版本管理。每版数据集都改个版本号并记录当时的分辨率和增强参数避免换了设备或模型后无法对比。我的习惯是把dataset_v3/这种目录建好里面同时放一段 README 记录采集时间和场景配比防止一个月后自己都忘了当初做了哪些负样本。希望这个从采集、标注、转换到验证回灌的流程能帮到你少走我当年走过的弯路。本文还有配套的精品资源点击获取
返回列表