☰
工程车检测数据集完全指南:从VOC格式到YOLO训练实战
2026/10/1 6:05:46 网站建设 项目流程

简介:这份工程车检测数据集围绕10111张原始图片构建,面向智慧工地、工程车辆识别、自动驾驶辅助感知等目标检测场景,覆盖水泥卡车、空载及载物自卸卡车、挖掘机、装载机等常见工程车辆类别,适合有标注需求的算法工程师、科研人员以及相关专业学生使用。下载压缩包内以2000个XML标注文件为主,文件为Pascal VOC格式,整体包体大小约663.9MB;每个XML文件对应一张图片的标注信息,可直接配合对应图片用于模型训练与评估,也可按需转换成YOLO、COCO JSON等主流格式,方便接入不同训练框架。目前已有165人学习下载,比较适合需要快速构建工程车检测训练集的场景。借助这套标注,可显著减少人工标注成本,覆盖多种类变体和工况,有助于提升模型在复杂施工环境下的识别鲁棒性。

1. 工程车检测数据集:从10111张原始图到工地车辆识别模型

工地上的摄像头经常拍到这样的画面:一辆水泥罐车堵在卸料口,后面排着两辆自卸卡车,一辆空载一辆满载,挖掘机在边上清理渣土。如果靠人盯监控,十几路画面根本看不过来。工程车检测数据集做的就是这个事——用10111张原始图片,按PASCAL VOC格式标注出水泥卡车、空载自卸卡车、载物自卸卡车、挖掘机和装载机五个类别,专门用来训练工地场景下的目标检测模型。它适合两类人:一是刚起步、不想从零采集标注的算法工程师,二是做智慧工地管理平台的团队,拿它当基线数据。要提醒的是,一万张图不等于一万个独立角度,后面会讲划分和补样的具体做法。

2. PASCAL VOC格式怎么看:先搞懂目录结构和五类目标约定

拿到工程车检测数据集,先别急着训练。压缩包解压后一般能看见 JPEGImages、Annotations、ImageSets 三层目录,这是 VOC 格式的标准长相。不少人不看目录直接拖进训练脚本,结果要么图像路径读不到,要么标注文件配对失败。花二十分钟把目录结构、XML 字段和类别语义理清,比后面踩坑再回头找快得多。

2.1 检测领域为什么还在用 PASCAL VOC 这套交接格式

PASCAL VOC 是 2000 年代早期开始的视觉目标识别竞赛用的格式,到 2007 版之后目录规范和 XML 结构基本稳定下来,很多标注工具默认就按它输出。现在 COCO 格式在学术论文里更常见,但工程车数据集、遥感数据集、工业缺陷数据集里,VOC 仍然是交付主力。原因是标注工具 LabelImg 保存出来就是 VOC XML,外包数据标注团队对这个格式熟练,交接时不容易产生理解偏差。你用 CVAT 也能导入,但 CVAT 默认导入的是 COCO 或 YOLO 格式,VOC 反而要先打包成 ZIP 再选特定导入项,多一步手续。

VOC 和 COCO 的差别主要在两点:VOC 每个目标一个 XML 文件,COCO 是所有标注堆在一个 JSON 里;VOC 用绝对像素坐标存目标框(xmin、ymin、xmax、ymax),COCO 也存像素坐标但格式是数组。如果你的下游是 YOLO,两边都得转,转的时候最容易丢的是 difficult 字段。这个字段在 VOC 里专门标记“人眼都难确认”的目标,后面的训练脚本如果不过滤它,会给模型灌进去一堆模糊框。

2.2 三层目录结构:JPEGImages、Annotations、ImageSets 分别存什么

典型的 VOC 数据集长这样:

engineering_vehicle/ ├── JPEGImages/ │ ├── IMG_0001.jpg │ └── IMG_0002.jpg ├── Annotations/ │ ├── IMG_0001.xml │ └── IMG_0002.xml └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── test.txt

