☰
车辆数据集YOLO标签格式转换与yolov8训练实战
2026/10/2 8:37:24 网站建设 项目流程

简介:面向目标检测学习者与算法开发者的YOLO车辆检测数据集,含汽车、自行车、公共汽车三类共969张带标签图像,已经完成训练/验证测试划分,并内置data.yaml配置,兼容YOLOv5至YOLO11等多版算法,下载后即可直接训练、验证与测试。包内共2000个文件,包括969个XML与969个TXT标签文件,分别对应VOC与YOLO两种标准格式,两类标签分文件夹存放,配合61张JPG原始图像和1个YAML配置文件,可满足完整的目标检测实验流程。YOLO标签采用归一化坐标表示,记录了类别索引、中心点位置和宽高比例,便于理解数据组织方式,也方便在不同框架或转换工具间复用。资源压缩包约57.08MB,目录结构清晰,省去自行采集、标注和划分数据的环节,可快速用于课程设计、论文实验或实际场景中的车辆检测项目。已有164人学习下载,适合想专注算法调优而非数据准备的目标检测入门及进阶用户。

1. yolo 车辆数据集:969 张标注图能帮你跑通哪一步

做目标检测的同行大多有过这种经历:模型选好、环境装好,手里却缺一份能直接喂给 yolo 算法的车辆数据集。自己爬图标注,几小时才凑出百来张,标完还得操心格式对不对。这份 969 张图像的车辆数据集,包含汽车、自行车、公共汽车三个类别,标签同时提供 yolo 格式(txt)和 voc 格式(xml),附带 data.yaml,train/val/test 已划分完毕。它的价值不在量大,而在开箱即用:解压后改个路径,就能进 yolov5、yolov7、yolov8、yolov9、yolov10、yolo11 训练。适合刚想跑通第一个检测模型的新手,也适合拿它做基准验证的熟手。

2. 双格式标签的坐标换算逻辑:YOLO txt 与 VOC xml 到底差在哪

拿到压缩包先别急着开训,把两种标签格式的坐标换算逻辑搞清楚,后面训练报错时你才知道去哪查。这份数据集把 yolo 格式和 voc 格式的标签分文件夹存放,本质上同一批标注目标用两种坐标系各写了一遍。YOLO 用的是归一化比例值,VOC 用的是绝对像素坐标,两者之间就靠图片的实际宽高做桥梁。

2.1 YOLO 归一化坐标:0~1 比例值的计算与解读

yolo 格式的每个 txt 文件里,每一行对应一个目标,结构是五个字段:

<class> <x_center> <y_center> <width> <height>

其中<class>是类别索引,从 0 开始,本数据集里 0 对应 car,1 对应 bicycle,2 对应 bus。后面四个值全部是相对图片宽高的比例,范围在 0 到 1 之间。也就是说,<x_center>不是目标框中心点的像素 x 坐标,而是中心点 x 除以图片宽度得到的商。同理<width>是目标框像素宽度除以图片宽度的比例。

举例:图片宽度为 1280 像素,某个汽车框中心点 x 坐标为 640,框宽为 320,那 txt 里记录的就是x_center = 640 / 1280 = 0.5,width = 320 / 1280 = 0.25。换一张宽度为 640 的图,同样位置和比例的框,yolo 标签写出来的还是0.5和0.25,分子变了但比例不变。

这个设计的直接好处是模型训练时不受输入图片分辨率影响。yolov8 训练时会把图像缩放到 imgsz(比如 640x640),如果标签里存的是绝对像素,缩放后所有框都得跟着重算;存成比例值,标注天然适配任意输入尺寸。代价是解析时你必须知道图片的真实宽高,否则没法把比例还原成像素坐标去做可视化或评估 IoU。

提示:训练前抽查一个 txt 文件,如果发现某个值大于 1 或者小于 0,说明标签的归一化过程出了岔子,这种情况后面第 5 章会专门讲怎么排查。

2.2 VOC xml 绝对坐标:bndbox 结构与像素坐标读取

