简介:面向建筑工地、工厂等安全生产场景,这份基于YOLO的数据集聚焦安全帽、反光衣和工作服的自动识别与检测,可直接用于人员着装合规性分析。资源包含2000个文件,压缩包约952.16MB,其中1999个txt文件提供YOLO格式的标注框信息,1个yaml文件定义类别名称和训练路径,解压后即可接入YOLOv5/YOLOv8等常见训练框架,无需额外转换格式。对算法开发者和安全管理人员而言,该数据集能支撑实时监控画面中的违规行为检测、报警与留证,有效降低施工和作业现场的安全风险。目前已有1599人学习下载,适用于智慧工地、工厂安全巡检等场景的模型开发与算法验证,相比自建数据集可大幅节省标注成本和时间。
1. 安全帽、反光衣、工作服三合一识别数据集:从解压到开训的落地拆解
先说结论。工地入口摄像头对着逆光、夜间补光不足的场地拍,安全帽和反光衣识别要落地,最花时间的不是 YOLO 调参,而是数据准备这一关。这套基于 YOLO 的安全帽/反光衣/工作服自动识别数据集,解压之后就是 YOLO 训练直接能吃的格式:类别恰好对应施工安全最常踩线的三样穿戴,标注是归一化坐标的 TXT 文件,train/val 划分也已经替你做好了。对于要在工地场景快速出一版安全帽检测模型的一线工程师、实验室里想完整跑一遍 YOLOv8 训练流程的学生,以及要做“识别能不能上现场”验证的集成商,这套数据至少能省掉两到三周标注时间。我按实际使用顺序,把目录结构、标签统计、训练配置和踩坑要点一个一个拆开讲。
2. 数据集结构拆解:目录树、标签坐标与类别分布
拿到压缩包,我习惯先解压、再去看目录树,而不是直接双击训练脚本。原因很简单:标注数据的目录结构决定了后面data.yaml里路径怎么写,类别编号决定了训练出来的框到底落在哪一类上。这几分钟不花,后面训练跑一半发现路径错了,重跑的时间成本翻倍。
常见做法是统一的 images/labels 两根分支,train/val 各自对应。假设解压后的根目录叫safety_helmet_vest_dataset,结构一般是:
unzip -q safety_helmet_vest_dataset.zip tree -L 2 safety_helmet_vest_datasetsafety_helmet_vest_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamlYOLO 系列工具的约定是:图像和标签分居images和labels两个目录,train/val 各自保持同名映射。images/train/00001.jpg对应的标注在labels/train/00001.txt,文件名严格同步,不能出现图片有标注、标注没图片这类半截文件。下载这类数据集时,如果压缩包里带了 README 或数据说明文档,我会先读一遍。标注规范会写明“安全帽只框帽体部分”“反光衣框到上衣区域”这种语义边界,别让后面的训练去猜。
2.1 一行标注 5 个数的含义
先抽一个标签文件看格式:
head -n 3 safety_helmet_vest_dataset/labels/train/00001.txt0 0.5123 0.4831 0.1321 0.2448 1 0.3340 0.6917 0.1775 0.2892 2 0.4217 0.7119 0.1133 0.2142每行表示一个目标框,共 5 个数:类别索引、中心点 x 坐标、中心点 y 坐标、框宽、框高。这里我把类别约定为 0 安全帽、1 反光衣、2 工作服,和data.yaml里的 names 一一对应。四个浮点数都是相对图像宽度和高度的归一化值,范围 0 到 1,不能直接当像素坐标用。
比如0.5123 0.4831这个中心点,要放到一张 1920×1080 的原图上,实际像素位置是x = 0.5123 * 1920,y = 0.4831 * 1080。我见过不少人把归一化坐标误当成像素值直接画框,结果所有框全挤在图像左上角,先画了框再回头检查标注就是白费功夫。
要把标注拉回原图肉眼检查,我一般会写一段小脚本:
import os import cv2 def draw_yolo_boxes(image_path, label_path, class_names): img = cv2.imread(image_path) h, w, _ = img.shape with open(label_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls = int(parts[0]) x_c, y_c, bw, bh = map(float, parts[1:]) x1 = int((x_c - bw / 2) * w) y1 = int((y_c - bh / 2) * h) x2 = int((x_c + bw / 2) * w) y2 = int((y_c + bh / 2) * h) color = (0, 255, 0) if cls == 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[cls], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return img if __name__ == "__main__": classes = ["helmet", "reflective_vest", "workwear"] img = draw_yolo_boxes( "safety_helmet_vest_dataset/images/train/00001.jpg", "safety_helmet_vest_dataset/labels/train/00001.txt", classes ) cv2.imwrite("check_00001.jpg", img)脚本逻辑不难:map(float, parts[1:])把字符串转成浮点数,四个坐标值做从归一化到像素的换算。max(0, y1 - 5)是防止框贴着图像上边缘时,文字跑到画面外变成负数坐标。跑完抽查若干张图,如果发现框和内容对不上,就要回头查标注文件是错标还是类别顺序写反了。
2.2 类别分布和负样本的统计
拆数据集时我会顺手统计每类框的数量,因为类别不平衡会直接影响训练时的采样权重。安全帽框数量明显多于反光衣,模型对安全帽这类特征收敛得更快,反光衣因为样本少、特征容易被淹没。具体差距有多大,用一个 Counter 就能看:
import os from collections import Counter label_dir = "safety_helmet_vest_dataset/labels/train" class_names = {0: "helmet", 1: "reflective_vest", 2: "workwear"} counter = Counter() total_boxes = 0 for fn in os.listdir(label_dir): if not fn.endswith(".txt"): continue with open(os.path.join(label_dir, fn), "r") as f: lines = f.readlines() if not lines: continue for line in lines: cls = int(line.strip().split()[0]) counter[cls] += 1 total_boxes += 1 for cls_id in range(3): print(f"{class_names[cls_id]}: {counter[cls_id]}") print(f"total boxes: {total_boxes}")我一般在拿到任何数据集后都会跑一遍这个统计,注意那些完全空的 txt 文件。空标注文件属于负样本,表示“这张图里没有检测目标”,它让模型学会什么情况下不应该输出框。但负样本要放在训练集里,如果大量负样本被分进 val 集,验证结果里 False Positive 会看着特别高,实际现场表现可能完全两码事。
还需要看的是目标尺度分布。把每个框的宽度、高度收集起来看中位数,能尽早判断目标是大是小:
import os import numpy as np sizes = [] label_dir = "safety_helmet_vest_dataset/labels/train" for fn in os.listdir(label_dir): if not fn.endswith(".txt"): continue with open(os.path.join(label_dir, fn), "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue _, _, _, bw, bh = map(float, parts) sizes.append((bw, bh)) sizes = np.array(sizes) print("框宽中位数:", np.median(sizes[:, 0])) print("框高中位数:", np.median(sizes[:, 1]))如果框宽中位数在 0.05 以下,说明数据集里有大量小目标。这种情况后面imgsz还按 640 跑,小目标特征会被压得很小,现场漏检的坑在训练前就埋下了。
2.3 读懂 data.yaml 里的字段
大多数工程化的标注数据集会自带一个data.yaml,供 ultralytics 直接引用。内容大致是:
path: /absolute/path/to/safety_helmet_vest_dataset train: images/train val: images/val names: 0: helmet 1: reflective_vest 2: workwearpath是数据集根目录的绝对路径,train和val是相对 path 的子目录。names 的索引顺序必须和标签文件里的类别编号完全一致,这一点我在后面训练章节会再强调一遍。如果压缩包里没带 yaml,自己补一个,命名保持简单,不要用带空格或中文的目录名,后续命令行参数解析容易出幺蛾子。
3. 用 YOLOv8 跑通训练流程:环境配置、损失函数与关键参数
目录看明白了,训练就不是黑匣子。我用 YOLOv8 做这一轮拆解,因为它在检测精度、训练速度和工程生态之间平衡得比较稳。做安全帽、反光衣这类穿戴识别,没必要第一次就用最复杂的模型,先把一个中体量模型跑通,再根据漏检率做针对性调整。
3.1 环境准备与预训练权重
训练前先建独立环境,避免把项目依赖和系统 Python 混在一起:
conda create -n safety-yolo python=3.10 -y conda activate safety-yolo pip install ultralytics torch torchvision python -c "import torch; print(torch.cuda.is_available())"torch.cuda.is_available()返回 True 说明 GPU 可用;返回 False 大概率是装成了 CPU 版 torch,或者服务器 CUDA 版本不匹配。yolo 预训练模型下载这一步,ultralytics 在首次训练时输入model=yolov8m.pt会自动拉取权重文件。离线服务器上常见做法是提前把.pt放到当前工作目录,避免训练中途卡在下载环节。
模型体量选型上,n、s 太小,工地场景小目标多,收不动;x 太大,显存需求和训练时长都往上跳。我一般从 m 起步,精度居中、部署可控,后面要提速再剪枝或换 s。
3.2 数据配置与类别映射
训练前把data.yaml准备好,路径用绝对路径省掉一堆相对路径的解析问题:
path: /data/datasets/safety_helmet_vest_dataset train: images/train val: images/val names: 0: helmet 1: reflective_vest 2: workwear这段 yaml 最核心的是 names 顺序。数据集里标签文件的类别编号 0、1、2 已经写死了,names 顺序不能随意调整。想把工作服放第一位,等于把所有标签重新做一遍映射。很多人复现时遇到“安全帽被识别成反光衣”,先查是不是 names 顺序和标签索引不对齐。
3.3 训练命令与损失函数的关系
cd /data/datasets yolo train \ data=safety_helmet_vest_dataset/data.yaml \ model=yolov8m.pt \ epochs=120 \ imgsz=640 \ batch=32 \ device=0 \ patience=15 \ project=runs/safety_detepochs=120是训练上限,patience=15表示连续 15 个 epoch 验证集指标没有提升就提前停,避免无效空转。imgsz=640是官方常用的输入分辨率,如果第 2 章统计时发现大量小目标,可以把imgsz=960再跑。batch=32在 24G 显存上跑 m 体量比较稳,显存不足就降到 16,不要硬扛。
训练日志里的 loss 是一个组合值,YOLOv8 把它拆成三块:box_loss、cls_loss、dfl_loss。box_loss 管预测框和真实框的位置误差,cls_loss 管类别判断误差,dfl_loss 是分布焦点损失,负责优化边框的边界细节,三者加权求和就是总 loss。我一般会盯着一件事:如果某个 loss 长时间不降,说明对应的问题出在数据层面而不是训练参数。比如 cls_loss 降不下去,先回数据集看标签是不是有错标、漏标,而不是急着改损失函数权重。
3.4 训练结束看什么指标
训练完跑一次验证:
yolo val model=runs/safety_det/weights/best.pt data=safety_helmet_vest_dataset/data.yaml输出里会有一个指标表,核心几列:
| 指标 | 含义 | 现场价值 |
|---|---|---|
| Precision | 预测框中真正目标的比例 | 误报多不多 |
| Recall | 真实目标中被找出来的比例 | 漏检多不多 |
| mAP50 | IoU 阈值 0.5 时的平均精度 | 常规验收参考 |
| mAP50-95 | 更严格的全程平均精度 | 小目标敏感 |
做工地安全帽识别,我更看重 Recall。漏检一个人比误报一个人严重太多,所以到调推理阈值时宁可牺牲一点 Precision,也要保住 Recall。训练阶段不要只看 mAP50 一个数字,要把三个类别各自的 Precision 和 Recall 拆开看,反光衣的短板往往被安全帽的高分掩盖掉。
4. 穿戴识别训练避坑清单:五个翻车现场与对应的排错方案
这部分是我复盘时最想留下的内容。每一条都是实际训练里真实见过的翻车现场,按“现象 → 原因 → 解决”的顺序写。
4.1 反光衣高光一过就漏检
现象:模型在正常光照下识别反光衣挺稳,现场一遇到强光、车灯直射,反光衣过曝变成一片白,框直接消失。
原因:训练数据里反光衣大多数是顺光和均匀光照的画面,极端曝光样本太少。反光衣材质本身反光率极高,过曝后纹理被抹平,模型没见过这种输入分布,自然认不出来。
解决:我习惯在训练前给图像加随机曝光扰动,模拟现场强光和逆光环境:
import random import numpy as np def exposure_shift(img, factor_range=(0.55, 1.65)): factor = random.uniform(*factor_range) img = img.astype(np.float32) * factor return np.clip(img, 0, 255).astype(np.uint8)img * factor就是整图亮度增益,factor 大于 1 往过曝方向走,小于 1 往偏暗方向走。工地出入口的摄像头经常逆光,取 0.55 到 1.65 这个范围能在不过度破坏特征的前提下覆盖大部分光照变化。注意 factor 上限设得太高会生成大片纯白区域,反而把反光衣的反光条特征洗没了。
4.2 远处小安全帽置信度压在 0.25 以下
现象:同一张画面里,近处工人的安全帽识别得很好,远处走过来的工人头上的安全帽只有二三十像素,置信度始终拉不上来,现场表现为时远时近、时灵时不灵。
原因:小目标在特征图下采样过程中信息严重丢失。imgsz=640 时,一个 30×30 像素的目标映射到 80×80 特征图只剩不到 4×4,特征基本被压没了。
解决:把训练和推理的 imgsz 提到 960,让目标在输入端保留更多像素:
yolo train ... imgsz=960 batch=16分辨率提高后显存占用涨得很快,batch 要同步降下来。我在 24G 显存上用 m 体量跑 960 分辨率,batch=16 是相对稳妥的组合,再低就观察显存占用动态调。
4.3 工作服和普通工装互相错认
现象:验证集里 workwear 类别的误报集中出现在普通深色工装、连体工作服上,反光衣反而很少被误认成工作服。
原因:这个问题的根源往往不在模型,而在标注语义边界。工作服和普通工装在外观上本来就连续,如果标注时有的框把带有反光条的工装算工作服,有的不算,模型会学到一套前后不一致的标准。
解决:先把标注规范定死。我约定工作服必须是有明显反光条、统一制服特征的画面,普通便装不上这个类。对已标注数据做一轮复核,把边界样本统一归到 hard example 集合,不要直接混在训练集里。标签不一致带来的模型困惑,靠调损失权重是救不回来的。
4.4 训练集 mAP 高、现场连续漏检
现象:训练和验证阶段的 mAP50 到了 0.9,看起来能交付,结果拿现场摄像头录的 10 分钟视频一跑,漏检连着出现。
原因:这是典型的数据划分泄漏。很多自采数据集把同一个摄像头拍摄的连续帧随机切进 train 和 val,两边的画面高度雷同,验证集等于泄了题。模型记住的是具体场景纹理,而不是泛化出来的安全帽和反光衣特征。
解决:重新按采集来源划分数据。我一般会把文件名前缀或拍摄时间当成分组依据,保证同一路视频的帧只出现在 train 或只出现在 val。这个操作做完,验证指标会下降一点,但现场表现的真实度上升一大截。
4.5 训练到一半 loss 突然变成 NaN
现象:前几十个 epoch 曲线正常,某个 epoch 开始 total loss 突然变成 nan,后面所有指标跟着崩。
原因:常见诱因有两个。一个是学习率在后期仍然偏大,梯度更新步子迈过头;另一个是 batch 里混进了损坏图片,比如下载途中被截断的 jpg,解码出来是一张残缺的纯色图,梯度异常放大。
解决:先把lr0从默认的 0.01 降到 0.005,同时固定seed让训练可复现:
yolo train ... lr0=0.005 seed=42再跑一轮前先做图片完整性检查:
from PIL import Image import os for root, _, files in os.walk("safety_helmet_vest_dataset/images"): for fn in files: p = os.path.join(root, fn) try: im = Image.open(p) im.load() except Exception as e: print("坏图:", p, e)im.load()会强制触发解码,截断文件到这一步就会抛异常。跑一遍把坏图挑出来,顺便把对应的 txt 标注一起移走,再重新训练,loss 曲线的翻车概率会低很多。
5. 验证与部署:混淆矩阵、阈值选择与 ONNX 导出
训练结束不意味着能直接上现场。从本地验证到工地工控机部署,中间还有三道工序。
5.1 先从混淆矩阵里读短板
yolo val跑完会生成confusion_matrix.png,横纵坐标是真实类别和预测类别,对角线越亮越好。我一般先找两类误报:反光衣被认成工作服,以及安全帽被认成背景。前者表示两个类别在视觉上过于接近,基本要回到标注规范处理;后者说明小目标漏检,优先考虑提高 imgsz,而不是硬调阈值。背景误报严重时,要回到负样本比例去补数据。
5.2 推理阈值怎么选:conf 和 iou
推理阶段最重要的两个参数是置信度阈值conf和 NMS 的iou:
from ultralytics import YOLO model = YOLO("runs/safety_det/weights/best.pt") results = model.predict( source="现场抓拍.jpg", conf=0.35, iou=0.5, imgsz=960, classes=[0, 1, 2], save=True, )conf=0.35的取舍逻辑是:先拿 100 帧现场画面在默认参数下跑一遍,记录所有预测框的置信度分布。反光衣特征特殊,置信度普遍在 0.4 上下,安全帽可以到 0.7,统一拿 0.35 作为门限两边都能保住。classes=[0, 1, 2]很容易忘,但不写的话,模型会把没见过的背景也硬输出成检测框。
5.3 导出 ONNX 部署到工控机
现场工控机一般不会有完整的 torch 环境,导出 ONNX 是标准做法:
yolo export model=runs/safety_det/weights/best.pt format=onnx opset=12 imgsz=960注意导出时的 imgsz 要和推理时一致,ONNX 模型的输入尺寸是固定的。后端用 onnxruntime 跑,最小依赖就能推理:
import onnxruntime as ort import cv2 import numpy as np sess = ort.InferenceSession( "best.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"] ) input_name = sess.get_inputs()[0].name img = cv2.imread("现场抓拍.jpg") img_resized = cv2.resize(img, (960, 960)) input_img = img_resized[:, :, ::-1].transpose(2, 0, 1) input_img = input_img[None].astype(np.float32) / 255.0 outputs = sess.run(None, {input_name: input_img})img[:, :, ::-1]是把 BGR 换成 RGB,transpose(2, 0, 1)把 HWC 变成 CHW,最后归一化到 0-1。这三步的格式必须和训练时完全对齐,否则同一个模型在不同机器上跑出两套结果,我踩过这个坑。需要 N 卡 GPU 加速时,再从 ONNX 转 TensorRT,能再压一截延迟。
5.4 交付前先拿没见过的视频过一遍
验证集指标再漂亮,也不代表现场能直接用。我的习惯是把模型拿到目标现场录一段没参与训练的 15 分钟视频,逐帧跑一遍,人工数出漏检和误报数量。不同品牌摄像头色彩倾向差异大,现场偏色和验证集不完全一致,这一步能提前发现颜色偏移带来的性能下降。真遇到色差严重的现场,先做输入端的颜色归一化,再考虑重新标数据。
6. 影子数据回灌法:用现场画面把模型再往上拽一截
标注数据集训练出来的模型到现场往往差最后一口气,我的做法叫影子数据回灌:把模型放到真实现场跑一段时间,把漏检、误检画面自动捞出来,重点补标注之后混回原数据再做一轮微调。
捞难例的脚本很简单:
from ultralytics import YOLO model = YOLO("runs/safety_det/weights/best.pt") for i, res in enumerate(model.predict( source="现场录屏.mp4", stream=True, conf=0.2 )): for box in res.boxes: if box.conf[0] < 0.3: res.save(f"hard_examples/frame_{i:05d}.jpg")conf=0.2故意放低,是为了把模型犹豫不决的框也捞出来。筛选原则有两个:一是模型给了三类之一的低置信度框,这种图片补上正确标签直接回灌;二是画面里明明有人,模型一个框都没出,这种场景多半出在反光衣过曝或极端逆光段,需要重新采集对应时段的画面,而不是靠重复回灌硬顶。
回灌节奏我一般控制在每次 300 到 500 帧新图,重新训 40 到 60 个 epoch。量太少,模型记不住新特征;一口气灌 2000 帧,模型容易过拟合到某一段光线下,白天的识别反而变差。从那以后,我每次做工地穿戴识别,都会先让模型在目标现场跑一整天,把统计漏检和低置信度帧变成固定流程,再做一轮影子数据回灌,最后才拿现场验收。这套做法比反复调阈值要稳定得多,也成了我交付前的习惯。希望帮到你。
本文还有配套的精品资源,点击获取