JPEGImages 放原始图片,一般 3 通道 JPG,不排除数据集里混了 PNG。Annotations 放同名 XML,里面记录这张图里所有目标的位置和类别。ImageSets/Main 下的 txt 文件是划分清单,每行一个图片文件名,不带扩展名。VOC 官方读取数据时,靠这些 txt 知道哪些图参与训练、哪些参与验证。

拿到数据集第一件事是检查配对完整性。常见问题是 JPEGImages 里 10111 张图,但 Annotations 里只有 10000 个 XML,多出来的图没有标注。用 Python 几行就能查:

import glob from pathlib import Path imgs = {Path(p).stem for p in glob.glob("JPEGImages/*.jpg")} xmls = {Path(p).stem for p in glob.glob("Annotations/*.xml")} print("缺标注的图:", len(imgs - xmls)) print("缺图片的XML:", len(xmls - imgs))

逻辑说明:这里用集合差找出名字不配对的文件。缺标注的图如果数量不大,可以从训练列表里剔除;缺图片的 XML 说明标注存在但图片丢了,要先找数据提供方要图,不要硬删,否则标注资源浪费了。另一个高频问题是大写扩展名 mismatch,比如图是 .JPG,XML 里写 .jpg,集合判断会认为它们是两个文件,建议统一转成小写后缀再比。

2.3 拆开一份 XML 标注:五个关键字段决定模型能否学对

随便打开一个 Annotations 里的 XML,结构类似这样:

<annotation> <folder>JPEGImages</folder> <filename>IMG_0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>cement_mixer</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>102</xmin> <ymin>220</ymin> <xmax>608</xmax> <ymax>742</ymax> </bndbox> </object> </annotation>

关键字段有五个:filename 决定图片文件名,size 里的 width 和 height 决定归一化分母,name 是类别名,bndbox 是目标框,difficult 是困难标记。其中最难处理的是 name 和 difficult 的组合。name 里可能同时出现 cement_mixer、cement mixer、CementMixer 三种写法,不统一会导致转换脚本识别成三个类。difficult 为 1 的框,在 VOC 官方评测里不计入正样本,但很多转换脚本会默认把它当成普通目标。

类别边界本身也要注意。这个数据集把自卸卡车拆成空载和载物两类,区别在于货斗状态:空载时能看到空货斗的结构纹理,载物时有堆积物或货斗抬升。挖掘机和装载机的差异在工程现场非常直观,但遮挡严重时静态图很难分。我一般建议拿到数据后先随机抽 50 张图,自己写脚本把 bounding box 画出来,肉眼过一遍类别名和框的位置,这一步能筛掉大量标注错位问题。

3. 从 VOC 到可训练的 YOLO 数据:类别统计、坐标归一化与划分

VOC XML 不能直接扔给 YOLO 训练。Ultralytics YOLO 默认读的是每张图一个同名 txt,每行格式是“类别ID 中心点x 中心点y 宽 高”,全部归一化到 0 到 1。所以动手训练前必须先把 10111 个 XML 转成 txt,并完成训练/验证/测试划分。这一步做不对,后面所有精度数字都是假的。

3.1 为什么必须从 VOC 转成 YOLO 的 txt 标签

YOLO 训练管线的数据加载器按目录扫描,图片路径从 data.yaml 里的 train 字段拿,标签路径默认和图片路径同级但扩展名换成 txt。VOC XML 是嵌套结构,YOLO 的 label 加载逻辑不认这种格式,直接改后缀也读不出来。还有一点:YOLO 的标签文件必须和图片同名,训练时会用图片的路径去拼标签路径,任何一端的文件名对不上就静默跳过。

另一个需要转换的原因和类别顺序有关。VOC 用字符串类别名,YOLO 用整数索引。转换脚本里 classes 列表的顺序就是训练时的类别 ID,顺序写错,模型学到的就是错位映射。比如列表写成["excavator", "loader", "cement_mixer", ...],那么挖掘机的框训练时会被当成装载机,推理时的输出名也跟着错。所以转换脚本里的顺序必须和后面 data.yaml 的 names 保持一致,最好从同一个文件读。

3.2 转换脚本:归一化、difficult 过滤与坐标 clip