voc 格式则完全不同,它是 Pascal VOC 的标注规范,用 xml 描述每个目标的类别和像素级边界框。一段典型的 voc 标签长这样:

<annotation> <filename>img_0689_348.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>car</name> <bndbox> <xmin>480</xmin> <ymin>300</ymin> <xmax>800</xmax> <ymax>620</ymax> </bndbox> </object> </annotation>

<size>节点记录图片宽高,<object>节点里<name>是类别名,<bndbox>里四个值就是左上角和右下角的绝对像素坐标。标注工具 labelImg 默认就输出这种格式,很多老项目的可视化脚本也只认 voc,所以虽然 yolo 训练不需要 xml,但当你需要把数据喂给非 yolo 框架,或者用视觉工具做二次检查时,xml 就派上用场了。

两种格式的换算关系可以整理成一张对照表:

含义yolo txt 字段voc xml 节点换算公式
类别第一列,索引object/name索引转名称靠 names 列表
框中心 xx_center(比例)(xmin + xmax) / 2像素值除以图片宽度
框中心 yy_center(比例)(ymin + ymax) / 2像素值除以图片高度
框宽width(比例)xmax - xmin像素宽除以图片宽度
框高height(比例)ymax - ymin像素高除以图片高度

注意<size>里的宽高不一定可信,有些标注工具在调整图片后没同步更新 size 节点。我一般直接用 OpenCV 或 PIL 读图片真实尺寸,而不是信任 xml 里写的值,这个习惯在转换脚本里尤为重要。

2.3 data.yaml 与目录结构:数据集划分的约定

yolov5 之后的大多数 yolo 版本都用 data.yaml 描述数据集,这份压缩包里已经带好了。核心内容就是三个路径加类别信息:

train: D:/vehicle_969/train/images val: D:/vehicle_969/val/images test: D:/vehicle_969/test/images nc: 3 names: ['car', 'bicycle', 'bus']

train、val、test分别指向训练、验证、测试集的图片目录,nc是类别总数 3,names按索引顺序列出类别名。yolov8 训练时读取标签的规则是:图片在train/images,标签就在同级的train/labels目录,文件名与图片同名,只是扩展名换成.txt。

解压后先跑一条命令确认结构是否符合这个约定:

cd /path/to/vehicle_969 find . -maxdepth 2 -type d | sort

常见的目标结构长这样:

vehicle_969/ ├── data.yaml ├── train/ │ ├── images/ │ │ └── img_0689_348.jpg │ └── labels/ │ └── img_0689_348.txt ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/

如果解压后看到的是单独的 images 文件夹加两个标签文件夹(yolo 目录和 voc 目录),说明划分信息可能在 data.yaml 里以别的字段体现,或者压缩包内另有划分清单。无论哪种组织方式,核心原则不变:训练前必须确认「图片、txt 标签、xml 标签三方文件名能对上」,这是后面一切操作的地基。

3. 把数据集跑进 yolov8:data.yaml 修改、训练启动与推理验证

格式逻辑清楚之后,下一步就是让数据真正转起来。yolov8 是目前用起来最省事的版本,一条命令就能完成训练,但「省事」的前提是环境干净、路径正确、参数合理。这章按我自己的操作顺序来讲,你照着走一遍就能出权重。

3.1 环境准备与目录核验

先装 ultralytics 包,它会连带装好 torch 和 torchvision:

pip install ultralytics

装完确认版本和显卡状态:

python -c "import ultralytics; print(ultralytics.__version__)" nvidia-smi

有显卡就放心用 GPU 训练,显存低于 6G 的话建议用yolov8n或yolov8s这类轻量权重起步。没有显卡也可以跑 CPU,把epochs调小、imgsz调到 416 就行。

接着做标签体检,我习惯用一段极简脚本扫描整个数据集,确认标签文件数量和图片数量能对上,格式没有明显异常:

