简介面向目标检测研究与工程应用这套建筑工地YOLO数据集包含训练集201张、验证集37张共238张真实工地图片覆盖工程机械、塔吊、人员、钢筋、脚手架等10类高频施工目标可直接支撑智慧工地安全监控、施工进度管理、施工机器人环境感知等任务。整套资源共478个文件核心为238张jpg原图与对应238个txt标注文件另附1个yaml配置文件和1个docx数据集说明文档压缩包仅19.82MB轻量易用。数据源自真实施工现场包含塔吊作业区、钢筋裸露区等高风险要素专项标注也包含机械与人员交互的密集场景可用于人员闯入预警、机械碰撞预防等安全防护系统开发。目前已有194人学习浏览适合需要快速获取工程类检测数据的建模者直接使用。1. 建筑工地目标检测数据集先回答三个问题再动手拿到这份建筑工地目标检测数据集 zip别急着解压扔进 YOLO 开训。工地场景的目标检测比通用场景刁钻——远处行人的安全帽只有十几个像素挖掘机臂和反光衣在逆光下轮廓粘连同一批图片里既有白天的监控截图又有傍晚的抓拍。这套数据集的价值不在“有多少张图”而在把工地最常见的几类目标整理成了 YOLO 能直接吃的图片txt 标注省掉爬素材和手写标注的体力活。适合两拨人一是已经跑通过 YOLO 但缺工地数据的开发者拿它做迁移训练二是要在自有镜头画面上做预研的人借它先验证安全帽、人员、机械检测的可行性。动手前先回答三个问题标注干不干净、验证集真不真实、类别均不均衡。这三个不解决训练出的 mAP 就是自欺欺人。2. 解包与数据体检目录结构、标注格式与第一个脚本我习惯拿到 zip 先做“数据体检”不做完不碰训练。工地数据的坑大多不在模型而在 txt 标注里藏着。解压之后先别急着数图片先花二十分钟把目录结构和标注格式摸清。2.1 目录结构images、labels、classes 从哪开始看解压后通常是标准的 YOLO 布局至少能看到 images 和 labels 两个平级目录图片是 jpg 或 png标注是同名的 txt。常见做法是先用命令行把顶层结构列出来确认有没有 classes.txt 或 data.yaml 这类说明文件unzip 建筑工地目标检测数据集-2.zip -d project find project -maxdepth 2 -type d | sort然后挑一张图打开它对应的标注文件看每一行到底写了什么。这一步能过滤掉八成后续会翻车的格式问题label_file project/labels/000001.txt with open(label_file, r, encodingutf-8) as f: for line in f: parts line.strip().split() print(parts)打印出来每一行是 5 个字段class_id、x_center、y_center、width、height。注意这四个坐标值全部是归一化到 0~1 的比例不是像素坐标。x_center 是目标中心点相对图片宽度的比例width 是目标宽度相对图片宽度的比例y 和 height 同理分母换成图片高度。class_id 对应 classes.txt 里的行号从 0 开始数。为什么先看目录和 txt工地数据集来源常是公开数据加人工补标混出来的不同批次目录层级可能不一样有的把标注直接放在图片同一目录下有的 labels 里嵌套按场景分了子目录。先摸清结构后面 data.yaml 的 path 才不会写错尤其是换机器跑的时候绝对路径一乱整套训练流程直接卡在第一行。2.2 第一个脚本统计 box 数量、宽高比与越界框这步我称为“标注体检”。三个最容易出问题的地方空标签文件、越界坐标、异常宽高比。写一个脚本把每个 txt 都扫一遍汇总成 CSV几秒钟就能看清整个数据集的健康状况。import os import csv from collections import Counter images_dir project/images labels_dir project/labels out_csv label_report.csv stats [] empty_files 0 cls_counter Counter() for name in os.listdir(images_dir): stem os.path.splitext(name)[0] txt os.path.join(labels_dir, stem .txt) if not os.path.exists(txt): stats.append([name, 0, missing_label, , ]) continue with open(txt, r, encodingutf-8) as f: lines [ln.strip() for ln in f if ln.strip()] if not lines: empty_files 1 stats.append([name, 0, empty_label, , ]) continue num_boxes len(lines) max_ratio 0.0 out_of_range False for ln in lines: parts ln.split() if len(parts) ! 5: stats.append([name, num_boxes, bad_format, , ]) break cls, cx, cy, w, h parts cls_counter[int(cls)] 1 cx, cy, w, h map(float, (cx, cy, w, h)) if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): out_of_range True max_ratio max(max_ratio, w / h if h 0 else 0) stats.append([name, num_boxes, out_of_range if out_of_range else ok, max_ratio, ]) with open(out_csv, w, newline) as f: writer csv.writer(f) writer.writerow([image, boxes, flag, max_wh_ratio, note]) writer.writerows(stats) print(空标注文件数:, empty_files) print(类别统计:, dict(cls_counter))逻辑说明脚本按图片主文件名找同名 txt统计每张图的 box 数量5 个字段缺一不可多了少了都算 bad_format归一化坐标只要不在 0~1 区间就标记 out_of_range顺手统计 w/h 的最大比值这个值一旦出现 10 以上基本可以判定是标注框宽度填成了整张图宽度属于转换脚本写错。参数说明如果目录不是 images/labels 平级改 images_dir 和 labels_dir 两个路径告警阈值建议在代码里直接调比如把 w/h 的告警改成大于 5.0 就输出 warning。这一步输出的 label_report.csv 就是“这份数据能不能直接训”的依据。如果 empty_files 超过 5%或者 out_of_range 超过 1%我一般先修数据再训练不然这些脏标注会一路带进 loss训练日志里根本看不出来。2.3 可视化抽查画框看标注漏标与错标一眼可见数值体检只能发现问题标注是否贴合目标、是不是漏标了一堆必须把框画出来人眼过一遍。用 OpenCV 快速画框把 class_id 显示在框上方批量导出预览图import os import cv2 def draw_boxes(img_path, txt_path, class_names, out_path): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.split() if len(parts) ! 5: continue cls_id, cx, cy, bw, bh parts cls_id int(cls_id) cx, cy, bw, bh map(float, (cx, cy, bw, bh)) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) label class_names[cls_id] if cls_id len(class_names) else str(cls_id) cv2.putText(img, label, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(out_path, img) class_names [person, helmet, vest, excavator, truck] for i, name in enumerate(os.listdir(project/images)): stem os.path.splitext(name)[0] draw_boxes(fproject/images/{name}, fproject/labels/{stem}.txt, class_names, fpreview/{stem}.jpg) if i 99: break逻辑说明把归一化坐标还原成像素坐标框直接叠加在原图上输出到 preview 目录。class_names 必须严格照抄 classes.txt 里的顺序顺序错一个所有标签含义就全错了。参数说明preview 数量可以按需截取抽样策略建议优先抽“检测框数量多的图”和“夜间、逆光图”这类图最容易暴露漏标。人眼抽查时重点看三类框明显大于目标、类别贴错、完全漏掉的远处小目标。漏标在工地数据里最隐蔽——训练时不报错只让模型那个位置学成背景推理时表现为无规律的漏检。3. 类别分布与数据划分验证集选不好mAP 就是假的数据体检通过后下一个动作是“看分布、定划分”。这一步做得糙后面训练阶段所有指标都要打个问号。我见过太多人随机切分完直接开训最后 val mAP 有 0.85现场一测直接掉到 0.5 以下问题就出在这里。3.1 类别与难例分布数据量不齐会带来什么用第 2 章的类别统计结果先看每一类的样本量。典型工地数据集的分布大致长这样具体以你手上的 zip 为准类别常见数量级典型难点person人员最多远近尺度差异大密集人群遮挡严重helmet安全帽中等小目标多正面背面外观差异大vest反光衣中等与背景亮度对比强夜间暗光难检excavator挖掘机少局部被挡臂架变形外观点少truck卡车少大目标容易裁切出界特征被截断类别不均衡的影响不是“mAP 会变低”而是“mAP 会骗人”person 和 helmet 量大把整体指标拉高excavator 和 truck 量少单类 AP 低到没法看但会被平均掉。所以训练完一定逐类看 AP别只盯着总表。注意如果 classes.txt 里的 class_id 不是连续的比如删过某个类训练前要重新映射成 0~N-1否则 YOLO 训练时要么因标签索引超出 num_classes 直接报错要么静默丢框后者更阴险。检查方法很简单统计出的类别编号如果出现 0、1、3、4 这种跳号就得处理。另外难例分布比类别数量更值得关注。工地场景里一张图同时出现几十个安全帽的密集样本和一张图只有一两个目标的稀疏样本训练难度完全不同。我一般会把每张图的 box 数量按桶统计一遍0~1 个、2~5 个、6~15 个、15 个以上看哪个桶占比最大。如果密集样本占比极低模型对楼梯口、钢筋堆放区这类高密度场景天然不敏感就算整体 mAP 看着不错实际部署也会在关键时刻掉链子。3.2 按场景切分 train/val随机切分为什么翻车最容易被新手忽略的是验证集的“场景独立性”。工地数据经常来自监控视频抽帧同一镜头连拍帧几乎一样。如果随机切分train 和 val 里会出现大量近似重复帧训练时模型等于见过答案val mAP 虚高 5 到 10 个点一到现场就垮。解决思路是按“场景”分组切分保证同一镜头的所有帧只进一边。常见做法是用文件名或目录前缀作为场景标识比如 camera_01_f001.txt 里面的 camera_01import os import random from collections import defaultdict random.seed(42) scene_groups defaultdict(list) for name in os.listdir(project/images): scene_key name.split(_)[0] # 按文件名前缀取场景 scene_groups[scene_key].append(name) val_ratio 0.2 val_images, train_images [], [] for scene, imgs in scene_groups.items(): if len(imgs) 2: continue random.shuffle(imgs) split_at int(len(imgs) * val_ratio) val_images.extend(imgs[:split_at]) train_images.extend(imgs[split_at:]) with open(train.txt, w) as f: f.write(\n.join(os.path.abspath(fproject/images/{n}) for n in train_images)) with open(val.txt, w) as f: f.write(\n.join(os.path.abspath(fproject/images/{n}) for n in val_images)) print(train:, len(train_images), val:, len(val_images))逻辑说明文件名前缀作为分组键先组内打乱再按比例取 val保证场景不串边。参数说明val_ratio 按总数据量调几千张用 20% 够用如果文件名没有规律就看图片 EXIF 或者按目录维度分组。如果这个 zip 里没有可用的场景标识兜底方案是把连续编号按区间切比如前 80% 的帧进 train、后 20% 帧进 val也比纯随机靠谱得多因为视频帧在时间上天然连续相邻帧内容高度相似区间切分至少模拟了“没见过的连续片段”这种更接近现场的情况。3.3 类不均衡处理要不要过采样别上来就调 loss看到类别比例差到 10:1 以上先别急着改 loss 权重先做数据层面的处理。最简单有效的是对少量类图片做整图过采样在 train.txt 里把 excavator、truck 占比高的图多写几遍让它被采样到的概率上来from collections import Counter cls_img_count Counter() with open(train.txt) as f: img_paths [p.strip() for p in f if p.strip()] for p in img_paths: txt p.replace(images, labels).replace(.jpg, .txt).replace(.png, .txt) with open(txt) as f: cls_ids {int(ln.split()[0]) for ln in f if ln.strip()} for c in cls_ids: cls_img_count[c] 1 min_count 300 oversampled [] for p in img_paths: txt p.replace(images, labels).replace(.jpg, .txt).replace(.png, .txt) with open(txt) as f: cls_ids {int(ln.split()[0]) for ln in f if ln.strip()} need any(cls_img_count[c] min_count for c in cls_ids) if need: oversampled.extend([p] * 3) # 复制3份写入训练列表 else: oversampled.append(p) with open(train_oversampled.txt, w) as f: f.write(\n.join(oversampled))逻辑说明只对包含少量类的图片重复写入训练列表不修改标注文件本身不占额外磁盘。复制 3 份是一个保守起点不是越多越好复制过猛会让模型对这批图片过拟合反而丢掉泛化。参数说明min_count 按总量调整总量五千张时设 300 合理总量一万张可以调到 500。过采样只在 train 上做val 标签分布保持原始状态否则验证结果失真。如果过采样之后类别还是偏再考虑改 loss 权重但 YOLOv8 的 cls loss 权重影响面很大调它极易把 person 的查全率压下去属于最后手段而且必须配合逐类 AP 观察不能只看总 mAP。4. 训练适配YOLOv8 全流程参数取舍与增强开关体检和切分都做完数据集基本干净了才轮到训练。这一章按 YOLOv8 的流程走一遍参数给的是我自己用的起点不是固定答案显存大小和数据量不同都要缩放。4.1 data.yaml 配置与首轮训练的初始参数YOLOv8 训练需要一个 data.yaml 指向数据。先看一份典型配置path: /data/project train: train.txt val: val.txt names: 0: person 1: helmet 2: vest 3: excavator 4: truck逻辑说明train 和 val 填 txt 文件的路径names 的顺序必须和标注文件里的 class_id 严格一致。参数说明names 建议显式写编号别省写成列表形式显式编号能避免和 classes.txt 对不上时排查半天。训练命令我给一个起点组合yolo detect train \ data/data/project/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ close_mosaic10 \ project./runs \ namesite_v1参数说明model 用 yolov8s.pt 起步工地数据量级在几千张时 s 比 n 收敛稳m 只在 s 效果不理想再上直接上 m 训练时间翻倍不说小数据还容易过拟合imgsz 默认 640如果后面发现远处安全帽召回低再升 1280代价是显存和训练时间几乎翻倍batch 按显存来8G 显存建议 1616G 可以 32再往上收益递减。close_mosaic10 表示最后 10 个 epoch 关掉 mosaic这是稳定收敛的关键别省。lr0 的 0.01 是官方默认几千张数据的微调场景可以降到 0.005收敛更平滑。另外有一个小数据集常用的技巧如果原模型是 COCO 通用权重而你的工地数据只有几千张我会在前两个 epoch 加 freeze10把 backbone 前 10 层冻结住让模型先适应工地图像的色调和分布两轮之后再解冻全量微调。做法就是训练命令里加 freeze10。这个技巧对小数据集特别稳可以避免一开始 loss 剧烈震荡。4.2 数据增强开关mosaic、HSV、翻转怎么组合YOLOv8 的增强默认值是官方在 COCO 上调出来的工地场景要按光线和尺度做针对性调整。我的习惯是先跑默认配方再动以下几个参数参数默认值工地场景建议说明hsv_h0.0150.02工地早中晚色温差异大稍加色相扰动hsv_s0.70.8反光衣和安全帽饱和度本来就高扰动不影响语义hsv_v0.40.5逆光与夜间样本需要更多亮度变化fliplr0.50.5水平翻转保留安全帽左右对称flipud0.00.0默认不开上下翻转破坏地面影子方向语义mosaic1.01.0对小目标提点有好处但训练后期要关闭flipud 在工地监控里建议保持 0。原因很直接摄像头固定朝下或朝前上下翻转后地面上的影子方向变成反的模型会学到错误的空间特征。mosaic 对小目标有效但训练后期不停会造成损失震荡所以配合 close_mosaic 使用。这些参数可以直接写进训练配置文件hsv_h: 0.02 hsv_s: 0.8 hsv_v: 0.5 fliplr: 0.5 flipud: 0.0 mosaic: 1.0 close_mosaic: 10提示增强参数一次只动一个改完对比 val mAP 再决定下一步。同时动三个参数翻车了都不知道是哪个改坏的。4.3 训练监控loss 曲线与 P/R 曲线到底看什么训练起来之后重点盯三类曲线box_loss、cls_loss、dfl_losstrain 和 val 两条线都看。train loss 一直降、val loss 掉到某个点开始回升就是过拟合这时候回退到 val 最低点的 checkpoint 或者提前停止。工地场景更常见的问题是 val loss 不降、val mAP 卡住不动这时候先查数据是不是 val 集里混了训练集同场景帧或者标注空文件太多。训练过程会自动生成 results.csv用脚本看最后几行比较快import pandas as pd df pd.read_csv(runs/site_v1/results.csv) tail df.tail(10) print(tail[[epoch, metrics/precision(B), metrics/recall(B), metrics/mAP50(B)]])逻辑说明results.csv 每行一个 epoch直接看尾部十行就能判断收敛状态。如果 val loss 连续二十轮不降就早停别硬烧显卡时间。除了 loss 还要看 precision 和 recall工地检测的诉求和通用场景不一样漏检一个安全帽可能出安全事故所以我优先保 recall 再看 precision。注意 val 阶段默认 conf 阈值是 0.001它只影响 PR 曲线上的点位置不参与 mAP 计算别拿 conf 阈值调 mAP调它没有意义。5. 避坑五个高频踩坑记录与对应修法这一章直接给节省时间的经验全是实际跑工地数据时反复遇到过的坑按现象、原因、解决三个维度写。5.1 标注与格式类class id 越界、names 错位、归一化偏移坑一训练一开始 loss 直接变 nan。现象是前几个 epoch 正常跑到第五轮左右 loss 跳到 3e38 或者直接 break有的版本不报错只是 loss 曲线突然断掉。原因几乎都是标注 txt 里出现了超过 classes 数量的 class_id模型计算损失时索引越界。解决训练前扫描全部 txtpython -c import glob ids set() for f in glob.glob(project/labels/*.txt): for line in open(f): ids.add(int(line.split()[0])) print(max class_id:, max(ids))必须小于 data.yaml 里 names 的长度发现越界就修标注重新生成标签。坑二某个类别 AP 恒为 0但样本量不小。现象是训练日志正常loss 曲线正常就是某一类的 AP 是 0.000。原因classes.txt 和 data.yaml 的 names 顺序不一致标注里的 0 实际对应的是另一个类模型从头到尾在学错标签。解决用第 2 章的类别统计脚本对照各 class_id 的频次和 names 逐项核对改完重新切分训练。这个坑最坑人的地方在于不报错指标也不会全崩只有那一个类孤立地异常。坑三推理时预测框整体偏移或尺寸离谱。现象是框比目标大一圈或者中心点位置偏到目标边缘。原因坐标从像素转归一化时用错分母常见是除以了长边而不是宽高各除各的或者用了 resize 之后的尺寸而不是原始尺寸。解决检查 txt 里是否有 cx、cy、w、h 大于 1 的值有就重新归一化。这条坑在人工标注导出时特别常见导出脚本写错的概率比标注员标错的概率大得多。5.2 训练与部署类显存、小目标、类别混淆坑四训练到一半显存爆掉OOM 退出。现象是 epoch 跑到一半终端打出 CUDA out of memory训练进程直接死掉。原因batch 开大了mosaic 增强在拼接四张图时又额外占内存峰值显存远超预期。解决先 nvidia-smi 看显存占用batch 减半close_mosaic 提前到 15还爆就把 imgsz 降到 640 以下但不推荐低于 480会明显损失小目标特征。换 yolov8n.pt 起步是最省事的方案省下的显存比调任何参数都多。坑五现场测试时远处安全帽漏检一片但 val mAP 看着还行。现象是现场抽查视频远处几个没戴帽子的工人根本没出框近处倒是全都检到了。原因640 分辨率下远处小目标不足 10 像素训练时特征很容易被背景淹没val 集里如果小目标占比低mAP 不会如实反映。解决训练时升 imgsz 到 1280或者推理阶段做滑窗切图把原图切成 640×640 的块分别检测再合并。切图推理只在没时间重训时应急效果立竿见影但速度慢一半。另外可以单独统计 val 集里小目标比例低于 20% 就要在评估时额外补一组小目标样本看召回。提示反光衣和人这两种类在图像上特征重叠反光衣套在人身上如果标注边界没有统一标准模型会反复横跳。要么明确“反光衣独立成框不含头手”要么明确“整人一个框反光衣属性留给分类模型”两种都行但必须全数据集统一这个统一标准要在动手训练前定完。6. 验证与进阶mAP 之外把模型调得更实用训练完别只报一个 total mAP展开看逐类指标。val 输出的 confusion_matrix.png 能直接告诉你错在哪对角线上亮是正解非对角线的亮点就是具体混淆对。比如 helmet 和 person 之间如果出现明显的互相误检要么回标注修改边界框要么考虑把“戴安全帽的人”拆成 person 和 helmet 两个独立检测任务先检人再判帽业务上更稳容错也更好解释。另一种常见做法是把细分类合并成大类先跑通比如 excavator 和 truck 数据量都少先合并成 vehicle 训练等整体流程稳定了再拆回细类这个思路在工程预研阶段特别实用。另一个实用技巧是固定验证集和随机种子。我每次对比实验都用同一个 val.txtseed 固定 42这样改动一个变量时指标变化能归因到模型而不是数据抖动。做消融时一次只动一个参数先动 imgsz再动增强最后动 loss 权重别同时改三个不然翻车了都不知道是哪个改坏的。导出环节也容易卡人。YOLOv8 导出 ONNX 时加 dynamicTrue 可以放开 batch 维但某些部署框架对 dynamic 支持不好如果只需要固定 batch 推理导出时直接指定 batch1 省去一堆兼容性工作推理速度还能快一点。NMS 阈值一般保持默认 iou0.7工地场景框多且密集时调到 0.5 能减少重复框但可能误杀两个挨着的安全帽要按现场实测调别拍脑袋。从那以后我每次拿到新的工地数据集都强制走一遍全套流程先跑数据体检脚本再过一遍可视化抽检然后按场景切分最后才允许自己碰 train 命令。这套流程救过我好几次希望帮到你。本文还有配套的精品资源点击获取