下面这段脚本是我在类似数据集上常用的转换逻辑,处理了类别名不统一、difficult 过滤和坐标越界三个问题:

import glob import xml.etree.ElementTree as ET from pathlib import Path CLASSES = ["cement_mixer", "dump_truck_empty", "dump_truck_loaded", "excavator", "loader"] def convert_one(xml_path, out_dir): root = ET.parse(xml_path).getroot() size = root.find("size") width = float(size.find("width").text) height = float(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text.strip().lower().replace(" ", "_") if name not in CLASSES: print(f"skip unknown class: {name}") continue diff = obj.find("difficult") if diff is not None and int(diff.text) == 1: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) xmin = max(0, min(xmin, width - 1)) ymin = max(0, min(ymin, height - 1)) xmax = max(0, min(xmax, width - 1)) ymax = max(0, min(ymax, height - 1)) cx = (xmin + xmax) / 2.0 / width cy = (ymin + ymax) / 2.0 / height bw = (xmax - xmin) / width bh = (ymax - ymin) / height cls_id = CLASSES.index(name) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_path = Path(out_dir) / (Path(xml_path).stem + ".txt") out_path.write_text("\n".join(lines) + "\n", encoding="utf-8") xml_list = glob.glob("/data/engineering_vehicle/Annotations/*.xml") for xml_path in xml_list: convert_one(xml_path, "/data/engineering_vehicle/labels")

参数说明:name 处理那行把大写转小写、空格转下划线,能兼容标注员手滑写出的变量写法。difficult 判断用if diff is not None and int(diff.text) == 1,既处理缺字段又处理值等于 1 的情况。坐标 clip 那四行是把越界框先夹紧到图像尺寸之内,再参与归一化,防止生成负值或超过 1 的标签。

跑完脚本后要抽查转换结果。我习惯随机挑 20 个 txt,对照原图和 XML 手动验证中心点、宽高是否合理,另一个更快的办法是直接用脚本画框检查,后面会说到。注意,如果某个 XML 里只有 difficult 为 1 的目标,转换后的 txt 会是空文件,这种情况是正常的,但训练时 Ultralytics 会跳过空标签目标图。

3.3 数据划分:先按批次再随机,否则验证集是假的

多数人拿到数据集后直接用 random.shuffle 把图片打乱,按 8:1:1 切成 train/val/test。对工程车数据这么做有个隐患:工地相机是连续拍摄的,同一个时间段里同一台挖掘机可能出现在相邻几十帧画面里。随机划分虽然图片本身不重复,但训练集和验证集里会出现同一台车的不同画面,模型相当于提前见过这台车的涂装和轮廓,验证精度会虚高。

import glob import random from pathlib import Path all_images = sorted(glob.glob("/data/engineering_vehicle/JPEGImages/*.jpg")) random.seed(42) random.shuffle(all_images) n = len(all_images) val_count = int(n * 0.1) test_count = int(n * 0.1) def save(txt_name, images): out_path = Path("/data/engineering_vehicle/ImageSets/Main") / txt_name lines = [Path(p).stem for p in images] out_path.write_text("\n".join(lines) + "\n", encoding="utf-8") save("train.txt", all_images[val_count + test_count:]) save("val.txt", all_images[:val_count]) save("test.txt", all_images[val_count:val_count + test_count])

逻辑说明:这段代码先按文件名排序再打乱,切出 10% 验证、10% 测试、80% 训练。随机种子固定为 42,保证每次跑结果一致。如果你的数据集文件名前缀里带时间戳或相机编号,建议先按前缀分组,再对分组 ID 做 shuffle,这样同一台设备不会同时出现在训练和验证里。

划分完成后还要检查一个细节:ImageSets/Main 里的 txt 存的是不带扩展名的文件名,而 YOLO 训练时不读这个 txt,它只按目录读。所以如果你要用 Ultralytics 训练,正确的做法是把图片按 train/val/test 复制或移动到不同子目录,而不是只写清单。我一般生成 txt 清单用于 VOC 语义下的离线评估,同时用另一个脚本把图片搬运到images/train、images/val、images/test,标签也同步搬,保证 YOLO 能按目录扫描到。

