建筑工地隐患检测的YOLOv5数据集制作与避坑指南
简介面向目标检测与建筑安全管理的YOLOV5格式数据包聚焦建筑工地安全隐患识别覆盖安全帽、口罩等10个常见检测类别适合训练施工现场人员安全监测、违规行为预警等模型。数据图像统一为640×640的RGB图片已做mosaic增强每张由四图拼接可有效提升模型对小目标的泛化能力整个数据集按yolov5目录结构组织训练集2605张、验证集196张每张图片均配有对应的txt标注文件类别文本信息完整拿到后可直接用于YOLOv5及其变体训练无需额外转换或清洗。数据集包含接近真实工地环境的多样场景四图拼接拓展了单张图像的上下文信息有助于模型在复杂背景下保持稳定识别。压缩包共2000个文件主体为1999个txt标签与1个可视化脚本show.py压缩包大小148.9MB脚本无需修改即可运行随机传入一张图片就能绘制边界框并保存到当前目录方便快速检查标注质量和模型效果。资源已有611人下载学习适合需要现成工地安全数据集进行模型验证、算法调优或毕业设计开发的初学者和工程师也能帮助开发者减少数据采集和标注的时间成本。1. 建筑工地隐患检测的数据集困境为什么YOLOv5目录格式比算法调参更值钱拿YOLOv5目录格式做建筑工地安全隐患检测最常见的误区是以为瓶颈在模型结构上实际卡在数据上。我之前接过一个工地安全帽项目客户给的4000多张图片里光是「中文类名、txt空文件、标注框越界」这几类问题就折腾了两个晚上等把目录格式和数据划分彻底理顺YOLOv5的小模型还没调超参精度就已经能落地了。这篇想说的是一套10类别的工地隐患数据集难的不是拍照和标注而是把原始标注转换成YOLOv5认可的images/labels双目录、归一化txt和data.yaml。做目标检测的人不少愿意把数据当工程认真对待的人不多。整理出一套干净的数据集比调三个月的参数都值钱。适合已经会跑YOLOv5的train.py但被数据整理和隐藏的坑绊住的研究者或工程同学。2. 先从10个类别和目录骨架下手一套能直接喂给YOLOv5的标注规范10类别听起来不多但如果类别之间互相打架标注员会崩溃模型也会学到错误的相关性。设计类别时我只看三件事能不能从单帧画面判定、类别之间是否互斥、样本是否容易收集。工地上的安全隐患类别很多但落到单帧视觉能判定的其实就那么几类。2.1 先定10类隐患类别怎么选才标得快、训得动建筑工地安全隐患检测圈内常见的做法是把「人员防护」和「环境危险」分开设计。人员防护类需要模型识别出「人」作为前提否则「未戴安全帽」这种负类就没有参照物。下面这套10类是我在类似项目里常用的基础集合覆盖了从佩戴检测到明火扬尘的大部分需求id类别名判定标准典型场景0person只要是人就算任意姿态的工人、行人1helmet头部戴有安全帽帽体完整覆盖头顶2no_helmet头部没有安全帽光头、戴草帽或布帽3vest穿着反光背心背心覆盖躯干4no_vest未穿反光背心普通T恤或裸身5smoke烟雾烟囱冒烟、焊接烟尘6fire明火焊接火花、火堆7excavator挖掘机等工程机械整车或挖机臂8fall_zone临边、洞口等危险区楼梯口、屋面边缘9bare_wire裸露电线线皮破损、私拉乱接这套设计里最典型的手法是「person 是否佩戴」的二元结构。如果只标no_helmet不标helmet模型会把所有「没帽子的头」当正样本却没机会认识「戴了帽子的头」训练时正负样本严重失衡。把helmet和no_helmet都作为类别标注员只需要盯住头判断标准就清晰了模型也能学到帽子和头之间的空间关系。这是反复试验后最稳的一种组织方式。类别交叉的坑也在这里一张图里同一个头不能既出现helmet框又出现no_helmet框。标注规范里我会明确写一条——「同一个目标只允许出现在一个类别中以遮挡更少、特征更完整的那帧为准」。另外遮挡超过70%的目标不标注标注框小于12像素的一律丢弃这些规则看着苛刻却能省下后面清洗数据的力气。2.2 YOLOv5目录骨架同名txt与归一化坐标YOLOv5目录格式是官方训练流程约定俗成的结构核心就是images和labels双目录一一对应。我从一开始就会把目录名全部设成英文小写禁止中文和空格因为后续在shell里转义、在Windows和Linux之间搬运空格和中文都是定时炸弹。site_safety/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamlimages里放jpg图片labels里放同名txt标注文件。比如images/train/camera1_00001.jpg对应labels/train/camera1_00001.txt。图片统一用jpg一是opencv读取png会比jpg慢一些二是很多手机拍摄的png带着EXIF旋转信息转成jpg时顺手把方向矫正掉能少踩一个坑。每个txt文件的每一行就是一个标注目标格式固定为五列类别id、归一化中心点x、归一化中心点y、归一化宽、归一化高。比如一个安全帽标注出来长这样1 0.518229 0.413542 0.093750 0.159028 2 0.618229 0.413542 0.093750 0.159028第一列的0、1、2是类别id对应data.yaml里names列表的下标不是类别名称。后面四个浮点数全部除以图片实际宽高做归一化取值范围在0到1之间。yolo训练时拿到txt会结合实际加载的图片尺寸去还原框所以txt里存的必须是比例值写绝对像素坐标会直接导致训练崩掉。2.3 data.yaml的写法与路径三种坑data.yaml是YOLOv5读取数据集信息的入口内容很短但坑不少。一套工地数据集的data.yaml长这样path: ../site_safety train: images/train val: images/val test: images/test nc: 10 names: 0: person 1: helmet 2: no_helmet 3: vest 4: no_vest 5: smoke 6: fire 7: excavator 8: fall_zone 9: bare_wirepath字段在较新的YOLOv5版本里写法是相对data.yaml文件所在位置的路径如果你用的版本不认path字段就把train、val改成绝对路径例如train: /data/site_safety/images/train。写绝对路径最省心缺点是换机器要改文件我一般写相对路径加path同时把整个数据集目录和YOLOv5仓库放在同一级路径关系就很好维护。names列表的顺序一旦固定后面所有txt里的类别id都对应这个顺序。中途加类别、调顺序等于所有标注全部重来这是血泪教训。另一个隐藏坑是names里不要写中文即使代码能读控制台输出和日志也会乱码排查起来非常痛苦。提示环境准备建议用conda单独建一个yolov5环境conda create -n yolov5 python3.8把数据集丢进去直接训练。yolov5训练自己的数据集第一步不是调参是先保证数据加载不出错。3. 用三个脚本把零散标注整理成标准数据集转换、划分与体检原始标注不可能一开始就是YOLOv5格式。目标检测常用标注工具里LabelImg保存的是VOC XMLlabelme保存的是JSONRoboflow在线工具导出的格式五花八门。第一步永远是转换第二步是划分第三步是体检。这三步都做完数据才算真正「能训练」。3.1 把labelme/BBox标注转成YOLO txt转换脚本与边界裁剪labelme标注界面灵活既能画矩形也能画多边形工地上截取不规则烟雾、明火用多边形比矩形准得多。但YOLOv5只吃矩形框所以转换时取多边形外接矩形。下面这段脚本我从第1次做数据集用到现在改改类别列表就能复用import json from pathlib import Path # 顺序必须和data.yaml的names一致这是铁律 CLASS_NAMES [person, helmet, no_helmet, vest, no_vest, smoke, fire, excavator, fall_zone, bare_wire] def labelme_json_to_yolo(json_path: Path, label_out_dir: Path) - None: with open(json_path, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in CLASS_NAMES: continue cls_id CLASS_NAMES.index(label) xs [p[0] for p in shape[points]] ys [p[1] for p in shape[points]] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 边界裁剪把拖出画面的框拉回来避免越界 x_min max(0, min(x_min, img_w)) x_max max(0, min(x_max, img_w)) y_min max(0, min(y_min, img_h)) y_max max(0, min(y_max, img_h)) w, h x_max - x_min, y_max - y_min if w 1 or h 1: # 太小或不可见的框直接丢 continue xc (x_min x_max) / 2.0 / img_w yc (y_min y_max) / 2.0 / img_h nw w / img_w nh h / img_h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) if lines: out_txt label_out_dir / (json_path.stem .txt) out_txt.write_text(\n.join(lines), encodingutf-8)脚本逻辑是逐条读取JSON里的shapes把多边形的所有顶点坐标投影到(0, img_w)和(0, img_h)之间再做归一化。边界裁剪不是可选项标注员在放大拖拽时很容易把框拖出画布几像素不裁会导致中心点或宽高的归一化数值大于1或者为负训练时要么报错要么出现莫名其妙的框。类别名不在CLASS_NAMES里的标注会被直接跳过。这一步有意为之防止原始标注里混入了「head」这类和类别表对不上的标签如果不跳过这些框会全部落到id0的person上整个数据集就被污染了。转换完成后检查labels目录里的txt数量和json数量对得上差太多就说明大部分标注框被过滤了需要回去看标注质量。3.2 划分train/val/test按场景分组而不是随机打散划分数据集是数据工程里最常见的「后悔药」入口。很多教程教随机划分工地项目这么干会吃大亏。工地数据大多来自摄像头连续视频抽帧同一个场景的相邻帧长得几乎一样随机划分时train和val里会混入高度相似的画面val精度虚高一换到新工地直接打回原形。我一般按场景分组文件名前缀如果是camera1_00001.jpg这种结构就取camera1作为场景ID保证同一个摄像头的所有帧只进一个集合。这样才能让val真正模拟「没见过的工地」。import random import shutil from pathlib import Path SRC_IMAGES Path(labeled_imgs) # 待划分的jpg目录 SRC_LABELS Path(labels_all) # 上一步转换出的txt目录 OUT Path(site_safety) # 数据集根目录 RATIO {train: 0.75, val: 0.15, test: 0.10} groups {} for img_path in SRC_IMAGES.glob(*.jpg): scene_id img_path.stem.split(_)[0] # 取前缀作为场景ID groups.setdefault(scene_id, []).append(img_path) gids list(groups.keys()) random.seed(42) random.shuffle(gids) num_val max(1, int(len(gids) * RATIO[val])) num_test max(1, int(len(gids) * RATIO[test])) val_gids set(gids[:num_val]) test_gids set(gids[num_val:num_val num_test]) train_gids set(gids[num_val num_test:]) for split_name, gid_set in [(train, train_gids), (val, val_gids), (test, test_gids)]: img_out OUT / images / split_name lbl_out OUT / labels / split_name img_out.mkdir(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) for gid in gid_set: for img_path in groups[gid]: shutil.copy(img_path, img_out / img_path.name) txt_path SRC_LABELS / (img_path.stem .txt) if txt_path.exists(): shutil.copy(txt_path, lbl_out / txt_path.name)设定随机种子random.seed(42)是为了让划分结果可复现换台机器重跑能拿到完全一样的train/val集合。比例上train:val:test我取75:15:10test只在最后评估用平时训练完全不碰它。划分的另一个细节是「整个场景一起进val」。假设有两个摄像头对着同一个工地不同角度它们在画面上完全不重叠但拍的是同一批工人、同一个危险区这两个摄像头也算同源场景条件允许时把同工地、同时段的数据归到同一组甚至直接用「工地A训练、工地B验证」的跨域验证来评估比任何随机划分都真实。3.3 数据体检用一段脚本扫出空txt、越界框和缺失标注服务和训练任务排了半天开机就崩多数时候不是GPU问题是数据里有脏文件。第3.3节这个脚本就是用来在训练前把脏数据全部揪出来。它检查三件事图片有没有同名txt、txt里每一行是不是5列、类别id和归一化坐标是否越界。def check_split(images_dir: Path, labels_dir: Path, nc: int): problems [] for img_path in sorted(images_dir.glob(*.jpg)): txt_path labels_dir / (img_path.stem .txt) if not txt_path.exists(): problems.append((img_path.name, 缺少同名txt)) continue lines txt_path.read_text(encodingutf-8).strip().splitlines() if not lines: problems.append((img_path.name, txt为空)) continue for line in lines: parts line.split() if len(parts) ! 5: problems.append((img_path.name, f行格式错误: {line})) break cls_id, xc, yc, w, h map(float, parts) if int(cls_id) nc: problems.append((img_path.name, f类别id越界: {cls_id})) break if not (0 xc 1 and 0 yc 1 and 1e-6 w 1 and 1e-6 h 1): problems.append((img_path.name, f坐标越界: {line})) break for img_name, err in problems: print(f{img_name}: {err}) return problems体检脚本输出的「缺少同名txt」和「类别id越界」是两类致命伤前者会让YOLOv5在训练中遇到「图片没有任何标注可加载」后者会在数据加载时直接抛出IndexError。空txt不致命但也不该留在训练集里它会让模型把这张图当纯背景处理如果原图确实有目标说明漏标了需要补标而不是删除。跑完脚本修复数据后再跑一遍确认problems为空。这一步完成后目录格式才算真正达到「能直接喂给train.py」的标准。4. 首轮训练怎么验证数据质量可视化检查、冒烟训练与增强边界数据格式化之后很多人急着开长训练我建议先花半小时做三件事画框看标注、跑一次短训练、检查增强设置。这三件事能提前暴露大多数数据问题比跑完50轮再看结果省时得多。4.1 标注可视化把每张图上的框画出来人眼复核数值体检能查出格式错查不出语义错。框是不是偏移了、类别是不是标反了、漏标了几个目标这些问题只有人能看出来。常见做法是写脚本把所有标注框画到图上按类别抽检每类随机看20到30个样本就够。import cv2 import numpy as np from pathlib import Path class_names [person, helmet, no_helmet, vest, no_vest, smoke, fire, excavator, fall_zone, bare_wire] def draw_yolo_boxes(img_path: Path, txt_path: Path, out_path: Path) - None: img cv2.imread(str(img_path)) h, w img.shape[:2] rng np.random.default_rng(2024) colors rng.integers(0, 255, size(len(class_names), 3)) for line in txt_path.read_text(encodingutf-8).strip().splitlines(): parts line.split() if len(parts) ! 5: continue cls_id int(parts[0]) xc, yc, bw, bh map(float, parts[1:]) 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 [int(c) for c in colors[cls_id]] cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(str(out_path), img)画框用的宽高必须是cv2.imread读出来的实际尺寸不是JSON里记录的尺寸。手机照片经过缩放、旋转后文件头信息和实际像素可能已经不一致用原始标注目录里的宽度直接乘坐标是很多踩坑的根源。框的坐标直接从归一化值还原到像素时记得所有计算用浮点最后再转int否则精度损失会让框偏移好几个像素。在无显示的服务器上我会把每个类别抽出的检查图拼成一张大图再下载下来看比一张张翻效率高得多。看到框和物体贴合、类别名称正确再进行下一步。4.2 首轮冒烟训练epochs、batch-size、img该给多大数据集检查完第一轮训练我从来不用完整配置而是跑一次「短冒烟」目标只是确认数据加载链路通、loss正常下降。常见的完整训练命令长这样conda activate yolov5 cd yolov5 python train.py \ --data /data/site_safety/data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 16 \ --epochs 50 \ --name site_safety_v1 \ --workers 4冒烟时可以把epochs压到10甚至5其他参数保持不变跑完看loss曲线有没有下降趋势。yolov5训练自己的数据集时最容易翻车的不是模型而是数据加载Windows下--workers设成大于0经常会导致卡死我一般在Windows上直接设0Linux服务器上设4或8。8G显存跑--batch-size 16 --img 640可能会显存溢出直接降到8或4别硬扛。--img 640是性价比最高的默认值。如果工地摄像头是1080p安全帽这种小目标在原图里可能只有30像素见方640输入意味着特征图对应的尺寸又会缩小训练会非常吃力。第一轮先用640把整个流程跑通后续针对小目标问题再提到--img 1280。显存不够时1280会把batch-size卡到4甚至2这时候优先保img而不是batch。看训练曲线有个简单判断前10个epoch的box_loss明显下降说明数据链路没问题如果loss乱跳不降先别调模型回去看标注。训练结束后看val/box_loss比train高多少差距过大基本就是过拟合工地小数据集上最常见的现象后面第5章展开讲。4.3 数据增强的边界mosaic、翻转、模糊不能照单全收YOLOv5默认开启的增强很强但工地场景有一些增强是「负优化」。我一般遵循一个原则第一轮全默认让模型先跑起来等baseline有了再逐步收紧增强配置。YOLOv5的增强参数在数据目录对应的hyp文件里常用几个我会这么设增强参数我一般怎么设理由mosaic0.5小目标多时可以保留但1.0会让安全帽尺度失真mixup0.1 或关工地样本重叠少mixup收益有限flipud0上下翻转等于把地面变天空监控场景物理上不成立fliplr0.5左右镜像在真实画面里常见安全帽左右对称可保留degrees5固定视角监控不宜大角度旋转mosaic是YOLOv5默认增强里影响最大的一个。它对小目标多、背景杂的工地数据有好处能把4张图的上下文拼到一起但mosaic1.0意味着每张训练图都是拼接图如果安全帽本身只有二三十像素拼接后尺度变化太剧烈模型容易学偏。第二轮训练我习惯把mosaic降到0.5。上下翻转是另一个容易忽略的点。工地的摄像头全部高架俯视flipud后的画面里地面在上、天空在下这种样本在真实部署里永远不会出现模型学到的是「带天空背景的地面」这种错误关联。YOLOv5默认的hyp里flipud一般是0但如果你用的第三方配置开了它建议直接关掉。5. 避坑建筑工地安全隐患数据集最容易翻车的5个地方数据转换和划分本身不难难的是排错。这一章写的是我做建筑工地隐患检测以来遇到的高频问题每一条都按「现象、原因、解决」讲清楚踩到直接照着修。5.1 中文类别名导致的奇葩报错现象训练启动后立刻报UnicodeDecodeError或者train.py日志里txt读取正常但类别显示乱码val指标诡异。原因names里写了中文比如安全帽。YOLOv5在数据加载、日志输出、结果绘图多个环节对字符串编码的处理并不一致Windows控制台默认GBKLinux默认UTF-8两边一混就出问题。解决names全部用英文小写加下划线。helmet、no_helmet、fall_zone这类命名既友好又稳定txt里存的是类别id本来就不依赖names的显示。如果你已经标完且txt里写的是中文别指望后续改名直接重新转换一遍标注。5.2 图片被翻转或旋转标注却还是旧坐标现象训练loss正常下降val精度也还行但可视化检测时框的位置整体偏移或者框永远贴着物体的一半。原因图片在采集后被翻转或旋转了比如手机自动旋转存图、后期为了「正」把图手动转了一下但txt坐标还是原图上的坐标。数据链路能读语义全错了。解决在转化标注之前先统一图片方向再转换坐标。如果确实对已经标注好的图片做了水平翻转txt坐标也要同步修正水平翻转后中心点x坐标变成1 - xc宽高不变。# 水平翻转图片后修正同名txt的坐标 for line in txt_path.read_text().strip().splitlines(): cls_id, xc, yc, w, h line.split() new_lines.append(f{cls_id} {1 - float(xc):.6f} {yc} {w} {h})这条是最容易发生的「黑匣子」式错误怎么会loss正常下降因为框的偏移是系统性的模型照样能拟合一部分特征等到了真实场景就原形毕露。每次做完图片变换抽几张可视化是人眼复核的重点。5.3 小目标太多模型学不动现象训练50轮class loss还在0.5以上安全帽和no_helmet这两类recall始终上不去val曲线像一条死线。原因工地摄像头全是高位俯拍一个工人全身只有100x100像素头部可能只有15x15像素。目标检测模型下采样32倍后15x15的头部只剩不到1个像素特征几乎全丢。解决首先是输入尺寸从640提到1280小目标特征能保留更多其次是做图像切片把1080p原图按2x2切成4张640的图分别训练第三是控制训练集里「中近景」和「远景」的比例宁可少放一些远距离小目标也不要让小目标占据一半以上样本否则模型被小目标淹没中近景也学不好。小目标检测本来就是目标检测里的难点靠数据增强救不回来从采集和划分阶段就该控制难度分布。5.4 val精度虚高换工地就崩现象本地val的mAP烧到0.93模型拿到另一个工地跑真实摄像头精度掉到0.4以下。原因数据集划分时用了随机抽样同一个摄像头同一时段抽出的连续帧一部分进了train一部分进了val。模型在val上看到的画面和训练时几乎一样泛化能力被严重高估。解决回到第3.2节的按场景分组划分用摄像头ID或时间段作为分组键。更进一步工地上有条件就用「工地A训练、工地B验证」的方式评估模拟真实部署的场景迁移。val数据一旦划分好就不要动了增补数据只往train里加这样才能保证不同版本模型在同一个val上可比。5.5 空txt、越界框和类别id越界现象训练中途报IndexError: index 10 is out of bounds for axis 0 with size 10或者某个epoch卡在加载阶段不动。原因txt里写入了一个类别id10但data.yaml里nc10合法id只到9。这通常在标注时用了「0到10」的计数习惯多标了一个类或者转换脚本里类别表对不上。空txt文件则会让模型把有目标的图当背景训练越界框会让计算损耗变成负数。解决直接跑第3.3节的体检脚本把生成的问题清单全部过一遍。类别id越界的删除该行坐标越界的把数值clip到合法范围空txt连同对应的图片一起移出训练集另放一个目录人工确认是否漏标。修复后再跑一次体检直到输出为空为止。数据不脏训练才不会半路翻车。6. 让数据集自转起来半自动标注、badcase回灌与版本管理数据集做完第一版训练出了baseline事情才刚开始。工地场景几个月一变今天没有明火明天就可能出现焊接作业模型在A工地好用换到B工地可能被塔吊背景干扰。数据是持续投入的工程不是一次性交付物。6.1 用训练好的模型做半自动标注第一版模型再弱也比人工从零画框快。对新采集的工地图片用best.pt跑一遍推理生成的txt直接作为预标注python detect.py \ --weights runs/train/site_safety_v1/weights/best.pt \ --source /data/new_frames \ --save-txt \ --conf-thres 0.5生成的labels在runs/detect对应目录下人工在标注工具里打开图片时只做框的微调和增删而不是从零画。这一步能把标注效率提升两三倍。伪标签直接用是有风险的conf低于0.5的框基本是误检宁可不标也不要放进来污染数据。6.2 用badcase回灌维护长尾场景线上真实跑起来后把漏检和误检的图片攒起来按时间加机位命名归档每周补标一次加入train集重新训练。badcase只进train绝不动val这样每次改完数据后拿同一份val评估能清楚看到改动是正向还是负向。顺手把最终训练的best.pt换到目标检测流程里整个闭环就转起来了。6.3 数据版本的后悔药数据目录改起来很快改坏一个文件就白干好几天。我会在每次重新标注后把images和labels打包成一个带版本号的zip存档至少保留最近三个版本。data.yaml和文件清单一起放进git改了什么类别、增了什么图片都有记录。没有版本管理的数据集就是一颗定时炸弹等后悔的时候已经来不及了。我自己的习惯是每次跑长训练前先跑一遍第3.3节的体检脚本再随机抽20张图画框肉眼过一遍确认没问题才开机。这个习惯帮我避开了至少两次「图片旋转过但标注没同步」的事故。数据工程慢就是快希望帮到你。本文还有配套的精品资源点击获取