PPE安全帽检测实战:从数据集构建到YOLO训练部署
发布时间:2026/9/30 10:04:04来源:尧图网络
简介面向施工工地、工厂等安全管理场景的YOLO安全帽/反光衣/工作服自动识别数据集支持对现场监控画面中人员的着装佩戴情况进行实时检测与预警发现未按要求穿戴时及时触发告警、抓拍和弹窗提示有助于降低安全隐患。压缩包共包含2000个文件其中1999个txt标注文件用于存储各目标的检测框类别与归一化坐标信息另含1个yaml配置文件用于指定数据集路径和类别名称整体约952MB标注格式规整可直接接入YOLOv5、YOLOv8等主流检测框架供模型训练与验证。目前已有1599人学习下载适合目标检测初学者和工程落地开发者快速构建人员安全着装识别模型。借助这套数据可以省去大量数据采集和人工标注成本快速迭代出适配施工、工厂等场景的反光衣、安全帽和工作服检测方案为实现事前预防、事中常态检测、事后规范管理提供数据支撑。1. 安全帽识别做不起来八成死在数据集上一个工地装了二十路摄像头算法跑了三个月漏检最多的不是没戴安全帽的人而是远处戴了白色安全帽、站在白色墙面前的人——模型把帽子和墙一起当成了背景。做基于YOLO的安全帽/反光衣/工作服自动识别真正卡住落地的不是YOLO版本选哪个、损失函数怎么调而是数据集有没有覆盖现场的真实分布。下载一个现成的安全帽数据集训完就上线的路子我试过测试集mAP50看着有0.85拿到工地现场直接掉到0.4。这个方向适合两类人要自己做安全管理的集成商和想把PPE检测做到能交付、能迭代的算法工程师。下面从数据集构建讲起一直讲到部署阶段怎么把误检压下去。2. 从现场到标注PPE数据集采集、清洗与标注规范2.1 先定类别边界反光衣、工作服和不知道算什么的样本很多人建数据集的第一步就是打开标注工具把看到的每一个人都框上安全帽、反光衣或工作服。这个做法会在训练阶段埋雷。类别边界不定义清楚模型学到的不是安全帽这个概念而是这一类样本的视觉近似平均值。我一般这样定义三类目标的标注条件安全帽必须戴在头顶才算正样本拿在手里、挂在墙上、放在桌上的都不算反光衣必须穿在身上且反光条可见才算正样本叠好放在旁边的算背景工作服最难定义因为颜色和款式每个工地都不一样只能约定同一场景下带反光条的深色工装算正样本。这样定的原因是推理阶段你要报警的是人没戴帽子、没穿反光衣这个事件而不是画面里出现了帽子。还有一个很多人忽略的点就是背景负样本怎么标。不要给没有人的图片强加一个背景框YOLO不需要你用框标背景它在训练时通过cls loss自动学习背景特征。但你要在数据集中保留一部分纯背景图这部分图能让模型在预测时对非目标区域输出低置信度后续减少误检就靠它。2.2 样本采集监控视频抽帧比网络爬图靠得住网络上的安全帽图片大多正面、光线好、目标居中训出来的模型一遇到俯拍监控画面就翻车。工地真实场景的特点是摄像头装在高处俯拍、目标远、角度刁、光线强或者暗。所以样本的首要来源应该是现场监控视频次选才是公开数据集和爬图。对监控视频做抽帧我用的是ffmpeg按帧率抽间歇抽帧比连续抽帧更划算——连续帧之间的画面变化太小还会造成训练集和验证集之间数据泄漏。一个可用的做法是每5秒抽一帧一个小时的视频能出720帧人工粗筛后保留200到400帧有效画面。ffmpeg -i input.mp4 -vf fps1/5,scale1280:720 -q:v 2 frame_%04d.jpg这条命令每5秒抽出一帧统一缩放到1280x720-q:v 2控制输出jpg质量在较高档位。抽完帧后要做一次人工清洗模糊的删掉、重复场景只留一帧、画面里目标小于30x30像素的单独归类。不要指望算法帮你清洗人眼过一遍2000张图十分钟的事换来的是训练时少踩很多坑。2.3 用LabelImg做一版YOLO格式标注的最小流程标注工具我用LabelImg它轻量、支持YOLO格式直接输出适合几百到几千张的中小型数据集。安装和启动流程如下pip install labelimg labelimg打开软件后先用Open Dir选择图片文件夹再用Change Save Dir选择标注输出目录。关键一步是在顶部菜单里把标注格式从PascalVOC切到YOLO这样保存出来的就是txt文件省掉后面转换格式的步骤。按下W开始画框画完按D切到下一张图A切回上一张CtrlS保存当前标注。这里有一个容易犯的错YOLO格式的标注类号是按照标注工具左侧的类别列表顺序定的所以从第一张图开始就要保证类别顺序固定。我的习惯是类别列表固定写成helmet、reflective_clothes、work_clothes三个顺序永远不变。标注时框要贴着目标边缘安全帽只框帽体部分不要连着人的头部一起框进去否则模型学到的就是头而不是帽子。数据集的规模方面我给出一个参考下限每类目标至少500个实例整体图片数1500到3000张背景图占10%到20%。低于这个规模你训出来的模型只能算demo拿去现场交付验收会返工。3. 数据划分与格式转换让训练集真正代表工地现场3.1 按场景划分训练验证集别用随机划分YOLO训练时如果不显式指定验证集默认会从训练集里切一部分出来。很多教程推荐随机划分但PPE检测场景里随机划分是个陷阱。同一段监控视频抽出来的连续帧画面背景几乎一样随机划分后训练集和验证集里各有一半相同的背景验证集mAP会虚高现场一换场景就露馅。我一般按视频片段或者按拍摄日期划分。来自同一路摄像头、同一个下午的帧全部进训练集或全部进验证集不做混分。这样验证集反映的是模型面对未见场景的真实水平。import os import random import shutil from collections import defaultdict img_dir images train_dir train val_dir val os.makedirs(train_dir, exist_okTrue) os.makedirs(val_dir, exist_okTrue) # 按文件名前段作为场景编号例如cam01_0023.jpg属于cam01场景 videos defaultdict(list) for fname in os.listdir(img_dir): scene fname.split(_)[0] videos[scene].append(fname) for scene, files in videos.items(): random.seed(42) random.shuffle(files) split int(len(files) * 0.8) for i, fname in enumerate(files): dst train_dir if i split else val_dir shutil.copy(os.path.join(img_dir, fname), os.path.join(dst, fname))这个脚本以文件名的前缀作为场景ID将每个场景内的图片按8:2划分到train和val目录。random.seed(42)保证每次运行划分结果一致。如果你的文件名没有场景前缀可以在采集阶段就按cam01_20231011这种格式命名后面会省很多事。3.2 把VOC/JSON转成YOLO的txt标注转换脚本与四个边界坑从公开数据集拿到的标注大多是VOC XML或者COCO JSON格式要转成YOLO用的txt。转换逻辑不难真正麻烦的是边界情况。下面这段是VOC转YOLO的常用脚本我加了两处防御逻辑。import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, out_txt_path, class_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue class_id class_map[name] box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 边界1坐标越界裁剪 x1 max(0, min(x1, width - 1)) x2 max(0, min(x2, width - 1)) y1 max(0, min(y1, height - 1)) y2 max(0, min(y2, height - 1)) # 边界2宽高为0的非法框直接跳过 if x2 x1 or y2 y1: continue x_center (x1 x2) / 2 / width y_center (y1 y2) / 2 / height box_width (x2 - x1) / width box_height (y2 - y1) / height lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) class_map {helmet: 0, reflective_clothes: 1, work_clothes: 2}这段脚本从VOC XML里读取图片宽高和目标框坐标将绝对像素坐标归一化为YOLO需要的中心点加宽高格式。class_map里没出现的类别名会被跳过这保证了类别顺序不随XML里的标签顺序变化。核心参数是归一化除数width和height一旦图片实际尺寸和XML里不一致所有框的位置都会偏。转换时值得注意的四个坑第一XML里的坐标可能因为标注手抖超出图片边界导致训练时解析报错要做裁切处理脚本里max/min已经做了第二一个XML文件里如果所有目标都被continue跳过生成的txt文件是空的——这没问题但别删掉这个txtYOLO要求每一张训练图片都有对应的txt哪怕内容是空的第三图片是灰度图或EXIF旋转过的图坐标会偏移建议训练前统一转成RGB并去掉EXIF方向信息第四不同数据集里同一个类别名可能映射到不同索引合并多个数据源时必须以class_map为唯一基准重建一次索引而不是简单拼接txt文件。3.3 训练前必看的三个统计量数据准备好了不要急着开训。先跑一个统计脚本看三个数字每类目标的实例数量、框宽高分布、小目标占比。这三个数字直接决定你在第4章怎么选模型和调参数。import os label_dir labels class_counts {} box_areas [] for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: for line in f: parts line.strip().split() class_id int(parts[0]) w float(parts[3]) h float(parts[4]) class_counts[class_id] class_counts.get(class_id, 0) 1 box_areas.append(w * h) for cid, cnt in sorted(class_counts.items()): print(fclass {cid}: {cnt} instances) small_ratio sum(1 for a in box_areas if a 0.01) / len(box_areas) print(fsmall target ratio: {small_ratio:.2f})脚本遍历标注目录统计每个类别的目标数量和框面积占比。框面积是相对于整张图的比值小于0.01意味着目标在640x640输入下大约不到64x64像素这类在监控场景里很常见。如果小目标占比超过20%在训练时要考虑把imgsz从640提高到960或者单独用小目标增强策略。4. 用YOLOv8训练PPE模型环境搭建、参数选择与结果读法4.1 环境搭建与预训练模型下载环境这一步是最不该卡人的却常常翻车。我推荐用一个独立的conda环境装最新稳定版ultralytics避免和别的项目互相污染依赖。conda create -n yolo python3.9 -y conda activate yolo pip install ultralytics这段命令创建Python 3.9环境并安装ultralytics框架它会连带装好torch和opencv。第一次执行yolo命令训练时框架会自动下载对应的预训练权重。如果你需要离线环境提前把权重下好放到项目目录训练时指定model./yolov8s.pt即可路径引用不需要记官方仓库地址。给PPE检测做训练我一般用YOLOv8s起步。YOLOv8n速度最快但精度上限偏低适合树莓派或RK3588这类边缘盒子跑YOLOv8s精度和速度平衡普通桌面显卡带得动交付时也方便蒸馏量化后再下放边缘设备。下面是一个粗略的选型参考模型适用场景上一版训练经验实测现场mAP50参考YOLOv8n边缘部署、树莓派收敛快小目标丢一半0.5~0.6YOLOv8s常规监控服务器速度和精度均衡0.7~0.8YOLOv8m精度优先服务器部署显存占用接近翻倍0.75~0.85现场mAP50并不是绝对数字不同数据分布差别很大但选型的逻辑是清楚的先跑通n或s确认数据集没问题再决定要不要上更大的模型。数据集质量差时换大模型只会让过拟合更快不会把0.4救成0.8。4.2 第一次训练命令与关键超参数训练自己的数据集核心是把数据配置文件写对。data.yaml里面最关键的是路径和类别名路径建议用绝对路径类别名要和标注文件里的索引一一对应。train: /data/ppe/train val: /data/ppe/val nc: 3 names: [helmet, reflective_clothes, work_clothes]写完后执行训练命令yolo detect train data/data/ppe/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ patience20epochs100是起步值PPE检测收敛一般在60到80轮之间100轮够你判断趋势batch16在12GB显存下搭配YOLOv8s没问题显存不够就降到8patience20表示验证集指标连续20轮不提升就早停能省时间device0指定用第一块GPU训练。首次训练建议开着cacheTrue把图片缓存到内存训练速度快很多第二次再设回False省内存。我见过很多人一上来就把epochs设成300其实没必要。你需要盯的是损失曲线和验证精度不是训练轮次。如果100轮还没收敛趋势大概率是数据有问题加轮次等于在错误的路上走更远。4.3 怎么判断模型真的收敛而不是过拟合训练结束后看两样东西runs/detect/train/目录下的results.png和验证集指标。前者画出了每条损失曲线后者在终端会打印每个epoch的mAP50和mAP50-95。判断收敛的标准不是看训练损失趋近于零而是看验证集mAP在训练后期是否稳定。如果训练损失持续下降但验证mAP在某个点后停滞甚至下降说明模型开始过拟合训练集的背景噪声应该减小模型规模或增加数据增强。混淆矩阵要从runs/detect/train/confusion_matrix.png里看。重点看安全帽类别所在的行真实标签列上的值代表召回率预测列上的值代表精确率。如果安全帽这一行的预测大量落到背景列说明有相当一部分安全帽被模型当成背景了优先去检查远处小目标的样本够不够。5. 避坑与排查PPE检测从数据到部署的6个高频问题5.1 训练刚启动就报错类别索引越界现象训练脚本跑起来几秒钟终端报RuntimeError: index out of range in self后面跟着一个巨大的数字。原因data.yaml里nc设成了3但标注txt文件里出现了class 4或更大的数通常是多个数据源合并时类别索引没对齐或者某个标注文件的类号写错。解决写个脚本扫描所有标注txt把出现的类别号打印出来再和data.yaml里的names一一对照。import os label_dir labels seen_classes set() for fname in os.listdir(label_dir): with open(os.path.join(label_dir, fname)) as f: for line in f: parts line.strip().split() if parts: seen_classes.add(int(parts[0])) print(classes in labels:, sorted(seen_classes))这段脚本把全量标注文件里的类别号收集起来如果出现0、1、2之外的数字就定位到具体文件修标注。不要靠肉眼检查上千个txt这个脚本一分钟跑完。5.2 损失函数在几十轮内不降反升学习率和batch配对问题现象box_loss和cls_loss前20轮正常下降20轮后开始波动甚至一路走高。原因训练前期的loss下降会推高梯度范数如果学习率偏大且batch较小梯度更新方向抖动太剧烈模型在最优点附近振荡。PPE检测里这类问题还有个常见诱因预训练权重没有正确加载等于从头学起感受野还没建立就被大学习率带偏。解决把lr0从默认的0.01降到0.001或者把batch从8提到16二选一先试。如果已经训到一半用yolo detect train resume modelruns/detect/train/weights/last.pt从断点继续配新的学习率参数。5.3 测试集mAP不错现场误检率却很高现象验证集mAP50有0.8拿现场摄像头跑一圈把蓝色塑料桶、红色灭火器、白色水管都框成了安全帽。原因数据分布漂移。模型在训练集里没见过这些负样本只能根据颜色和形状猜猜错的代价很低。解决回到现场采集半小时负样本视频抽帧后加入训练集。负样本不需要标注任何框直接作为背景图混进数据目录就行。这是PPE检测里性价比最高的一步往往能让误检率降一半以上。5.4 训练中BN崩溃导致loss爆炸现象训练到几十轮loss曲线突然冲上几十甚至几百然后变成nan之后再也回不来。原因YOLO的C2f结构里有大量BN层学习率过大或batch太小会让BN层的running_mean和running_var发生数值漂移一旦漂到极端值后续梯度全被污染。解决这类问题靠调低学习率基本能避免lr00.001配合batch16几乎不会触发。如果已经崩了直接删掉runs/detect/train/下对应权重重新训不要试图在nan的基础上恢复——最好的后悔药是训练前把超参数设稳。5.5 混淆矩阵总合不唯一是算错了吗现象打开训练输出的confusion_matrix.png横着加竖着加都到不了100%看起来像是bug。原因YOLO的混淆矩阵每个格子的值是按该行真实样本数归一化的不同行的归一化基数不同加总自然不为1。此外矩阵里还包含背景列背景列统计的是预测为某类但实际是背景的样本数这个数字训练时不会参与loss不代表模型哪里坏了。解决读这个矩阵时按行看每一行代表一类真实目标被预测到了哪里行内加总接近100%就正常。5.6 显存不够训练中断现象batch16跑YOLOv8m报CUDA out of memory。原因输入分辨率乘以batch再乘以模型深度显存占用是乘积关系不是叠加关系。解决先降到batch8再不行把imgsz从640降到512。注意imgsz降低会直接削弱小目标检测能力所以最优先还是调batch。实在不行就用yolo detect train modelyolov8s.pt换小模型工地场景的实时性要求往往更偏向小模型。6. 部署进阶把模型接到实时视频流并降低误报6.1 用一段Python脚本跑通摄像头实时识别训练完事的best.pt要接到现场监控流上才算落地。我一般先用一段最简脚本验证推理通路再考虑抽帧和报警逻辑。import cv2 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) cap cv2.VideoCapture(rtsp://192.168.1.100/stream1) frame_idx 0 results None while True: ret, frame cap.read() if not ret: break frame_idx 1 if frame_idx % 3 0: results model(frame, conf0.5, iou0.45)[0] if results is not None: for box in results.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) conf float(box.conf[0]) cls int(box.cls[0]) label f{model.names[cls]} {conf:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(PPE Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break代码的核心是跳帧推理每3帧只推理1帧中间2帧直接复用上一次的检测结果绘制这样能把单路视频流的帧率从约15fps拉到接近30fps视觉上检测框会有一点延迟但画面是连贯的。conf0.5是起步阈值现场误报多时就调到0.6漏检多就调回0.4iou0.45控制两个重叠框合并的激进程度目标密集时调低到0.4能保留更多独立框。6.2 用置信度延迟确认来代替单纯提阈值把阈值从0.5硬提到0.7误报会减少但远处小目标的漏检也会增加。我常用的做法是连续确认同一个检测框位置连续3帧置信度都超过0.45才触发一次报警单帧的闪烁结果直接忽略。这样误报率下降的幅度比单纯提阈值更明显而真实目标因为连续出现几乎不受影响。报警触发后再把结果截屏存盘留作后续人工复核和新样本收集。边缘部署场景下误检率高往往是这个逻辑没做而不是模型本身不行。6.3 版本化数据集比调参更值钱YOLO系列算法已经很成熟PPE检测项目的上限由数据集决定。我踩过最大的坑是模型上线后就不再迭代三个月后现场新增了一批反光条更细的新款工服之前8成的检测率直接掉到6成。之后的习惯是每周收集一次现场误报和漏检截图攒够两三百张就混入训练集重新训一版并给数据集打上版本号。模型文件也同步命名比如best_v3_20241118.pt。这样每次升级都有据可查现场出了问题能快速回退到正常版本。这个办法不复杂但比任何调参技巧都管用希望这些经验帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网