3.4 统计五类样本量:决定增强和损失权重

转换完之后先别急着配置训练,统计一下各类目标框的数量。工程车数据集的类别天然不均衡:水泥卡车和自卸卡车是工地主力,出现频次可能远超挖掘机和装载机。不均衡到一定程度,小类别的 AP 会被模型牺牲掉。

import glob import xml.etree.ElementTree as ET from collections import Counter counter = Counter() for xml_path in glob.glob("/data/engineering_vehicle/Annotations/*.xml"): root = ET.parse(xml_path).getroot() for obj in root.iter("object"): name = obj.find("name").text.strip().lower().replace(" ", "_") counter[name] += 1 total = sum(counter.values()) for name, count in counter.most_common(): print(f"{name:20s} {count:6d} {count/total:.1%}")

参数说明:脚本遍历所有 XML,统计每个类别名的出现次数。total 是所有目标框的总数,输出里能看到每个类别的占比。如果某个类别占比低于 10%,训练时就要考虑做类别重采样,或者在损失函数里提高该类别的权重。另一种做法是用 mosaic 增强把该类别的小目标多拼几张,但工程车是大目标,拼出来的效果有限,我一般优先调损失权重。

4. 用 YOLOv8 训练工程车检测模型:data.yaml、分辨率与增强参数

数据转好、划分妥当之后,进入训练环节。目标检测框架选 Ultralytics YOLO 是目前最省事的路径,文档全、报错友好、权重好下手。工程车检测属于中等目标、类别差异明显的任务,不需要用超大模型硬怼,关键是分辨率和数据增强要匹配工地现场的光照条件。

4.1 模型档位选择:为什么 10111 张图用 n 或 s 就够

10111 张图、五个类别,放在检测任务里属于中等偏小规模。类别数不多,目标体积大,用 YOLOv8x 或 YOLOv8l 这类大模型,参数量上去了,但数据规模撑不起训练,容易过拟合。我的判断是:边缘盒子部署选 YOLOv8n,追求精度且卡上有 24G 显存选 YOLOv8s。n 模型参数量少,前向速度快,适合同时跑多路视频流的真实项目;s 模型在五类区分度不够的细节上(比如空载和载物自卸车)会有更好的表现。

如果拿不准,就用同一份数据分别跑 n 和 s,各训 50 个 epoch,对比验证集的 mAP 和推理耗时。工程现场对误检的容忍度低,但对延迟容忍度更低,四路摄像头同时解析的场景下,n 的优势比想象中大。

4.2 data.yaml:路径和类别名必须和转换脚本一致

训练前先写 data.yaml,Ultralytics 靠它找图片、找标签、对类别名。路径用绝对路径最稳,特别是换机器训练时,相对路径经常因为启动目录变化而读不到数据。

path: /data/engineering_vehicle train: images/train val: images/val test: images/test names: 0: cement_mixer 1: dump_truck_empty 2: dump_truck_loaded 3: excavator 4: loader

参数说明:path 是数据集根目录,train/val/test 是相对于 path 的子目录。names 的索引顺序必须和转换脚本里的 CLASSES 完全一致,否则数据标签的 ID 和名称对不上。工程车数据集里最容易出的问题就是数据提供方交付的类别名顺序和你的 CLASSES 不一致,报错还好办,最怕不报错,模型正常训练,但实际输出类别名错位。

4.3 分辨率决策:640 和 1280 之间取舍什么

输入分辨率是工程车检测里影响最大的参数。640 是 YOLO 默认值,训练快、显存占用低,但对小型目标或远距离目标不友好。工地上相机视野大,一辆挖掘机在画面里可能只占 200×150 像素,货斗状态这种细粒度特征在 640 分辨率下会被压掉。我建议至少用 1280 起步,尤其是自卸卡车空载和载物这种细微差异,分辨率不够时模型只能靠颜色和纹理猜。

