简介:面向自动驾驶感知、车辆属性识别与目标检测方向的开发者与算法学习者,这份资源提供了一套可直接用于训练与验证的新能源汽车类型识别数据集,支持VOC格式标注,适合作为车型分类、目标检测课程设计或竞赛项目的训练素材。压缩包内共2000个文件,全部为xml标注文件,整体约254.13MB,每份标注对应一张正常采集的车辆图像,覆盖特斯拉、北汽新能源、宝马、比亚迪秦、比亚迪宋、比亚迪唐、奇瑞、易达、福特、江淮汽车等多个品牌与车系,可用于多类别车型识别任务。目前已有750人学习下载,说明该数据集在车型识别方向具有一定参考价值。读者可借助这些标注快速构建训练集与验证集,省去人工标注成本,直接投入模型训练与调参,也可用于对比不同检测网络在真实道路场景下的表现,排查类别混淆与漏检问题。
1. 新能源汽车类型识别:5391 张实拍图能跑出什么结果
手里有一份 5391 张正常采集的车辆信息数据集,标注是 VOC 格式,覆盖特斯拉、北汽新能源、宝马、比亚迪秦/宋/唐、奇瑞、易达、福特、江淮等车型。这个标题背后要解决的事很具体:给定一张街拍或卡口抓拍图,判断它属于哪个新能源品牌/车系。它适合三类人——做停车场车型统计的、做充电桩车位管理的、以及想拿现成数据练手目标检测的算法工程师。VOC 格式意味着标注是 XML,每张图对应一个同名 xml,里面是 bndbox 坐标加类别名。5391 张不算大,但类别跨度从轿车到 SUV,从国产到合资,够跑通一个可用的多分类检测器。真正麻烦的不是模型,是类别不均衡和 VOC 转 YOLO 时的坐标越界,这两点后面会重点拆。
2. 先搞清楚 VOC 标注长什么样,再谈训练
2.1 VOC XML 的字段含义与类别命名坑
VOC 格式的标注文件是 XML,一张图一个文件,文件名和图片名一致,只是后缀不同。核心字段就几个:folder、filename、size(宽高和通道数)、object(可多个),每个 object 里有name、pose、truncated、difficult和bndbox。bndbox里是xmin/ymin/xmax/ymax,注意这是绝对像素坐标,不是归一化值,而且原点在左上角。
类别命名是第一个大坑。标题里列了「比亚迪秦、比亚迪宋、比亚迪唐」,但实际 XML 里的name可能是「比亚迪」「BYD」「byd_qin」「秦」混着来。我一般先跑一遍全量统计,把所有name值去重打印出来,人工归并成一张映射表。这一步不做,后面训练出来的类别数对不上,模型直接学废。
import os import xml.etree.ElementTree as ET from collections import Counter # 统计 VOC 数据集里所有类别名及其出现次数 def count_voc_classes(anno_dir): counter = Counter() for fname in os.listdir(anno_dir): if not fname.endswith('.xml'): continue tree = ET.parse(os.path.join(anno_dir, fname)) root = tree.getroot() for obj in root.findall('object'): name = obj.find('name').text.strip() counter[name] += 1 return counter if __name__ == '__main__': # 把这里换成你的 Annotations 目录 result = count_voc_classes('./Annotations') for cls, cnt in result.most_common(): print(f'{cls}: {cnt}')这段代码遍历所有 XML,用Counter累计每个类别名出现次数。跑完你会看到类似「比亚迪秦: 420」「特斯拉: 380」这样的分布。参数上唯一要改的是anno_dir路径。逻辑说明:findall('object')会拿到每张图里所有标注框,一个框算一次,所以统计的是框数量而非图片数量。如果某个类别框数低于 100,训练时基本会被淹没,需要做重采样或加权重。
2.2 类别不均衡的量化判断与处理策略
5391 张图里,特斯拉和比亚迪大概率占大头,易达、江淮这种可能只有几百甚至几十张。判断标准很简单:最大类框数 / 最小类框数 > 10就算严重不均衡。处理方式我一般按优先级来:先试类别加权损失,再试过采样,最后才考虑合成数据。
加权损失在 YOLO 系列里可以通过修改cls损失的权重实现,但更省事的做法是在数据加载时给稀有类别更高的采样概率。下面是一个按类别框数倒数加权的采样器思路,用 PyTorch 的WeightedRandomSampler实现。
import os import xml.etree.ElementTree as ET import torch from torch.utils.data import WeightedRandomSampler # 为每张图计算采样权重:图中稀有类别越多,权重越高 def build_sample_weights(img_dir, anno_dir, class_to_idx): weights = [] for fname in sorted(os.listdir(anno_dir)): if not fname.endswith('.xml'): continue tree = ET.parse(os.path.join(anno_dir, fname)) root = tree.getroot() img_weight = 0.0 for obj in root.findall('object'): name = obj.find('name').text.strip() if name in class_to_idx: # 权重取类别频率的倒数,频率越低权重越大 img_weight += 1.0 / class_freq[name] weights.append(img_weight if img_weight > 0 else 1e-6) return torch.tensor(weights, dtype=torch.float) # class_freq 来自上一步的统计结果,class_to_idx 是类别到索引的映射 # sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True)逻辑说明:class_freq是 2.1 里统计出的每个类别框数,class_to_idx是归并后的类别映射。每张图的权重等于它包含的所有类别频率倒数之和,稀有类别贡献更大权重。参数上num_samples一般设成数据集大小,replacement=True表示有放回采样,保证稀有类被反复抽到。注意权重不要直接当 loss 用,这里只影响采样概率。
3. 把 VOC 转成 YOLO 格式:转换脚本与四个边界坑
3.1 坐标归一化与越界裁剪
YOLO 格式要求每行class_id x_center y_center width height,全部归一化到 0~1。转换时最容易翻车的是xmax超过图片宽度、xmin小于 0,或者xmax <= xmin。这些在 VOC 里合法(标注员手抖),但直接归一化会得到负数或大于 1 的值,训练时 loss 直接 NaN。
import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(anno_dir, img_dir, out_dir, class_to_idx): os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(anno_dir): if not fname.endswith('.xml'): continue tree = ET.parse(os.path.join(anno_dir, fname)) root = tree.getroot() # 从 XML 的 size 字段拿图片宽高,比重新读图快 size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in class_to_idx: 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) # 边界裁剪:把坐标夹到 [0, 宽高] 范围内 xmin = max(0.0, min(xmin, img_w)) xmax = max(0.0, min(xmax, img_w)) ymin = max(0.0, min(ymin, img_h)) ymax = max(0.0, min(ymax, img_h)) # 跳过无效框:宽或高为 0 if xmax - xmin < 1 or ymax - ymin < 1: continue 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'{class_to_idx[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') # 写出同名 txt txt_name = fname.replace('.xml', '.txt') with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(lines))逻辑说明:先读 XML 里的size拿宽高,避免重复打开图片。裁剪用max(0, min(x, limit))把坐标夹住。xmax - xmin < 1的判断是过滤掉宽或高不足 1 像素的废框。参数上class_to_idx必须和 2.1 归并后的类别表一致,否则类别索引错位。输出目录out_dir里每个 txt 和原图同名,YOLO 训练时按文件名配对。
3.2 训练集/验证集划分的随机种子问题
划分数据集时,如果按文件名排序后直接切前 80%,很可能出现「训练集全是特斯拉,验证集全是比亚迪」的惨剧,因为采集时可能按车型分批存图。正确做法是先打乱再切,并且固定随机种子,保证每次复现一致。
import os import random def split_dataset(txt_dir, val_ratio=0.2, seed=42): random.seed(seed) all_files = [f for f in os.listdir(txt_dir) if f.endswith('.txt')] random.shuffle(all_files) val_count = int(len(all_files) * val_ratio) val_files = all_files[:val_count] train_files = all_files[val_count:] return train_files, val_files逻辑说明:random.seed(42)固定种子,保证每次运行划分结果相同。val_ratio=0.2是常见比例,5391 张里约 1078 张做验证。注意这里只返回文件名列表,实际训练时用这些列表去索引图片和标注。如果某个类别在验证集里一个框都没有,说明划分还是偏了,需要分层抽样,但那是进阶操作,先跑通再说。
4. 训练配置与参数怎么调:从 5391 张图跑出可用模型
4.1 输入分辨率与 batch size 的取舍
5391 张图,如果原图是 1920x1080,直接训练显存吃不消。常见做法是缩到 640x640 或 512x512。分辨率越低,小目标越容易丢,但车辆在图中通常占比较大,640 够用。batch size 取决于显存,8GB 卡跑 YOLOv5s 大概能到 16,跑 YOLOv8n 能到 32。我一般先用小模型快速验证流程,再换大模型提精度。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| imgsz | 640 | 车辆目标较大,640 足够 |
| batch | 16 | 8GB 显存下的安全值 |
| epochs | 100 | 5391 张图,100 轮通常收敛 |
| lr0 | 0.01 | 初始学习率,配合余弦退火 |
| optimizer | SGD | 小数据集上比 Adam 稳 |
4.2 数据增强的边界:别把车增强没了
YOLO 默认开启 mosaic、HSV 抖动、随机翻转。mosaic 把四张图拼一张,对小数据集很有效,但车辆识别里有个坑:水平翻转后,车标和车牌文字会反,如果模型依赖这些特征,翻转反而有害。我一般保留 mosaic 和 HSV,关掉水平翻转,或者把翻转概率降到 0.1。
# data.yaml 示例 path: ./dataset train: images/train val: images/val nc: 10 names: ['tesla', 'beijing_eu', 'bmw', 'byd_qin', 'byd_song', 'byd_tang', 'chery', 'yida', 'ford', 'jac']逻辑说明:nc是类别数,names顺序必须和class_to_idx一致。path是数据集根目录,train和val是相对路径。这个 yaml 直接喂给 YOLO 训练脚本。参数上nc改成你归并后的实际类别数,别照抄 10。
5. 避坑与排查:5391 张图训练时最容易翻车的 5 个点
5.1 现象:loss 从第一轮就是 NaN
原因:VOC 转 YOLO 时坐标没裁剪,出现负数或大于 1 的值,归一化后计算 loss 直接爆炸。解决:回到 3.1 的转换脚本,确认xmin/xmax都夹到了[0, img_w],并且跳过了宽高为 0 的框。转换完随机抽 20 个 txt 文件,肉眼检查每行第二个和第三个值是否在 0~1 之间。
5.2 现象:验证集 mAP 一直是 0
原因:验证集的 txt 文件和图片没对上,或者data.yaml里的val路径写错。YOLO 找不到标注就当成全负样本,mAP 自然为 0。解决:检查images/val和labels/val里的文件名是否一一对应,用ls images/val | wc -l和ls labels/val | wc -l对比数量。数量不一致就是转换时漏了。
5.3 现象:模型只认特斯拉,其他车全标成特斯拉
原因:类别不均衡太严重,特斯拉框数占了一半以上,模型学到「猜特斯拉期望损失最小」。解决:按 2.2 做加权采样,或者把特斯拉的标注框随机丢弃 30% 再训练。另一个办法是先把特斯拉和其他类做二分类,跑通后再细分。
5.4 现象:训练到 50 轮后 mAP 突然掉下去
原因:学习率没退火,或者过拟合。5391 张图不算多,100 轮后模型开始背训练集。解决:加早停,验证集 mAP 连续 10 轮不升就停。同时把lr0从 0.01 降到 0.001 再跑一次对比。如果训练集 loss 还在降但验证集 loss 升了,就是过拟合,加 dropout 或减模型参数量。
5.5 现象:推理时同一辆车一会儿识别成比亚迪秦一会儿识别成比亚迪宋
原因:秦和宋的外观差异小,模型在低分辨率下分不清。解决:把输入分辨率从 640 提到 768 或 896,让车标和格栅细节保留更多。另一个办法是加一个「比亚迪」粗分类头,先分品牌再分车系,两级分类比一级多分类更容易收敛。
6. 用混淆矩阵定位误判,把 5391 张图的价值榨干
训练完别只看 mAP,混淆矩阵才是定位问题的关键。YOLO 验证时会输出confusion_matrix.png,但那个图类别多了看不清。我一般自己用 sklearn 跑一遍,把预测结果和真值对齐后算矩阵。
from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt # y_true 和 y_pred 是展平后的类别索引列表 cm = confusion_matrix(y_true, y_pred, labels=list(range(num_classes))) plt.figure(figsize=(12, 10)) sns.heatmap(cm, annot=True, fmt='d', xticklabels=class_names, yticklabels=class_names) plt.xlabel('Predicted') plt.ylabel('True') plt.savefig('confusion_matrix_detail.png', dpi=150)逻辑说明:y_true是验证集所有框的真实类别索引,y_pred是模型预测的类别索引,长度必须一致。labels参数保证矩阵行列顺序和class_names对应。跑完看对角线,如果「比亚迪秦」和「比亚迪宋」交叉位置数字很大,说明这两个类互相误判严重,回到 5.5 提分辨率或做两级分类。
一个具体技巧:把误判样本单独抽出来,按置信度从高到低排序,看前 50 张。如果高置信度误判集中在某个角度(比如全是车尾),说明训练集里这个角度的样本太少,针对性补采 200 张就能明显改善。5391 张图不算多,但用好了足够跑出一个能上线的车型识别模型。我自己的习惯是每次训练完先看混淆矩阵再看 mAP,矩阵里藏着的才是真正要修的东西。希望帮到你。
本文还有配套的精品资源,点击获取