ARTICLE DETAIL

资讯详情

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

1400张车辆图片VOC数据集:从标注解析到YOLOv5训练全流程

1400张车辆图片VOC数据集:从标注解析到YOLOv5训练全流程 简介这份车辆检测数据集包含1400张真实场景图片与一一对应的标注文件覆盖公交车、家用车、消防车、工程车等常见类型面向Python、人工智能与深度学习方向开发者用于目标检测模型的训练与验证可直接适配YOLOv3、YOLOv4、YOLOv5等主流框架解决车辆识别场景中数据获取和人工标注成本高的痛点。整个资源包共2800个文件由1400个jpg图像和1400个xml标注文件组成其中xml文件标定了每个车框的位置和类别由专业人员手工标注质量可靠。压缩包采用rar格式整体大小约709.83MB解开后图片与标注按序号一一对应便于划分训练集和测试集。资源上线以来已有2475人学习下载基于这些数据训练出的车辆检测模型识别准确率超过98%覆盖类型多样对学士论文、算法竞赛或工业级车辆检测项目均具备实用参考价值。1. 1400张车辆图片数据集到手先看懂这批VOC标注能干什么想用YOLOv5训一个车辆检测模型的人大多卡在数据准备这一关。自己从行车记录仪抽几百帧图不难难的是标注一张图里五六台车框要贴边类别不能混遇到互相遮挡还得判断到底算不算正样本。这种活干到两百张人就容易怀疑人生。这份“1400张车子的数据集”是已经手工标注完成的成品覆盖公交车、家用车、消防车、工程车等常见车型每张图配一份VOC格式的XML标注文件坐标框和类别都已对齐可以直接喂给YOLOv3/v4/v5训练车辆检测模型。对做停车场识别、车流统计、交通监控的从业者来说它既是能跑基线的数据源也是完整走通“VOC标注到YOLO训练”这条pipeline的最短路径。适合两类人想自己动手复现一次完整目标检测流程的新手以及需要一份干净数据验证算法改动效果的老手。2. 数据集的成色与结构VOC标注文件里到底存了什么2.1 JPEGImages、Annotations、ImageSets各自的职能一份标准的VOC数据集解压后长这样JPEGImages/ ├── 0265.jpg ├── 0242.jpg └── ... Annotations/ ├── 0265.xml ├── 0242.xml └── ... ImageSets/ └── Main/JPEGImages放的是原始图片Annotations放的是与图片同名的XML标注文件ImageSets/Main则是放划分列表的地方比如train.txt、val.txt。这套1400张的数据集目录结构基本就是这三块。如果压缩包里没有ImageSets也不用慌第3章的脚本会自己生成划分文件。我拿到手的第一步不是急着跑训练而是先确认三件事图片分辨率、文件名是否规范、XML是否每张图都有。项目里列出的文件名看起来是0265.jpg这类的四位数字编号这种命名是安全的。但我见过不少同类数据包里混进带空格或中文的文件名YOLO读路径时容易翻车这个问题第5章会专门讲。目录角色拿它干什么JPEGImages原始图像训练输入、抽样预览AnnotationsVOC XML标注转换为YOLO TXT标签ImageSets/Main数据集划分决定train/val/test为什么要强调目录结构因为YOLO训练时并不直接消费XML它只认TXT标签里的坐标和类别。但VOC转YOLO的脚本必须依赖目录关系来批量读取同名的jpg对应同名的xml靠os.path.splitext就能建立对应不需要额外维护清单。图片分辨率值得先看一眼。如果单张分辨率在1080p上下输入尺寸imgsz取640就是常规操作如果图片本身只有五六百像素强行拉大到640反而会把车辆边缘拉糊。我用一段内联Python看前20张的宽高python - EOF from PIL import Image import glob for p in glob.glob(JPEGImages/*.jpg)[:20]: print(p, Image.open(p).size) EOF注意两点一是所有图是否同一个分辨率二是最小边的数值离640有多远。如果高低分辨率混着来YOLOv5会做自适应缩放等比填充后容易产生黑边模型见过这种输入后推理时反而要多一次letterbox操作。2.2 XML标注内容拆解坐标框、类别名与难例标记随便打开一个XML内容是典型的Pascal VOC结构annotation filename0265.jpg/filename size width1920/width height1080/height depth3/depth /size object namecar/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin362/xmin ymin401/ymin xmax845/xmax ymax902/ymax /bndbox /object /annotation重点在bndbox的四个值xmin、ymin是左上角坐标xmax、ymax是右下角坐标单位是像素不是归一化值。YOLO的TXT标签要的是归一化中心点加宽高所以转换脚本必须拿到size里的width和height去做除法不能凭空估算。name字段是类别名这份数据集具体用哪组英文名以你实际解压后的XML为准常见做法是统一小写家用车用car公交车用bus消防车用fire_engine工程车写engineering_vehicle或truck取决于当初标注时的命名习惯。拿到手第一件事用2.3节的统计脚本把所有出现过的name值拉出来看有没有中文、空格、大小写混杂。表示目标是否被截断 表示是否算难例。这两个值对训练影响很大。有些标注工具会把被挡了一半的车标成difficult1如果转换脚本把这些框也当正样本模型会学到一堆遮挡目标也能标全框推理时误检率直线上升。稳妥的做法是转换脚本直接跳过difficult1的框只留干净样本训练这个逻辑我放在第3章的脚本里一并处理。2.3 类别分布与图片多样性先查清楚哪类车多、哪类车少训练样本不平衡是最隐蔽的坑家用车识别得很好消防车工程车几乎不输出。原因是模型在训练里没见过几个消防车正样本梯度贡献全被car淹没。所以跑训练前我养成了一个习惯先统计每个类别的真实数量import xml.etree.ElementTree as ET import glob from collections import Counter counter Counter() files glob.glob(Annotations/*.xml) for f in files: tree ET.parse(f) root tree.getroot() for obj in root.findall(object): name obj.findtext(name) if name: counter[name] 1 print(counter)这段代码遍历全部XML用findall(object)找到所有目标再取name字段累加。xml.etree.ElementTree是Python标准库不需要额外安装用findtext比find().text更安全因为字段缺失时返回None而不是抛异常。1400张图跑完你会得到一个类似{car: 1280, bus: 210, fire_engine: 68, truck: 33}的输出数字不一定长这样我只是拿常见比例举例。看到列表后做两个决定。第一某个类别只有几十张图第4章训练时别指望它能自己学好要考虑少样本策略。第二把这个类别名列表记下来后面data.yaml的names顺序直接按这个列表排序固定住不要每次跑都换顺序。图片多样性也要抽样看。文件名只能告诉你编号看不出拍摄场景。我一般会随机抽20张拼个图montage $(ls JPEGImages/*.jpg | shuf -n 20) -geometry 44 preview.jpgmontage是ImageMagick的命令环境里没有的话先apt install imagemagick。一眼看20张图里有哪些角度、光线、遮挡判断这份数据更偏城市道路还是停车场或高速。如果全是车头正面拍的车模型会对角度极其敏感侧面车一来就漏检。3. 把VOC转成YOLO格式划分脚本与TXT标签生成3.1 VOC、YOLO、COCO三种标注的差异与转换关键VOC坐标和YOLO坐标不是同一个坐标系。VOC用左上角加右下角的绝对像素坐标YOLO的TXT每一行是class_id x_center y_center width height并且后四个值全部归一化到0到1之间。归一化的意思是把像素坐标除以图片本身的宽和高x_center ((xmin xmax) / 2) / image_widthy_center ((ymin ymax) / 2) / image_heightwidth (xmax - xmin) / image_width。三个格式放在一起看更清楚格式坐标表示后缀主要使用方VOC绝对像素 x1,y1,x2,y2xmlLabelImg、VOC工具链YOLO归一化中心点宽高txtYOLOv3/v4/v5/v8COCO JSON绝对像素segmentationjsonDetectron2、MMDetection转换最常翻车的位置在“宽高到底除以谁”。有的脚本偷懒直接用xmax - xmin当作目标宽不除以图片宽看起来单目标没问题一旦两个目标重叠坐标全偏。我一般会强制在脚本里用PIL重新读一次图片的宽高而不是完全信任XML里的size字段因为个别标注工具写size时把width和height写反过。3.2 转换脚本实现读取XML、归一化坐标、随机划分数据集下面这个脚本直接扔到数据集根目录跑它做三件事遍历Annotations下所有XML跳过difficult标注把每个目标转成YOLO TXT标签按85%、10%、5%的比例拆出train、val、test保证类别ID按字母排序固定。脚本存成voc2yolo.pyimport xml.etree.ElementTree as ET import os import random from pathlib import Path from PIL import Image random.seed(42) # 固定随机种子保证每次划分一致 IMG_DIR JPEGImages ANN_DIR Annotations TXT_DIR labels IMAGE_TXT_DIR ImageSets/Main train_ratio, val_ratio 0.85, 0.10 # 剩下5%留给测试 def convert(xml_path, img_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_w, img_h Image.open(img_path).size # 以实际图片为准 lines [] for obj in root.findall(object): if int(obj.findtext(difficult, 0)) 1: continue # 跳过难例避免把遮挡样本当正例 name obj.findtext(name) cls_id CLASS_NAMES.index(name) box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) txt_path Path(out_dir) / (Path(xml_path).stem .txt) with open(txt_path, w) as f: f.write(\n.join(lines)) return len(lines) if __name__ __main__: all_names [] for xml in Path(ANN_DIR).glob(*.xml): tree ET.parse(xml) for obj in tree.findall(object): nm obj.findtext(name) if nm not in all_names: all_names.append(nm) all_names.sort() # 类别名按字母排序保证class id稳定 CLASS_NAMES all_names print(类别顺序:, CLASS_NAMES) os.makedirs(TXT_DIR, exist_okTrue) os.makedirs(IMAGE_TXT_DIR, exist_okTrue) items [] for xml in Path(ANN_DIR).glob(*.xml): stem xml.stem img_path Path(IMG_DIR) / (stem .jpg) if not img_path.exists(): print(f缺少图片: {stem}) continue n convert(str(xml), str(img_path), TXT_DIR) if n 0: items.append(stem) random.shuffle(items) n_train int(len(items) * train_ratio) n_val int(len(items) * val_ratio) for split, names in [(train, items[:n_train]), (val, items[n_train:n_train n_val]), (test, items[n_train n_val:])]: with open(f{IMAGE_TXT_DIR}/{split}.txt, w) as f: f.write(\n.join(names)) print(split, len(names))逻辑拆开看。第一步扫一遍所有XML收集不重复的类别名并按字母排序这步最关键。类别名的排序顺序决定class id今天跑和明天跑顺序不一样标签和yaml就对不上了。第二步遍历所有XML用PIL实际读取图片宽高再归一化不用XML里size字段的原因前面说了。第三步把有目标的图片按比例随机划分85%、10%、5%对1400张图是合理的测试集留70张左右足够刷出一个可信的mAP。脚本里的train_ratio和val_ratio直接可调想留更多测试数据改成0.8和0.1也能跑。3.3 转完后的自检看类别数量、查坐标越界、清空标签转换完成不等于转换正确。最常见的失败模式是labels目录下TXT全部生成成功但里面有行坐标归一化后大于1或者小于0。原因多半是bndbox里出现了负坐标目标在图片边缘被截断时有的标注工具会把负坐标写进XML。YOLO训练遇上越界标签轻则警告重则训练直接崩溃。import glob from collections import Counter counter Counter() bad_coord 0 empty [] for txt in glob.glob(labels/*.txt): with open(txt) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty.append(txt) continue for line in lines: parts line.split() cls_id int(parts[0]) counter[cls_id] 1 vals list(map(float, parts[1:])) if any(v 0 or v 1 for v in vals): bad_coord 1 print(类别id统计:, counter) print(越界行数:, bad_coord) print(空标签文件:, len(empty))这段脚本处理三类问题。空标签文件意味着转换脚本把某个XML里的目标全跳过了可能这张图全是difficult目标。越界行意味着坐标算错要回头查原始XML的bndbox。类别id统计能和2.3节的XML name统计对上账如果id0对应的类别和想象不一致检查CLASS_NAMES的排序结果。检查出问题就回到转换脚本修不要拖到训练阶段否则一张坏样本能让validation loss曲线冒出一个解释不了的凸起。4. 用YOLOv5训练车辆检测参数设置与训练结果解读4.1 数据集yaml配置与类别名对齐自己采集数据后用YOLOv5训练自己的数据集时接入的入口就是data.yaml。上一章生成的labels目录和ImageSets/Main放进YOLOv5项目目录后写一个yaml描述数据集train: images/train val: images/val test: images/test nc: 4 names: 0: bus 1: car 2: fire_engine 3: truck这里有个硬性要求names列表必须和第3章脚本里CLASS_NAMES变量的排序完全一致不能凭记忆填。TXT标签里的class_id是数字YOLO训练时拿这个数字去names里找类别名。如果labels里class_id1是caryaml里names[1]却写了bus模型会把所有家用车学成公交车而且loss曲线完全正常。最稳的做法是把第3章脚本打印出的类别顺序列表直接复制进yaml一个字符都不改。这条流水线上最容易出的“玄学”问题就在这里。注意train和val的路径前缀。上面的images/train指YOLOv5根目录下的images/train它里面只放图片。图片放images/train标签就放labels/train目录名和父目录名的对应关系是定死的。YOLOv5内部用img2label_paths把images替换成labels来推导标签路径如果你把图片放在train_images目录下标签在labels/train模型就读不到。这个不符合代码风格的问题会直接导致训练时每张图都报找不到标签。4.2 train.py核心参数epochs、batch-size与imgsz怎么配配置完成后开始训练我一般这样启动python train.py \ --data data.yaml \ --weights yolov5s.pt \ --epochs 150 \ --batch-size 16 \ --imgsz 640 \ --device 0 \ --workers 8 \ --cache ram逐个说参数。--weights yolov5s.pt用COCO预训练过的s模型当起点。对于1400张的数据集从零训练大模型大概率学不满用小模型起步收敛快。显存只有6G的话batch-size降到8去掉cache ram。--epochs 150对这个小数据集偏多但配合mosaic增强不至于过拟合得太难看。训练时主要看best.pt它在验证集上表现最优的轮次才保存。--imgsz 640是常规输入尺寸如果数据里大量车辆都很小像素面积小于32乘32640会把它们缩成一个点考虑提到960或1280显存消耗也会涨。--cache ram把1400张图一次性读进内存避免每轮都走磁盘IO内存少于16G就删掉这个参数。训练结束后看runs/train/exp目录下的weights文件夹。best.pt是验证集上mAP最优的权重last.pt是最后一轮的权重。部署和验证都优先用best.ptlast.pt只在你打算继续训练时用。4.3 训练结果怎么看results.png、混淆矩阵与PR曲线训练日志跑完exp目录下自动生成results.png一张图汇总loss、precision、recall、mAP曲线。先看val/box_loss这条线是否平稳下降再看mAP_0.5那条线在最后几轮还在往上爬。ls runs/train/exp/输出里会有weights、confusion_matrix.png、PR_curve.png、results.png等文件。confusion_matrix.png的横纵坐标是类别对角线越亮越好。如果某一类大面积被归到background类说明样本太少或者特征不明显。PR_curve.png的曲线往右上角顶得越饱满说明阈值选择空间越大。这里是最容易藏问题的地方mAP高不代表每个类都好。1400张数据训练出的模型很可能car的AP到0.99但fire_engine的AP只有0.5平均值看着能过0.9打开逐类的AP50那一行立刻现原形。所以别只盯总mAP一定要逐类核对这也是第6章验证“98%识别度”的正确姿势。5. 训练自己的车辆数据集避坑从标注翻车到训练中断的五个真实现场5.1 现象val_loss降不下来mAP看着还行但召回率低得吓人原因数据集本身只有1400张类别又多模型学不到足够多的正样本特征。消防车、工程车这类小样本类召回率被拖到0.3以下平均一下数字还能看单拉出来惨不忍睹。解决先按第2.3节的统计结果确认哪些类少。少样本类不要硬靠加大epoch而是用yolov5s预训练权重做迁移学习让网络复用COCO里学到的车辆底层特征。如果还不行给样本少的类别单独补充标注图哪怕从原图里裁剪、翻转、轻微缩放复制一批也是办法。5.2 现象训练和验证都正常推理时家用车的框却标成bus原因XML里的类别名和yaml里names的顺序错位class id错乱。YOLO训练读的是TXT里的数字id不是名称。labels目录里class_id1本来是caryaml里names[1]写了bus从第一轮开始所有car样本都被当成bus来学。解决重新核对labels目录里class_id1的TXT行内容、转换脚本打印的CLASS_NAMES、data.yaml的names顺序三方对齐。从那以后我每次拿到新数据集都强制把这三处放在一起比对一次。5.3 现象批量训练时报No such file or directory或加载图片失败原因文件名里有中文、空格、特殊字符。VOC格式的filename字段允许带任意字符但YOLO读路径时用的是glob和os.path遇到空格多的路径直接翻车。我自己碰到过文件名最后带一个看不见的\xa0排查到怀疑人生。解决拿到数据集先统一重命名规则是纯数字或字母加下划线for f in JPEGImages/*.jpg; do base$(basename $f) newbase$(echo $base | tr -cd [:alnum:]_\.) mv $f JPEGImages/$newbase done文件名改了XML里的filename字段和文件名也要同步改不然图片和标注就断了。最稳的办法是先建一张文件名对照表再统一mv。5.4 现象训练到一半服务器断了想续训却发现last.pt不存在原因YOLOv5默认在训练结束时保存best.pt和last.pt但训练中断时可能正好没走到保存权重的节点last.pt还是上一轮的旧权重甚至还没生成。解决训练命令里加--save-period 10让每10轮固定存一份权重。恢复训练时用python train.py --data data.yaml --weights runs/train/exp/weights/last.pt --resume--resume会读对应目录里的opt.yaml自动恢复epoch、batch等参数比手动重填参数稳得多。同时用--project runs/train_yolo区分多次实验别让exp1、exp2互相覆盖。5.5 现象消防车识别精度远低于其他类甚至完全检不出原因类别样本太少加上消防车外观特殊红色车身加警示条纹模型在没有足够正样本的情况下学不到稳定特征反而把它归到背景或bus。解决两步走。第一步看confusion_matrix.png确认误归到哪一类。第二步做样本增强常见做法是对少样本类做重复采样手动复制并做平移、缩放、翻转增强把数量提到能支撑训练的底线。训练时不要全局关闭mosaic它对少样本类合成样本的帮助非常明显。6. 验证模型是否真有98%识别度mAP0.5与标注质量自检6.1 mAP0.5和“识别度98%”之间的换算关系先澄清一个容易误解的点。数据描述里写的“识别度超过98%以上”在目标检测场景通常指mAP0.50.98也就是IoU阈值为0.5时所有类别的平均精度。它不等于分类准确率也不是说每张图都能框出98%的车。想验证这个数字不能只看训练日志要用独立的测试集过一遍val.py。6.2 测试集实测与PR曲线读数python val.py \ --data data.yaml \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --conf-thres 0.001 \ --iou-thres 0.6 \ --task test参数含义--task test指定用第3章划分出的test.txt做评估--conf-thres设成0.001是为了画出完整的PR曲线看到每个置信度下的表现--iou-thres取0.6比COCO默认的0.5更严格。跑完看每类的AP50数值注意有没有某一类和其他类差开一大截。差得多就回到第5章的少样本处理方法去补数据。6.3 标注质量自检脚本找坏图、空标签、错格最后我每次拿到新数据集都会强制跑一遍这个自检脚本import xml.etree.ElementTree as ET from pathlib import Path for xml in Path(Annotations).glob(*.xml): tree ET.parse(xml) root tree.getroot() img Path(JPEGImages) / (xml.stem .jpg) if not img.exists(): print(缺图, xml.stem) for obj in root.findall(object): b obj.find(bndbox) x1, y1, x2, y2 (float(b.findtext(k)) for k in (xmin, ymin, xmax, ymax)) if x2 x1 or y2 y1: print(坐标错, xml.stem, obj.findtext(name))它检查图片是否缺失、坐标是否反向。走一遍没报错这份数据才敢进训练管线。从那以后我每次新拿到数据集都强制先过这个脚本再谈训练参数省了好多次标数据标到怀疑人生的返工时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表