分辨率翻倍,计算量大约翻四倍。1280 分辨率加 batch 16,需要的显存量级在 20G 以上,RTX 3090 或 4090 能跑,8G 显存的小卡就只能往 640 回去。折中做法是训练阶段用 1280,推理阶段降到 960,因为训练时需要更细的梯度信息,推理时只需要前向传播结果。

4.4 训练命令与增强参数:一万张图怎么防过拟合

训练命令我习惯写成这样,便于复现和换参:

yolo detect train \ data=/data/engineering_vehicle/engineering_vehicle.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=1280 \ batch=16 \ workers=8 \ patience=30 \ cache=True \ mosaic=1.0 \ close_mosaic=10 \ hsv_h=0.015 \ fliplr=0.5 \ project=run_engine_vehicle \ name=exp_s_1280

参数说明:epochs 设 200,配合 patience=30 早停,如果连续 30 个 epoch 验证指标不涨就自动停,不用守着看。mosaic=1.0 开启马赛克增强,把多张图拼成一张训练,放大样本多样性;但 mosaic 会改变目标比例,最后 10 个 epoch 用 close_mosaic=10 关掉,让模型在接近真实分布的图片上微调。fliplr=0.5 做水平翻转增强,车辆左右对称,这个增强安全且有效。hsv_h=0.015 控制色调扰动,工地光照从早到晚变化大,适度的色相扰动能提升模型的鲁棒性。cache=True 把图片缓存进内存,10111 张图大约几十 G 内存,但能显著减少磁盘读取卡顿。

如果你的卡只有 8G 显存,把 imgsz 降到 640、batch 降到 8,其余参数不变。训练时间会短很多,但验证集指标通常比 1280 低 3 到 5 个点,这个差距在工程车这种大目标任务里主要来自货斗状态识别的丢失。

5. 工程车数据集使用避坑:五个真实翻车现场

这一章不写理论,只写我实际遇到过并修过的坑。每条都是“现象 → 原因 → 解决”的格式,照方抓药即可。

5.1 类别名不统一:训练时类别数突然多了一个

现象:转换脚本跑完后统计标签,发现类别数不是 5 而是 7,训练时 loss 正常下降,但验证集的某些类别 AP 恒为 0。原因:XML 里的类别名存在cement_mixer、cement mixer、CementMixer三种写法,数据标注阶段人工输入没有强制统一。解决:转换脚本里加一行name = name.strip().lower().replace(" ", "_"),把空格和下划线都归一到下划线。更稳妥的做法是先统计所有出现的原始 name 值,打印出来人工过一遍,再决定映射关系。

5.2 difficult 框当成正样本:损失收敛但 mAP 一塌糊涂

现象:训练过程正常,验证集 mAP 在 0.6 左右上不去,抽查预测结果发现模型大量输出置信度低、框不齐的检测结果。原因:XML 中 difficult=1 的目标被转换脚本当成普通正样本。这些目标往往遮挡严重、非常模糊或目标极小,连标注人员都要商量半天才能确定,模型强行学习会引入大量噪声。解决:转换脚本里遇到 difficult=1 的框直接 continue,不写入 txt。如果你发现整个数据集 difficult 比例超过 5%,说明标注团队对模糊目标的判断不一致,需要回看标注文档重新界定。

5.3 空载和载物自卸车互相串:标注规范先立住

现象:模型把空载自卸车的金属货斗结构识别成载物,或者反过来把载满土方的自卸车标成空载。原因:货斗状态在部分拍摄角度下视觉差异太小。侧前方能看到货斗内部,空载时能看到金属底板纹理;但俯视角度只看得到货斗顶部,空载和载物的差异基本消失。解决:标注规范里要写明判断优先级——先看货斗是否抬升,抬升视为空载或卸料状态;再看货斗内部是否可见堆积物;两者都不可见时标 difficult=1 而不是硬猜。如果业务上只关心“有没有自卸车”,把两个类合并成 dump_truck 更稳,训练难度直接降一档。

5.4 随机划分导致验证集“提前见过”同一台设备

