四类车辆数据集XML标注压缩包:从解压到YOLO训练全流程
发布时间:2026/9/13 3:13:37 作者:尧图编辑部 阅读量:1,286

简介面向目标检测与YOLO系列模型学习者、嵌入式竞赛参赛者这份数据集由作者自摄并整合VOC2012而成包含3000余张国内外真实道路图片覆盖公交车、小汽车、摩托车、自行车四类目标同时涵盖晴天、阴天、雨天、低能见度等复杂场景可为多样化的道路交通目标检测任务提供扎实数据基础。每张图片均配有VOC格式XML标注文件可与JPG原图一一对应直接接入YOLOv5、YOLOv8等检测框架参考使用YOLOv5s.pt训练300轮即可获得较好检测效果适用于课程设计、毕设或嵌入式比赛中的模型方案验证。压缩包共5858个文件含2929张JPG图片与2929个XML标注文件整体352.7MB结构简单、无需二次清洗下载后即可划分训练集与验证集。已有911人学习/下载适合算法复现、精度对比、迁移学习与轻量化模型调优等场景也可作为自建数据集扩展和标注格式转换的参考。1. 为什么需要这套四类车辆数据集及XML标注压缩包一套同时覆盖机动车、自行车、摩托车、巴士的车辆数据集配上xml标注压缩包是训练多类车载视觉模型的常见起点。这类数据集通常以Pascal VOC格式发布图像与同名XML文件放在一起下载后是一个压缩包。拿到压缩包后的大多数人并不是直接训练而是先做格式转换、数据清洗、类别统计再喂给YOLO或MMDetection。本文将按“解压→解析XML→转YOLO格式→拆分训练集”这条线把数据集从压缩包变成可训练的样本集途中还会给出几个让你少踩坑的参数。适合刚拿到数据集但不知道从哪下手的工程师也适合想检查别人给的数据集是否规范的算法同学。2. 解压压缩包后先看清目录和XML标注结构2.1 用tree和Python统计目录结构拿到“机动车自行车摩托车巴士数据集及xml标注压缩包”之后第一件事不是立刻读XML而是先确认压缩包内目录是否规整。常见的压缩包有两种组织方式一种是所有图片和XML文件平铺在同一个目录下另一种是分成images和annotations两个子目录。先执行一条命令看全貌unzip -l vehicle_dataset.zip | head -50如果压缩包太大unzip -l的输出会滚动很久所以我一般会加head截断。这里-l只列出内容而不解压能快速判断目录层级。如果输出里出现了__MACOSX或.DS_Store说明压缩包来自macOS解压后要记得清理。接着解压到指定目录避免所有文件散落到当前目录造成混乱mkdir -p dataset unzip -q vehicle_dataset.zip -d dataset-q静默模式不加的话在文件多时会刷屏。解压后看目录树find dataset -maxdepth 2 -type d | sortmaxdepth 2只看两层够用了。如果发现大量图片和XML混在一起可以用下面的Python脚本统计各类文件数量import os from pathlib import Path root Path(dataset) exts {.jpg: 0, .jpeg: 0, .png: 0, .xml: 0} for p in root.rglob(*): if p.suffix.lower() in exts: exts[p.suffix.lower()] 1 print(exts).rglob(*)递归遍历所有子目录统计常见图片扩展名和XML文件数量。如果图片数量和XML数量不一致先别急用1.1.1里的小脚本找出哪些图片没有XML。常见原因是有部分背景图或损坏文件混进来了。2.2 XML标注的必读字段和坐标系这套数据集的标注是XML格式也就是Pascal VOC标准。打开一个标注文件看看cat dataset/images/000001.xml典型内容如下annotation folderimages/folder filename000001.jpg/filename size width1920/width height1080/height depth3/depth /size object namecar/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin300/xmin ymin200/ymin xmax800/xmax ymax700/ymax /bndbox /object /annotation这里filename是图片名size是图片原始宽高object表示一个目标name是类别名bndbox是左上角和右下角坐标。坐标单位是像素原点是图片左上角x轴向右y轴向下。这个坐标系转换到YOLO格式时必须要除以width和height做归一化。值得注意的是truncated和difficult两个字段。difficult1表示该目标很难辨认很多转换脚本会直接忽略这类目标。本数据集如果包含difficult建议先统计有多少再决定是否保留。另外有些标注文件里可能没有size字段或者图像实际尺寸和XML里对不上这种情况会在转YOLO格式时引发坐标越界我们在第3章会专门处理。2.3 用ElementTree快速解析一个XMLPython标准库xml.etree.ElementTree足够解析这类文件不需要装额外的lxml。我通常先写一个小函数把单个XML解析成dict方便后续调试import xml.etree.ElementTree as ET from pathlib import Path def parse_voc_xml(xml_path: str) - dict: tree ET.parse(xml_path) root tree.getroot() data { filename: root.findtext(filename), width: int(root.find(size/width).text), height: int(root.find(size/height).text), objects: [], } for obj in root.findall(object): name obj.findtext(name) difficult int(obj.findtext(difficult, 0)) bndbox obj.find(bndbox) data[objects].append({ name: name, difficult: difficult, xmin: int(bndbox.findtext(xmin)), ymin: int(bndbox.findtext(ymin)), xmax: int(bndbox.findtext(xmax)), ymax: int(bndbox.findtext(ymax)), }) return data print(parse_voc_xml(dataset/annotations/000001.xml))findtext可以带默认值避免缺少字段时抛异常。这段代码能打印出结构化数据方便你确认文件名和坐标是否正确。跑通之后再批量统计整个数据集的类别分布from collections import Counter counter Counter() for xml_file in Path(dataset/annotations).glob(*.xml): data parse_voc_xml(str(xml_file)) for obj in data[objects]: counter[obj[name]] 1 print(counter)我一般会先打印类别分布因为很多所谓“机动车数据集”的类别名并不统一可能是car也可能是vehicle、motorvehicle甚至混用了中英文。看到计数后你需要决定是保留原类别名还是做一次重映射。3. 把XML标注转成YOLO格式的txt3.1 为什么需要转换YOLO系列v5/v8/v11默认的标注格式不是XML而是每个图片对应一个txt文件。txt的每一行是class_id x_center y_center width height其中x_center、y_center、width、height都是相对于图片宽高的归一化浮点数取值范围在0到1之间。这样可以避免不同分辨率图像带来的尺度问题也方便训练时统一插值。而Pascal VOC的bndbox是绝对像素的左上角右下角坐标需要通过公式转换x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height如果你的训练脚本是MMDetection或自己用PyTorch写的也可能直接读XML但YOLO生态目前最流行的还是txt。所以拿到数据集第一步永远是“转格式”。3.2 类别映射表与转换脚本转换前先建立类别映射表。本数据集包含四个类别常见的中文对应如下标注名含义类别IDcar机动车含小轿车、SUV等0bicycle自行车1motorcycle摩托车2bus巴士3这个顺序可以自己定但一旦定下来就不能改因为训练后的模型输出数字序号你需要对照这张表给用户解释结果。注意“机动车”在英文标注中可能写成car、truck、vehicle在写映射表时你需要把原始标注里所有等价名字都映射到同一个ID。下面是一个完整的转换脚本import xml.etree.ElementTree as ET from pathlib import Path # 类别映射原始标注名 - YOLO类别ID CLASS_MAP { car: 0, vehicle: 0, motorvehicle: 0, truck: 0, bicycle: 1, motorcycle: 2, bus: 3, } # 忽略这些名字的目标 IGNORE {person, traffic_light, background} def convert_xml_to_yolo(xml_path: Path, txt_path: Path): tree ET.parse(xml_path) root tree.getroot() width int(root.find(size/width).text) height int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.findtext(name) if name in IGNORE: continue if name not in CLASS_MAP: print(f警告: {xml_path.name} 包含未映射类别 {name}) continue cls_id CLASS_MAP[name] difficult int(obj.findtext(difficult, 0)) # 如果difficult1且不想纳入训练直接跳过 if difficult 1: continue bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) # 坐标裁剪防止越界 xmin max(0, min(xmin, width - 1)) ymin max(0, min(ymin, height - 1)) xmax max(0, min(xmax, width - 1)) ymax max(0, min(ymax, height - 1)) # 过滤面积太小或无效的框 if xmax - xmin 1 or ymax - ymin 1: continue x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height bw (xmax - xmin) / width bh (ymax - ymin) / height # 归一化后可能因pixel取整导致微超出再做一次裁剪 x_center max(0, min(x_center, 1.0)) y_center max(0, min(y_center, 1.0)) bw max(0, min(bw, 1.0)) bh max(0, min(bh, 1.0)) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) # 写txt即使没有目标也生成空文件YOLO允许 txt_path.write_text(\n.join(lines)) # 批量转换 xml_dir Path(dataset/annotations) txt_dir Path(dataset/labels) txt_dir.mkdir(parentsTrue, exist_okTrue) for xml_file in xml_dir.glob(*.xml): txt_file txt_dir / (xml_file.stem .txt) convert_xml_to_yolo(xml_file, txt_file)运行完后检查一个txt文件cat dataset/labels/000001.txt输出可能是0 0.350000 0.450000 0.250000 0.300000 3 0.700000 0.650000 0.200000 0.220000每行的五个数字分别对应类别ID、x中心、y中心、宽、高。注意这里我没有验证图片是否和XML一一对应你需要自己跑一段代码检查。3.3 参数说明与异常处理上面脚本有几个关键参数值得调整。CLASS_MAP是核心。你的数据集中“机动车”类别可能被拆成了car和truck如果你的任务不区分这两类就把它们映射到同一个ID。如果任务需要区分那就不要合并并相应调整ID数量。IGNORE集合用于过滤明显不相关的目标。有些数据集在行人检测场景中会附带person标注但这套四类车辆数据集按理说不会可保不齐有杂项。过滤时注意只要原始标注里有‘person’目标计数就会变化建议运行完脚本后打印一次类别总数确认。坐标裁剪和面积过滤是必要的。我在真实数据里遇到过xmax小于xmin的样本多半是标注软件的手误。这段代码里的max(0, min(...))能防止负坐标导致训练loss变成nan。如果不想丢弃面积小的目标可以调低阈值。还有一点XML里的size和真实图片尺寸不一致的问题。如果图片被压缩过但XML没更新归一化就会出错。稳妥办法是转换时用cv2.imread获取实际宽高替代XML里的值代价是慢一点。折中方案是只检测size字段缺失时再用图片读取if width is None or height is None: import cv2 img cv2.imread(str(img_path)) height, width img.shape[:2]4. 数据集划分与YOLOv8训练前的配置4.1 按比例划分train/val/test转换完txt后下一步是把数据集切成训练集、验证集和测试集。常见比例是8:1:1或8:2:0。要注意的是划分时不能让同一张图片的多个副本同时出现在训练集和验证集中否则会造成数据泄漏。如果你不知道原压缩包内是否已经去重可以先用md5检查重复图片md5sum dataset/images/*.jpg | sort | uniq -w32 -d-w32只比较前32个字符也就是md5值本身。有重复时你需要在划分脚本里对重复的图片保留一份。下面是一个简单划分脚本import random from pathlib import Path random.seed(42) image_dir Path(dataset/images) label_dir Path(dataset/labels) images sorted(image_dir.glob(*.jpg)) sorted(image_dir.glob(*.png)) random.shuffle(images) train_ratio 0.8 val_ratio 0.1 n len(images) n_train int(n * train_ratio) n_val int(n * val_ratio) splits { train: images[:n_train], val: images[n_train:n_trainn_val], test: images[n_trainn_val:], } for split_name, imgs in splits.items(): split_img_dir Path(fdataset/{split_name}/images) split_label_dir Path(fdataset/{split_name}/labels) split_img_dir.mkdir(parentsTrue, exist_okTrue) split_label_dir.mkdir(parentsTrue, exist_okTrue) for img in imgs: # 复制图片 (split_img_dir / img.name).symlink_to(img.resolve()) # 复制对应标签 label label_dir / (img.stem .txt) if label.exists(): (split_label_dir / label.name).symlink_to(label.resolve())这里用symlink建立软链接而不是复制文件能在不浪费磁盘的情况下完成划分。如果你要打包数据集给别人再改成shutil.copy2。注意random.seed(42)让划分结果可复现否则每次跑结果不同后续实验没法对比。4.2 生成data.yamlYOLOv5和YOLOv8都要求提供YAML文件描述数据集路径、类别数和类别名。常见格式如下# dataset.yaml path: /absolute/path/to/dataset # 数据集根目录 train: train/images val: val/images test: test/images nc: 4 names: [car, bicycle, motorcycle, bus]path最好写绝对路径相对路径在某些情况下会从cwd解析训练脚本一旦换了工作目录就会找不到数据。如果你在服务器上做实验建议把path用环境变量替换比如path: ${DATASET_ROOT}然后运行前export DATASET_ROOT/data/vehicle。names的顺序必须和转换txt时的类别ID完全一致否则模型会把第0类当car实际却是bus训练半天看不出问题验证时才发现mAP莫名其妙很低。4.3 启动训练的命令与参数有了划分好的目录和YAML就可以用YOLOv8开始训练。如果你本地显存有限可以用YOLOv5的nano模型先跑通流程。以YOLOv8为例yolo detect train \ datadataset.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ cacheFalseyolov8n.pt是预训练权重cacheFalse表示不把整张图缓存到内存适合大数据集。imgsz640是训练分辨率如果你的图片本身就是1920x1080想要保留小目标信息可以改成imgsz1280但显存也会翻倍。batch需要根据显存调整常见的8GB显卡用batch16可能会OOM改成8或4。对YOLOv5命令稍有不同python train.py \ --data dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0--img参数生效后会自动对训练图片做letterbox填充不会因为图片比例不同而出错。训练过程中观察输出重点关注box_loss、cls_loss和mAP50。如果mAP50在50个epoch后还在0.1以下多半是标签有问题先别急着调超参数回到第3章的转换脚本检查类别映射或坐标归一化。5. 训练前验证标注质量与三个常见坑5.1 可视化检查坐标是否越界即使转换脚本做了坐标裁剪也不代表标注语义就正确。我习惯在训练前随机挑20张图把标注框画出来看看。写一个快速验证脚本import cv2 from pathlib import Path def draw_yolo_boxes(img_path, label_path): img cv2.imread(str(img_path)) h, w img.shape[:2] colors [(0,0,255), (0,255,0), (255,0,0), (0,255,255)] if label_path.exists(): for line in label_path.read_text().strip().splitlines(): cls_id, xc, yc, bw, bh map(float, line.split()) # 反算像素坐标 xmin int((xc - bw/2) * w) ymin int((yc - bh/2) * h) xmax int((xc bw/2) * w) ymax int((yc bh/2) * h) color colors[int(cls_id) % len(colors)] cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) cv2.putText(img, str(int(cls_id)), (xmin, ymin-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) return img img_path Path(dataset/val/images/000042.jpg) label_path Path(dataset/val/labels/000042.txt) vis draw_yolo_boxes(img_path, label_path) cv2.imwrite(check.jpg, vis)如果xmin或ymin小于0或者xmax大于w就说明坐标越界。调试落笔时可以专门用断言检测assert 0 xmin xmax w assert 0 ymin ymax h很多数据集的越界问题就出在图片被旋转或裁剪后XML没同步更新。可视化时重点关注小目标比如远处的摩托车如果框明显偏离车身那就要考虑这个数据集的标注质量是否可靠。5.2 三个常见坑及规避第一个坑是解压权限问题。网上下的压缩包有时权限位没设置好解压后文件是-rw-------训练脚本用其他用户身份读取时会报Permission denied。解决办法是解压后统一修复权限find dataset -type f -exec chmod 644 {} \; find dataset -type d -exec chmod 755 {} \;如果遇到压缩包本身拒绝访问先查看属性是否被加密某些在Windows下打包的zip会带只读属性。第二个坑是“压缩包内包含嵌套压缩包“。如果你解压后发现还有一个.rar或.zip在内部需要先确认里面是不是标注的备份。碰到过数据集把图像放在压缩包内XML在外面导致图片无法直接关联。这时要先把内层压缩包解压到正确位置再重新生成标签。不要手工改XML文件名一定会漏。第三个坑是类别不平衡。观察类别分布如果bus只有100个样本即使整体数据集有10万张图片模型也基本学不到巴士特征。你可以用torchvision中的WeightedRandomSampler给少数类加权或者用数据增强特别是crop、mosaic增加少数类图像。最简单的验证方法是先只训练bus一个类看看能不能收敛到mAP0.5再回来调全模型。这样能快速定位是数据量问题还是标注问题。除了这三个坑还有一个常见误区把test集当val集用。如果你在第4章划分时设置了test那么在训练脚本里val参数应指向划分好的val目录不要把test目录填进val。否则你最后评估的“验证集”其实是你一直没用过的测试集导致模型调参时无法发现过拟合。验证工作做好后再回来检查一下第3章生成的labels目录是否有空的txt文件表示该图片没有标注。如果数量超过图片总数的5%建议要么删除这些图片要么给数据集补充标注否则训练时YOLO会把这些空标签视为背景图增加样本不均衡的概率。本文还有配套的精品资源点击获取