☰
挖掘机检测模型训练:VOC数据体检与YOLO格式转换实战
2026/10/2 3:35:04 网站建设 项目流程

简介:面向计算机视觉与目标检测学习者的挖掘机图像数据集,包含约700张已完成人工标注的图片,符合VOC标准标注格式,可直接用于训练YOLO等目标检测模型,也可转换为COCO或其他框架格式;聚焦工程车辆典型场景,适合建筑工地安全监控、机械设备远程巡检等应用。全包共1364个文件,其中679个jpg与对应679个xml标注文件一一配对,另含5个txt说明及1个zip包,整体112.49MB,结构清晰。已有1415人浏览学习,对于需要练习目标检测全流程的开发者来说,能省去采集和标注的繁琐环节,快速进入模型训练与调优阶段,也可用于课程设计与算法实验。

1. 挖掘机数据集700张VOC:标注完成只是开始,格式过关才算数

在施工安全监控和渣土车进出场识别这类项目里,挖掘机检测是最常见的需求之一:防碰撞预警、禁区闯入、作业计数都离不开它。手里这份已标注完成的挖掘机数据集,700张左右、VOC格式,来自工地摄像头或无人机航拍视角,覆盖正面挖土、侧面回转、远处小挖机、近景大臂遮挡等典型场景。它适合三类人:刚拿到外包标注想验收入库的算法工程师,准备用YOLO训练自己检测器的入门学习者,以及想评估“700张到底够不够用”的后端同学。我的看法是:700张不算多,但如果VOC格式体检干净、转换参数正确、训练策略对路,完全能落地一个可用的挖掘机检测模型;反过来,格式上任何一个粗心点,都会让后面几天的训练白费。

2. VOC标注格式体检:拿到700张挖掘机数据先做三件事

标注完成的VOC数据集,不等于可以直接训练。VOC这种标签格式在国内数据交易和外包交付里最常见,但它和主流检测框架默认支持的格式并不一致,第一步不是急着写训练脚本,而是把数据集里里外外检查一遍。

2.1 目录结构先过一遍:JPEGImages、Annotations、ImageSets缺一不可

一份规范的VOC数据集,目录通常长成这样:

VOCdevkit/ └── VOC2007/ ├── JPEGImages/ # 原图,jpg或png ├── Annotations/ # 同名xml标注文件 ├── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── trainval.txt └── labels/ # 部分转换后的数据里会有,VOC原生没有

JPEGImages放原图,Annotations放同名XML标注,ImageSets/Main下放划分用的txt文件。txt里每一行是不带扩展名的图片名,train.txt、val.txt、test.txt分别对应训练、验证、测试集。很多从外包手里拿到的VOC数据集未必有标准的ImageSets目录,可能只有JPEGImages和Annotations两个文件夹,外加一个总的划分脚本,这也能用,只是要多做一步自己分集。

拿到压缩包后,我一般先跑一条命令看数量和名字是否对齐:文件名不一致,多半是某一张图漏标、或者重命名时把xml搞乱了。这个数不需要多精确,但“700张左右”到底是多少张、对应多少xml必须心里有数。

ls JPEGImages | wc -l ls Annotations | wc -l ls Annotations | sed 's/\.xml$//' > /tmp/ann_names.txt ls JPEGImages | sed 's/\.\(jpg\|jpeg\|png\)$//' > /tmp/img_names.txt diff /tmp/ann_names.txt /tmp/img_names.txt | head -20

如果diff有输出,说明有图片没有对应标注,或者有标注找不到原图。常见做法是留下有标注且有原图的那部分,缺一边的直接排掉。700张这个体量,人工核对也不累,但写命令更可靠。有个容易忽略的坑:JPEGImages里混进了缩略图或png截图,xml的size字段和图片实际分辨率对不上,这种会在后面转换时框偏移,最好一开始就看清楚。

2.2 XML标注字段逐项审查:类别名、bndbox、difficult都要管