现象:训练时验证 mAP 到 0.85,上线到新工地实测只有 0.5。原因:随机划分数据集时,同一台设备不同帧的图片同时落在训练集和验证集,模型在训练阶段已经把该设备的涂装和轮廓背下来了,验证精度虚高。解决:不要对图片文件直接随机,按拍摄批次分组,再把整组数据划到训练或验证。工程车数据集的图片名如果是cam01_20240901_120001.jpg这种带相机编号和时间戳的格式,就按前缀cam01分组;如果没有分组信息,用文件名里时间戳的小时段做粗分组,能缓解部分问题。

5.5 文件名配对失败:转换脚本漏掉上千张图

现象:转换完发现只有 8000 个 txt,比 XML 少了 2000 个。原因:XML 的 filename 字段里写的是C:\data\IMG_0001.jpg或IMG_0001.JPG,而 JPEGImages 里实际文件名是大写 JPG;或者 XML 里文件名带了额外空格。用 XML 的 filename 字段去拼路径时,匹配不上,脚本跳过或写入错误目录。解决:转换脚本里不要信任 XML 的 filename 字段,直接用Path(xml_path).stem取 XML 自己的文件名,再去 JPEGImages 目录里找同名图片。扩展名统一成小写后再比,找不到的图打印到日志,人工确认。

6. 验证与部署技巧:用混淆矩阵和时序投票压住误检

训练结束不代表模型能用。工程车检测上线后面临的主要问题不是漏检,而是误检——建筑围挡上的图案、树影、甚至有塔吊阴影都会被识别成挖掘机。单帧检测的置信度阈值调来调去,压住误检的同时又把真目标漏了,这个局面对抗了很久。后来发现,工地视频天然是连续帧,用帧级投票代替单帧阈值,比反复调参数有效得多。

先跑一次验证拿到混淆矩阵,看清模型到底在哪个类别上犯糊涂:

yolo detect val \ model=run_engine_vehicle/exp_s_1280/weights/best.pt \ data=/data/engineering_vehicle/engineering_vehicle.yaml \ split=test \ project=run_engine_vehicle \ name=val_test

逻辑说明:split=test 指定在 test 子集上验证。输出目录里会生成 confusion_matrix.png 和 results.csv,重点看空载自卸卡车和载物自卸卡车之间有没有互相串,再看挖掘机和装载机是否混淆。如果混淆集中在这两对,单靠加数据不如调整标注规范,把不可区分的样本删掉比硬学更见效果。

帧级投票的实现在思路上很直接:对同一路视频流,维护一个最近 10 帧的检测结果队列,同一个位置的目标连续出现至少 6 次才输出,否则丢弃。

from collections import deque frame_window = deque(maxlen=10) def frame_filter(dets): frame_window.append(dets) counter = {} for det_list in frame_window: for det in det_list: key = (det["cls"], int(det["x1"] // 20), int(det["y1"] // 20)) counter[key] = counter.get(key, 0) + 1 return [key for key, cnt in counter.items() if cnt >= 6]

参数说明:key 由类别加坐标量化构成,x1 // 20 把坐标压到 20 像素的网格里,同一目标的框即使有抖动也会落在相邻网格,视为同一个目标。连续 10 帧出现过 6 次以上才输出,能滤掉大部分单帧误检。代价是真实目标出现时间不足 0.6 秒(以 10FPS 采样估算)时会被漏掉,工程车移动慢,这个代价可以接受。

补数据也有技巧。模型在验证集上的错误预测图可以用脚本批量导出,人工快速扫一遍,挑出置信度高但预测错的图,回标后补进训练集。这类高质量错误样本比随机追加几千张普通图更有价值,因为它们正好卡在模型决策边界上。做完这一步再回头跑一轮训练,置信度阈值可以适当下调,因为帧级投票兜住了误检。

踩过这么多坑之后,我现在的习惯是拿到任何检测数据集,先花一个下午做配对检查、格式转换和类别统计,再开始训练。前面省掉的时间,后面都会在精度排查和标注返工上还回来。工程车检测这个方向的数据集不是很多,能拿到 10111 张完整 VOC 标注的,值得认真对待。希望这篇拆解能帮你把数据集用出应有的价值,少走几趟我当年的弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询