☰
1400张车辆图片VOC数据集:从标注解析到YOLOv5训练全流程
2026/10/8 10:54:58 网站建设 项目流程

简介:这份车辆检测数据集包含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> <filename>0265.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>car</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>362</xmin> <ymin>401</ymin> <xmax>845</xmax> <ymax>902</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值拉出来,看有没有中文、空格、大小写混杂。

表示目标是否被截断, 表示是否算难例。这两个值对训练影响很大。有些标注工具会把被挡了一半的车标成difficult=1,如果转换脚本把这些框也当正样本,模型会学到一堆遮挡目标也能标全框,推理时误检率直线上升。稳妥的做法是转换脚本直接跳过difficult=1的框,只留干净样本训练,这个逻辑我放在第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 +4+4 preview.jpg

montage是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_width,y_center = ((ymin + ymax) / 2) / image_height,width = (xmax - xmin) / image_width。

三个格式放在一起看更清楚:

格式坐标表示后缀主要使用方
VOC绝对像素 x1,y1,x2,y2xmlLabelImg、VOC工具链
YOLO归一化中心点+宽高txtYOLOv3/v4/v5/v8
COCO 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.py:

import 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_ok=True) os.makedirs(IMAGE_TXT_DIR, exist_ok=True) 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统计对上账,如果id=0对应的类别和想象不一致,检查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_id=1是car,yaml里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乘32,640会把它们缩成一个点,考虑提到960或1280,显存消耗也会涨。--cache ram把1400张图一次性读进内存,避免每轮都走磁盘IO,内存少于16G就删掉这个参数。

训练结束后看runs/train/exp目录下的weights文件夹。best.pt是验证集上mAP最优的权重,last.pt是最后一轮的权重。部署和验证都优先用best.pt,last.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_id=1本来是car,yaml里names[1]写了bus,从第一轮开始所有car样本都被当成bus来学。

解决:重新核对labels目录里class_id=1的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%识别度:mAP@0.5与标注质量自检

6.1 mAP@0.5和“识别度98%”之间的换算关系

先澄清一个容易误解的点。数据描述里写的“识别度超过98%以上”,在目标检测场景通常指mAP@0.5=0.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'))

它检查图片是否缺失、坐标是否反向。走一遍没报错,这份数据才敢进训练管线。从那以后我每次新拿到数据集,都强制先过这个脚本,再谈训练参数,省了好多次标数据标到怀疑人生的返工时间。希望帮到你。

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

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

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

立即咨询