工地安全识别数据集构建与YOLO训练避坑指南

发布时间:2026/10/30 10:41:48
工地安全识别数据集构建与YOLO训练避坑指南

简介:这份资源面向计算机视觉学习者、算法工程师及工地/工厂安全管理信息化开发者,提供安全帽、反光衣与工作服自动识别的YOLO数据集,用于训练和验证人员着装合规检测模型,可支撑实时监控画面中的违规告警、抓拍与弹窗提醒等应用。压缩包共2000个文件,以1999个txt标注文件和1个yaml配置文件为主,txt文件承载图像标注信息,yaml用于定义数据集路径与类别配置,整体约952.16MB,规模适中,便于直接接入YOLO训练流程。目前已有1601人学习下载,具备一定参考热度。借助该数据集,读者可快速搭建施工工地、工厂等场景的着装检测实验,完成模型训练、评估与调优,并对照标注内容理解安全帽、反光衣、工作服等类别的样本分布,为后续部署到OpenCV+YOLO实时检测方案提供数据基础,适合需要落地安全信息化管理的中高级视觉开发者。

1. 工地安全识别数据集:为什么你的 YOLO 模型总在反光衣上翻车

做过工地视觉项目的人多半遇到过这种场景:安全帽检测 mAP 能到 0.9,一加上反光衣类别就掉到 0.6 出头,工作服更是时好时坏。问题往往不在模型结构,而在数据集本身——安全帽是刚性目标、边界清晰,反光衣是柔性目标、高反光、易被遮挡,工作服又和普通外套高度相似。这三类目标放在同一个 YOLO 检测头里,数据分布差异极大,标注口径稍有偏差,训练就会变成玄学。

这个方向要解决的核心问题很具体:构建一套能同时覆盖安全帽、反光衣、工作服三类目标的标注数据集,并让 YOLO 系列模型在上面稳定收敛。适合谁?做智慧工地、园区安防、电力巡检的算法工程师,以及想用 YOLO 入门工业检测的学生。下面从数据采集、标注规范、格式转换、训练调参到避坑,按我实际落地的顺序讲一遍。

2. 三类目标的标注口径与采集策略:先定规则再动手

2.1 安全帽、反光衣、工作服的边界怎么划

标注口径不统一是数据集报废的头号原因。我一般会在动手标第一张图之前,先写一份三页以内的标注手册,把下面这些规则钉死。

安全帽的判定标准相对简单:只要画面中人员头部佩戴了硬质防护帽,无论颜色,都标helmet。未佩戴的头部标head,不要标成person的一部分。这里有个容易翻车的点——安全帽被拿在手里、挂在胳膊上、放在地上,都不算佩戴,不标helmet。我见过不少开源数据集把手里拿的帽子也标了,模型学出来就是到处乱框。

反光衣的判定要复杂一些。我的规则是:人员躯干部分穿着带有反光条的上衣,且反光条面积在画面中可见,标vest。如果只露出袖子或衣领的一小角,不标。反光衣被安全绳、工具包遮挡超过一半时,标vest但在后续训练中作为困难样本单独统计。这里的关键是「可见反光条」这个条件,因为纯色荧光绿外套和反光衣在低分辨率下几乎无法区分。

工作服的判定最容易出问题。我的做法是:人员穿着统一制式工装(通常是蓝色或灰色连体服/分体服),且与普通外套有明显区分时,标uniform。如果工人穿的是自己的夹克、羽绒服,不标。实际项目中,工作服和反光衣经常同时出现——工人穿工作服外面套反光背心,这时候两个类别都要标,允许框重叠。

提示:三类目标的标注顺序建议是person→helmet/head→vest→uniform,先框人再框穿戴物,避免漏标。

2.2 采集场景的覆盖度与数量分配

数据集的质量上限在采集阶段就决定了。我一般按下面的维度做场景矩阵,每个格子至少采 200 张有效图。

维度取值说明
光照白天顺光、白天逆光、黄昏、夜间补光夜间是反光衣检测的试金石
距离近景(<5m)、中景(5-15m)、远景(>15m)远景小目标决定模型上限
遮挡无遮挡、部分遮挡、严重遮挡遮挡样本占比建议 20%-30%
角度正面、侧面、背面、俯视俯视是塔吊监控的常见视角
天气晴天、阴天、雨天、雾天雨天反光衣反光特性变化大

