尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

交通标志图像分类数据集构建全指南:划分、去重与训练实战

交通标志图像分类数据集构建全指南:划分、去重与训练实战 简介交通标志物图像分类数据集包含训练集、验证集和测试集适合深度学习初学者与智能交通方向开发者用于图像分类模型训练。数据覆盖红绿灯、限速、左右转等43个常见交通标志类别目录按类别分好可直接用ImageFolder打开也可作为yolov5分类数据集使用。压缩包共2000个文件含1999张jpg图片与1个中文字典json文件后者对应43类交通标志物便于标签映射与结果解读。资源包约58.4MB规模适中便于快速迭代与教学演示划分好的三个子目录让训练、验证、测试流程更清晰无需额外处理即可上手。目前已有333人学习适合用来练习图像分类完整流程或作为课程实验数据。1. 交通标志图像分类数据集别把它当成普通图片文件夹你从某个开源渠道下载了一套交通标志图片文件夹按 train/val/test 切好里面又按类别放好子目录。训练三五轮验证集准确率到了 98%你很高兴可一测测试集准确率只剩 80%。问题通常出在数据集组织上训练集、验证集、测试集之间存在相似图片泄漏或者类别分布顺序被固定导致验证集和训练集过于相似。交通标志物图像分类数据集的坑就在这里。它和通用图像分类数据集一样需要标签、划分和增强但标志类别长尾、拍摄角度和光照变化剧烈且同类标志在不同地区有不同变体任何轻率的目录划分都会直接污染你对模型能力的判断。这篇文章先讲清数据集的构成和划分原则再给出能直接跑的切分脚本和训练脚本最后聊几个验证集和去重上的常用技巧。2. 交通标志图像分类数据集的构成从类别体系到目录约定要做一个可靠的交通标志图像分类数据集先别急着拷图片。第一件要做的事是把类别体系定下来否则后续的划分和训练都会返工。2.1 交通标志类别体系决定数据集的边界交通标志在形态上可以粗分为警告标志三角形、黄底黑边、禁令标志圆形、红边、指示标志圆形或矩形、蓝底等。它们之间的形状差异较大但同一大类内的子类别差异很小比如限速 20 和限速 30。如果你要做的是一个通用图像分类数据集我一般会建议按业务需求决定类别粒度需要回答“这块牌子是什么”就按完整语义类别拆只需要回答“是否危险”就合并成二分类。这里的关键是图像分类算法的输出空间由数据集的类别清单决定。如果你后续把数据集交给 YOLO 做检测那可以使用相同的类别 ID但数据组织方式不同而在纯分类任务里一张图片只能有一个标签。如果同一张图片里有多个标志要么切图后标注成单标志要么使用多标签分类。交通标志物图像分类数据集的标签通常是单标签因此切图或裁剪是一个重要的前置环节。我习惯先写一个classes.txt0 speed_limit_20 1 speed_limit_30 2 no_entry 3 yield 4 stop ...每行一个类别 ID 和名称后续所有脚本都从它读取。这个文件的顺序决定了模型输出层的索引中途不要改否则已训练的模型头就废了。另外类别命名建议用英文小写加下划线避免路径中出现中文和空格带来的跨平台问题。2.2 训练集、验证集、测试集在交通标志任务中的分工这三个集合都在数据集目录下但作用不能混。训练集用于更新模型权重验证集用于选超参数、做早停和比较模型测试集只允许在最终评估时用一次。很多初学者会把验证集当测试集反复调得到一个“看起来很好”的准确率实际上模型已经在验证集上过拟合了。在交通标志场景里图片通常来自连续视频帧同一块牌子会出现在相邻几帧里。如果划分时不做去重训练集里几乎每一张验证集图片的相似帧都会出现在训练集里导致模型“记住”了标志的出现位置和背景。因此合理的划分单位是“标志实例”而不是“图片”。如果你的原始数据带有视频帧 ID 或 GPS 位置最好先把同一个标志的图片聚成一个组再对组做划分。下面是一个典型的比例和用途参考集合建议比例用途特点训练集70%更新模型参数类别分布应尽量按真实场景保留长尾验证集15%调超参、早停、选模型每类至少留一个样本且与训练集实例不重叠测试集15%最终评估性能来自不参与训练的设备或时段2.3 目录结构按类别建子文件夹而不是单一 CSVPyTorch 的ImageFolder和 TensorFlow 的image_dataset_from_directory都直接支持“根目录/类别名/图片”的目录结构。这种结构的好处是加载代码不需要改类别顺序自动按文件夹名排序可视化时也容易人工检查。一个常见做法是这样traffic_sign_dataset/ ├── train/ │ ├── speed_limit_20/ │ │ ├── 0001.jpg │ │ └── 0002.jpg │ ├── stop/ │ └── ... ├── val/ │ ├── speed_limit_20/ │ └── ... └── test/ ├── speed_limit_20/ └── ...为什么不建议用单个 CSV 保存标签因为 CSV 里写死路径后每次移动数据集都要改配置而且在查看图片时无法快速定位到文件夹。目录结构本身自带标签还能配合ImageFolder直接统计每个类别的样本数省掉一堆代码。唯一要注意的是类别文件夹名必须与classes.txt一致否则读取时顺序会错位。3. 用 ImageFolder 组织数据集从原始图片到可训练的目录拿到原始图片后第一件事是清理而不是立即切分。交通标志图片常有损坏文件、重复文件和下载不完全的占位图。下面按顺序做。3.1 剔除损坏文件和重名文件一般的图片解码库可以检测坏图。用 PIL 逐张打开捕获异常并删除或移动到一个 ignore 目录from PIL import Image from pathlib import Path source_dir Path(raw_images) bad_dir Path(bad_images) bad_dir.mkdir(exist_okTrue) for img_path in source_dir.rglob(*.*): if img_path.suffix.lower() not in {.jpg, .jpeg, .png, .bmp}: continue try: with Image.open(img_path) as im: im.verify() # 验证文件完整性 except Exception: img_path.rename(bad_dir / img_path.name) print(fbad: {img_path})这段代码先按扩展名过滤再逐张校验。verify()不会解码整图速度很快但如果图片本身是截断的会在这里抛出异常。重名问题可以用文件内容做哈希处理也可以直接把图片按“来源 ID拍摄时间”重命名。我一般会先计算每张图片的 MD5把重复 MD5 的文件只保留一份避免同一个文件被同时放进训练集和测试集。3.2 用脚本把原始标注转换成 ImageFolder很多公开数据集提供的是 CSV 或 JSON 标注。假设你的原始数据是meta.csv包含filepath,class_id两列可以这样生成目录import csv import shutil from pathlib import Path class_names {} with open(classes.txt) as f: for line in f: cid, name line.strip().split() class_names[cid] name with open(meta.csv) as f: reader csv.DictReader(f) for row in reader: src Path(row[filepath]) dst_dir Path(all_images) / class_names[row[class_id]] dst_dir.mkdir(parentsTrue, exist_okTrue) shutil.copy2(src, dst_dir / src.name)这里把all_images作为统一暂存目录。copy2会保留文件时间戳后面如果要做时序划分时间戳是有用的元信息。转换完成后先目测抽查几个类别文件夹确认没有装错。3.3 按比例切分训练集、验证集、测试集切分时最简单的做法是用splitfolders它可以把指定目录下的类别子文件夹按比例随机切分splitfolders --input all_images --output traffic_sign_dataset --ratio 0.7 0.15 0.15这个命令会为每个类别创建三份目录。但要注意splitfolders默认按图片文件随机切分如果原始数据里有同一标志的多帧图片就有泄漏风险。因此我更推荐写一个按“实例分组”的脚本先根据文件名前缀比如同一标志的编号分组再对组做切分。import random from pathlib import Path import shutil random.seed(42) src_root Path(all_images) out_root Path(traffic_sign_dataset) ratio (0.7, 0.15, 0.15) for class_dir in sorted(src_root.iterdir()): if not class_dir.is_dir(): continue images sorted(class_dir.glob(*.jpg)) groups {} for img in images: # 假设文件名格式: instance_id_frame.jpg inst img.name.split(_)[0] groups.setdefault(inst, []).append(img) group_items list(groups.values()) random.shuffle(group_items) n_train int(len(group_items) * ratio[0]) n_val int(len(group_items) * ratio[1]) for split_name, subset in zip( (train, val, test), (group_items[:n_train], group_items[n_train:n_train n_val], group_items[n_train n_val:]), ): out_dir out_root / split_name / class_dir.name out_dir.mkdir(parentsTrue, exist_okTrue) for im in subset: shutil.copy2(im, out_dir / im.name)这里的关键是按“实例组”切分而不是按单张图片切分。参数ratio是三元组顺序对应训练集、验证集、测试集。固定random.seed(42)是为了让划分结果可复现。如果instance_id在你的文件名里不存在就先按视频帧或 GPS 坐标聚类一次否则这个脚本的作用等同于splitfolders。4. 训练一个基线交通标志分类模型参数与增强策略有了目录就进入训练。这里我们用 PyTorch torchvision 微调一个预训练模型。交通标志图像通常是小尺寸标志但分辨率可以缩放到 32×32 到 224×224 之间。如果数据量极少直接训练一个自定义 CNN 也可以但预训练模型收敛更快。4.1 用 ImageFolder 加载数据并计算数据集的均值、标准差数据增强前要做归一化。ImageFolder 会把每个类别读出来但我们最好自己计算整个训练集的均值、标准差而不是直接使用 ImageNet 的数值。交通标志图像的颜色分布明显偏向红蓝用 ImageNet 的均值也能跑但会让输入分布的偏移更大。import torch from torchvision import datasets, transforms DATA_ROOT traffic_sign_dataset # 先不缩放只用 ToTensor 统计 dataset datasets.ImageFolder( rootf{DATA_ROOT}/train, transformtransforms.ToTensor(), ) loader torch.utils.data.DataLoader(dataset, batch_size64, shuffleFalse) mean torch.zeros(3) total 0 for imgs, _ in loader: b, c, h, w imgs.shape # 每个通道按 batch 内像素累加 mean imgs.sum(dim[0, 2, 3]) total b * h * w mean / total print(mean)这个写法能得出均值但标准差严格来说要用二次矩。我通常直接用per_image归一化的小工具或者干脆使用 ImageNet 的mean/std。在交通标志任务中模型结果对归一化参数不太敏感关键还是要做几何增强。4.2 交通标志数据增强平移、旋转和颜色扰动交通标志拍摄时经常因为车头方向导致标志倾斜固定的水平翻转对某些类别可能是致命的比如“向左转弯”和“向右转弯”标志水平翻转后语义完全不同。所以数据增强不能无脑RandomHorizontalFlip。建议的增强组合train_transform transforms.Compose([ transforms.RandomAffine(degrees10, translate(0.05, 0.05), scale(0.9, 1.1)), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1), transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ])参数说明RandomAffine的degrees10限制小角度旋转因为实际拍摄标志不会倒置translate控制平移比例scale模拟远近变化。ColorJitter用来模拟不同天气下的亮度、对比度和饱和度变化。注意我没有用RandomHorizontalFlip因为很多箭头类标志经水平翻转后会变成另一个类别。4.3 微调一个预训练分类器的最小训练脚本使用resnet18把最后一层换成类别数只训练全连接层或者解冻最后几个 block。为了在 CPU 上也能跑resnet18仍有点重但这里展示的是标准流程。import torch import torch.nn as nn from torchvision import models, transforms, datasets from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) num_classes 20 # 实际以你的类别清单为准 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) model.to(device) train_loader DataLoader( datasets.ImageFolder( traffic_sign_dataset/train, transformtrain_transform, ), batch_size32, shuffleTrue, num_workers4, ) val_loader DataLoader( datasets.ImageFolder( traffic_sign_dataset/val, transformtransforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]), ), batch_size32, shuffleFalse, num_workers4, ) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) for epoch in range(10): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) out model(imgs) pred out.argmax(dim1) correct (pred labels).sum().item() total labels.size(0) print(fepoch{epoch1}, loss{running_loss/len(train_loader):.4f}, val_acc{correct/total:.4f})代码逻辑训练阶段用model.train()开启 dropout 和 BN 的 batch 统计验证阶段用model.eval()并关闭梯度。CrossEntropyLoss内部包含 softmax所以模型输出不需要再做 softmax。优化器用Adam默认lr1e-4适合微调太高会破坏预训练特征。batch_size根据显存调整CPU 训练时建议降到 8 或 16。4.4 训练集、验证集、测试集在训练循环中如何被使用下面把训练循环中的角色再说清楚数据集是否计算梯度是否影响学习率常见错误训练集是是数据没打乱导致每轮见过的顺序一样验证集否是早停用验证集调学习率后又重复评估造成选择偏差测试集否否在测试集上反复调参污染最终指标测试集只在所有超参和模型选择完成后再运行一次。如果要调整epoch数就观察验证集 loss不要看测试集。5. 验证集使用技巧与数据集审查类别不均衡、去重和误标检查5.1 训练集图像去重算法感知哈希与近似重复检测交通标志视频帧里同一标志连续几十帧极为相似。如果训练集和验证集都来自同一视频那验证集准确率会虚高。训练集图像去重算法常用感知哈希pHash先把图像缩放到 32×32转灰度做 DCT保留低频系数再二值化成 64 位哈希。import numpy as np import cv2 def phash(image_path, hash_size32): img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (hash_size, hash_size)) img np.float32(img) dct cv2.dct(img) low dct[:8, :8] med np.median(low) return (low med).flatten().astype(np.uint8)两个哈希之间的汉明距离小于某个阈值比如 8就认为是近似重复。对大规模样本先按直方图或文件名分组再两两比较避免 O(n²)。如果发现重复对保留拍摄时间最早或清晰度最高的那张把其余放进训练集无害但放进验证集或测试集就是泄漏。5.2 用混淆矩阵检查类别混淆验证集准确率只能说明整体水平交通标志分类场景里更关心哪些类别互相混淆。用验证集预测结果生成混淆矩阵能看到具体错分流向。from sklearn.metrics import confusion_matrix import numpy as np all_labels [] all_preds [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) out model(imgs) all_labels.extend(labels.cpu().numpy()) all_preds.extend(out.argmax(dim1).cpu().numpy()) cm confusion_matrix(all_labels, all_preds) np.fill_diagonal(cm, 0) # 只看错误 print(cm)把对角线清零后最大值的行就是最容易错的类别。如果这些类别在测试集里样本很少最终准确率会被少数几个类别拉低。解决办法是给损失函数加类别加权或者对样本少的数据类做过采样。5.3 测试集污染检查分布对比即使划分正确也可能在标注环节引入污染。我在最终跑测试集之前会先统计三个集合的类别分布。如果测试集的类别比例与真实业务分布差太多那得到的准确率参考价值有限。from collections import Counter train_set datasets.ImageFolder(traffic_sign_dataset/train) val_set datasets.ImageFolder(traffic_sign_dataset/val) test_set datasets.ImageFolder(traffic_sign_dataset/test) print(Counter(train_set.targets)) print(Counter(val_set.targets)) print(Counter(test_set.targets))三个分布相差不大时说明切分在类别维度上是均匀的。这不能保证实例不重叠但能快速暴露哪些类别只在测试集里出现。如果测试集里长尾类别比训练集还多评测结果会偏向高准确率。这一步确认后再跑最终评估得到的数字才有比较价值。本文还有配套的精品资源点击获取
返回列表