☰
VOC车辆检测数据集:XML与TXT双格式详解及YOLO训练避坑指南
2026/10/1 3:27:54 网站建设 项目流程

简介:面向目标检测模型训练与验证的车辆检测数据集,涵盖公交、轿车、SUV、出租车和卡车五类常见车型,可接入YOLO、SSD等主流算法流程,适合算法研究者、竞赛选手以及需要自建数据集的开发者使用。压缩包共2000个文件,主要包括699张JPG车辆图片、700个TXT标签和699个XML标签,TXT与XML分别存放在独立文件夹中,对应YOLO与VOC两种常用标注格式,整个压缩包约466.3MB。目前该数据集已有961人学习下载,可用于车型识别、车辆统计、检测算法效果对比等实验任务。TXT标签便于直接参与YOLO系列训练流程,XML标签保留标准结构,方便迁移至其他检测框架或二次转换;图片覆盖多种拍摄场景,五类车型均有较多样本,能有效支撑模型训练、验证与部署前评估,大幅节省数据采集与人工标注时间。

1. VOC格式车辆检测数据集:双格式标注怎么读才不翻车

做目标检测的绕不开VOC格式的数据集,尤其是车辆检测这种经典场景。这份数据集最有价值的部分不是那些jpg,而是每张图对应的两套标注:xml一套、txt一套,五个类别bus、car、suv、taxi、truck,覆盖城市道路和停车场的绝大多数机动车形态。我拿到手的第一反应是:训练前必须想清楚一件事——这份数据的标注到底该信哪一份。xml保留完整VOC语义,供Faster R-CNN这类检测框架使用;txt是YOLO系训练直接吃的归一化坐标。两套格式并存,既是方便也是陷阱,读错一处,训练白跑。适合谁用?刚接触检测、需要一份干净数据跑通pipeline的新手,以及要做车辆检测预研、想快速评估模型表现的工程师。下面按读标注、转格式、做体检、避坑、看指标这条线,把这份资源完整拆一遍。

2. 标注文件长什么样:xml与txt双轨格式逐字段拆解

图片只是素材,标注才是这台模型的燃料。图片只能告诉你“路上有车”,标注才能告诉模型车在哪里、是什么车型。这份数据集把同样的标注内容用xml和txt各存一份,初看冗余,实际是故意的:xml保留完整的VOC语义,给人看、给VOC系训练脚本用;txt把坐标归一化,给YOLO系训练直接吃。下面把两部分拆开看。

2.1 五种车辆类别与文件命名规律

从文件名就能把类别认个大半:car_125.jpg、car_102.jpg前缀是car,SUV_77.jpg、SUV_74.jpg前缀是SUV,对照类别表还能看到bus、taxi、truck前缀的同名文件。这种“类别前缀+序号”的命名方式在自采数据集里非常常见,优点是定位某类样本时直接用通配符就能筛出来,比如在终端执行ls SUV_*.jpg,就能把SUV类所有图片单独列出来看。

不过文件名前缀只能当参考,真正的类别依据是xml里object的name字段。原因很简单:人工收集图片时,可能把一辆外观偏圆润的SUV归到car目录,或者某辆停在路边的bus被拍成了truck视角,这些误标只会在标注文件里暴露。所以后续所有类别统计都以xml里的name为准,不以文件名为准,这是处理这类数据集的一个基本习惯。

类别上共5类:bus、car、suv、taxi、truck,正好覆盖城市道路和停车场最常见的机动车类型。对检测任务而言,这5类外形差异够大——公交车是典型的大矩形目标,小轿车是中尺寸目标,SUV高度和长度接近轿车但轮廓更敦实,出租车靠涂装和顶灯区分,卡车有长车厢。用同一套训练参数能跑出可区分性,各类别之间也不至于互相严重干扰,作为目标检测入门或车辆场景预研的数据是够格的。

2.2 VOC XML标注字段逐项拆解

VOC格式的xml每张图对应一个文件,文件名和jpg同名。打开任意一个xml,结构基本如下:

<annotation> <folder>JPEGImages</folder> <filename>car_125.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>420</xmin> <ymin>300</ymin> <xmax>1180</xmax> <ymax>760</ymax> </bndbox> </object> </annotation>

重点看几个字段。size节点里的width和height是原始图像的像素尺寸,这是后面所有归一化坐标运算的分母,读错一个数,整张图的框全部偏移。object节点里的name是类别名,必须和类别文件里的名字逐一对应,任何一个字符不一致,包括大小写差异,都可能导致训练时类别错位或目标被忽略。bndbox里的xmin、ymin、xmax、ymax是目标框的四个像素坐标,在VOC规范里xmax、ymax指框右下角像素的真实坐标值,也就是说框占用的像素是xmin到xmax这个闭区间。转YOLO格式时这一两个像素要不要处理,不同实现口径不同,对训练影响很小,但你心里得知道这个差异存在。