数量上,我的经验是:安全帽实例至少 8000 个,反光衣实例至少 6000 个,工作服实例至少 4000 个。如果工作服样本太少,可以考虑先用安全帽+反光衣两类跑通,再增量加工作服。不要一上来就三类一起训,否则工作服类别会拖累整体收敛。

采集设备方面,工地场景我一般用 1080P 网络摄像机和手机各采一部分。手机拍的要特别注意曝光,反光衣在自动曝光下容易过曝成一片白,建议锁定曝光补偿 -0.7EV 左右。视频抽帧的话,相邻帧差异小于阈值就丢弃,避免大量重复样本。

2.3 用脚本做采集后的初筛与去重

采完的原始图不能直接标,先跑一遍初筛。下面这个脚本做两件事:剔除模糊图、剔除近似重复图。

import cv2 import numpy as np from pathlib import Path from skimage.metrics import structural_similarity as ssim def blur_score(img_path): """拉普拉斯方差,值越小越模糊""" img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE) if img is None: return 0 return cv2.Laplacian(img, cv2.CV_64F).var() def dedup_images(img_dir, blur_thresh=80, ssim_thresh=0.92): """先按模糊度过滤,再按 SSIM 去重""" img_dir = Path(img_dir) files = sorted(img_dir.glob("*.jpg")) kept, hashes = [], [] for f in files: score = blur_score(f) if score < blur_thresh: continue # 模糊图直接丢 img = cv2.imread(str(f), cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (256, 256)) is_dup = False for h in hashes: if ssim(img, h) > ssim_thresh: is_dup = True break if not is_dup: kept.append(f) hashes.append(img) return kept if __name__ == "__main__": result = dedup_images("./raw_images", blur_thresh=80, ssim_thresh=0.92) print(f"保留 {len(result)} 张")

blur_thresh=80是我在工地场景下的经验值,低于这个数的图基本看不清反光条。ssim_thresh=0.92控制去重力度,调低到 0.88 会保留更多相似样本,调高到 0.95 去重更狠。注意 SSIM 比较的是灰度缩略图,对光照变化不敏感,所以不会误删不同光照下的同一场景。跑完这个脚本,原始图通常能砍掉 30%-40%,标注成本直接降下来。

3. 从 LabelImg 到 YOLO 格式:标注流程与转换脚本

3.1 标注工具选型与团队协作规范

单人或小团队,LabelImg 够用,轻量、支持快捷键。三人以上协作,我建议直接上 CVAT 或 Label Studio,因为需要审核机制和标注一致性检查。不管用哪个工具,类别名必须统一成英文小写:helmet、head、vest、uniform、person。不要用中文,不要用SafetyHelmet这种驼峰,YOLO 训练时类别名不一致会直接报错。

标注文件默认是 PASCAL VOC 的 XML 格式,每个 XML 对应一张图。团队协作时,我会要求标注员每标完 200 张就导出一份统计:各类别实例数、平均框面积、宽高比分布。如果某一类平均框面积突然变小,说明标注员开始偷懒标小框,需要及时纠正。

3.2 VOC 转 YOLO 格式的完整脚本

YOLO 需要的是每张图一个.txt,每行class_id x_center y_center width height,全部归一化到 0-1。下面这个脚本处理转换,同时做边界检查和类别映射。

import xml.etree.ElementTree as ET from pathlib import Path import shutil # 类别映射,顺序决定 class_id CLASSES = ["helmet", "head", "vest", "uniform", "person"] CLASS_TO_ID = {c: i for i, c in enumerate(CLASSES)} def voc_to_yolo(xml_path, out_dir, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip().lower() if name not in CLASS_TO_ID: continue # 未知类别跳过,避免训练时报错 bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 边界裁剪,防止标注越界 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) if xmax - xmin < 2 or ymax - ymin < 2: continue # 过滤无效框 xc = (xmin + xmax) / 2 / img_w yc = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{CLASS_TO_ID[name]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") out_path = Path(out_dir) / (Path(xml_path).stem + ".txt") out_path.write_text("\n".join(lines)) def batch_convert(xml_dir, out_dir, img_dir): Path(out_dir).mkdir(parents=True, exist_ok=True) for xml_file in Path(xml_dir).glob("*.xml"): img_file = Path(img_dir) / (xml_file.stem + ".jpg") if not img_file.exists(): continue import cv2 img = cv2.imread(str(img_file)) h, w = img.shape[:2] voc_to_yolo(xml_file, out_dir, w, h) if __name__ == "__main__": batch_convert("./annotations", "./labels", "./images")