import os from pathlib import Path base = Path('D:/vehicle_969') for split in ['train', 'val', 'test']: img_dir = base / split / 'images' lbl_dir = base / split / 'labels' if not img_dir.exists(): print(f'{split}: images dir not found') continue imgs = {p.stem for p in img_dir.glob('*.jpg')} lbls = {p.stem for p in lbl_dir.glob('*.txt')} missing = imgs - lbls extra = lbls - imgs print(f'{split}: images={len(imgs)}, labels={len(lbls)}, ' f'missing={len(missing)}, extra={len(extra)}')

这段脚本把三个划分下的图片和标签按文件名(去掉扩展名)做集合差。missing表示有图但没标签,extra表示有标签但没图。正常情况两个数都应该是 0,出现任何一个都说明文件配对有问题,直接进训练必然浪费时间。

3.2 data.yaml 配置与训练命令

打开 data.yaml,把路径改成你机器上的实际绝对路径。注意 yaml 文件里路径不要带中文,Windows 用户尤其要避开C:\用户\张三\桌面这种路径,OpenCV 对非 ASCII 路径支持很差,训练时图片会一批批地读不出来。

改完后启动训练:

yolo detect train \ data=D:/vehicle_969/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0

几个关键参数的含义和调整思路:

  • model=yolov8n.pt:预训练权重,n 是 nano 最轻量版。第一次跑建议用 n,验证流程没问题再换 s 或 m 提精度。
  • epochs=100:训练轮数。969 张图的数据量不大,100 轮足够收敛,跑完看曲线如果还在下降可以续到 150。
  • imgsz=640:训练时统一缩放到 640x640。本数据集图片如果本身是 1280x720 之类的分辨率,640 是稳妥的默认值;想保留小目标细节可以设 768,但显存占用会明显上升。
  • batch=16:批大小,按显存调整。6G 显存用 16 配 640 可能会爆,报CUDA out of memory就降到 8 或 4。
  • device=0:使用第一块 GPU。CPU 训练就改成device=cpu,同时把 epochs 降到 30 先验证流程。

如果你用的是 yolov5 仓库,命令会稍不同:

python train.py --data data.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100

标签格式两者通用,txt 文件和 data.yaml 的结构一样,所以本数据集在 yolov5 和 yolov7 上也都能直接用,不用做额外转换。

3.3 权重验证:图片推理与指标读取

训练结束后,最优权重在runs/detect/train/weights/best.pt。用它对测试集做一次推理,既验证模型效果,也顺手确认标签格式从头到尾都没问题:

yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=D:/vehicle_969/test/images \ save=True \ save_txt=True

save=True保存画了检测框的图片,save_txt=True把预测结果导出成 yolo 格式文本,方便后续和真实标签做对比。预测完输出目录里会有一个labels子文件夹,每个 txt 的格式和训练标签一模一样,第一列是类别索引,后面是归一化的框坐标。

训练过程的指标在runs/detect/train/results.csv里,用 pandas 读一下就能看到每个 epoch 的mAP50和mAP50-95。本数据集类别少、目标大,100 轮正常结果 mAP50 应该在 0.8 以上,如果远低于这个数,别急着调模型,先回头查标签,问题大概率在数据侧。

4. 标签互转实战:txt 与 xml 双向转换脚本及边界处理

这份数据集同时给了 txt 和 xml,多数情况下你不需要自己转换。但换个角度想:正因为两种格式都有,你完全可以拿它当「标准答案」来校验自己的转换脚本。以后拿到只有单格式的公开数据集时,这套脚本就是你的后悔药。

4.1 xml 转 txt:读取 bndbox 并做归一化

从 voc 转 yolo 的核心就一句话:把 bndbox 的四个像素值换算成 0~1 比例。但真正写代码时有两个坑:图片宽高从哪读、越界框怎么处理。

