ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

挖掘机检测模型训练:VOC数据体检与YOLO格式转换实战

挖掘机检测模型训练:VOC数据体检与YOLO格式转换实战 简介面向计算机视觉与目标检测学习者的挖掘机图像数据集包含约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 folderJPEGImages/folder filenameexcavator_0231.jpg/filename size width1920/width height1080/height depth3/depth /size object nameexcavator/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin426/xmin ymin315/ymin xmax872/xmax ymax691/ymax /bndbox /object /annotation这里有三处必须盯紧。第一是 类别名不同标注员可能在同一批数据里混写“excavator”“digger”“挖掘机”这对后面类别映射是致命的第二是 四个坐标xmin必须小于xmaxymin必须小于ymax而且坐标不能超出图片宽高第三是 和 这两个字段标不标都行但如果你转换时把difficult1的目标也计入损失挖掘机被铲斗遮挡、只露出一半车身的样本会把训练集搞脏。类别统一的问题尤其隐蔽。一份700张的数据里如果绝大多数标的是“excavator”只有零星几张被标成“digger”转换脚本按类表映射时digger会被当成第二个类或者被直接跳过最终mAP表现很怪。遇到这种情况先统计所有XML里出现的namegrep -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_okTrue) os.makedirs(out_img_dir, exist_okTrue) 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(fcp {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_okTrue) os.makedirs(images/val, exist_okTrue) os.makedirs(labels/train, exist_okTrue) os.makedirs(labels/val, exist_okTrue) for split, idx in [(train, train), (val, val), (test, test)]: for base in idx: os.rename(fimages/{base}.jpg, fimages/{split}/{base}.jpg) os.rename(flabels/{base}.txt, flabels/{split}/{base}.txt) with open(f{split}.txt, w) as f: for base in idx: f.write(fimages/{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显存batch8更稳如果显存充足或开AMP混合精度batch32也行。这里有一个经验值训练轮数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 \ modelyolov8m.pt \ dataexcavator.yaml \ epochs120 \ imgsz640 \ batch16 \ device0 \ workers4 \ patience20这条命令里modelyolov8m.pt是预训练权重m是中等规模700张数据用n太弱、用x太浪费m属于性价比最高的档位。patience20是早停耐心值连续20轮val mAP不上升就自动停配合epochs120能防止把时间耗在无效训练上。每个参数看完训练日志后都能调但有一个原则batch、lr和imgsz三者是联动的盲目调其中一个会让训练曲线立刻失真。4.3 数据增强离线增强和在线增强怎么分配700张原始样本不做增强很难压住过拟合。YOLO自带在线增强默认的mosaic、fliplr、hsv扰动等开箱即用。但这里针对挖掘机场景有两个建议。第一小心fliplr。挖掘机有左右回转臂左右翻转后语义依然成立所以fliplr0.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进程被杀现象batch32训练到第3轮CUDA out of memorypython进程直接退出。原因imgsz640时显存占用随batch线性增长8G显存显卡开batch32很容易爆。另外workers4这个参数会让数据加载进程抢占CPU内存数据量大时CPU内存也会不够。解决显存不够就把batch降到8或者16AMP混合精度能省大约一半显存。更实用的一招是检查显卡是不是被别的进程占着用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(dataexcavator.yaml, splitval, conf0.25, iou0.5) print(metrics.box.map50) print(metrics.box.map)conf0.25是经验值低于这个阈值推理时误检太多高于这个阈值远处小目标漏检太多。iou0.5对应VOC时代的评估口径如果交付标准要更严改成0.75再看一次mAP这两个数之间的差距能反映框的定位精度。最后说一个我自己养成的收尾习惯每次转换完数据集把检查脚本、转换脚本、划分种子固定成一个script文件夹和数据放在一起。下次补数据到800张、900张时一条命令重新生成全套YOLO标签不用再靠记忆手工重做。数据标注是一次性的但数据流转是长期的把格式体检和转换做成可重复流程比多训一轮模型更值钱。希望帮到你。本文还有配套的精品资源点击获取
返回列表