CLASSES列表的顺序就是训练时的类别索引,一旦确定不要改,改了要重新生成所有标签。边界裁剪那几行是后悔药——LabelImg 有时候会标出负坐标或超出图宽的框,不裁剪的话 YOLO 训练时归一化会出负数,loss 直接 NaN。xmax - xmin < 2过滤掉宽度小于 2 像素的框,这种框多半是误标。

3.3 数据集划分与 data.yaml 配置

转换完标签,按 8:1:1 划分训练/验证/测试。划分时要注意:同一段视频抽的帧不能跨集合,否则验证集精度虚高。我一般按视频源分组划分,而不是按图片随机划分。

# 目录结构 dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

data.yaml内容如下:

path: ./dataset train: images/train val: images/val test: images/test nc: 5 names: ['helmet', 'head', 'vest', 'uniform', 'person']

nc是类别数,必须和names长度一致。如果只做安全帽和反光衣两类,改成nc: 2,names: ['helmet', 'vest'],同时标签文件里的 class_id 也要对应改。我见过有人改了 yaml 没改标签,训练时类别全乱,mAP 看着还行但预测结果完全对不上。

4. YOLO 训练参数怎么设:从 yolov8n 到 yolov8m 的实测对比

4.1 模型选型与输入分辨率

工地场景我一般从 yolov8n 起步跑通流程,再换 yolov8s 或 yolov8m 提精度。yolov8n 在 1080P 下推理能到 100+ FPS(T4 TensorRT),适合多路实时;yolov8m 精度高 3-5 个点,但 FPS 降到 30 左右。如果标题里提到 T4 1080P25 帧多路检测,那基本锁定 yolov8n 或 yolov8s。

输入分辨率默认 640,但反光衣的细长反光条在 640 下容易糊。我的做法是训练用 640,推理用 960 或 1280 做对比。如果显存够,直接训练 960 效果更好,但 batch size 要降到 8 或 16。

4.2 关键训练参数与含义

yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ momentum=0.937 \ weight_decay=0.0005 \ warmup_epochs=3 \ box=7.5 \ cls=0.5 \ dfl=1.5 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ degrees=5 \ translate=0.1 \ scale=0.5 \ mosaic=1.0 \ mixup=0.1 \ copy_paste=0.1 \ patience=30 \ device=0

lr0=0.01是初始学习率,yolov8 默认 0.01,如果 loss 震荡厉害降到 0.005。lrf=0.01是最终学习率系数,余弦退火到lr0 * lrf。box=7.5是框回归损失权重,反光衣边界模糊时可以适当调高到 8.5。cls=0.5是分类损失权重,工作服和普通外套混淆严重时调到 0.8。hsv_s=0.7饱和度增强,对反光衣颜色变化有帮助。copy_paste=0.1是分割增强,检测任务里也能用,增加小目标样本。

mosaic=1.0默认开启,但最后 10 个 epoch 建议关掉,让模型在真实分布上收敛。patience=30是早停耐心值,30 个 epoch 没提升就停。

4.3 训练过程监控与指标解读

训练时重点看三个指标:box_loss、cls_loss、mAP50。box_loss降到 0.5 以下算正常,cls_loss降到 0.3 以下。如果cls_loss卡在 0.8 不降,多半是工作服和反光衣类别混淆,需要检查标注。

验证集上分类别看 mAP。安全帽一般能到 0.92+,反光衣 0.85 左右,工作服 0.75 左右算正常。如果反光衣 mAP 低于 0.7,先查验证集里夜间样本占比,夜间反光衣过曝会导致漏检。工作服 mAP 低于 0.6,考虑合并到person类别或者增加样本。

注意:训练日志里的instances数量要关注,如果某一类实例数远少于其他类,模型会偏向多数类。我的经验是各类实例数比例不要超过 3:1。

5. 避坑与排查:反光衣漏检、工作服误报、标签错位的处理

5.1 反光衣夜间漏检严重

现象:白天反光衣 mAP 0.88,夜间掉到 0.45,大量漏检。

原因:夜间补光灯下反光条过曝成白色,与安全帽颜色接近,模型把反光衣误判为安全帽或背景。另外夜间训练样本不足,模型没学到夜间反光特征。

解决:夜间样本单独增广,用hsv_v=0.6做亮度扰动,模拟不同补光强度。同时在验证集里单独统计夜间子集 mAP,不要只看整体。如果夜间样本实在少,用copy_paste把白天反光衣实例贴到夜间背景上,但要注意光照一致性,贴完做一次高斯模糊融合边缘。

5.2 工作服被误报为反光衣