import xml.etree.ElementTree as ET from pathlib import Path import cv2 CLASSES = ['car', 'bicycle', 'bus'] def xml_to_yolo(xml_path: str) -> list: tree = ET.parse(xml_path) root = tree.getroot() # 图片宽高以实际文件为准,不信任 xml 里的 size 节点 img_path = xml_path.replace('.xml', '.jpg') img = cv2.imread(img_path) img_h, img_w = img.shape[:2] lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in CLASSES: print(f'skip unknown class: {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) # 越界修正,某些标注工具会画出图像边界外的框 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) 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 if w <= 0 or h <= 0: continue lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') return lines

参数说明:CLASSES列表的顺序必须和 data.yaml 里的 names 完全一致,否则类别索引会整体错位。img_w、img_h我用 cv2 从图片文件读,而不是读 xml 的 size 节点,因为实际项目里经常遇到 xml 的 size 是旧值的情况。越界修正用min(max(...))把坐标夹取回图像范围内,这是转换脚本里最容易被忽略的一步——标注框画出去一点在 labelImg 里显示不出来,但训练时会让模型学到错误的边界信息。

批量转换时对每个 xml 调一次这个函数,写出的 txt 存到目标 labels 目录就行。

4.2 txt 转 xml:反算像素坐标与越界修正

反向转换是把 0~1 比例还原成像素值,公式是像素 = 比例 × 图片宽高。注意还原后要用int()取整,因为 xml 的 bndbox 规定是整数像素坐标。

import xml.etree.ElementTree as ET import cv2 CLASSES = ['car', 'bicycle', 'bus'] def yolo_to_xml(txt_path: str, out_xml_path: str) -> None: img_path = txt_path.replace('.txt', '.jpg') img = cv2.imread(img_path) img_h, img_w = img.shape[:2] root = ET.Element('annotation') filename = ET.SubElement(root, 'filename') filename.text = str(Path(img_path).name) size = ET.SubElement(root, 'size') ET.SubElement(size, 'width').text = str(img_w) ET.SubElement(size, 'height').text = str(img_h) ET.SubElement(size, 'depth').text = '3' for line in Path(txt_path).read_text().strip().splitlines(): if not line.strip(): continue parts = line.split() cls_id = int(parts[0]) x_center = float(parts[1]) y_center = float(parts[2]) w = float(parts[3]) h = float(parts[4]) # 反算像素坐标,并夹到图像边界内 xmin = int(max(0, (x_center - w / 2) * img_w)) ymin = int(max(0, (y_center - h / 2) * img_h)) xmax = int(min(img_w, (x_center + w / 2) * img_w)) ymax = int(min(img_h, (y_center + h / 2) * img_h)) if xmax <= xmin or ymax <= ymin: continue obj = ET.SubElement(root, 'object') ET.SubElement(obj, 'name').text = CLASSES[cls_id] bndbox = ET.SubElement(obj, 'bndbox') ET.SubElement(bndbox, 'xmin').text = str(xmin) ET.SubElement(bndbox, 'ymin').text = str(ymin) ET.SubElement(bndbox, 'xmax').text = str(xmax) ET.SubElement(bndbox, 'ymax').text = str(ymax) tree = ET.ElementTree(root) tree.write(out_xml_path, encoding='utf-8', xml_declaration=True)

这里最容易犯的错是反算后不取整直接写浮点数,voc 规范里 bndbox 是整数,labelImg 打开时可能解析异常。另外反算后会遇到 xmax 小于 xmin 的退化情况,必须跳过,否则生成的 xml 打开就是错的。

4.3 转换过程里的两类坑:空标签与文件名错位

转换脚本写对了,批量跑的时候还有两个隐蔽问题。

第一个是空标签。有的图片确实没有目标,对应 txt 或 xml 文件是零字节的。转换时如果直接read_text()会得到空字符串,循环自然跳过,没问题。但如果你在转换前用「有文件就算有标签」的逻辑去统计,空文件会被算成有效标签,导致后面训练日志里这张图显示 no labels found。我一般会在转换脚本里额外输出空文件清单,确认哪些图真的没目标,而不是文件损坏。

