☰
YOLO车辆分类检测实战:从VOC/COCO/YOLO标签转换到训练避坑指南
2026/10/10 20:25:20 网站建设 项目流程

简介:这份资源面向计算机视觉入门与进阶开发者,提供一套真实场景下的YOLO车辆分类检测数据集,可用于目标检测模型的训练、验证与调优。数据经labelimg精细标注,标注框质量较高,并同时提供voc(xml)、coco(json)与yolo(txt)三种格式标签,分别存放于不同文件夹,可直接对接YOLO系列检测框架,省去格式转换的繁琐步骤。压缩包共约2000个文件,以1250个xml标注、741个txt标签为主,另含少量html教程、py划分脚本与yaml配置文件,整体约51.91MB,体积轻便易于下载与本地部署。资源附带数据集划分脚本,可按需自行切分训练集、验证集与测试集,并配有环境搭建与训练教程,帮助读者快速跑通从数据准备到模型训练的完整流程。目前已有621人学习下载,适合需要快速验证检测思路、补充车辆类别数据或搭建课程实验的读者参考使用。

1. 拿到一份 YOLO 车辆分类检测数据集,先别急着开训

你从群里或者某个资源站下到一个压缩包,名字长得离谱:1000 张图片、voc/coco/yolo 三套标签、划分脚本、训练教程全塞在一起。第一反应通常是解压、找个 train.py、把 data 路径一改、开跑。然后大概率翻车——要么标签路径对不上,要么类别名和图片对不上,要么训完发现 mAP 低得离谱,回头一看标注框全是错的。

这份数据集真正值钱的地方不是那 1000 张图,而是它把车辆检测这个任务从「找数据」这一步直接跳过了。车辆分类检测在工业界落地场景极多:停车场车位占用判断、路口流量统计、自动驾驶感知模块的预研、甚至高速卡口的车型粗分类。1000 张图不算多,但足够你把 YOLO 的完整链路跑通一遍——从标签格式转换、数据划分、训练调参到推理验证。适合两类人:刚入门 YOLO 想找个真实任务练手的新手,以及手头有业务需求、想快速验证车辆检测可行性的工程师。下面我按自己实际跑这类数据集的顺序,把每一步拆开讲。

2. 三种标签格式到底怎么选:voc、coco、yolo 的差异与转换逻辑

2.1 三种格式的本质区别不在文件后缀,在坐标系

很多人以为 voc 就是 xml、coco 就是 json、yolo 就是 txt,这只说对了外壳。真正的差异是坐标表示方式,这直接决定了你转换时会不会把框画歪。

Pascal VOC 用的是绝对像素坐标,一个框记成(xmin, ymin, xmax, ymax),原点在图片左上角。它的 xml 里还带size节点记录宽高,所以框的位置是「钉死」在像素上的。COCO 用的是绝对像素的(x, y, width, height),注意这里是左上角坐标加宽高,不是中心点。而 YOLO 用的是归一化后的中心点坐标(x_center, y_center, width, height),四个值全部除以图片宽高,落在 0 到 1 之间。

这个差异带来的直接后果:VOC 和 COCO 互转基本是纯数学,YOLO 转换必须知道每张图的实际尺寸。如果你手上只有 yolo 标签却丢了原图尺寸,那这个标签基本废了——这也是为什么数据集里通常会保留 voc 或 coco 作为「母格式」。

格式坐标类型框表示类别存储典型文件
VOC绝对像素xmin,ymin,xmax,ymax标签内写类别名.xml
COCO绝对像素x,y,w,h单独 categories 字段.json
YOLO归一化cx,cy,w,h类别索引,单独 classes 文件.txt

2.2 用脚本把 VOC 转成 YOLO:一份能直接跑的转换代码

数据集里如果给的是 voc 标签,而你要用 ultralytics 系的 YOLO 训练,第一步就是转格式。下面这段是我常用的转换脚本,核心是把绝对坐标归一化,同时生成classes.txt。

