
简介面向车辆外观识别与图像分类任务数据集中覆盖白、黑、灰、银、红、蓝、棕等15个常见车辆颜色类别并已划分训练集与测试集适合用于CNN分类模型训练、车辆颜色识别算法验证以及图像分类教学实践。压缩包共约2000个文件以1998张jpg图像为主体另附json类别标注文件与show.py可视化脚本包体约575.27MB可借助json查看完整的类别映射与标签设置。目前已有358人学习下载。资源目录按训练集、测试集分类存放结构清晰运行show脚本可随机展示样本图像便于快速检查各类别的样本分布与标注质量也能用于迁移学习或类别不均衡分析适合直接接入常见分类项目或作为车辆颜色识别任务的有力数据补充。1. 车辆颜色图像分类为什么 10,000 张已标注数据依然会翻车我去年帮一个停车场项目做车辆颜色图像分类训练集精度刷到 97%以为稳了结果换了个入口摄像头的数据准确率直接掉到 82%。排查下来问题不在模型而在数据标注和分布的细节上。这份约 10,000 张、已标注的车辆颜色图像分类数据集恰好适合用来做这类通用数据集的 baseline 验证它把颜色分类里最常见的标注方式、类别分布和训练流程都暴露出来了。适合两类人一是做车辆识别系统、需要先跑通分类管线的工程师二是想搞懂图像分类数据集怎么组织、怎么喂给模型、怎么评估的入门者。你可以把它当成一个不算复杂、但足够真实的练手样本先把颜色分类这件事跑明白再迁移到自己的业务场景。2. 先读懂数据集目录结构、颜色类别与划分方式的三项必备认知2.1 文件组织方式与标注的真实形态拿到数据集第一步不是急着训练而是先搞清楚文件怎么组织的。车辆颜色分类数据集的常见做法是“分类文件夹 清单 CSV”两条线并行图像按颜色分目录存放同时有一个 labels.csv 把每张图的名字、颜色名和标签编码汇总在一起。路径/文件作用train/训练集图像按颜色类别分子目录val/验证集图像目录结构与 train 一致test/测试集图像用于最终评估labels.csv每行一张图文件名、颜色标签、标签编码为什么我强调看 labels.csv 而不是只看文件夹名因为有些版本的标注藏在图像文件名里比如black_000123.jpg而有些则用一串数字 ID 命名真值只在 CSV 里。以 CSV 为主链路是稳妥做法常见格式是每行三列img_name, color, label_id其中label_id从 0 开始递增。拿到手先跑一个完整性检查把缺失、空值、类别数量一次看全。import pandas as pd df pd.read_csv(labels.csv) print(df.head()) print(df[color].value_counts()) print(df.isna().sum())这段代码的value_counts()直接输出每个颜色的样本数能一眼看出类别分布是否均匀isna().sum()检查 CSV 里有没有空行或缺失标签。我一般还会加一步assert df[img_name].nunique() len(df)确保没有重复图片因为重复样本会同时污染训练集和验证集让评估分数虚高。2.2 颜色类别体系与分布不均的真相这类车辆颜色数据集的颜色类别通常覆盖 12 类左右映射关系可以这样定义label_idcolor中文对应0black黑色1white白色2gray灰色3silver银色4red红色5blue蓝色6green绿色7yellow黄色8orange橙色9brown棕色10purple紫色11pink粉色注意 silver 和 gray 是分开的两类但实际拍摄里两者的区分度很低这几乎是所有车辆颜色数据集的通病也是后面训练时准确率上不去的重灾区。我见过的大多数此类数据集分布都不均匀黑色、白色、灰色三类往往占掉一半以上黄色、紫色、粉色这种小众颜色可能每类只有几百张。这种头重脚轻的分布直接决定了不能只用准确率做唯一指标必须要看各类别的召回率否则一个“全预测黑色”的模型也能拿到不错的总分。类别映射建议用英文小写加整数 ID别直接用中文或带空格的名字做目录名。中文路径在部分图像读取库里会出编码问题而带空格的目录名会在 shell 脚本拼接路径时制造一堆转义麻烦。这个映射表最好是项目里唯一的真值来源训练脚本和评估脚本都从它导入避免各处维护一份不一致的列表。2.3 这个数据集能做什么、不能做什么用途是否合适说明单标签图像分类合适这就是本数据集的定位baseline 效果对比合适同一数据集上对比不同模型迁移学习实验合适验证预训练权重在颜色任务上的表现目标检测训练不合适没有边界框标注不适合跑 YOLO 系模型多标签属性识别不合适只有颜色标签没有车型、品牌等属性特别提醒一句如果你是想拿它跑 YOLOv8 训练自己的数据集需要的是每个目标一个边界框的 txt 标注文件而不是这种图像级分类标签。拿分类数据集直接喂给检测网络等于把数据集的定位搞错了训练脚本会在标注解析阶段直接翻车。这类资源适合做的是分类任务的前期验证先把颜色识别管线跑通再决定要不要补标注做检测。3. 从零搭建分类管线PyTorch 数据加载与增强的完整写法3.1 自定义 Dataset把 CSV 标签变成可迭代的数据流from torch.utils.data import Dataset import pandas as pd from PIL import Image import os class VehicleColorDataset(Dataset): def __init__(self, csv_path, img_dir, transformNone): self.df pd.read_csv(csv_path) self.img_dir img_dir self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img_path os.path.join(self.img_dir, row[img_name]) image Image.open(img_path).convert(RGB) label int(row[label_id]) if self.transform: image self.transform(image) return image, label这段代码的核心是__getitem__按索引取一行 CSV拼出图像路径读图并转成 RGB再把标签转成整数返回。convert(RGB)这一步很多人会漏但车辆图像里确实存在灰度保存或带 alpha 通道的样本不统一转换会在后续送入网络时报错。transform从外部传入这样训练和推理可以复用同一个 Dataset 类只换预处理策略。int(row[label_id])确保返回的是 Python 整数而不是 numpy int64避免后续喂给交叉熵损失时出现类型告警。3.2 训练集与验证集切分分层是底线from sklearn.model_selection import train_test_split df pd.read_csv(labels.csv) train_df, val_df train_test_split( df, test_size0.1, stratifydf[label_id], random_state42, shuffleTrue ) train_df.to_csv(train_split.csv, indexFalse) val_df.to_csv(val_split.csv, indexFalse)这里stratifydf[label_id]是按类别比例分层抽样保证黑色车占 30%那训练集和验证集里也都约占 30%否则小众颜色可能只出现在训练集或只出现在验证集里评估结果毫无参考价值。test_size0.1表示切 10% 出来做验证。random_state42固定随机种子保证每次重跑切分结果一致这对后续调参时做公平对比很重要。切分后把两份 CSV 落盘训练脚本直接读这两个文件避免每次启动都重新切一次导致实验不可复现。我一般还会在随机切分的基础上加一步按车辆 ID 分组。因为同一个 ID 下的多张图来自同一辆车把它们同时放进训练集和验证集模型学到的是“记住这辆车”而非“理解颜色”。随机切分在学术 benchmark 上没问题但迁移到真实摄像头场景时验证集和训练集的相似度会虚高这也是很多颜色分类模型上线后掉点的核心原因之一。3.3 数据增强颜色任务的增强组合要克制from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.75, 1.0)), transforms.ColorJitter( brightness0.3, contrast0.3, saturation0.3, hue0.0 ), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ])这里最重要的一个参数是hue0.0。颜色分类任务里色调偏移会让红色变成橙色、蓝色变成紫色等于把标注里的真值语义直接破坏掉。亮度、对比度、饱和度三个维度可以适度扰动用来模拟白天、黄昏、逆光下的拍摄差异但 hue 必须关掉。RandomResizedCrop(scale(0.75, 1.0))做了轻度裁剪缩放模拟远近景变化同时不会把车身局部裁掉太多。验证集用Resize(256) CenterCrop(224)保持和训练输入尺寸一致但不引入随机性这是评估的公平底线。Normalize 的 mean 和 std 使用 ImageNet 统计值适配加载预训练权重的 ResNet 系列模型。4. 模型选型与训练参数用 ResNet-18 收敛的四个关键设置4.1 颜色是全局低频特征不需要太深的网络车身颜色的判别依赖大面积区域的颜色均值响应而不是局部纹理和边缘细节。色调统计本质上是图像的低频信息因此 ResNet-18 这一层级的网络在这个任务上完全够用ResNet-50 带来的收益很小反而更容易在小数据集上过拟合。对比项ResNet-18ResNet-50参数量约 11M约 25M训练耗时低高约 2~3 倍颜色分类收益足够边际收益很小过拟合风险低高真要说两者的差别主要出现在银/灰这种需要上下文信息的类别上ResNet-50 凭借更大的感受野能抢回一点准确率但训练时间会拉长不少。我的一般做法是用 ResNet-18 先跑通再看混淆矩阵决定要不要换大模型而不是直接上最大的网络。10,000 张图、12 类的分类规模ResNet-18 是这个资源场景下性价比最高的起点。4.2 训练循环与超参数AdamW Cosine 的稳定组合import torch import torch.nn as nn from torchvision import models from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes 12 model.fc nn.Linear(model.fc.in_features, num_classes) criterion nn.CrossEntropyLoss() optimizer AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() for images, labels in train_loader: images, labels images.cuda(), labels.cuda() outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() val_accuracy evaluate(model, val_loader) print(fepoch {epoch} | loss {loss.item():.4f} | val_acc {val_accuracy:.4f})这段代码里有三个值得注意的设置。第一model.fc nn.Linear(model.fc.in_features, num_classes)把 ImageNet 预训练的分类头替换成 12 类输出头前面的卷积层权重全部保留作为迁移起点这样加载预训练权重后不需要从零学习底层特征。第二lr1e-3对迁移学习来说是一个中等偏稳的学习率配合weight_decay1e-4做 L2 正则能有效压制新初始化的全连接层的过拟合。第三CosineAnnealingLR(optimizer, T_max30)让学习率在 30 个 epoch 内从初始值余弦下降到接近 0训练后期可以做更精细的收敛。batch size 我建议设 64。这个数值在单张 2080Ti 或 3060 上都能跑得动配合输入尺寸 224单 step 的显存占用在 2GB 以内。如果显存紧张可以降到 32但学习率最好同步降到 5e-4否则梯度更新步长偏大前期 loss 会抖动。4.3 早停机制别让验证集告诉你已经过拟合class EarlyStopping: def __init__(self, patience5, min_delta0.001): self.patience patience self.min_delta min_delta self.counter 0 self.best_score None self.should_stop False def __call__(self, val_acc): if self.best_score is None: self.best_score val_acc return if val_acc self.best_score self.min_delta: self.counter 1 if self.counter self.patience: self.should_stop True else: self.best_score val_acc self.counter 0patience5的意思很简单连续 5 个 epoch 验证精度没有超过历史最好值加min_delta就停。min_delta0.001是让“进步”至少超过千分之一才算数避免验证精度在 0.001 内抖动时反复判定为新高。早停配合 cosine 调度有个细节要留意如果触发了早停调度器本身还没走完此时保存模型的时机很重要。我一般会在验证精度创新高时单独复制一份best_model_state停训后用这个备份恢复而不是直接用最后一个 epoch 的权重因为最后一个 epoch 很可能已经过拟合了。5. 常见问题与避坑准确率上不去的五条真实原因5.1 深蓝被错分到黑色现象混淆矩阵里深蓝色样本大量落入黑色列单类召回率只有 60% 出头整体准确率被拖低 2~3 个点。原因低光照或逆光场景下深蓝车身的 RGB 均值与黑色车非常接近卷积网络学的是颜色分布统计当两个分布高度重叠时就分不开。如果数据增强里开了 hue 扰动蓝色还会被进一步偏移到暗色区域加剧混叠。解决把增强里的 hue 参数设为 0增加 brightness 扰动模拟不同光照同时在训练时对深色系样本做针对性采样。还有一个思路是训练两阶段模型第一阶段先区分深色系和浅色系第二阶段在深色系内部细分蓝与黑。实践证明第二阶段只需要把深蓝和黑的样本拿出来微调不必重新训练整个网络。5.2 验证集一换就掉点现象在自带验证集上准确率 95%换一个来源的测试数据掉到 85% 以下模型像突然“失灵”了。原因随机切分验证集时同一辆车不同角度的照片可能同时出现在训练和验证集里模型记住的是车辆个体而不是颜色规律。一旦数据来源变化记忆失效泛化能力现出原形。解决按拍摄时间、地点或车辆 ID 分组切分保证验证集与训练集没有同一辆车。常见做法是先按文件名前缀聚合车辆 ID再用GroupShuffleSplit对车辆 ID 做切分而不是对单张图做切分。如果数据集没有提供车辆 ID 字段可以按拍摄时间戳分组兜底时间上不重叠的两段数据本身就近似两个数据源。5.3 银、灰、白三个近亲色互相串现象这三类在验证集上两两混淆严重模型输出置信度还都很高经常是“银车被判成灰灰车被判成白”。原因银色和灰色在普通 JPEG 压缩后纹理细节被抹掉特征几乎一致白色车在阴影里拍出来和灰色更接近。这类边界情况靠网络结构很难根治。解决在预处理阶段加白平衡校正常用做法是灰度世界假设——把 RGB 三个通道的均值归一化到同一水平。推理时对 softmax 输出加一个仲裁逻辑prob torch.softmax(logits, dim1) silver_prob prob[:, 3].item() gray_prob prob[:, 2].item() if abs(silver_prob - gray_prob) 0.05: # 用灰度均值做二选一仲裁 gray_value image.convert(L).resize((1, 1)).getpixel((0, 0)) pred 3 if gray_value 128 else 2这段代码的核心是当银色和灰色的 softmax 概率差小于 0.05 时模型自己也没把握此时退回灰度统计做兜底。灰度值偏高判银、偏低判灰能把这一对近亲色的串类率降下来不少。5.4 ColorJitter 的 hue 把颜色语义改坏现象用了默认参数的数据增强后训练损失正常下降但验证时对红色和橙色的判断完全错乱红色车大量被判成橙色。原因transforms.ColorJitter(hue0.1)会让同一张红色车图在训练中时而呈现橙色、时而偏紫模型学不到“红色是红色”这个不变的语义。损失还在降但模型学的是数据增强的噪声不是颜色规律。解决hue0.0饱和度扰动控制在 0.3 以内。这也是图像分类里少见的“增强过度反而毁任务”的案例颜色分类的增强必须模拟光照变化而不是改变色相。这个坑调参时最容易忽略因为训练指标看起来一切正常只有做错题分析时才发现是增强把标注语义搞乱了。5.5 类别不平衡把小众颜色吞掉现象整体准确率 94%但黄色、紫色、粉色召回率不到 50%而且这几类的 loss 始终降不下去。原因数据集中黑、白、灰占比过半交叉熵损失被头部类别主导尾部类别梯度贡献太小模型倾向于把所有不确定样本都归到高频类别。解决用WeightedRandomSampler按类别频率的反比抽样让模型每个 epoch 见到的小众颜色样本量更均衡。from torch.utils.data import WeightedRandomSampler labels train_df[label_id].values class_counts np.bincount(labels) weights 1.0 / class_counts[labels] sampler WeightedRandomSampler(weights, num_sampleslen(labels), replacementTrue) train_loader DataLoader(train_dataset, batch_size64, samplersampler)这里的class_counts[labels]给每张图分配一个权重样本量越大的类别权重越低。replacementTrue表示允许重复采样num_sampleslen(labels)保持每轮迭代步数不变但小众颜色被抽到的概率显著提升。如果还想更激进可以换成 focal loss把gamma设成 2.0让模型主动关注难分类样本。6. 进阶验证混淆矩阵与迁移学习是上线前的最后一道关口6.1 混淆矩阵可视化脚本from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(10, 10)) plt.imshow(cm, cmapBlues) plt.colorbar() plt.xticks(range(12), class_names, rotation45) plt.yticks(range(12), class_names) plt.xlabel(Predicted) plt.ylabel(True) plt.show()分类任务只盯一个准确率等于把黑匣子留到最后。混淆矩阵能直接告诉你串类发生在哪一对颜色上是光照问题、类别不平衡问题还是类别定义本身就有重叠。我拿到训练好的模型第一件事永远是看这张图而不是看总分数。6.2 迁移学习比从零训练更稳model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 12) for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True先冻结全部卷积层只训练新分类头用 1e-3 的学习率跑 10 个 epoch再解冻最后一组残差块继续微调。这个两段式做法比直接完整微调更稳尤其适合 10,000 张这种中等规模数据集。冻结阶段让分类头先适应颜色特征空间解冻阶段再用小学习率修一下高层语义比从零训练收敛快最终精度也更高。上线前我还会做最后一件事对 softmax 输出设置拒绝阈值。如果最高置信度低于 0.8就输出 unknown让系统承认“不确定”而不是硬给一个可能错误的颜色。这个习惯帮我挡掉了很多线下测试发现不了的边界误判。从那以后我每次做图像分类项目都强制自己走一遍“看分布、分层切分、关 hue、出混淆矩阵、设拒绝阈值”这条固定流程踩过的坑基本都堵上了。希望帮到你。本文还有配套的精品资源点击获取