第二个是文件名错位。批量转换时如果用xml_path.replace('.xml', '.jpg')去关联图片,前提是文件名一一对应。但有些数据集里图片扩展名混用 jpg 和 jpeg,或者 xml 文件名与图片文件名本身就不一致。稳妥的做法是先构造{stem: img_path}的映射,再遍历 xml 去查,查不到就报错而不是静默跳过。本数据集文件名是规范的img_0689_348.jpg这类编号,一般不会出问题,但写成防御式总没坏处。

5. 避坑实录:车辆数据集从解压到出模型的五个翻车现场

这部分是我在类似数据集上反复踩过的坑,每一条都是真金白银换来的。按「现象 → 原因 → 解决」写,你对照自己的报错信息就能定位。

5.1 解压后路径带中文导致训练失败

现象:Windows 上把 zip 解压到桌面或D:\数据集\车辆969这类中文路径,改好 data.yaml 启动训练,报FileNotFoundError或者某些图片加载失败,而且失败是零星的,不是全部图片都读不出来。

原因:OpenCV 的imread在 Windows 上对非 ASCII 路径支持不稳定,中文字符在处理时可能乱码导致文件找不到。yolov8 的 dataloader 对不同图片可能走不同读取分支,所以表现是随机失败,特别迷惑人。

解决:解压到纯英文路径,比如D:\datasets\vehicle_969,data.yaml 里所有路径用绝对路径且全英文。zip 解压后我习惯先看一眼目录树,确认没有乱码目录名,再跑训练。

5.2 类别索引越界:class index 3 is out of range

现象:训练刚启动,数据集加载阶段报错,提示标签里的类别索引超出了nc定义的范围,比如报出索引 3,但 data.yaml 里nc: 3、names只有 0、1、2 三个类别。

原因:txt 标签文件第一列写的是 3,说明标注时出现过第 4 类,或者某个 voc 转 txt 的脚本用了不同的类别顺序,把 bus 映射成了 3 而不是 2。

解决:扫描所有 txt 标签,找出第一列的最大值,跟nc-1对比:

from pathlib import Path label_dir = Path('D:/vehicle_969') max_cls = -1 for txt in label_dir.rglob('*.txt'): for line in txt.read_text(encoding='utf-8').strip().splitlines(): if not line.strip(): continue cls = int(line.split()[0]) if cls > max_cls: max_cls = cls print('max class index found:', max_cls)

如果最大值等于nc或更大,用脚本把那行改掉,或者回头检查 voc 标签里的类别名是否超出了['car', 'bicycle', 'bus']这个集合。

5.3 可视化看不到标注框:文件名错位与空标签

现象:训练进行中,日志里显示no labels found或者标签数量为 0;用可视化工具打开图片也看不到任何检测框,但标签文件确实存在。

原因:文件名对不上。比如图片是img_0689_348.jpg,标签却是img_0689_349.txt,yolo 的 dataloader 按同名字典匹配图片和标签,匹配不上就默认这张图没有标签。另一种原因是标签文件是零字节空文件,内容为空自然没框。

解决:用 3.1 节那段集合差脚本重新检查一遍missing和extra的数量。空文件就看文件大小:

find D:/vehicle_969 -name "*.txt" -size 0c -print

这条命令列出所有零字节的标签文件,确认它们是「真的没有目标」还是「转换时写空了」。

5.4 验证集指标虚高:类别分布不均造成的假象

现象:训练完看 results.csv,val 的 mAP50 高达 0.92,很漂亮。但随手拿几张测试图推理,自行车和公交车的检测效果明显拉胯,误检、漏检不少。

原因:数据集划分时没有按类别分层。如果验证集里 80% 的框都是汽车,模型只要把汽车学好,mAP 就能被拉得很高,而自行车、公交车的真实水平被稀释了。

解决:拿到任何数据集先统计每个划分下的类别实例数。对 yolo 标签来说,直接数每个 txt 的行首数字:

