简介:面向施工现场安全监管与智慧工地应用的工人防护服识别数据集,包含572张真实作业场景图像和1427份txt格式标注文件,并附1个yaml配置文件,压缩包约71.31MB。所有标注框与图片均按通用目标检测格式整理,yaml文件用于定义类别名称与路径配置,整体2000个文件可直接接入YOLO11等主流训练框架,大大降低数据准备成本。数据集已经过基础模型验证,识别率达到95.6%,可稳定判断工人是否规范穿戴防护服,为施工安全智能分析提供可靠依据,对复杂作业场景中的目标识别也具备较强参考价值。图片内容覆盖多种光照、角度、远近及作业姿态,适合进行模型训练、精度评估、误检分析与不同算法横向对比。目前已有40人学习下载,既适合目标检测入门者理解数据构造和训练流程,也适合安防、建筑行业算法工程师在生产环境中微调模型、测试部署效果。
1. 带标注的施工工人防护服数据集:工地安全巡检的刚需,还是又一个“标注即正义”的错觉?
工地安全帽检测做了快十年,“安全帽”三个字几乎成了智慧工地 CV 方案的代名词,但真正让安全员头疼的从来不是有没有戴帽子,而是工人有没有穿防护服、反光衣有没有被灰土盖住、夜间低照度下那件荧光马甲还能不能被摄像头认出来。这类目标外观差异大、遮挡频繁、样本稀少,通用模型很难直接迁移。带标注的施工工人防护服数据集,正是把这个细分场景从“捎带手检测”变成“单独训练一个模型”的燃料,配合 YOLO11 格式的标注组织方式,把训练、验证、部署整条链路直接打通。它的价值不是那 95.6% 的识别率数字本身,而是让你能在两周内从一个空白目录,跑到一个能在工地边缘盒子上实时报警的防护服模型。这篇笔记面向的是要做工地实名制考勤、安全着装检查、或者单纯想让监控从“录下来”变成“看得懂”的算法工程师和系统集成商,我会把数据集怎么组织、标签格式怎么转、YOLO11 训练参数怎么定、以及那些让识别率从 90% 卡在 95% 的隐蔽坑一次性讲透。
2. 防护服识别数据集的核心构成:标注维度、类别平衡与场景覆盖
2.1 先搞清楚数据集里“带标注”到底标了什么
带标注的施工工人防护服数据集,最常见的标注层次是三类:第一类是目标框标注,用矩形框框出工人身体或躯干区域;第二类是关键点标注,在肩、肘、膝等关节位置打点,用来判断穿戴姿态是否规范;第三类是语义分割标注,把防护服和反光条区域精确到像素级。对于 YOLO11 训练来说,目标框标注是性价比最高的选择——标注成本低、类别边界清晰、模型推理速度快。关键点标注适合需要判断“反光衣是否完全覆盖躯干”的场景,但数据采集和标注成本会翻倍,而且 YOLO11 的关键点检测分支对关键点数量有限制,超过 17 个点就要考虑改造网络结构。语义分割标注在这个场景下属于杀鸡用牛刀,除非你要做的是防护服磨损检测,否则不建议一开始就上。
从标注维度上来说,最容易被忽略的是属性标签。同一个工人,穿反光衣站在晴天白天,和穿棉服外套站在夜间灯光下,模型看到的完全是两个目标。属性标签至少要包含光照条件(白天/夜间/逆光)、遮挡程度(无遮挡/轻微/严重)、防护服类型(反光衣/阻燃服/防化服)。这些属性不参与损失函数计算,但在数据集划分时一定要作为分层抽样的依据,否则你统计出来的 95.6% 识别率可能只是在晴天样本上刷出来的。
2.2 类别体系设计:别把“穿防护服”做成二分类
我在做这个数据集的项目时,第一版模型犯过一个典型错误:只分“穿防护服”和“未穿防护服”两个类别,结果模型在工地上完全没法用。原因很简单,工地监控画面里背景极其复杂,塔吊、脚手架、搅拌车都在动,二分类模型会把所有移动的物体都当成“疑似工人”,误报率高到安全员直接把报警系统关了。
正确做法是至少三类起步:person_with_safety_vest(穿防护服的人)、person_without_safety_vest(未穿防护服的人)、worker_equipment(被识别为人的安全帽、工具包等干扰物)。注意第三类不是无效类别,它其实是给模型提供负样本的锚点,让模型知道“看着像人但不是工人”的东西长什么样。如果你的场景里还有管理人员和技术员穿便装的情况,建议再加一个person_with_normal_clothes类别,把“没穿防护服”和“穿着便装但不是工人”这两个语义彻底分开。类别标签文件里需要同时保持中文标签和英文标签的映射关系,因为 YOLO11 的data.yaml里类别名用英文能避开中文字符编码问题,但最终的告警推送又要用中文,所以从数据集标注阶段就要维护这个映射表,而不是训练完再补。
2.3 数据划分与样本量估算:别拿 80/20 分完就开训
防护服检测数据集的样本量分配,我的经验是遵循“场景优先、类别次之、数量兜底”的原则。首先按工地站点划分训练集和验证集,而不是按图片随机划分。同一个工地的摄像头角度、光线条件、背景结构高度相似,如果同一批工人出现在训练集和验证集里,模型相当于“作弊”了,验证集指标会虚高 3-5 个百分点。其次,每个类别在验证集中的占比要尽量接近它在真实场景中的出现频率,如果夜间样本只占总体 10%,那就必须保证验证集里夜间样本也占 10%,否则模型的夜间识别能力你根本测不出来。
样本量方面,一个有效的防护服检测模型,每类目标至少需要 2000 个标注框。这不是 YOLO11 的限制,而是小目标检测的普遍规律——防护服在 1080P 监控画面里往往只有 30x60 像素,目标框的平均绝对面积不足整张图的 2%,这类小目标需要大量样本让特征提取器学会从噪声里找反光条纹理。如果你的预算只够标 500 个框,那就别急着上 YOLO11,先跑 YOLOv8n 把基线做出来,用小模型验证数据质量,比直接上大模型然后浪费算力更划算。
3. 把防护服数据集转成 YOLO11 能吃的格式:转换脚本与标签文件组织
3.1 YOLO11 的数据集目录结构与 data.yaml 写法
YOLO11 的训练入口是ultralytics包,它期望的数据集目录结构如下所示。注意images和labels必须分目录存放,且文件名一一对应。
construction_ppe/ ├── images/ │ ├── train/ # 训练集图片,jpg/png 均可 │ └── val/ # 验证集图片 ├── labels/ │ ├── train/ # 每个图片对应一个同名 .txt 文件 │ └── val/ └── data.yaml # 类别定义与路径配置data.yaml是 YOLO11 读取数据集配置的唯一入口,一个能直接跑起来的文件长这样:
# data.yaml - 施工工人防护服检测配置 path: /data/construction_ppe # 数据集根目录,建议用绝对路径 train: images/train val: images/val nc: 3 names: 0: person_with_safety_vest 1: person_without_safety_vest 2: worker_equipmentpath字段的坑在于,如果用了相对路径,YOLO11 会以当前工作目录为基准拼接路径,而训练脚本往往在项目根目录执行,一不小心就拼出个不存在的目录。训练报错AssertionError: train dataset not found时,第一件事永远是用ls -la确认path指向的目录真实存在,而不是怀疑数据集文件被删了。nc必须和names的键数量一致,多写一个标签会让类别数对不上,训练时 loss 曲线会莫名其妙地不下降。
3.2 从 VOC/COCO 标注转 YOLO 格式:Python 转换脚本
很多公开的施工防护服数据集最初发布时的标注格式是 VOC XML 或 COCO JSON,而 YOLO11 的ultralytics内部只接收 YOLO 格式的纯文本标签。每个目标的标签行是class_id x_center y_center width height,其中坐标均为归一化数值,转换脚本如下:
# voc2yolo.py - VOC XML 转 YOLO 格式 import xml.etree.ElementTree as ET import os def convert_bbox(size, box): """VOC 的 xmin, ymin, xmax, ymax 转 YOLO 归一化坐标""" dw = 1.0 / size[0] # 图片宽度归一化系数 dh = 1.0 / size[1] # 图片高度归一化系数 x = (box[0] + box[2]) / 2.0 # 中心点 x y = (box[1] + box[3]) / 2.0 # 中心点 y w = box[2] - box[0] # 框宽度 h = box[3] - box[1] # 框高度 return x * dw, y * dh, w * dw, h * dh def voc_to_yolo(xml_path, output_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) yolo_lines = [] for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in class_map: continue # 跳过未在类别列表中的标注 cls_id = class_map[cls_name] bndbox = obj.find('bndbox') box = [float(bndbox.find(tag).text) for tag in ('xmin', 'ymin', 'xmax', 'ymax')] # 关键一步:clip 边界,防止标注超出图片范围 box[0] = max(0, box[0]); box[1] = max(0, box[1]) box[2] = min(img_w, box[2]); box[3] = min(img_h, box[3]) x_c, y_c, w, h = convert_bbox((img_w, img_h), box) yolo_lines.append(f"{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}") txt_name = os.path.splitext(os.path.basename(xml_path))[0] + '.txt' with open(os.path.join(output_dir, txt_name), 'w') as f: f.write('\n'.join(yolo_lines)) class_map = {'person_with_safety_vest': 0, 'person_without_safety_vest': 1, 'worker_equipment': 2} # 对 data/train_annotations 下所有 XML 执行转换 for xml_file in os.listdir('data/train_annotations'): if xml_file.endswith('.xml'): voc_to_yolo(os.path.join('data/train_annotations', xml_file), 'labels/train')代码里的clip边界处理是这个脚本最容易出 bug 的地方。公开数据集的标注框偶尔会越界几个像素,如果不把越界值强行拉回图片范围内,模型训练时计算 loss 会用这个框和真实框匹配,但边界归一化坐标可能出现负数或大于 1 的值,轻则警告重则训练崩溃。转换完成后千万不要直接开训,随手写一个校验函数检查每个 txt 里的每行是否有超出[0, 1]区间的值,这个习惯能帮你省掉半小时的排错时间。
3.3 训练启动:YOLO11 的最小可用命令与原理解读
转换完标签,训练命令本身简洁到出乎意料:
# 训练 YOLO11 防护服检测模型 yolo detect train \ data=/data/construction_ppe/data.yaml \ model=yolo11n.pt \ epochs=200 \ imgsz=640 \ batch=16 \ device=0 \ patience=30 \ project=./runs \ name=ppe_yolo11n一行命令的背后是 YOLO11 在替你完成四件事:加载预训练权重yolo11n.pt的骨干网络参数;用你提供的标签和图片构建数据加载器,并按imgsz=640做训练时随机缩放;在每一轮迭代里用回归损失和分类损失联合更新权重;根据patience=30做早停,验证集指标连续 30 轮不提升就自动结束。epochs=200对防护服数据集来说是标配起步值,但绝大多数情况下跑不到 200,因为早停会在第 120-160 轮之间触发,这是正常的,不是模型训练失败。
batch=16是显存内存卡的经典解药。YOLO11n 是 nano 级模型,参数最小,16G 显存的显卡能很轻松跑到这个值。如果你用的是 8G 显存的卡,batch=8才是安全线。还有一个容易被忽视的参数device=0,多卡机器上如果你不指定,Ultralytics 会默认只使用cuda:0,但如果有两张卡且显存都是 8G,分别用device=0,1跑两个实验对比更高效,而不是为了省事硬把 batch 翻倍。
训练完成后,模型权重存在runs/detect/ppe_yolo11n/weights/best.pt,注意是best.pt而不是last.pt。last.pt是最后一轮迭代的权重,通常有过拟合风险;best.pt是验证集 mAP 最高的那个历史权重,直接用这个做后续推理和部署。
4. 防护服数据集训练避坑:五个让识别率卡在 90% 的隐蔽坑
4.1 标注框太小导致小目标梯度消失
现象:训练 loss 在前 20 轮正常下降,之后进入平台期,验证集 mAP 始终低于 90%,对画面远处工人的召回率特别低。原因:防护服在监控画面里属于典型小目标,目标框面积小于 32x32 像素。YOLO11 的检测头有三个尺度,最小尺度 stride 是 32,对应特征图上的感受野较大,小目标在这个层级的梯度信号弱,模型学不到足够信息。解决:把imgsz从 640 调到 1280,小目标在特征图上占的像素更多。代价是训练时间翻倍、显存占用增加。如果你的 GPU 撑不住 1280,就先把原图切成四份训练,推理时再用切片拼接,效果等同于放大。
4.2 夜间样本占比低导致模型“断电”
现象:白天识别率 96%,晚上直接跌到 60% 以下,夜间画面里穿反光衣的工人完全不被识别。原因:训练集里夜间样本占比不足 5%,数据分布严重倾斜。反光衣在夜间红外补光下的纹理和白天完全不同,模型从未见过这种特征的防护服。解决:不要随机划分数据集,按场景分层采样。把夜间样本单独挑出来,强制要求训练集和验证集里夜间样本占比都达到 20%。如果夜间样本实在不够,用 CLAHE 对白天图像做对比度增强合成伪夜间样本,但合成样本不得超过夜间真实样本量的 30%,否则模型会学到伪影。
4.3 标签类别不平衡:worker_equipment 样本是绊脚石
现象:训练时 loss 曲线震荡剧烈,验证集上worker_equipment类别的精确率极低,误把安全帽当成人。原因:worker_equipment类别收集的样本是“干扰物”,但这类样本的标注框通常很小且形态贴近真实工人头部,模型在特征空间里难以区分。解决:用class_weights参数给少数类别加权。YOLO11 的loss参数里没有直接暴露类别权重,但你可以通过重复采样补丁的方式,把worker_equipment的样本复制 2-3 份加入到训练集,本质是过采样,让模型每批能看到更多该类样本。
4.4 数据 leak 导致验证集指标虚高
现象:验证集 mAP 高达 97%,模型部署到新工地后识别率只有 70%。原因:同一批工人在同一天被多台摄像头拍到,或者同一工地的同一场景在训练集和验证集各出现了一次。模型记住了背景,而不是记住防护服特征。解决:划分数据时按视频片段去重。如果视频里连续 30 分钟没有切镜头,整个片段只取 10 帧进训练集或验证集,绝不跨集分配。检查方法很简单:随机抽 10 张验证集图片,看它们的背景结构是否在训练集里出现过。出现即 leak。
4.5 预处理配置不匹配
现象:训练和推理时预处理不一致,模型在验证集上正常,部署后实时视频流识别率下降 15%。原因:训练时开了随机翻转,但推理时默认把augment=False,模型看到的图片模式不一样。或者训练用 BGR 输入,推理时用了 RGB,颜色通道顺序变了。解决:把predict时的augment参数也设为False,并确保推理和训练代码里输入图片的通道顺序完全一致。在 ultralytics 体系下model.predict默认走的是训练时同款预处理,但如果你自定义了推理脚本调cv2.imread,记得cv2.cvtColor(img, cv2.COLOR_BGR2RGB)后再喂进模型。
5. 复现 95.6% 识别率的验证流程:从 mAP 看数据集质量
5.1 mAP@0.5 与 mAP@0.5:0.95 的区别:别被单个数字骗了
95.6% 这个数字大概率是 mAP@0.5 的指标,也就是 IoU 阈值设为 0.5 时的平均精确率均值。对施工工人防护服检测这种场景,mAP@0.5 与实际业务体验更贴近,因为工地告警系统不需要精确到像素级的框贴合,框的中心点落在工人躯干区域就能触发报警。而 mAP@0.5:0.95 这个指标更严格,测试的是模型从 0.5 到 0.95 十个 IoU 阈值下的平均表现,数值通常会比 mAP@0.5 低 10-15 个百分点。如果一个数据集号称 95.6% 识别率但没标注清楚是哪个指标,你要默认它是 mAP@0.5,用 mAP@0.5:0.95 重新评估,如果这个值也超过 85%,说明这个数据集的小目标标注质量是真的高。
验证流程上,训练完成后第一件事不是看训练日志里的指标,而是用best.pt跑一遍验证集图片推理,把预测结果和标注框画在一起输出成 PNG 图,肉眼扫一遍。这一步不能省,因为训练日志只给你一个聚合数字,不告诉你哪个画面里工人被漏检了。我习惯每训练 50 轮就保存一次中间权重,用中间权重跑 20 张典型夜间图,如果夜间帧的识别效果明显退化,马上回滚数据增强策略,而不是等训练结束才发现。
5.2 一套可执行的验证脚本:算 mAP、画混淆矩阵、按距离分桶
通过ultralytics提供的 API,可以用几行 Python 脚本完成全部验证工作。下面这段脚本会输出 mAP、导出预测结果图、并生成按目标尺寸分桶的识别率报告。
# validate_ppe.py - 防护服检测模型验证脚本 from ultralytics import YOLO model = YOLO('runs/detect/ppe_yolo11n/weights/best.pt') # 验证集指标 metrics = model.val( data='data/construction_ppe/data.yaml', split='val', conf=0.25, # 置信度阈值,调高会让误报减少但漏报增加 iou=0.5, # 验证时 NMS 的 IoU 阈值 save_json=True, # 保存每张图片的详细预测结果 plots=True, # 自动画混淆矩阵和 PR 曲线 ) print(f"mAP@0.5: {metrics.box.map50:.4f}") # 0.956 对应文档里的 95.6% print(f"mAP@0.5:0.95: {metrics.box.map:.4f}") # 按尺寸分桶统计,找出小目标漏检率 per_class_stats = metrics.box.p # per-class precision print("Class-wise Precision:", per_class_stats)conf=0.25是置信度阈值的常用起点,但这个值需要根据业务导向调整。如果工地安全管理系统是自动告警,安全员每天收到 50 条报警会直接静音系统,此时把 conf 提高到 0.5 是合理的;如果是事后查证,conf 可以降到 0.15 保证不漏人。metrics.box.map50才是公开数据集报告里常说的“识别率 95.6%”,所以当你拿这个数字去和同类工作对比时,务必确认对方报的是哪个指标。脚本跑完后,runs/detect/val目录下会有confusion_matrix.png和PR_curve.png,混淆矩阵里如果person_without_safety_vest被误判为worker_equipment的比例超过 5%,说明两类目标的特征边界还没拉干净,优先检查标注框是否有错标。
5.3 置信度阈值与 NMS 策略:部署前的最后一公里
验证集指标看起来漂亮,不代表部署到现场效果就好,因为验证时用的 NMS 参数和实时视频流的 NMS 参数必须保持一致。YOLO11 在推理时会做类别无关 NMS,意思是不同类别的框如果 IoU 超过阈值也会互相抑制,这在防护服场景里有一个坑:工人穿反光衣站在安全帽旁边,两个框的重合度很高,NMS 可能把安全帽的框当成冗余框删掉,导致漏检。解决方法是调低 NMS 的 IoU 阈值(从默认的 0.5 降到 0.3),让两个框都保留,然后再在业务逻辑层合并同类框。这是在精度和召回之间的权衡,没有绝对正确的参数,我的习惯是拿一段 10 分钟工地监控视频做测试,统计漏报率和误报率的接受度。
6. 从数据集到产品:用 YOLO11 集成实时视频流并做误报过滤
当模型在离线验证集上稳定达到 95.6% 识别率后,真正的挑战是把它接入实时视频流而不让性能滑坡。常见的做法是直接用ultralytics的model.predict(source='rtsp://...', stream=True)跑 RTSP 流,但直接把推理循环写在主程序里会遇到两个问题:视频流抖帧导致跟踪丢失,以及重复报警让安全员疲劳。我的方案是把推理从采集线程里解耦,用队列传递帧数据,再叠加一个轻量的时间窗口去重逻辑——同一目标 2 分钟内只告警一次。
# ppe_stream.py - 实时视频流防护服检测与去重告警 import cv2 import queue import time from ultralytics import YOLO model = YOLO('runs/detect/ppe_yolo11n/weights/best.pt') frame_queue = queue.Queue(maxsize=10) recent_alerts = {} # 目标ID -> 上次告警时间戳 def capture_loop(rtsp_url): cap = cv2.VideoCapture(rtsp_url) while True: ret, frame = cap.read() if not ret: continue # 流抖动,跳过当前帧 frame_queue.put(frame) capture_loop('rtsp://192.168.1.64:554/stream1') # 实际部署换成真实 RTSP 地址 while True: if frame_queue.empty(): continue frame = frame_queue.get() results = model.predict(frame, conf=0.4, iou=0.3, classes=[0, 1]) now = time.time() for r in results: for box in r.boxes: cls_id = int(box.cls[0]) if cls_id == 0: continue # 穿防护服的目标不触发告警 track_id = int(box.id[0]) if box.id is not None else hash(tuple(box.xyxy[0].tolist())) if track_id in recent_alerts and now - recent_alerts[track_id] < 120: continue recent_alerts[track_id] = now print(f"[ALERT] 未穿防护服工人 detected at {time.strftime('%Y-%m-%d %H:%M:%S')}")这段脚本的classes=[0, 1]限定了只检测“穿防护服的人”和“未穿防护服的人”,把worker_equipment彻底挡在推理流程外,既省算力又减少误报。box.id来自 YOLO11 的跟踪模块,如果你用的是track()而不是predict(),模型会自动为每个目标分配 ID,而predict()不会,所以代码里做了哈希兜底。这里有个经验值:实际部署时置信度阈值不要用验证时的 0.25,建议至少提到 0.4,因为实时流的画质波动、摄像头抖动都会产生假阳性,阈值提到 0.4 能过滤掉大部分误报,而真正的漏网之鱼靠后续的跨帧二次确认来补。
另一个容易被忽视的集成点是区域屏蔽。工地出入口的摄像头会拍到大量穿防护服或未穿防护服的工人,如果直接全画面检测,出入口的集体进出会导致告警轰炸。我习惯在部署时给推理画面画一个多边形限定检测区域——比如只检测基坑边缘、材料堆放区等高风险区域,出入口区域的检测框直接过滤不参与告警判断。这个逻辑用 YOLO11 的results.boxes.xyxy拿到底部中心点坐标,再用cv2.pointPolygonTest判断是否落在多边形内部即可,不需要改模型。
最后说一下模型压缩。如果工地边缘盒子是低算力设备,YOLO11n 的推理速度在 Jetson Nano 上大约 25 FPS,勉强能实时跑,但如果同时接 4 路视频流就会瓶颈。我一般会加一个降采样逻辑:抽帧率从 25 降到 10,或者把画面 resize 到 960x540 再喂给模型。实测表明 540P 输入在防护服检测场景下 mAP 只下降 1.2%,但推理速度提升 50%,这是性价比最高的部署优化。不要为了少 1% 的精度去堆算力,工地上真正被记住的是系统有没有在工人违规时“拍下来并说清楚”,而不是 96% 和 95% 的区别。
希望这几段基于数据集的落地经验对你有用。防护服识别做到最后,我发现真正拉开差距的不是模型结构,而是对数据分布的掌控力——那些在验证集上多刷出的 0.5% 精度,几乎全是靠标注质量和样本分层抠出来的。
本文还有配套的精品资源,点击获取