还有两个容易忽略的字段:truncated和difficult。truncated表示目标是否被图像边缘截断,difficult表示目标是否属于难例,比如严重遮挡或极小目标。很多自采数据集不填这两个字段,有的直接省略节点。如果xml里根本没有difficult节点,解析脚本必须做判空容错,否则训练到一半报KeyError,回头一查又是标注文件的老问题。这个坑在踩坑章节会再展开。

2.3 YOLO txt标注与XML的映射关系

另一份txt标注是给YOLO系吃的,每行代表一个目标,格式是固定的五个数值:类别id、归一化中心点x、归一化中心点y、归一化宽、归一化高。对应到实际文件里大致长这样:

0 0.4167 0.4907 0.3958 0.4259 2 0.6234 0.3750 0.1875 0.2037

第一个数字是类别id,0对应类别文件里的第一行。如果你把类别文件按bus、car、suv、taxi、truck排序,那么0就是bus,2就是suv。后面四个小数全部是0到1之间的归一化值,不携带任何像素单位。以第一行为例,0.4167这个x_center,用xml里的width=1920反算回去:0.4167 × 1920 = 800.06,正好等于上面示例里(xmin + xmax) / 2 = (420 + 1180) / 2 = 800。这套映射关系搞明白,后面写转换脚本就是套公式的事。

txt和xml双份存在的最大价值是互相校验。把txt里的四个归一化值反算回像素后,和xml的bndbox对一遍,对得上说明数据干净;对不上,要么图片被resize过而标注没跟着重算,要么转换过程出了差错。我一般把这种不一致当作数据质量的硬伤,因为训练时模型读txt,做错误分析时人看xml,两边不一致会让人怀疑到底该信哪份,这种数据宁可先修掉再进训练管线。

3. 把数据集喂给YOLO:XML转txt的换算公式与转换脚本

如果只训YOLO,txt这份已经能直接用,但我还是建议你把xml转一遍。理由有两个。第一,自己跑一遍转换脚本等于给全部标注做了一次格式体检,脚本能正常跑完,说明每张图的xml结构完整、坐标没有越界,这比肉眼抽查可靠得多。第二,这份txt只覆盖现有的图片,将来你补拍一批新照片,新标注大概率还是以xml格式存下来,到那时还是得转。与其临场翻旧代码,不如现在就把转换逻辑吃透,顺手沉淀成自己的工具脚本。

3.1 从VOC像素坐标到YOLO归一化坐标:计算口径先立住

YOLO训练时读的txt要求相对图像的归一化坐标,好处是不同分辨率的图片可以放进同一个batch,模型不需要关心输入图片的绝对像素值。换算公式只有四条,我把它们和VOC字段的对应关系放到一起看更直观:

VOC XML字段YOLO txt字段换算公式本例数值(1920×1080)
xmin / xmaxx_center(xmin + xmax) / 2 / width(420+1180)/2/1920 = 0.4167
ymin / ymaxy_center(ymin + ymax) / 2 / height(300+760)/2/1080 = 0.4907
xmax - xminbox_width(xmax - xmin) / width(1180-420)/1920 = 0.3958
ymax - yminbox_height(ymax - ymin) / height(760-300)/1080 = 0.4259

四条公式全部要除一个分母:图片宽高。所以解析xml时,必须先把size节点里的width和height读出来,否则算出来的坐标全是错的。这里最容易踩的坑是猜尺寸:看到文件名里带1080p就觉得是1920×1080,实际上很多图是原图直接存的,宽高比不一定是16:9。老老实实从xml里读,别猜。另一处口径问题在前面提过,VOC里xmax是框右下角像素坐标,用xmax - xmin算宽会比真实目标宽度多1像素,对1920宽的图来说影响在0.05%量级,训练场景可以忽略;如果做精度敏感的测量任务,可以改成xmax - xmin + 1,看你的项目要求,我通常直接用不减1的版本。

提示:所有归一化坐标都以xml里size节点的width和height为分母,训练时即使开了rect或multi-scale,也不会帮你修正错误的归一化坐标,源头错了后面全是错的。

3.2 一个可以直接跑的转换脚本

以下脚本把Annotations文件夹下的xml逐张转成YOLO格式txt。这份数据集本身自带txt,你可以把脚本输出当参照,和自带txt比对,行数一致基本就能确认数据干净。

