资讯详情

YOLOV5建筑工地隐患检测数据集制作与训练避坑指南

📅 2026/10/8 7:14:36 | 华诺云谱 👁 阅读
YOLOV5建筑工地隐患检测数据集制作与训练避坑指南
简介面向目标检测学习者和智慧工地安全项目开发者的建筑工地安全隐患检测数据集按YOLOV5标准目录存放图像与标注文件下载后无需额外转换即可直接进入训练流程。数据覆盖helmet、mask、car等10个类别包含640×640的RGB图像2801张其中训练集2605张、验证集196张并已完成mosaic增强单张图像由四张原图拼接而成有助于增强模型在小目标和遮挡环境下的表现。压缩包共2000个文件核心为1999个标签txt另附show.py可视化脚本可随机读取一张图片绘制边界框并保存到当前目录便于快速核对标注效果。压缩包约148.9MB已有611人学习下载10类别名称txt同步提供适合直接用于YOLOV5系列模型的训练与验证。1. 建筑工地隐患检测为什么先要一份 YOLOV5 目录格式的数据集“建筑工地安全隐患检测”这个方向最容易卡住的不是模型选型而是一份能直接开训的、按 YOLOV5 目录格式组织好的目标检测数据集。和 CCPD、HRSC2016 这类公开数据集不同工地场景几乎没有现成的大规模基准集可用现场素材往往是一段段监控视频、随手拍的照片和无人机巡飞截图要落到 10 类别的隐患检测上第一步就是把这堆素材整理成 images/ 与 labels/ 双目录、txt 归一化坐标、data.yaml 配套的标准结构。这篇文章从目录格式拆解、10 类的类别规划、标注规范、校验脚本和训练时的常见问题讲起适合准备拿 YOLOv5 训练自己的数据集、或者想验证手上数据是否合格的检测工程师。2. 拆开 YOLOV5 目录格式images/labels 双树和 data.yaml 的写法2.1 双目录树才是 YOLO 系列的通用语言YOLOV5 目录格式从本质上说不是某个版本独有的黑匣子而是一套“图片在左、标注在右、同名对应”的约定。我一般会先建两棵完全平行的目录树train、val、test 三层对齐而不是所有图片堆在一个文件夹里。datasets/ ├── images/ │ ├── train/ │ │ ├── site01_cam02_000123.jpg │ │ └── site01_cam02_000124.jpg │ ├── val/ │ │ ├── site03_cam01_000045.jpg │ │ └── site03_cam01_000046.jpg │ └── test/ │ └── site05_cam03_000007.jpg ├── labels/ │ ├── train/ │ │ ├── site01_cam02_000123.txt │ │ └── site01_cam02_000124.txt │ ├── val/ │ │ ├── site03_cam01_000045.txt │ │ └── site03_cam01_000046.txt │ └── test/ │ └── site05_cam03_000007.txt └── data.yaml图片和标签文件名必须完全一致只是扩展名不同。图片可以是 jpg 或 png标签一律是 txt。这个对应关系是 YOLO 系列训练器查找标注的唯一依据缺一个 txt这张图就会被当作背景图参与训练模型等于在学“这里什么都没有”。labels 目录下的每个 txt 文件内容不是像素坐标而是归一化坐标每行代表一个目标0 0.521484 0.318750 0.067188 0.118750 1 0.507812 0.335156 0.037500 0.076563五个数字从左到右分别是类别 ID、目标中心点 x、目标中心点 y、目标宽度 w、目标高度 h。x、y、w、h 全部除以图片宽高缩放到 0 到 1 之间。这个归一化格式从 YOLOV5 一直沿用到 YOLOv8中间换模型结构、换训练框架标注文件都不用再动这也是这套目录格式能长期使用的原因。2.2 data.yaml把目录树翻译给训练器听的索引文件有了图片和标签之后训练器还缺一个入口文件也就是最常见的 data.yaml。它告诉训练器训练集、验证集在哪里以及类别名按什么顺序排。path: ../datasets/site_safety train: images/train val: images/val test: images/test names: 0: person 1: hardhat 2: safety_vest 3: safety_rope 4: fire_extinguisher 5: electric_box 6: warning_line 7: scaffold 8: tower_crane 9: excavatorpath 是数据集根目录train、val、test 可以像上面这样写相对路径也可以直接写绝对路径。names 列表的下标必须和 txt 里每行第一个数字严格对应否则会出现“person 框里飘出 hardhat 框”的典型错乱。我见过的另一种组织方式是目录里不拆 train/val而是用 train.txt、val.txt 两个索引文件每行写一张图片的完整路径。YOLOV5 和 YOLOv8 都支持这种写法但可维护性差一些因为新增数据时要同时改两个索引文件而目录树方式只需要把文件放进对应文件夹。对工地这种会持续补充数据的场景目录树方式更省事。2.3 目录格式上常见的三个变体怎么选第一个变体是 images 和 labels 下都不拆 val只有 train 和 test训练时用训练器自带的比例再切分。这种做法如果 train/test 来自不同工地模型在测试集上的分数会非常难看因为工地场景差异太大不同项目部的摄像头角度、光照、围挡颜色完全不同强行共用一套权重不现实。第二个变体是在根目录放多个 yaml比如 full.yaml 对应 10 类完整版ppe.yaml 只保留 person、hardhat、safety_vest 三类。这个设计在工地场景里很实用有些巡检项目只关心人员防护装备不需要检测塔吊和挖掘机。切换任务只是换一个 yaml 文件不用动目录结构。第三个变体是 images/train 和 labels/train 不在同一级比如 labels 全部统一放在 label/ 根目录靠文件名前缀区分 train/val。这种做法一旦文件多了就会失控校验脚本也要额外处理路径映射不值得。建议一开始就按标准双树结构建好后面无论用 YOLOV5 还是 YOLOv8导入数据零成本。3. 10 个类别怎么定从安全帽到警戒线的标注规范与边界判定3.1 10 类怎么规划直接影响模型能不能收敛标题里写了 10 类别但具体是哪 10 类不同团队规划完全不同。我按工地隐患排查最常见的目标组合给出一套可落地的类别映射实际使用时按项目合同和验收标准增减。类别 ID类别名检测目标说明0person人员主体包括站立、行走、弯腰等姿态1hardhat安全帽只标帽子本体2safety_vest反光背心只标躯干上的背心区域3safety_rope安全绳、安全带4fire_extinguisher灭火器5electric_box配电箱、电箱6warning_line警戒线、安全锥桶7scaffold脚手架8tower_crane塔吊9excavator挖掘机、装载机等工程机械这套类别规划的逻辑是“静态设施 人员装备 大型机械”三类混搭。人员装备类目标尺寸小、数量多、互相遮挡严重是训练难度的大头大型机械类目标尺寸大、样本少、类别特征明显属于容易学但容易过拟合的类别。类别数量不是越多越好。10 类已经是一个中等规模的数据集规划如果每个类别只有一两百个实例模型会严重偏向样本多的类别。我处理工地数据时有个习惯先看每个类别的实例数分布如果某个类别占比低于 3%要么补充数据要么把这个类别合并到上级类别里而不是强行保留。3.2 标帽子和标人哪个对语义可判定边界工地隐患检测里最典型的标注争议是安全帽。常见要求是检测“未戴安全帽的人员”很多新手会直接标一个“person_without_hardhat”类别。这个方案在实践中效果很差因为一张远景监控画面里人可能只有 15 个像素宽帽子只有 6 个像素人眼都分不清戴没戴标注员只能靠猜标注一致性崩了模型自然学不到稳定特征。我一般会拆成 person 和 hardhat 两个独立类别训练结束后在后处理阶段判断如果一个 person 框的头部区域同时存在 hardhat 框判定为已佩戴如果 person 框内没有 hardhat 框判定为未佩戴。这样标注任务从“主观判断”变成了“画框”标注质量大幅提升。def judge_helmet(person_box, helmet_boxes): px1, py1, px2, py2 person_box head_y1 py1 head_y2 py1 (py2 - py1) * 0.35 head_box (px1, head_y1, px2, head_y2) for hb in helmet_boxes: hx1, hy1, hx2, hy2 hb ix1 max(head_box[0], hx1) iy1 max(head_box[1], hy1) ix2 min(head_box[2], hx2) iy2 min(head_box[3], hy2) if ix2 ix1 and iy2 iy1: return True return False这段逻辑的核心是把 person 框的上部 35% 区域当作头部区域再和每个 hardhat 框做交集判断。参数 0.35 是经验值对监控视角比较适用如果是无人机俯拍视角人的头部占比会更小这个比例要下调到 0.2 左右。反光背心也遵循同样的原则。标背心不是标整个人更不是标“穿背心的人”只标躯干上的反光条区域。模型的任务是认出反光衣这个物体而不是理解“穿了”这个高层语义。3.3 遮挡、极小目标和模糊帧的标注规则工地场景的遮挡问题比一般数据集严重得多。工人在脚手架后面走动、两人并排交谈、安全帽被手挡住这些都是日常画面。我给标注团队定的规则是目标被遮挡面积超过 50% 不标遮挡在 30% 到 50% 之间可以标但框要贴合可见部分不脑补被挡住的部分如果目标完全被我方目标挡住不标。极小目标方面监控画面里远景的塔吊操作人员可能只有 10 像素大小这种目标即使标了模型也很难学到有效特征。我一般会设一个底线目标短边小于 8 像素不标。但这就带来一个矛盾恰好是这种远景小目标最容易出现安全隐患。解决思路不是硬标而是把高清监控画面按 2x2 切块后放大标注这样小目标在中近景变成了正常尺寸。模糊帧直接删不要标。工地摄像头经常有晨雾、扬尘、夜间强光画面模糊时人眼都看不清边界标注员画出来的框基本是猜的。这类数据留在训练集里唯一的贡献就是拉低收敛速度。删除比标注更划算。标注时还有一类容易漏掉的目标是警戒线。警戒线是细长条目标标注框是扁长方形YOLO 的 anchor 机制对这种极端长宽比目标本身就不友好。如果项目里警戒线这个类别一直 recall 上不去可以考虑把安全锥桶单独拆出来一个类别或者调整标注策略只标围挡区域这些都属于类别规划阶段就要想清楚的事。4. 数据校验与 train/val 划分用一段 Python 把工地数据拉回正轨4.1 先跑一遍校验脚本把数据里的大坑扫出来拿到一批工地数据后我的习惯是先写脚本做全量校验而不是直接开训。校验脚本主要查四类问题图片没有对应标签、标签坐标越界、类别 ID 越界、图片文件损坏。下面这段脚本可以覆盖大多数情况。import os from pathlib import Path import cv2 IMG_DIR Path(images) LBL_DIR Path(labels) CLASS_NUM 10 problems [] # 扫描所有图片检查对应标签是否存在、图片能否正常解码 for img_path in IMG_DIR.rglob(*.jpg): rel img_path.relative_to(IMG_DIR) lbl_path LBL_DIR / rel.with_suffix(.txt) img cv2.imread(str(img_path)) if img is None: problems.append(f损坏图片: {img_path}) continue h, w img.shape[:2] if not lbl_path.exists(): problems.append(f缺少标签: {img_path}) continue lines lbl_path.read_text().strip().splitlines() if len(lines) 0: problems.append(f空标签: {lbl_path}) continue for line in lines: parts line.split() if len(parts) ! 5: problems.append(f字段数错误: {lbl_path}: {line}) continue cls_id, xc, yc, bw, bh parts cls_id int(cls_id) xc, yc, bw, bh map(float, (xc, yc, bw, bh)) if cls_id 0 or cls_id CLASS_NUM: problems.append(f类别越界: {lbl_path}: {line}) if xc 0 or xc 1 or yc 0 or yc 1: problems.append(f中心点越界: {lbl_path}: {line}) if bw 0 or bh 0 or bw 1 or bh 1: problems.append(f宽高越界: {lbl_path}: {line}) print(f共发现问题 {len(problems)} 条) for p in problems[:50]: print(p)逻辑说明分三层。第一层用cv2.imread检查图片是否损坏这一步能提前暴露摄像头断流产生的半截文件。第二层检查标签文件是否存在以及是否为空文件空 txt 会让这张图被当作纯背景如果工地数据里有大量空标签模型会把所有目标都学成背景。第三层逐行解析五个字段重点看归一化坐标是否落在 0 到 1 之间。特别注意bw 1这种情况。很多人从 VOC 转 YOLO 格式时会忘记除以图片宽度直接写像素值这会导致训练时 loss 直接 nan。这个脚本会在开训前把这个错误拦下来。这个脚本里 CLASS_NUM 要改成你数据集实际的类别数。如果类别规划是 later 重新调整的把类别越界检查改成打印警示而不是直接报错因为有些历史标注里可能混入了废弃类别。4.2 按工地分组划分 train/val不搞全局随机划分数据集时最常见的错误是全局随机乱序。工地数据往往是按摄像头录制的视频帧采样来的同一段视频里相邻帧高度相似。如果全局随机划分同一段连续画面会同时出现在 train 和 val 里模型相当于参考答案考试val mAP 虚高一上真实现场就原形毕露。正确做法是按工地编号或摄像头编号分组保证同一个摄像头的画面只出现在一个集合里。import random from pathlib import Path from collections import defaultdict random.seed(42) IMG_DIR Path(images) VAL_RATIO 0.2 groups defaultdict(list) for img_path in IMG_DIR.rglob(*.jpg): # 文件名示例: site01_cam02_000123.jpg # 取 site01 作为分组键也可以取 cam02 更细粒度 group_key img_path.name.split(_)[0] groups[group_key].append(img_path) val_files [] train_files [] for group_key, items in groups.items(): random.shuffle(items) cut int(len(items) * VAL_RATIO) for p in items[:cut]: val_files.append(p) for p in items[cut:]: train_files.append(p) # 把划分结果写成索引文件训练时直接指向这两个 txt with open(train.txt, w) as f: f.write(\n.join(str(p) for p in train_files)) with open(val.txt, w) as f: f.write(\n.join(str(p) for p in val_files)) print(ftrain: {len(train_files)}, val: {len(val_files)})这段代码的关键是分组键的选择。如果每个工地有多台摄像机建议取site01_cam02作为键如果只有工地编号取 site01 就够了。分组粒度越细val 的评估结果越保守也更接近真实部署表现。这里还有一个容易被忽略的点写索引文件时写的图片路径要和 yaml 里的 path 能拼成有效路径。如果 yaml 里 path 写的是../datasets/site_safety索引文件里写绝对路径最稳妥不依赖相对路径的层级关系。划分完之后再扫一遍 val 里每个类别的实例数确认每个类别在验证集里都有足够样本。有些类别如果全部分布在某一个工地而这个工地恰好被划进 trainval 里就永远看不到这个类别mAP 会显示为 0这时候需要手动把这个工地的部分样本挪到 val。4.3 可视化抽查把标签画回图上别信坐标数字脚本跑完只是第一步。任何标注数据在训练之前都应该抽样把标注框画回原图让肉眼过一遍。这一步能发现脚本查不出来的问题框和物体错位、类别名标反、漏标严重。import cv2 from pathlib import Path IMG_DIR Path(images) LBL_DIR Path(labels) OUT_DIR Path(preview) OUT_DIR.mkdir(exist_okTrue) NAMES [person, hardhat, safety_vest, safety_rope, fire_extinguisher, electric_box, warning_line, scaffold, tower_crane, excavator] for img_path in sorted(IMG_DIR.rglob(*.jpg))[:20]: img cv2.imread(str(img_path)) h, w img.shape[:2] lbl_path LBL_DIR / img_path.relative_to(IMG_DIR).with_suffix(.txt) if not lbl_path.exists(): continue for line in lbl_path.read_text().strip().splitlines(): cls_id, xc, yc, bw, bh line.split() cls_id int(cls_id) xc, yc, bw, bh map(float, (xc, yc, bw, bh)) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) color (0, 200, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, NAMES[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) out_path OUT_DIR / img_path.name cv2.imwrite(str(out_path), img) print(f预览图已保存到 {OUT_DIR})这段代码做的事情是把归一化坐标乘回图片宽高画上矩形框和类别名最后存到 preview 目录。我通常抽查 20 到 50 张图覆盖不同工地、不同时段、不同远近视角。抽查时重点看三类问题类别框是不是贴着物体外轮廓、小目标有没有被漏掉、两个挨在一起的目标有没有被画成一个框。可视化抽查也是和标注团队对齐标准的有效手段。标注规范写得再细没有预览图做对照都是空谈。我把这套流程跑完一遍之后才会进入训练阶段这个习惯帮我挡掉过很多次“训练三天发现标注错了一半”的翻车事故。5. 避坑排查训练集里最容易翻车的 5 类数据问题5.1 loss 变 nan、训练中途断掉现象训练跑到第 50 轮左右loss 突然变成 nan日志里出现 inf训练进程随后挂掉。原因90% 的情况是 labels 里存在越界坐标。常见来源有两个一个是标注工具导出时没有做归一化直接写了像素值另一个是图片有 EXIF 旋转信息标注时看到的是旋转后的画面实际读取时图像被翻转坐标整体错位。解决在训练前跑一遍第 4.1 节的校验脚本把越界坐标全部刷出来。如果是像素坐标统一除以对应图片的宽高。如果是 EXIF 旋转问题先批量把图片转正再重标。校验脚本跑通之前不要开始训练这条规则没有例外。5.2 验证 mAP 很高现场全是误检现象val 集上 mAP 到 0.9看起来成绩不错拿到工地现场视频一测安全帽框乱飞柱子、树叶都被识别成 person。原因train 和 val 没有按工地或摄像头隔离。同一台摄像机的相邻帧被随机分到了两个集合模型记住了这条摄像头的画面特征而不是泛化出“人”和“帽子”的概念。这是数据划分问题不是模型问题。解决按第 4.2 节的思路以工地编号和摄像头编号为粒度分组划分。划分后 val mAP 通常会下降几个点但下降后的数值才是真实水平。如果按分组划分后 mAP 依然很高说明数据的多样性足够可以继续训练如果掉得非常厉害说明当前数据量不够支撑 10 类检测需要先补数。5.3 类别错乱person 框里飘出 hardhat 框现象模型输出的框坐标是对的el标签却串了person 被识别成 hardhat塔吊被识别成 excavator。原因标注脚本里的类别 ID 顺序和 data.yaml 的 names 顺序不一致。比如标注时 0 号是 hardhatyaml 里 0 号却是 person整个模型从第一个 epoch 起就在学一套错乱的映射后面对齐坐标自然一塌糊涂。解决以 data.yaml 的 names 顺序为唯一标准标注工具导出时直接读取 yaml 生成类别下拉表避免两头各写一遍。训练前把 labels 里出现过的类别 ID 集合打印出来和 names 的长度做对比看有没有超出范围。这种错误代价极高一旦模型训完才发现等于全部返工。5.4 图片加载报错、Dataset not found现象训练刚启动就报Dataset not found或者Assertion cur_anchor 0 failed数据集路径检查不过。原因目录结构和预期不一致。最常见的是 images/train 下还有子目录YOLO 的 dataloader 默认不递归读图或者 labels 目录名写成了 label 单数再或者图片格式包含了训练器不认识的.bmp、.webp等。解决严格按照第 2.1 节的目录树重建不要自己发明目录名。图片全部统一转成 jpg用mogrify或 Python 批处理都行格式统一能省掉一堆兼容性麻烦。如果数据集根目录路径里带了中文或空格改成纯英文路径很多训练框架对非 ASCII 路径支持不完整。5.5 小目标漏检远处塔吊上的人不见了现象近景的 person、hardhat 都能检出但画面拉远后塔吊操作室窗户里的人、脚手架远端的人员几乎全部漏检PR 曲线上 person 类别的 recall 明显低于其他类别。原因模型输入尺寸默认 640一张 1920x1080 的画面压缩到这个尺寸后10 像素的小目标在特征图上可能只剩 1 个像素。经过五次下采样之后目标信息已经完全消失不是超参调得不好是网络结构在这个尺度下根本看不到目标。解决两个方向。第一个是把输入分辨率提到 1280训练显存占用翻倍但小目标 recall 提升显著第二个是把原图切成 2x2 或 3x3 的块对每块独立训练和推理再用非极大值抑制合并结果。对工地这种固定摄像头场景切图推理更实用因为目标不会跨块移动块与块之间的重叠区域损失可以控制在 10% 以内。6. 进阶小目标增强和类别重加权把工地数据训得更扎实如果基础训练跑通了但近景好用、远景拉胯问题通常不在模型结构而在训练策略。我常用的组合拳是三类Mosaic 增强只保留前半程、小目标复制粘贴、类别重加权。Mosaic 增强在 YOLOV5 里默认开启但对小目标其实是双刃剑。四张图拼接后每张图被等比缩小到原来的二分之一本来就小的目标变得更小。建议在最后 20 个 epoch 关掉 Mosaic让小目标在完整尺度下被模型重新学习一轮。这个技巧对工地数据尤其有效因为监控画面的主体尺度分布很极端近景占一半、远景占一半。python train.py --data data.yaml --weights yolov5s.pt --img 1280 --batch 8 --epochs 300 --close-mosaic 20参数说明--img 1280把输入分辨率提高一倍小目标特征保留更多但显存占用大约是 640 时的四倍batch 从默认 16 降到 8--close-mosaic 20表示最后 20 个 epoch 关闭马赛克增强。如果显存跑不动 1280退而求其次用 960效果也好于 640。类别重加权解决的是样本不均衡。安全帽、person 这类样本可能有几千个safety_rope、fire_extinguisher 可能只有一两百个。单纯加数据不现实现实做法是在损失函数里给少样本类别更高的权重。YOLOV5 的 loss 里对每个类别都有独立的 cls 系数手动把少样本类别的系数从默认 0.5 调到 1.0 到 1.5可以让模型在训练时更关注这些类别。这个参数不用调大超过 2.0 之后模型会在少数类别上过拟合导致其他类别 recall 下降。验证阶段不要只看总 mAP。用val.py --class 3单独测 safety_rope 的 recall或者画每个类别的 PR 曲线看少样本类别的曲线下面积是否和其他类别差距过大。训练结束后用热力图工具可视化模型的注意力区域如果模型对警戒线的注意力集中在背景纹理而不是线上说明标注边界有问题需要回头检查标注而不是继续调参。一个后期要部署到边缘设备的团队我还会建议在数据层面多做一步把标注错误的样本人工复查一遍尤其是边界清晰的“高置信度误检”样本。这些样本往往暴露了类别定义模糊的问题修正它们比升级模型结构带来的收益更大。我的习惯是每次训完一轮先不看总 mAP先看每类 recall 的排名排名最低的类别就是下一轮优化的焦点。数据质量、标注规范、训练策略都对齐之后这份工地数据集才算真正被用透。希望这些习惯能帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