VOC的XML标注文件结构不复杂,但字段含义会影响后面转换和训练。打开一个典型的标注文件看看:

<annotation> <folder>JPEGImages</folder> <filename>excavator_0231.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>excavator</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>426</xmin> <ymin>315</ymin> <xmax>872</xmax> <ymax>691</ymax> </bndbox> </object> </annotation>

这里有三处必须盯紧。第一是 类别名,不同标注员可能在同一批数据里混写“excavator”“digger”“挖掘机”,这对后面类别映射是致命的;第二是 四个坐标,xmin必须小于xmax,ymin必须小于ymax,而且坐标不能超出图片宽高;第三是 和 ,这两个字段标不标都行,但如果你转换时把difficult=1的目标也计入损失,挖掘机被铲斗遮挡、只露出一半车身的样本会把训练集搞脏。

类别统一的问题尤其隐蔽。一份700张的数据里,如果绝大多数标的是“excavator”,只有零星几张被标成“digger”,转换脚本按类表映射时,digger会被当成第二个类或者被直接跳过,最终mAP表现很怪。遇到这种情况,先统计所有XML里出现的name:

grep -h "<name>" Annotations/*.xml | sort | uniq -c

如果类别数超过设计值,比如出现“excavator”和“digger”,需要一个脚本把后者批量替换成前者,而不是直接转换。挖掘机这个场景就一个类,类别表越简单越不容易错。顺带说一句,这里不要只做一次性清理,把替换脚本留下来,因为在验证集或后续补充的几百张数据里,同样的命名问题一定还会再出现。

2.3 写个脚本批量自检标注合法性

目录和字段看完,再上一道保险:用一个Python脚本全量检查每个XML的完整性和坐标合法性,重点是找出“空标注文件”和“坐标越界”两类问题。空标注文件指的是xml里没有任何object节点,这种文件训练时会被跳过但不会报错,表现为损失收敛慢、recall偏低;坐标越界则会在缩放归一化时产生大于1或小于0的数值,轻则框偏移,重则NMS出警告。

import os import xml.etree.ElementTree as ET ann_dir = 'VOCdevkit/VOC2007/Annotations' for name in sorted(os.listdir(ann_dir)): if not name.endswith('.xml'): continue tree = ET.parse(os.path.join(ann_dir, name)) root = tree.getroot() objs = root.findall('object') if len(objs) == 0: print(f'{name}: 空标注,无object') for obj in objs: cls = obj.find('name').text 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) w = float(root.find('size/width').text) h = float(root.find('size/height').text) if xmin >= xmax or ymin >= ymax: print(f'{name}: 坐标倒置 {cls} ({xmin},{ymin},{xmax},{ymax})') if xmin < 0 or ymin < 0 or xmax > w or ymax > h: print(f'{name}: 越界 {cls} ({xmin},{ymin},{xmax},{ymax}) 图片 {w}x{h}')

这段脚本的检查逻辑很简单:遇到没有object的XML,标记为空标注;遇到xmin和xmax相等或倒置的坐标,说明标注工具卡顿或者手抖;坐标超出图像宽高,说明xml的size字段和图片真实分辨率不一致,很可能图片被压缩过但标注没跟着调。正常跑完这个脚本,错误输出为空或者只有一两行,就可以放心进下一步;如果刷屏,那就先把这些脏数据修掉再继续。

提示:检查出来的问题不要只记在Excel里。直接在Annotations目录里修复,或者把问题名单另存为fix_list.txt,训练前统一处理,不然沉淀下来的只有“我好像遇到过这个问题”。

3. 把VOC转成YOLO格式:转换脚本与四个必调参数

VOC格式体检干净,接下来就是转换。这一步决定了后面训练脚本能不能直接跑,也是整个链路里最容易出“玄学”问题的环节。

3.1 为什么要转:VOC的坐标是绝对像素值,YOLO要的是归一化相对值

YOLO系列训练时读取的label格式是“类别ID + 归一化中心点坐标 + 归一化宽高”,每行五个数,对应一个目标;而VOC的XML存的是xmin、ymin、xmax、ymax四个绝对像素坐标。如果直接把VOC塞给YOLO,数据加载器会报label格式错误,或者干脆把第一列当成类ID、后面四列当成乱坐标去算损失,结果训练个几十轮mAP一直是零。

转换的核心公式就四个:

x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height label_w = (xmax - xmin) / image_width label_h = (ymax - ymin) / image_height

注意分母必须用图片实际宽高,也就是XML里 节点的值,而不是你猜测的某个固定分辨率。工地图片来源复杂,同一批数据里1920×1080和1280×720混着是常事,转换脚本里写死一个宽度是后面框发飘的主要原因。

3.2 转换脚本:以XML的size字段为准做归一化

下面这个脚本是业内最常见的做法,把Annotations目录下的XML一个不漏地转成YOLO的txt标签,存到labels目录,同时把原图按相同文件名放进images目录。因为已经有700张,体量不大,用单线程串行跑没问题。

import os import xml.etree.ElementTree as ET classes = ['excavator'] # 类别表,只保留实际需要的类 ann_dir = 'VOCdevkit/VOC2007/Annotations' img_dir = 'VOCdevkit/VOC2007/JPEGImages' out_label_dir = 'labels' out_img_dir = 'images' os.makedirs(out_label_dir, exist_ok=True) os.makedirs(out_img_dir, exist_ok=True) for xml_name in os.listdir(ann_dir): if not xml_name.endswith('.xml'): continue tree = ET.parse(os.path.join(ann_dir, xml_name)) root = tree.getroot() img_w = float(root.find('size/width').text) img_h = float(root.find('size/height').text) filename = root.find('filename').text lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in classes: print(f'{xml_name}: 跳过未注册类别 {name}') continue cls_id = classes.index(name) 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) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h bw = (xmax - xmin) / img_w bh = (ymax - ymin) / img_h lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}') if not lines: print(f'{xml_name}: 转换后为空') continue base = os.path.splitext(xml_name)[0] with open(os.path.join(out_label_dir, base + '.txt'), 'w') as f: f.write('\n'.join(lines)) src_img = os.path.join(img_dir, filename) if os.path.exists(src_img): os.system(f'cp "{src_img}" "{os.path.join(out_img_dir, base + os.path.splitext(filename)[1])}"') else: print(f'{xml_name}: 找不到原图 {filename}')

脚本逻辑分成三步:先读XML里的size字段拿到图片宽高,再遍历object节点把每个目标的坐标归一化,最后写到以图片名命名的txt文件。这里有两个容易忽略的参数:第一,classes列表的顺序就是后续YOLO训练时的类别ID顺序,一旦定下来中途不要改动;第二,输出的小数精度至少保留6位,归一化后宽高在0到1之间,位数太少会导致相邻目标的框重叠。原图输出我建议用绝对路径或者统一根目录,后面配置data.yaml时直接指向这个目录,省去一堆路径拼接的问题。

3.3 划分train/val/test:先看类别分布再切

700张数据怎么分成train、val、test?常见做法是7:2:1,但直接random.shuffle有一个隐患——某些场景(比如夜间、雨天、远处小目标)可能全被分到训练集,验证时看到的都是好样本,mAP虚高。更稳妥的办法是先把所有标注文件里每个目标的尺寸和所在图片信息统计一遍,再按图片级stratify划分。

import os import random random.seed(42) names = [] for f in os.listdir('images'): if f.lower().endswith(('.jpg', '.png', '.jpeg')): names.append(os.path.splitext(f)[0]) random.shuffle(names) n = len(names) def split_ratio(ratio): return int(n * ratio) train = names[:split_ratio(0.7)] val = names[split_ratio(0.7):split_ratio(0.9)] test = names[split_ratio(0.9):] os.makedirs('images/train', exist_ok=True) os.makedirs('images/val', exist_ok=True) os.makedirs('labels/train', exist_ok=True) os.makedirs('labels/val', exist_ok=True) for split, idx in [('train', train), ('val', val), ('test', test)]: for base in idx: os.rename(f'images/{base}.jpg', f'images/{split}/{base}.jpg') os.rename(f'labels/{base}.txt', f'labels/{split}/{base}.txt') with open(f'{split}.txt', 'w') as f: for base in idx: f.write(f'images/{split}/{base}.jpg\n')

这段脚本执行后,train.txt、val.txt里保存的是相对路径,YOLO的data.yaml里配上path根目录即可。划分前记得固定随机种子,这个习惯能让复现实验省掉很多纠缠,而且换了机器也能重新生成同一份划分。划分完可以随手看下每张图里目标框的平均尺寸,如果val集里大目标占比特别高,而test集里全是小目标,那这个划分本身就不公平,建议重新分。

4. 只有700张怎么练:挖掘机检测的训练参数与数据增强

转换完成,目录就绪,接下来进入训练阶段。700张图对一个检测模型来说属于中小规模,直接照着COCO的默认参数跑,大概率训练很久且mAP不理想;真正的高手是把数据特点和模型能力对齐。

4.1 挖掘机的目标大小决定imgsz和batch的选择

施工场景里挖掘机有两种典型形态:一种是近景特写,整个画面就是一台挖机,目标框能占到600×500这样的像素;另一种是远景俯拍,整片工地里挖机只有100×80甚至更小。这两种形态对输入分辨率的要求完全相反。样本量只有700张时,我建议imgsz取640而不是1280。原因很简单:640能把内存和训练速度控制在合理范围,700张这点数据量喂给1280分辨率,模型学到的细节多但过拟合也快,mAP提升有限。

batch的选取受显存约束,常见做法是从16起步。如果NVIDIA显卡只有8G显存,batch=8更稳;如果显存充足或开AMP混合精度,batch=32也行。这里有一个经验值:训练轮数100到150之间,不要贪多。700张数据,120轮足够模型拟合训练集,再往上loss降得慢,val集mAP反而掉。

4.2 写data.yaml并执行训练命令

YOLO系列(以YOLOv8为例)需要一个data.yaml描述数据集路径和类别,内容很简单,但是路径必须和刚才生成的目录一致。

path: /home/user/excavator_dataset train: images/train val: images/val names: 0: excavator

注意names的编号必须和转换脚本里classes的顺序一致。如果转换时classes = ['excavator'],这里names里也只有索引0。假如你在转换脚本里加了第二个类别,比如['excavator', 'background'],这就是个坑,因为VOC里不会有background这个类,背景目标会全部变成误检。

训练命令按照ultralytics的规范写:

yolo detect train \ model=yolov8m.pt \ data=excavator.yaml \ epochs=120 \ imgsz=640 \ batch=16 \ device=0 \ workers=4 \ patience=20

这条命令里model=yolov8m.pt是预训练权重,m是中等规模,700张数据用n太弱、用x太浪费,m属于性价比最高的档位。patience=20是早停耐心值,连续20轮val mAP不上升就自动停,配合epochs=120能防止把时间耗在无效训练上。每个参数看完训练日志后都能调,但有一个原则:batch、lr和imgsz三者是联动的,盲目调其中一个会让训练曲线立刻失真。

4.3 数据增强:离线增强和在线增强怎么分配

700张原始样本,不做增强很难压住过拟合。YOLO自带在线增强,默认的mosaic、fliplr、hsv扰动等,开箱即用。但这里针对挖掘机场景有两个建议。

第一,小心fliplr。挖掘机有左右回转臂,左右翻转后语义依然成立,所以fliplr=0.5是安全的。但如果你标注的是“挖机正面对车头”这类有方向性判别的任务,比如识别前进方向,fliplr要关掉。

第二,mosaic增强默认是开,我建议保留,但把mosaic概率从1.0降到0.5。mosaic把四张图拼成一张,小目标检测能力提升明显,但挖掘机本身是大目标,过多mosaic会让整个目标被切分、标注框被截断,反而增加无效学习。

以下是针对这个场景的一组合理增强参数,在ultralytics里通过augment参数关掉部分默认增强,其他用默认值:

参数经验值说明
hsv_h0.015hue扰动别超过这个值,工地尘土会偏色
hsv_s0.5饱和度变化适中,保留挖掘机黄黑配色辨识度
hsv_v0.4亮度扰动,应对早晚光线差异
fliplr0.5左右翻转,挖掘机对称性允许
mosaic0.5保留但降频,避免破坏大目标
scale0.5随机缩放,模拟远近距离变化

这些参数不需要一上来就全部打开。先用默认训练一轮看曲线,再根据val loss的变化决定往哪个方向加强度。700张数据最怕的不是增强不够,而是增强过头——把挖掘机挖到颜色失真、形状变形,学到的特征就不具备工地实景的代表性了。

5. 常见问题排查:挖掘机VOC数据训练YOLO的6个坑

这部分是拿实战翻车记录换来的。前两个是格式和转换阶段最高频的坑,中间两个是训练参数相关,最后两个常见于验证评估。

5.1 坑:loss下降缓慢,mAP从始至终是0

现象:训练了40轮,box_loss和cls_loss都收敛了,但val集的mAP50一直零,召回率零,predict一张图一个框都画不出。

原因:最常见的是类别ID映射错位。转换脚本里classes=['excavator'],但data.yaml里的names可能写成了{0: 'digger'},或者标注XML里既有excavator又有digger,转换脚本把digger按未注册类别跳过,导致训练时一个正样本都没有,模型从头到尾学了个“啥都不检测”。

解决:先看转换后生成的labels目录里每一个txt是否非空,再有针对性地看其中一行是否形如0 0.52 0.48 0.30 0.22,第一列是整数类别ID。然后在data.yaml里确认names和你classes的顺序一一对应。最后用yolo train前先跑一次yolo val做一个纯预训练权重的baseline,如果baseline能出框说明数据集路径没问题,问题出在标签上。

5.2 坑:验证集mAP很高,但工地实景检测不到

现象:val集mAP50到了0.92,看起来性能不错;拿去测一段施工监控视频,两三台挖掘机一台都没框出来。

原因:这个翻车大多是训练集和测试场景分布不一致。你从网上或外包拿到的700张,如果都是近景正面特写,模型学到的是“黄色大块头+履带”的组合;而监控俯拍是远景小目标,一台挖机在画面里只有120×80像素,模型在640分辨率下根本看不清这种尺寸的目标。

解决:重新审视数据集。从700张里把占比超过一定比例的同质样本筛一部分出来,人为制造“远中近、大小目标、遮挡与非遮挡”三类均衡,或者直接去补拍、补充远景样本。如果补数据不现实,就把输入分辨率从640提到960,配合mosaic增强,小目标mAP会有明显改善,代价是显存占用变大和训练变慢。

5.3 坑:train.txt里路径对不上,程序启动就报错

现象:训练命令一下,数据加载器报Assertion ... image not found,眼看路径就在那里,却死活加载不出来。

原因:train.txt里写了/home/user/excavator_dataset/images/train/xxx.jpg,但data.yaml的path写的是/home/user/excavator_dataset,两个路径拼在一起变成了重复路径。另一个更隐蔽的情况是Windows上生成路径时用了反斜杠,Linux训练时读不了。

解决:统一用相对路径,data.yaml的path只写根目录,train和val只写images/train这种相对路径。转换脚本里不要用os.getcwd()拼接路径,跨机器跑时绝对路径一定会出问题。Windows下生成txt后,用sed -i 's|\\|/|g' train.txt把反斜杠替换成正斜杠再送入训练。

5.4 坑:数据增强太猛,挖掘机被增强成“非挖掘机”

现象:训练完的模型在雨夜场景下把路灯杆、水泥搅拌车误检成挖掘机,误检率居高不下。

原因:hsv_v和hsv_h调太高,把黄色挖掘机在暗光下增强成了接近灰黑色的剪影,模型学会的是“一个深色大目标=挖掘机”。工地夜间有大量高杆灯、吊车臂,这些形状和挖掘机大臂相似,容易被误检。

解决:把hsv_v从默认的0.6调回0.4以内,hsv_h从0.05调回0.015,并适当调低mosaic的缩放下限,避免单张图被拉得过暗。增强是让模型看更多“合理的变形”,不是看更多“诡异的变色”。黄黑配色是挖掘机最重要的视觉身份,颜色被增强破坏后,模型只能去学形状,而形状恰恰是最容易碰瓷的。

5.5 坑:转换后标签txt为空,但XML里明明有object

现象:转换脚本打印了xxx.xml: 转换后为空,打开XML看到object节点都在,目标也没被截断。

原因:object节点里的 和你classes列表对不上。比如XML里写的是“Excavator”,首字母大写,和你列表里的小写“excavator”不匹配,脚本直接continue了。还有一种情况是目标被标成“excavator_0”这种带编号的类名。

解决:在转换脚本的class匹配位置打印一下实际读到的name,把整个数据集里出现过的所有name先统计一遍再写classes。建议在做第2章体检时就把这一步做了,省得转换一半才发现。

5.6 坑:训练到一半OOM,进程被杀

现象:batch=32训练到第3轮,CUDA out of memory,python进程直接退出。

原因:imgsz=640时显存占用随batch线性增长,8G显存显卡开batch=32很容易爆。另外workers=4这个参数会让数据加载进程抢占CPU内存,数据量大时CPU内存也会不够。

解决:显存不够就把batch降到8或者16,AMP混合精度能省大约一半显存。更实用的一招是检查显卡是不是被别的进程占着,用nvidia-smi看一眼,把那些僵尸进程清掉再启动。OOM不是代码问题,是资源规划问题,别动训练参数,优先清理环境。

6. 交付前两步:用badcase回查和混淆矩阵给700张数据集把关

训练完成、模型能用,不代表这个700张数据集已经完成了使命。我的习惯是上线前多花半天做一次“坏例回查”和“混淆矩阵复核”,这两步能防止交付给业务方一个看起来mAP高、实际上一场就翻车的模型。

坏例回查的做法很简单:选几十张val集图片,用训练好的模型推理一遍,把预测正确率最高和最低的图片并列排开,逐一比对。我一般会重点看三类图片:一是远处小目标,看模型是不是直接漏检;二是挖掘机大臂和车身同色的角度,看是不是把大臂和车身拆成了两个框;三是阴影和夜间图片,看模型有没有把阴影边界当成目标边界。查完之后回到标注层面,如果发现某类图片总是漏检,说明训练集里这类样本太少,先补这类样本再训练,比盲目加epochs有效得多。

混淆矩阵复核更定量。使用训练好的模型评估val集,得到每一类的PR曲线和混淆矩阵。在只有挖掘机一个类别时,混淆矩阵能直接告诉你:有多少背景区域被模型当成了挖掘机(FP)、多少挖掘机目标没被召回(FN)。

# 使用ultralytics的验证接口,一键得到混淆矩阵 from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') metrics = model.val(data='excavator.yaml', split='val', conf=0.25, iou=0.5) print(metrics.box.map50) print(metrics.box.map)

conf=0.25是经验值,低于这个阈值推理时误检太多,高于这个阈值远处小目标漏检太多。iou=0.5对应VOC时代的评估口径,如果交付标准要更严,改成0.75再看一次mAP,这两个数之间的差距能反映框的定位精度。

最后说一个我自己养成的收尾习惯:每次转换完数据集,把检查脚本、转换脚本、划分种子固定成一个script文件夹,和数据放在一起。下次补数据到800张、900张时,一条命令重新生成全套YOLO标签,不用再靠记忆手工重做。数据标注是一次性的,但数据流转是长期的,把格式体检和转换做成可重复流程,比多训一轮模型更值钱。希望帮到你。

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

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

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

立即咨询