简介:面向车辆检测与目标识别研究的工程车辆图像数据集,共收录1000张已标注图片,涵盖重型卡车、沥青车、搅拌车、清障车、洒水车、拖拉机、挖掘机、压路机、吊车、自卸车等常见类型,可支撑YOLO、Faster R-CNN、SSD等深度学习模型的训练与评估,适用于自动驾驶、智能交通监控、工地安全等场景。压缩包内共1998个文件,由999张jpg原图与999个xml标注文件组成,整体约77.42MB;xml文件按PASCAL VOC/COCO格式记录每辆车的位置边界框与类别信息,可直接投入模型训练流程,省去手动标注成本。每张图像均由专业人员精确标注,边界框与类别一应俱全,有助于提升算法在实际工程环境中的泛化能力。目前已有3916人学习下载,对于需要高质量车辆检测数据的开发者和研究人员而言,是一份可直接上手的实用资源。
1. 工程车辆数据集到手先别急着训:1000张已标注图能撑起多大场面
接到一份「工程车辆数据集1+1000张IMG+已标注」的交付,很多人第一反应是直接扔进YOLO。我的建议是先停一下。1000张已标注图片,做工地出入口识别、渣土车车厢状态检测、挖掘机违规作业告警这类场景的算法验证和项目demo,完全够用;但要直接扛夜间、扬尘和恶劣天气上线,数据量还差一个量级。这篇文章把这个数据包从验收到训练、从踩坑到补数据的完整链路讲清楚,适合刚拿到数据集、想尽快跑通检测模型并交付结果的人照着做。这里说的IMG指交付的图片包,可能是jpg、png,也可能混着硬件采集的raw格式导出的bmp,统一转码是第一步。
2. 数据体检先行:把IMG图片和标注文件整理成可训练的标准格式
拿到手先别急着配环境。工程车辆数据集最常见的问题不是模型训不动,而是图片和标注对不上。所谓「已标注」,常见交付是VOC格式的xml,也可能是labelme导出的json,少数情况下直接就是YOLO的txt。不管哪种,都要先做一遍完整清点,把图片、标注、尺寸三者的对应关系理清楚,后面训练才不会翻车。
2.1 第一步清点:图片-标注对应关系与损坏文件检查
写个脚本把所有图片过一遍,这是整个流程里性价比最高的一步:
import os from PIL import Image import xml.etree.ElementTree as ET img_dir = "images" ann_dir = "annotations" missing_ann = [] broken_img = [] size_unmatch = [] for img_name in os.listdir(img_dir): stem, ext = os.path.splitext(img_name) if ext.lower() not in (".jpg", ".jpeg", ".png", ".bmp"): continue ann_file = os.path.join(ann_dir, stem + ".xml") if not os.path.exists(ann_file): missing_ann.append(img_name) continue try: im = Image.open(os.path.join(img_dir, img_name)) w, h = im.size except Exception: broken_img.append(img_name) continue tree = ET.parse(ann_file) size = tree.getroot().find("size") aw = int(size.find("width").text) ah = int(size.find("height").text) if (aw, ah) != (w, h): size_unmatch.append(img_name) print("缺标注:", len(missing_ann), missing_ann[:10]) print("损坏图片:", len(broken_img), broken_img[:10]) print("尺寸不一致:", len(size_unmatch), size_unmatch[:10])这段脚本输出三个清单。缺标注的图片在训练时不报错,但会白占batch,并把对应位置的特征拉偏;损坏图片会在dataloader读图时崩掉,崩得毫无规律;尺寸不一致说明标注软件在resize后导出的xml宽高和原图对不上,转换坐标时会让目标框整体偏移。三者都命中过,其中尺寸不一致最隐蔽,训练能跑,mAP死活上不去。
修复损坏图片,可以用PIL重存一遍,顺手把bmp统一转成jpg:
from PIL import Image import os for fn in os.listdir(img_dir): if fn.lower().endswith(".bmp"): im = Image.open(os.path.join(img_dir, fn)).convert("RGB") out = fn[:-4] + ".jpg" im.save(os.path.join(img_dir, out), quality=95) os.remove(os.path.join(img_dir, fn))bmp单张动辄几MB,YOLO训练时反复读盘,会拖慢整个训练节奏。转jpg后体积缩小一个量级,质量损失对检测任务影响很小。如果缺标注超过5%,建议先找交付方补全再往下走。血泪经验:带缺漏的数据集训出来的模型,召回率会莫名低一截,这口锅后面很难甩掉。
2.2 标注格式归一:VOC/COCO/YOLO三种结构的选择
工程车辆数据集很少直接给YOLO格式。常见交付是VOC xml(labelImg导出)或COCO json(labelme批量导出,coco2017数据集结构也是这个套路)。YOLO训练要的是每张图一个同名txt,类别用索引表示。VOC转YOLO的核心脚本:
import xml.etree.ElementTree as ET classes = ["excavator", "loader", "dump_truck", "roller"] def voc_to_yolo(xml_file, out_file): tree = ET.parse(xml_file) root = tree.getroot() size = root.find("size") w = float(size.find("width").text) h = float(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in classes: continue box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) xc = (x1 + x2) / 2 / w yc = (y1 + y2) / 2 / h bw = (x2 - x1) / w bh = (y2 - y1) / h xc = min(max(xc, 0.0), 1.0) yc = min(max(yc, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) lines.append(f"{classes.index(name)} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") with open(out_file, "w", encoding="utf-8") as f: f.write("\n".join(lines))关键点:classes列表的顺序决定txt里每个数字代表哪个类别,必须和后面数据集yaml里的names保持一致,索引对不上模型就白训了。clamp那四行是防呆,有些xml里标注框会超出图像边界,直接写进txt会让目标框中心落到图外,YOLO的anchor匹配直接失效。标完记得检查一下输出txt里有没有出现负坐标或大于1的坐标。
如果交付的是COCO json,转YOLO的思路类似,遍历annotations数组,每条的bbox是[x, y, width, height]绝对像素值,除以图像宽高就是归一化坐标:
import json def coco_to_yolo(json_path, img_dir, label_dir): with open(json_path, encoding="utf-8") as f: data = json.load(f) img_map = {img["id"]: img["file_name"] for img in data["images"]} cat_map = {cat["id"]: i for i, cat in enumerate(data["categories"])} anns_by_img = {} for ann in data["annotations"]: img_id = ann["image_id"] anns_by_img.setdefault(img_id, []).append(ann) for img_id, anns in anns_by_img.items(): img_file = img_map[img_id] stem = os.path.splitext(img_file)[0] # 读取图片真实尺寸,避免用json里的宽高(可能被resize过) im = Image.open(os.path.join(img_dir, img_file)) w, h = im.size lines = [] for ann in anns: cat_idx = cat_map[ann["category_id"]] x, y, bw, bh = ann["bbox"] xc = (x + bw / 2) / w yc = (y + bh / 2) / h lines.append(f"{cat_idx} {xc:.6f} {yc:.6f} {bw / w:.6f} {bh / h:.6f}") with open(os.path.join(label_dir, stem + ".txt"), "w") as f: f.write("\n".join(lines))注意这里读取的是图片文件的实际尺寸,而不是json里记录的宽高。很多标注工具在标注时预览图是缩放过的,json里存的是预览尺寸,直接拿来归一化会让所有框集体偏移。如果交付的是旋转框标注(四点坐标),先别急着转成水平框,那是第5章mmrotate的活,转了反而丢信息。
2.3 数据集目录结构与类别统计:先把家底盘清楚
统一成YOLO风格的目录结构,这是后面所有训练命令的地基:
construction_vehicle/ ├── images/ │ ├── train/ # 约700张 │ ├── val/ # 约200张 │ └── test/ # 约100张 ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── construction_vehicle.yamltrain和val的划分不要随机打散,按工地或拍摄时段分。同一个工地同一个摄像头的连续帧,同时进train和val,模型等于开卷考试,val指标虚高,上线就露馅。如果交付方没给划分,按文件名前缀分桶是最快的办法。
在写yaml之前,先统计一遍类别,把「挖掘机」「excavator」「挖机」这类同义写法统一掉:
import os import xml.etree.ElementTree as ET def count_classes(ann_dir): counter = {} for fn in sorted(os.listdir(ann_dir)): tree = ET.parse(os.path.join(ann_dir, fn)) for obj in tree.getroot().iter("object"): name = obj.find("name").text counter[name] = counter.get(name, 0) + 1 return counter for cls, cnt in sorted(count_classes("annotations").items(), key=lambda x: -x[1]): print(f"{cls}: {cnt}")这一步一定要做。工程车辆数据集的类别名往往五花八门,同一种车四五个叫法。类别统计看一眼就知道哪些是少数类,后面第4章的类不均衡处理就靠这份清单定位。顺便说一句,有些交付的xml里object没有name字段,只有id,这种通常是对应某个类别文件,需要找交付方要映射表,别自己猜。
3. 用YOLOv8训练自己的工程车辆数据集:最小命令与必调参数
YOLOv8算得上当前把「训练自己的数据集」这件事做得最省心的框架。ultralytics把数据加载、增强、训练、评估、导出一条链路都封装好了,工程车辆这类垂直场景用默认配置加几个关键参数就能跑,但前提是知道每个参数在1000张这个量级下该怎么设。
3.1 数据划分与最小训练命令
数据划分这一步,我一般写个简单的Python脚本按文件名前缀分桶,避免人工拖动文件时漏掉对应label:
import os import shutil from collections import defaultdict base = "construction_vehicle" img_dir = os.path.join(base, "all_images") label_dir = os.path.join(base, "all_labels") groups = defaultdict(list) for fn in os.listdir(img_dir): prefix = fn.split("_")[0] # 按文件名前缀分工地 groups[prefix].append(fn) # 按前缀分组后,每组按7:2:1分到train/val/test for prefix, files in groups.items(): files.sort() n = len(files) for i, fn in enumerate(files): stem = os.path.splitext(fn)[0] if i < int(n * 0.7): split = "train" elif i < int(n * 0.9): split = "val" else: split = "test" shutil.copy(os.path.join(img_dir, fn), os.path.join(base, "images", split, fn)) shutil.copy(os.path.join(label_dir, stem + ".txt"), os.path.join(base, "labels", split, stem + ".txt"))按前缀分组而不是随机划分,是为了避免同源连续帧泄漏。如果文件名里看不出场景信息,就按时间戳排序后切片,保证同一时间段只进一个split。
目录就绪后,训练命令很简单:
yolo detect train \ data=construction_vehicle.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=20 \ seed=42 \ device=0yolov8s.pt是预训练权重,在COCO上训过。工程车辆这种垂直场景用预训练权重迁移,比从头训收敛快得多,1000张的数据从头训基本是浪费算力。如果显存吃紧,batch降到8、imgsz降到512,效果差距不大,先跑通再说。
3.2 超参数到底怎么调:5个必调参数说明
| 参数 | 我的默认值 | 调法 | 踩坑点 |
|---|---|---|---|
| model | yolov8s.pt | 数据少用s或m,别一上来就x | x模型在1000张图上必过拟合,val mAP反而更低 |
| imgsz | 640 | 原图分辨率接近640就不用硬拉大 | 把640的图拉到1280,小目标没变清晰,显存先崩 |
| epochs | 150 | 看val loss收敛就停,不必硬跑满 | 1000张图150轮已经偏多,配合patience用 |
| batch | 16 | 显存不够降8,不要降到1 | batch太小BN统计量漂移,mAP抖动 |
| patience | 20 | 早停防过拟合 | 设太大等于没设,设太小在波动期误停 |
这些参数不是玄学,是按数据量推出来的。1000张规模下,模型容量一大就记题,yolov8x参数量是s的5倍多,泛化全靠数据多样性,硬上大模型等于背答案。imgsz是另一个大坑:施工监控的原始视频流经常是1920x1080,抽帧后挖掘机可能只占一百多个像素,硬拉高分辨率对检测帮助有限,真正的瓶颈是标注质量和场景多样性。先把这两件事做好,再考虑调参。
3.3 训练结果怎么判读:别只盯mAP
训练结束会输出一组指标。mAP50和mAP50-95最先看,但很多人忽略了混淆矩阵和P-R曲线。工程车辆数据集的几个典型现象:
- loss曲线在epoch 40左右降到平,后面一直震荡,说明lr该衰减了,或者数据里有脏标注。
- 混淆矩阵里loader和dump_truck互相串,通常是把轮式装载机和自卸车标注搞混了,这种错标,改标注比调模型快。
- val的mAP明显高于train的mAP,那不是模型好,是验证集太简单或划分泄漏。
训练日志里每轮的val box loss可以拉出来画个曲线。如果val loss在某个epoch之后稳定回升,而train loss还在降,说明已经走进过拟合区间。这时候要么提前停,要么把增强参数调弱再训。YOLOv8的runs目录下自带results.png,直接看那张图就行。模型训完顺手跑一次验证集推理,把结果图拼一张大图肉眼过一遍,这比任何指标都诚实。
4. 1000张小数据集的4个典型坑:过拟合、漏标、类不均衡和验证集污染
小数据集训练翻车就那么几类,下面这四个坑是我在工程车辆项目里反复踩过的,每个都按「现象 → 原因 → 解决」写清楚,遇到可以直接照方抓药。
4.1 现象:loss降了mAP上不去,train mAP 90,val mAP 60
原因:模型在训练集上已经开始背题,验证集泛化不住。叠加验证集太小,比如只有几十张图,mAP波动会非常大,偶尔一个错检就能把指标拉下去好几个点。解决方式分两头走:一是把val扩到200张以上,宁可少训点也别让验证集失真;二是用K-Fold交叉验证重新评估,ultralytics官方给了K-Fold脚本思路,把数据切成5份轮流做验证,得到稳定指标。过拟合明显的,把weight_decay从默认0.0005提到0.001再训一轮,通常能压住一点。
4.2 现象:召回率低,挖掘机经常漏检,尤其远处的小目标
原因:人工复查标注时发现大量漏标——标注员只框了近处大目标,远处小挖掘机全成了背景。模型学到的场景里「挖掘机=大目标」成立,小目标全是负样本,自然检不出来。解决:用训好的模型对训练集做一次预测,把置信度0.3以下的框导出来人工过一遍,能找回不少漏标。这是标注质检的常规做法,实测1000张图找回100多个漏框很常见。补标后再训一轮,召回率会明显回升,这是改标注比改模型快的典型场景。
4.3 现象:mosaic增强一开,小目标反而没了
原因:YOLOv8默认开启mosaic,四张图拼一张,工程车辆这种大目标经常被裁到边缘,小目标被拼图切割后几乎不可见。mosaic适合密集小目标场景,对工地监控的单车大目标反而有害。解决:训练后期关掉mosaic,YOLOv8直接给了参数:
yolo detect train \ data=construction_vehicle.yaml \ model=yolov8s.pt \ epochs=150 \ close_mosaic=10close_mosaic=10表示最后10个epoch关闭mosaic,让模型在真实分布上做最后收敛。这个参数对工程车辆基本是必开。同样的道理,hsv_h、hsv_s颜色增强对夜间和扬尘场景有帮助,但别把饱和度拉得太猛,黄色挖掘机变成橙色,模型的颜色特征就乱套了。小数据集最怕增强过度,增强是给大数据集锦上添花的,不是给小数据集雪中送炭。
4.4 现象:压路机死活训不出来,mAP只有20多
原因:类别极不均衡。工程车辆数据集里常见分布是挖掘机500张、渣土车300张、装载机150张、压路机50张。少样本类别正样本不足,特征学不出来,且验证集里这类样本也少,mAP指标对它是失效的。解决:最直接的办法是把少类图片做离线增强,随机旋转、平移、亮度变化各生成几份,把数量拉平到接近中位类别。更稳的做法是给loss按类别加权,让模型在少类上犯错时付出更大代价。如果允许引入外部数据,公开的自动驾驶数据集里卡车类可以参考,注意domain gap——监控视角和车载视角的差异很大,引入后要人工筛查。
4.5 现象:离线测试效果很好,现场demo被当场打脸
原因:验证集污染。这是最隐蔽的一个坑。数据包里的图片往往来自同一条视频流按帧抽出的连续序列,随机切分后,训练集里某帧的相邻帧出现在验证集里,模型等于提前看到了答案。解决:按拍摄时间或工地分组划分,同一段视频的帧只能进同一个split。这条规则要在2.3分桶脚本里强制实现,不能偷懒用随机random_split。验证集污染是数据科学的后悔药无法覆盖的问题,只能从划分逻辑上根除。
5. 进阶用法:旋转框检测与数据扩充优先级
工程车辆和普通轿车最大的区别是长条结构。挖掘机的动臂、渣土车的车斗在俯视监控里经常斜着停,水平框会把大量背景框进去,两台相邻车辆的水平框一重叠,NMS直接把目标消掉。这种情况用旋转框检测更贴合,mmrotate是MMDetection生态里做旋转框的主流工具,训练DOTA数据集那一套配置可以直接迁移过来。需要把标注转成DOTA的四点格式,labelme导出四个角点后再按x1,y1,x2,y2,x3,y3,x4,y4排列,训练命令大致是:
python tools/train.py configs/oriented_rcnn/oriented_rcnn_r50_fpn_1x_dota.py具体config路径以mmrotate官方仓库为准。但坦白讲,1000张做旋转框检测是偏少的,DOTA那种规模一般要几千张起步。如果当前水平框的mAP已经够用,优先把扩充数据放在第一位,旋转检测是第二步的事。
扩充数据的优先级,我按性价比排:新增工地场景和拍摄时段(扩大场景多样性)>补小目标标注>夜间和扬尘数据>简单复制增强。扩充不是无脑加图,工程车辆数据集的瓶颈通常在场景单一——同一个工地的2000张图,不如三个工地的1000张图有用。每补一批数据,就重跑一次2.1的体检和2.3的类别统计,确认新数据没有破坏原有分布。
最后一条教训:我最早接手类似数据包时,急着跳过体检直接训练,结果验证集泄漏让demo指标虚高,现场测试被甲方当场指出漏检,返工了整整一周。从那以后,凡是拿到数据先跑第2章的检查脚本,成了固定流程。这个小习惯省下的返工时间远超脚本本身,希望帮到你。
本文还有配套的精品资源,点击获取