简介:面向施工工地、工厂等安全管理场景的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 "fps=1/5,scale=1280: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切回上一张,Ctrl+S保存当前标注。
这里有一个容易犯的错: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_ok=True) os.makedirs(val_dir, exist_ok=True) # 按文件名前段作为场景编号,例如"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文件是空的——这没问题,但别删掉这个txt,YOLO要求每一张训练图片都有对应的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(f"class {cid}: {cnt} instances") small_ratio = sum(1 for a in box_areas if a < 0.01) / len(box_areas) print(f"small target ratio: {small_ratio:.2f}")脚本遍历标注目录,统计每个类别的目标数量和框面积占比。框面积是相对于整张图的比值,小于0.01意味着目标在640x640输入下大约不到64x64像素,这类在监控场景里很常见。如果小目标占比超过20%,在训练时要考虑把imgsz从640提高到960,或者单独用小目标增强策略。
4. 用YOLOv8训练PPE模型:环境搭建、参数选择与结果读法
4.1 环境搭建与预训练模型下载
环境这一步是最不该卡人的,却常常翻车。我推荐用一个独立的conda环境装最新稳定版ultralytics,避免和别的项目互相污染依赖。
conda create -n yolo python=3.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.6 |
| YOLOv8s | 常规监控服务器 | 速度和精度均衡 | 0.7~0.8 |
| YOLOv8m | 精度优先,服务器部署 | 显存占用接近翻倍 | 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 \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ patience=20epochs=100是起步值,PPE检测收敛一般在60到80轮之间,100轮够你判断趋势;batch=16在12GB显存下搭配YOLOv8s没问题,显存不够就降到8;patience=20表示验证集指标连续20轮不提升就早停,能省时间;device=0指定用第一块GPU训练。首次训练建议开着cache=True把图片缓存到内存,训练速度快很多,第二次再设回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 model=runs/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发生数值漂移,一旦漂到极端值,后续梯度全被污染。解决:这类问题靠调低学习率基本能避免,lr0=0.001配合batch=16几乎不会触发。如果已经崩了,直接删掉runs/detect/train/下对应权重重新训,不要试图在nan的基础上恢复——最好的后悔药是训练前把超参数设稳。
5.5 混淆矩阵总合不唯一,是算错了吗
现象:打开训练输出的confusion_matrix.png,横着加竖着加都到不了100%,看起来像是bug。原因:YOLO的混淆矩阵每个格子的值是按该行真实样本数归一化的,不同行的归一化基数不同,加总自然不为1。此外矩阵里还包含背景列,背景列统计的是"预测为某类但实际是背景"的样本数,这个数字训练时不会参与loss,不代表模型哪里坏了。解决:读这个矩阵时按行看,每一行代表一类真实目标被预测到了哪里,行内加总接近100%就正常。
5.6 显存不够训练中断
现象:batch=16跑YOLOv8m,报CUDA out of memory。原因:输入分辨率乘以batch再乘以模型深度,显存占用是乘积关系,不是叠加关系。解决:先降到batch=8,再不行把imgsz从640降到512。注意imgsz降低会直接削弱小目标检测能力,所以最优先还是调batch。实在不行就用yolo detect train model=yolov8s.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, conf=0.5, iou=0.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,视觉上检测框会有一点延迟但画面是连贯的。conf=0.5是起步阈值,现场误报多时就调到0.6,漏检多就调回0.4;iou=0.45控制两个重叠框合并的激进程度,目标密集时调低到0.4能保留更多独立框。
6.2 用置信度延迟确认来代替单纯提阈值
把阈值从0.5硬提到0.7,误报会减少,但远处小目标的漏检也会增加。我常用的做法是"连续确认":同一个检测框位置,连续3帧置信度都超过0.45才触发一次报警,单帧的闪烁结果直接忽略。这样误报率下降的幅度比单纯提阈值更明显,而真实目标因为连续出现,几乎不受影响。报警触发后再把结果截屏存盘,留作后续人工复核和新样本收集。边缘部署场景下误检率高往往是这个逻辑没做,而不是模型本身不行。
6.3 版本化数据集,比调参更值钱
YOLO系列算法已经很成熟,PPE检测项目的上限由数据集决定。我踩过最大的坑是模型上线后就不再迭代,三个月后现场新增了一批反光条更细的新款工服,之前8成的检测率直接掉到6成。之后的习惯是每周收集一次现场误报和漏检截图,攒够两三百张就混入训练集重新训一版,并给数据集打上版本号。模型文件也同步命名,比如best_v3_20241118.pt。这样每次升级都有据可查,现场出了问题能快速回退到正常版本。这个办法不复杂,但比任何调参技巧都管用,希望这些经验帮到你。
本文还有配套的精品资源,点击获取