import xml.etree.ElementTree as ET import os class_names = ['bus', 'car', 'suv', 'taxi', 'truck'] def convert_xml(xml_path, txt_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) if img_w <= 0 or img_h <= 0: print(f'[skip] {xml_path}: invalid size {img_w}x{img_h}') return lines = [] for obj in root.findall('object'): name = obj.find('name').text difficult = obj.find('difficult') if difficult is not None and int(difficult.text) == 1: continue if name not in class_names: print(f'[warn] {xml_path}: unknown class "{name}"') continue cls_id = class_names.index(name) bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) if xmin < 0 or ymin < 0 or xmax > img_w or ymax > img_h: print(f'[warn] {xml_path}: bbox out of range for {name}') 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}') with open(txt_path, 'w') as f: f.write('\n'.join(lines)) xml_dir = 'Annotations' txt_dir = 'labels_yolo' os.makedirs(txt_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue base = os.path.splitext(xml_file)[0] convert_xml(os.path.join(xml_dir, xml_file), os.path.join(txt_dir, base + '.txt'))

脚本里有几处是刻意加的防护。difficult为1的目标直接跳过,因为VOC原版里这类目标要么严重遮挡、要么极小,强行训练容易把梯度带偏,转换时就该过滤掉。unknown class的warn用来抓xml里手滑打错的类名,比如taxi拼成txxi,这种错误会直接导致后续类别id错位。bbox out of range的warn查坐标是否越界,越界框在mosaic这类需要裁剪拼接的增强里最容易产生空标注,提前定位能省不少排查时间。输出格式里保留6位小数,足够表达亚像素精度,又不至于让txt文件体积膨胀。

执行很简单:

python convert_xml.py

跑完先对比txt文件夹里同名文件的行数和自带txt是否一致。一致,大概率没转错;不一致,回到warn输出里逐条看,基本就是类名拼写或difficult过滤导致的差异。

3.3 类别文件与data配置的顺序敏感性

YOLO系训练还需要一个类别列表和一个data配置文件。类别列表一行一个类名,顺序必须和转换脚本里的class_names完全一致,yolov5的yaml里这样写:

names: 0: bus 1: car 2: suv 3: taxi 4: truck

data配置则把训练路径、验证路径、类别数和类别名指给训练器:

train: ./images/train val: ./images/val nc: 5 names: ['bus', 'car', 'suv', 'taxi', 'truck']

三个地方必须保持同一套顺序:转换脚本里的class_names、yaml里的names、txt里的类别id。最常见的翻车方式是把suv写在car前面,结果所有标注为car的txt都成了1类,模型训练完把轿车认成SUV,mAP还显示正常——因为mAP只算预测框和真值框的匹配度,类别错位它根本检查不出来。

我一般拿到数据集,先跑统计脚本确认各类别id数量正常,再把上一章的txt反算回像素与xml对比做一遍,两边对上才敢开训。数据源头错了,后面调参全是白费功夫,先花十分钟把格式这条链路同步好,比训练时反复试参数划算得多。

4. 训练前的数据体检:类别统计与标注可视化

很多新手拿到数据集就开训,训完发现某一类mAP特别低,回头查才知道这一类总共才十几张样本,前面几轮训练全在做无效功。数据体检这一步花十分钟,能省掉后面好几轮调参时间。体检分三块:类别分布统计、标注框可视化检查、不平衡样本的处理方案。

4.1 统计脚本:先看各类别样本量再决定策略

统计不是数文件名前缀,而是按xml里object的name字段数,脚本很短:

import os import xml.etree.ElementTree as ET from collections import Counter xml_dir = 'Annotations' counter = Counter() for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() for obj in root.findall('object'): name = obj.find('name').text if name: counter[name] += 1 for cls_name, count in counter.most_common(): print(f'{cls_name}: {count}')

输出大概长这样,具体数值以你实际统计为准:

car: 486 suv: 312 truck: 198 bus: 154 taxi: 112

这个统计结果直接决定后面的策略。如果最少的类(比如taxi)样本量是car的三分之一左右,属于可接受范围,用默认参数训练就能有不错效果;如果差距超过5倍,就得在重采样、copy-paste增强或损失加权里选一种,否则少样本类会被多样本类持续压制,AP很难提上去。顺着这个脚本还能追加一个目标面积统计:把每张图里目标的宽高乘像素宽高得到面积,再除以图片面积算占比。车辆检测的典型难点就是bus、truck大目标与car、suv远距离小目标并存,面积分布能直接告诉你数据里小目标占比有多高。