import os import xml.etree.ElementTree as ET from PIL import Image # 类别列表,必须和你的标注一致,顺序决定索引 CLASSES = ["car", "bus", "truck", "van"] class_to_id = {c: i for i, c in enumerate(CLASSES)} def voc_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) 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() # 用同名图片读取真实宽高,别信 xml 里的 size,有时是错的 img_name = xml_file.replace(".xml", ".jpg") img_path = os.path.join(img_dir, img_name) w, h = Image.open(img_path).size lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text.strip() if cls_name not in class_to_id: continue # 遇到未定义类别直接跳过,避免索引错乱 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) # 归一化中心点与宽高 cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{class_to_id[cls_name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(out_dir, xml_file.replace(".xml", ".txt")), "w") as f: f.write("\n".join(lines)) # 写出类别文件,训练时 data.yaml 的 names 要和它一致 with open(os.path.join(out_dir, "classes.txt"), "w") as f: f.write("\n".join(CLASSES)) voc_to_yolo("annotations_xml", "images", "labels_yolo")

逻辑说明:脚本遍历 xml,对每个 object 取类别名映射成索引,再把绝对坐标转成归一化中心点格式。参数上CLASSES必须和数据集实际类别完全一致,多一个少一个都会让索引整体错位。cx/cy/bw/bh保留 6 位小数是 YOLO 官方推荐的精度,太少会累积误差。注意我用 PIL 重新读图片尺寸而不是用 xml 里的size,因为不少数据集的 size 节点是标注工具随手写的,和真实图片对不上,这是血泪经验。

2.3 COCO 转 YOLO 时最容易忽略的 id 映射

如果数据集给的是 coco 的 json,转换逻辑类似,但有个坑:COCO 的category_id往往不是从 0 连续开始的,可能是 1、3、7 这种。而 YOLO 要求类别索引从 0 连续。所以你必须先建一张category_id -> 连续索引的映射表,不能直接把 category_id 当索引写进 txt。

import json from PIL import Image with open("annotations.json") as f: coco = json.load(f) # 建立连续索引映射,sorted 保证每次运行顺序一致 cat_ids = sorted([c["id"] for c in coco["categories"]]) id_map = {cid: i for i, cid in enumerate(cat_ids)} img_info = {img["id"]: img for img in coco["images"]} for ann in coco["annotations"]: info = img_info[ann["image_id"]] w, h = info["width"], info["height"] x, y, bw, bh = ann["bbox"] # COCO 是左上角 x,y + 宽高 cx = (x + bw / 2) / w cy = (y + bh / 2) / h nw = bw / w nh = bh / h line = f"{id_map[ann['category_id']]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}" # 按 image_id 追加写入对应 txt

这里id_map是关键,sorted保证映射稳定,否则两次运行类别顺序可能不一样,训练结果无法复现。COCO 的 bbox 是左上角加宽高,转中心点时要先加半个宽高,这一步写错框会整体偏移半个身位。

3. 划分脚本怎么用:训练集、验证集、测试集的比例与随机种子

3.1 为什么不能随手 8:1:1 切

1000 张图,如果按 8:1:1 切,测试集只有 100 张。车辆检测里如果某些类别样本本来就少(比如 truck 只有 80 张),一切就可能出现某个类别在验证集里一张都没有,训完评估时那一类的 mAP 直接是 0,你还以为是模型不行。常见做法是先按类别分层抽样,保证每个类别在三个集合里的比例接近。数据集自带的划分脚本如果只是random.shuffle后切片,那它没做分层,你得自己补。

另一个问题是随机种子。不固定种子,每次运行划分结果都不同,你调参时根本分不清是参数起作用还是数据换了。我一般把种子写死成 42,并且把划分结果落盘成三个 txt 文件,训练时直接读文件而不是每次重新切。

3.2 一份带分层和固定种子的划分脚本