现象:工人穿蓝色工装,模型框出vest,置信度 0.6 左右。

原因:标注时把带反光条的工装和纯工装都标了vest,或者uniform样本里混入了反光衣。模型学到的特征边界模糊。

解决:重新审核标注,纯工装一律标uniform,只有可见反光条的才标vest。如果两类确实难分,考虑合并成一个workwear类别,或者用多标签分类代替检测。我一般会在训练前跑一遍标签可视化,把vest和uniform的框画出来人工过一遍。

5.3 标签错位导致 loss NaN

现象:训练第一个 epoch 就 NaN,或者 loss 突然飙到几百。

原因:VOC 转 YOLO 时坐标没归一化,或者 class_id 超出nc范围。也有可能是图片和标签文件名不匹配,YOLO 读到了错误的标签。

解决:转换后跑一遍校验脚本,检查所有坐标是否在 0-1 之间,class_id 是否小于nc。文件名不匹配的问题,用diff对比 images 和 labels 目录的文件名列表,缺的补上或删掉。

from pathlib import Path def validate_labels(label_dir, nc): for txt in Path(label_dir).glob("*.txt"): for i, line in enumerate(txt.read_text().strip().split("\n")): if not line: continue parts = line.split() cls_id = int(parts[0]) coords = [float(x) for x in parts[1:]] if cls_id >= nc: print(f"{txt} 行{i}: class_id {cls_id} 超出范围") if any(c < 0 or c > 1 for c in coords): print(f"{txt} 行{i}: 坐标越界 {coords}")

5.4 验证集精度虚高

现象:验证集 mAP 0.95,实际部署掉到 0.6。

原因:训练集和验证集有同一段视频的相邻帧,模型见过相似画面。或者验证集场景太单一,全是白天顺光。

解决:按视频源分组划分数据集,同一视频的帧只进一个集合。验证集要覆盖所有光照和角度,我一般会手动挑 50 张困难样本单独做测试集,不参与训练和调参。

5.5 小目标安全帽漏检

现象:远景画面里安全帽只有 10x10 像素,模型完全漏检。

原因:输入分辨率 640 下,10 像素目标经过下采样后特征几乎消失。

解决:训练分辨率提到 960 或 1280,或者用切片推理(SAHI)。如果显存不够,用imgsz=960加batch=8。另一个办法是在数据增强里加scale=0.3,让模型多见小目标。

6. 用 SAHI 切片推理把远景安全帽召回率拉上来

训练完模型,部署时最头疼的还是远景小目标。我试过直接上 1280 推理,T4 上 FPS 掉到 15,多路就不够了。后来改用 SAHI 切片推理,640 模型也能把远景召回率提 20 个点。

SAHI 的思路很简单:把大图切成有重叠的小块,每块单独推理,再合并结果。对 1080P 图,我一般切 640x640,重叠 128 像素。这样每个安全帽在某个切片里至少占 30 像素以上,模型能正常检出。

from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction import cv2 # 加载 YOLO 模型 detection_model = AutoDetectionModel.from_pretrained( model_type="yolov8", model_path="runs/detect/train/weights/best.pt", confidence_threshold=0.3, device="cuda:0" ) # 切片推理 result = get_sliced_prediction( "test_far.jpg", detection_model, slice_height=640, slice_width=640, overlap_height_ratio=0.2, overlap_width_ratio=0.2, perform_standard_pred=True # 同时跑整图推理,保留大目标 ) # 导出结果 result.export_visuals(export_dir="./sahi_output") for pred in result.object_prediction_list: print(pred.category.name, pred.score.value, pred.bbox.to_xyxy())

overlap_height_ratio=0.2表示切片间重叠 20%,太小会漏掉跨切片目标,太大推理变慢。perform_standard_pred=True让整图也跑一遍,避免切片把大目标切碎。实测在 1080P 工地图上,SAHI 把远景安全帽召回率从 0.55 提到 0.78,反光衣从 0.62 提到 0.81,代价是推理时间增加约 2.5 倍。如果 T4 上跑多路,可以只对画面下半部分做切片,上半部分天空区域直接整图推理,省一半时间。

最后说个习惯:我每次训完模型,都会把验证集里所有漏检和误检的图单独存一个文件夹,标上日期和模型版本。下次改数据集或调参时,先拿这批困难样本跑一遍,看有没有改善。这个习惯帮我省了很多次「改了参数但不知道有没有用」的纠结。希望帮到你。

本文还有配套的精品资源,点击获取