4.2 标注可视化:隐患都在框上

统计只能看数量,框画得准不准必须靠眼睛。我习惯在训练前随机抽20张图,用OpenCV把xml里的框画出来存成预览图,扫一遍就能发现三种典型问题:坐标偏移、类别标注错、重复框。

import cv2 import os import xml.etree.ElementTree as ET import random xml_dir = 'Annotations' img_dir = 'JPEGImages' out_dir = 'preview' os.makedirs(out_dir, exist_ok=True) sample_files = random.sample( [f for f in os.listdir(xml_dir) if f.endswith('.xml')], 20 ) def draw_xml(img_path, xml_path): img = cv2.imread(img_path) tree = ET.parse(xml_path) root = tree.getroot() for obj in root.findall('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = int(float(bbox.find('xmin').text)) ymin = int(float(bbox.find('ymin').text)) xmax = int(float(bbox.find('xmax').text)) ymax = int(float(bbox.find('ymax').text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(ymin - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) return img for xml_file in sample_files: img_path = os.path.join(img_dir, xml_file.replace('.xml', '.jpg')) xml_path = os.path.join(xml_dir, xml_file) preview = draw_xml(img_path, xml_path) cv2.imwrite(os.path.join(out_dir, xml_file.replace('.xml', '.jpg')), preview) print(f'预览图已保存到 {out_dir},共 {len(sample_files)} 张')

画框用绿色、类别名用红色是固定参数,你要批量标注检查可以按类别换色。检查时重点看三处:框是否包含完整车体而不带过多背景;车头朝向不同时框是否都贴住边缘;有没有一辆车被多个框重叠覆盖的情况。任何一处出问题,优先改标注再训练,不要指望模型自己学出正确边界,标注错的数据对模型来说就是噪音。预览图生成后存在preview文件夹里,用看图工具快速翻一遍,比在终端里看日志直观得多。

4.3 类别不平衡的三种处理思路

如果统计发现某类样本明显偏少,三种方案按需选。第一是重采样,训练时让少样本类的图片在每个epoch里多出现几次,yolov5里的--img-weights配合--multi-scale能做第一轮按类别权重的预选,但实现比较隐晦,不够可控。第二是离线copy-paste,把少样本类目标从原图裁剪下来,随机粘贴到其他图片的空白区域,同时同步更新标注,对车辆这种不依赖背景的目标效果很好,但要注意粘贴时别遮住原有目标,也别贴到道路外的异常位置。第三是损失函数加权,在loss计算时给少样本类乘一个大于1的系数,这个手段对中后期精度提升有限,更适合作为微调手段。

我的习惯是先用重采样把类别比例压到3倍以内,再配合copy-paste把最少的类补上一部分。如果你的数据统计下来比例本身正常,直接跳过这节开训完全没问题,体检的价值就在于让你知道该不该花这个功夫。

5. 踩坑记录:VOC车辆数据集训练中的四个高频问题

下面把我在类似车辆数据集上反复翻过的车集中记录一下。每条都是真实遇到过的场景,按现象、原因、解决三段写,你跑的时候遇到同样问题,直接对照着查。

5.1 一类mAP始终为零,loss却正常下降

现象:训练loss正常下降,验证集五个类别里四个都在爬升,唯独某一类的mAP从第一个epoch开始就是0,训练结束依然纹丝不动。这不是训练轮数不够,而是数据或标注在训练前已经埋了雷。

原因:xml里difficult字段为1的难例被当成了正样本。这类目标通常是严重遮挡、极小或轮廓不清的车辆,标注者打上difficult标记后,VOC官方评测会直接忽略它们。但很多转换脚本没有过滤逻辑,把它们全部转成txt送进训练,模型在loss里反复被这些不可学的样本带偏,表现为某一类mAP一直上不去。

解决:转换脚本里加一行过滤,见第3章代码里这段:

difficult = obj.find('difficult') if difficult is not None and int(difficult.text) == 1: continue

如果你的xml里连difficult节点都没有,记得先判空再取整,不要直接int(obj.findtext('difficult')),那个写法在节点缺失时直接抛异常。从那以后我拿到带xml的数据集,第一件事就是统计difficult样本数量,做到心里有数。

5.2 txt和xml坐标对不上,而且差的是固定倍数

现象:同一个目标,xml里xmin是420,把txt的x_center反算回像素却是630,整体往右下偏移,而且每张图偏移比例接近。模型训练时框的位置整体偏,val的mAP低得不正常。

原因:txt不是从这份xml对应的原图转出来的,而是从另一份不同分辨率的图片标注转的。车辆数据集经常被二次压缩或resize,原图1920×1080压成1280×720后,如果标注没跟着重算,归一化坐标会整体错位。我之前就遇到过图片被脚本批量resize但txt沿用旧标注的情况,训练出来所有框都偏。

解决:抽三张图做反算验证,把txt第一个坐标乘以当前图片的实际宽度,看是否等于xml里的x_center像素值。图片实际尺寸用cv2.imread读出来确认,不要看文件名后缀猜。对不上的数据,要么用对齐后的xml重新转一遍txt,要么整张图作废,千万别留着继续训。

5.3 小目标漏检严重,大目标一切正常

现象:训练完跑val,公交车、卡车这类大目标检测效果不错,远处的小轿车和SUV漏检率很高,看起来像模型对大目标过拟合。

原因:图片尺寸不统一,训练时统一resize到640×640,远处车辆被压到十几个像素,特征几乎消失。这类数据集的图片来源杂,有行车记录仪截图、有监控画面、有手机随手拍,分辨率差异很大,直接统一resize会把小目标压没。

解决:训练时开--multi-scale,让模型每个batch随机换输入尺寸,等效于尺寸扰动增强;同时把--img设成数据集中大多数图片的实际尺寸而不是默认值。如果小目标占比仍然高,可以在mosaic增强阶段给小目标单独做一次上采样放大,保证它在训练图片里至少占到几十个像素。对这份车辆数据集,bus、truck是天然大目标,car、suv里混着很多远距离小目标,这条尤其值得重视。

5.4 所有类别mAP都很低,模型像随机猜测

现象:训练能正常跑完,epoch数也不少,但验证集所有类别的mAP都不到0.5,比随机好不了多少。这时候人容易怀疑模型结构或超参,先冷静查数据。

原因:类名大小写和空格造成的类别错位。Windows下SUV和suv在文件系统里不区分大小写,但检测框架按字符串建类别字典,两者是两个完全不同的键。如果xml里写的是SUV,类别文件里写的是suv,这个类就会被跳过或归到背景,模型等于少学了一类。

解决:全流程统一类名口径。先跑脚本把xml、txt、yaml、class文件里的类名全部小写化并去掉首尾空格,再跑一次类别统计确认五个类都能数到。快速检查xml里到底出现了哪些类名,用这一条命令最直接:

grep -rh '<name>' Annotations | sort | uniq -c

凡是和class_names有字符差异的,全部在转换脚本的warn输出里能看见。类名这种细节,错一个字符就是半天的无效训练,排查时永远先把数据链路完整跑一遍再动模型。

6. 验证这一份数据集:mAP指标与检测结果可视化

训完不能只看训练loss,要拿独立验证集算指标,再把检测结果画出来和标注并排看,这一步才算真正把数据集验收掉。

6.1 双指标判断训练效果

我习惯同时看mAP@0.5和mAP@0.5:0.95两个指标。前者反映框大致位置对不对,后者对边框精度要求苛刻,两者差距大说明框的定位不够准。车辆检测里小目标多,mAP@0.5:0.95普遍偏低属于正常现象;但如果mAP@0.5很高、0.5:0.95极低,优先怀疑标注框本身就不贴边,回到第4章的可视化检查重新过一遍标注。

6.2 检测可视化与置信度阈值

跑val模式输出带预测框的图片,用yolov5的detect是:

python detect.py --weights runs/train/exp/weights/best.pt \ --source data/images/val \ --conf-thres 0.25 --iou-thres 0.45

conf-thres设0.25是兼顾召回和误检的常规值,正式测试时分别试0.1、0.25、0.5三档,观察误检和漏检的权衡。如果0.25下大量出现低置信度框,说明模型对目标把握不足,优先加数据或增强,不要盲目堆训练轮数。数据集说明里的参考文章贴过这套数据的检测效果示例,把你自己跑出来的可视化结果和它对一遍,水平接近说明流程没跑偏,差得远就按前几章逐项复查数据链路。

6.3 copy-paste增强的一个小参数

对少样本类做copy-paste时,粘贴的目标要做小幅缩放和旋转,别直接原尺寸贴上去,否则模型会记住固定尺寸和角度,泛化反而变差。缩放范围0.8到1.2、旋转±15度,这两个参数是我试过比较稳的范围。

从那以后,我每次拿到新的检测数据集,都会强制走一遍“读标注→转格式→统计→可视化→试训→可视化验证”的完整流程,哪怕是只有几十张的小数据集也不跳过。这套流程帮我在多种数据集上少走了很多弯路,希望帮到你。

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

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

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

立即咨询