import os import random from collections import defaultdict random.seed(42) # 固定种子,保证可复现 def split_dataset(label_dir, out_dir, train_r=0.8, val_r=0.1): # 按类别把图片分组,实现分层抽样 cls_to_imgs = defaultdict(list) for txt in os.listdir(label_dir): if not txt.endswith(".txt") or txt == "classes.txt": continue with open(os.path.join(label_dir, txt)) as f: for line in f: cid = line.split()[0] cls_to_imgs[cid].append(txt) break # 一张图只按第一个类别归类,简化处理 train, val, test = [], [], [] for cid, imgs in cls_to_imgs.items(): imgs = list(set(imgs)) random.shuffle(imgs) n = len(imgs) n_train = int(n * train_r) n_val = int(n * val_r) train += imgs[:n_train] val += imgs[n_train:n_train + n_val] test += imgs[n_train + n_val:] os.makedirs(out_dir, exist_ok=True) for name, data in [("train", train), ("val", val), ("test", test)]: with open(os.path.join(out_dir, f"{name}.txt"), "w") as f: f.write("\n".join(data)) print(f"train:{len(train)} val:{len(val)} test:{len(test)}") split_dataset("labels_yolo", "splits")

逻辑说明:先按类别把图片归组,再在每个类别内部打乱切分,这样每个类别在三个集合里都有分布。参数train_r、val_r控制比例,测试集是剩下的部分。random.seed(42)是后悔药,调参阶段千万别省。注意一张图如果有多个类别,这里只按第一个类别归类,严格分层应该按多标签处理,但车辆检测里一图多类的情况少,这样简化够用。

3.3 划分完必须做的一致性检查

切完不是就完事了。我习惯跑一个检查脚本,确认三件事:每个 txt 里的图片在 images 目录都存在、每张图的标签文件存在、标签里的类别索引没超过classes.txt的行数。这三条任何一条不满足,训练时要么报错要么静默丢样本。检查逻辑很简单,遍历 splits 里的文件名,os.path.exists判断图片和标签,再读标签第一列和类别总数比大小。这一步花两分钟,能省掉后面半小时的排查。

4. 训练教程落地:从 data.yaml 到第一次跑通

4.1 data.yaml 的四个字段一个都不能错

ultralytics 系的 YOLO 训练入口是model.train(data="data.yaml"),这个 yaml 就四个关键字段:path、train、val、names。path是数据集根目录,train和val是相对 path 的图片列表文件路径,names是类别名列表,顺序必须和标签索引一致。

path: /home/user/vehicle_dataset train: splits/train.txt val: splits/val.txt names: 0: car 1: bus 2: truck 3: van

最常见的翻车是names顺序和转换脚本里的CLASSES不一致。比如转换时 car 是 0,yaml 里 car 写成了 1,那模型学到的「car」其实是 bus,训完推理全是错的。另一个坑是train路径写成了图片目录而不是列表文件,YOLO 会去目录里找图片但找不到标签,直接报 no labels found。

4.2 第一次训练的命令与关键参数

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ seed=42 \ project=runs/vehicle \ name=exp1

参数说明:model=yolov8n.pt用 nano 版先跑通,1000 张图用大模型容易过拟合且慢。imgsz=640是车辆检测的常用输入尺寸,再大显存吃不消,再小远处车辆特征会糊。batch=16看显存调,8G 显存跑 640 大概能到 16。lr0=0.01是初始学习率,YOLO 默认值,数据量小可以降到 0.005 更稳。patience=20是早停,20 轮验证集没提升就停,省时间。seed=42和划分脚本保持一致,保证数据顺序可复现。

第一次跑建议先epochs=10验证链路通不通,看 loss 有没有正常下降、验证集有没有输出。通了再拉到 100 轮。如果 10 轮内 loss 不降反升,八成是标签格式或类别映射错了,别急着调参。

4.3 训练过程中该盯哪几个指标

终端会打印 box_loss、cls_loss、dfl_loss 和 mAP50、mAP50-95。box_loss 管框的位置准不准,cls_loss 管分类对不对,dfl_loss 是分布焦点损失,影响框的精细度。三个 loss 都应该整体下降,中间有波动正常。mAP50 是 IoU 阈值 0.5 时的平均精度,车辆检测里这个值到 0.8 以上算不错,0.6 以下要查数据。mAP50-95 更严格,通常比 mAP50 低 0.15 到 0.25。