from collections import Counter from pathlib import Path def count_classes(label_dir: str) -> Counter: counter = Counter() for txt in Path(label_dir).glob('*.txt'): for line in txt.read_text().strip().splitlines(): if line.strip(): counter[int(line.split()[0])] += 1 return counter for split in ['train', 'val', 'test']: c = count_classes(f'D:/vehicle_969/{split}/labels') print(split, dict(sorted(c.items())))

如果验证集里某类只有几个实例,训练时要么别用随机划分,按类别比例分层抽样;要么把该类样本补到验证集里。本数据集已经划分好,正常来说分布是均匀的,但拿到手自己确认一遍总归放心。

5.5 训练不收敛:标签坐标越界与 imgsz 过高的叠加效应

现象:loss 前几十个 epoch 不降反升,mAP 全程为 0,训练结束后 best.pt 和 last.pt 几乎没有区别。

原因:常见是两个因素叠加。一是标签坐标越界,比如width或height为 0、中心点超出 0~1,模型在 loss 计算时拿到无效的目标框;二是imgsz设得比图片实际分辨率还大,导致图片被暴力放大,目标被插值糊掉。

解决:先扫标签越界,再调imgsz。越界扫描脚本:

from pathlib import Path for txt in Path('D:/vehicle_969').rglob('*.txt'): for i, line in enumerate(txt.read_text().strip().splitlines(), 1): if not line.strip(): continue parts = line.split() if len(parts) != 5: print(f'{txt}:{i} bad field count: {line}') continue _, xc, yc, w, h = map(float, parts) if not (0 <= xc <= 1 and 0 <= yc <= 1 and 0 < w <= 1 and 0 < h <= 1): print(f'{txt}:{i} out of range: {line}')

运行后如果有输出,把对应行修正或删除;没有输出说明标签本身干净,那就把imgsz调小一档(比如从 640 降到 512),或者把学习率从默认值往下调。yolov8 默认 lr 是 0.01,小数据集上如果扩增太猛,可以加--lr0 0.005试试。

6. 让 969 张图发挥更大价值:数据增强与混淆矩阵验证

模型跑通只是第一步。969 张图不算多,想让最终效果更扎实,重点放在两个地方:训练时的数据增强策略,和训练后的错误分析。

6.1 按场景开关 mosaic 增强

yolov8 默认开启 mosaic 增强,把四张图拼成一张训练,对小目标检测和类别不平衡很有帮助。但对本数据集要留意:车辆是典型的大目标,mosaic 拼图后目标被缩小的概率不低,如果训练时发现 mAP50 上去了但 mAP50-95 上不去,很可能是 mosaic 把大目标变成了小目标,模型对小尺寸目标的泛化没跟上。

我一般在大目标数据集上把 mosaic 概率降到 0.5 而不是关掉,留一半 epoch 用完整图片训练。迁移到别的框架时,对应参数通常是mosaic或augment配置。这个值该怎么调,直接看训练曲线:loss 前 20 轮降得快是好事,但如果 mAP 到 60 轮还在原地抖动,就把增强降一档试试。

6.2 用混淆矩阵定位类别混淆焦点

训练完成后,runs/detect/train目录下有一个confusion_matrix.png,这是我最先看的图。它展示真实类别与预测类别的对应关系,对角线越亮越好。对车辆数据集来说,常见混淆点是公交车和汽车——两者外形接近,如果该位置有亮斑,说明特征没学够,优先补充的是这两类的样本而不是盲目加 epochs。

另一个实用习惯是把测试集预测结果导出来,挑几类典型错误做人工复盘。比如把save_txt=True的结果和 voc 标签的 xml 放到同一个可视化脚本里比对,找出漏检的图片,看是目标太小、遮挡严重、还是类别本身模糊。这类错误分析比调参更能指引下一步方向,比如针对性地对误检样本做复制粘贴增强。

从那以后我每次拿到新数据集,都会先跑一遍标签体检脚本,再扫一遍越界和空文件,最后看一眼混淆矩阵才决定要不要调参。这套流程在 969 张的小数据集上能省下大半天的返工时间,希望帮到你。

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

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

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

立即咨询