如果 cls_loss 一直很高但 box_loss 正常,说明框找对了但类别分不清,多半是类别样本不均衡或者类别名映射有问题。如果 box_loss 高,检查标注框有没有越界或者宽高为负的情况——转换脚本里如果 xmax 小于 xmin,归一化后宽高会是负数,YOLO 会静默忽略这个框。

5. 避坑与排查:车辆数据集训练中最容易翻车的 5 个点

现象一:训练报错「No labels found in ...」。原因通常是 data.yaml 的 train 指向了图片目录而不是划分好的 txt 列表,或者标签目录结构和 YOLO 预期的不一致。YOLO 默认认为标签在图片同级的 labels 目录、文件名相同只是后缀不同。解决:确认 train.txt 里每行是图片路径,且对应标签文件存在;如果标签和图片不在同一父目录,用path字段统一根目录。

现象二:训完 mAP 极低,但 loss 看着正常。最常见原因是类别索引错位。转换脚本的 CLASSES 顺序和 data.yaml 的 names 顺序不一致,模型学的类别和评估的类别对不上。解决:把 classes.txt 和 data.yaml 的 names 逐行比对,顺序必须完全一致。另一个可能是标注框本身质量差,1000 张图里如果有几十张框画歪了,mAP 会被拉低。

现象三:验证集某个类别 mAP 为 0。该类别在验证集里样本数为 0,划分时没做分层。解决:用第 3 章的划分脚本按类别分层,或者手动把该类别的一部分图挪进验证集。如果该类别总样本就极少(少于 20 张),考虑合并到相近类别,比如 van 并进 car。

现象四:推理时框的位置整体偏移。转换时坐标系搞混了。VOC 和 COCO 是左上角坐标,YOLO 是中心点,如果转换时忘了减半宽高或者忘了归一化,框会偏。解决:拿一张图,用转换后的 txt 反算出像素坐标,和原图上的框对比,肉眼就能看出偏没偏。

现象五:训练速度异常慢或显存爆。imgsz设太大或者batch超过显存。1000 张图用 640 尺寸、16 batch 在 8G 显存上是安全的。如果爆显存,先把 batch 降到 8,再考虑降 imgsz 到 512。另外workers参数在 Windows 上设大了会卡死,设成 0 或 2 比较稳。

6. 用验证集反查标注质量:一个能省下重标成本的技巧

训完模型别只看 mAP 数字,把验证集的预测结果和真实标签叠在一起看,能反查出标注问题。具体做法:用训好的模型对验证集跑推理,把预测框和真实框画在同一张图上,预测用红框、真实用绿框。如果某个框红绿偏差很大,要么是模型没学好,要么是标注本身就歪了。1000 张图里人工检查标注要花很久,但只看「红绿不一致」的那几十张,效率高得多。

from ultralytics import YOLO import cv2 model = YOLO("runs/vehicle/exp1/weights/best.pt") results = model.predict("val_images", conf=0.25, save=False) for r in results: img = cv2.imread(r.path) # 画预测框,红色 for box in r.boxes: x1, y1, x2, y2 = map(int, box.xyxy[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) # 真实框从同名 txt 读取后画绿色,此处省略读取逻辑 cv2.imwrite(f"check/{r.path.split('/')[-1]}", img)

参数上conf=0.25是置信度阈值,调低能看到更多预测框,方便对比;调高只看高置信的。这个技巧的价值在于:如果某张图模型预测得很准但真实标签框偏了,说明是标注错误,改标注比调模型划算。我一般会把红绿偏差大的图挑出来,人工复核一遍,往往能发现 5% 到 10% 的标注问题,重标这部分比全量重标省太多。

最后说个习惯:每次换数据集或者改转换脚本,我都会先拿 20 张图跑一遍完整链路,确认标签、划分、训练、推理都通了,再上全量。1000 张图全量跑一次要几十分钟,20 张图几分钟就能暴露大部分格式问题。这个习惯帮我省过很多次通宵重跑的命。希望帮到你。

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

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

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

立即咨询