ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TT100K交通标志数据集实战:VOC与YOLO双格式解析及YOLOv8训练指南

TT100K交通标志数据集实战:VOC与YOLO双格式解析及YOLOv8训练指南 简介中国交通标志TT100K检测数据集面向智能交通、自动驾驶与计算机视觉方向的研究者和开发者用于交通标志识别与检测模型的训练与评测。资源同时提供Pascal VOC与YOLO两套主流标注格式图片与标注一一对应省去格式转换环节可直接接入常见检测框架。压缩包共约2000个文件以1999个xml标注文件和1个说明用txt为主整体约401.64MB涵盖7962张交通场景jpg图片标注类别达45种覆盖国内常见交通标志类型。目前已有1337人学习下载具备一定参考热度。借助这批数据读者可开展卷积神经网络特征提取与分类实验训练大规模检测模型并针对真实道路场景验证算法的泛化能力与准确性为智能交通系统相关研究提供扎实的数据支撑。1. TT100K 交通标志数据集7962 张图、45 类、VOC 与 YOLO 双格式到底怎么用做交通标志检测的同行大概率都经历过这个场景手头有个路侧感知或者车载辅助驾驶的小项目算法框架选好了YOLO 环境也配通了结果卡在数据上——公开数据集要么类别太少只有限速和停止要么标注格式跟训练脚本对不上要么图片分辨率低到标志只有十几个像素。TT100K 就是在这个节点上被反复搜到的名字而这份「VOCYOLO 双格式、7962 张、45 类别」的打包版本本质上是把原始 TT100K 里可用性最高的那部分子集提前做完了格式转换和类别裁剪让你跳过最耗时的数据清洗环节直接进训练。它解决的不是「有没有数据」的问题而是「数据能不能直接喂给 YOLOv8/v11 训练脚本」的问题。VOC 格式的 XML 标注保留了原始框的完整信息适合做数据审计、可视化核对和转其他框架YOLO 格式的 txt 标注则是拿来即用的训练输入。45 个类别覆盖了限速、禁令、指示、警告几大类比只做红绿灯检测的任务丰富得多。适合谁正在做交通标志检测课程设计、路侧感知原型、或者想拿一个中等规模数据集验证自己 YOLO 改进模块是否有效的人。如果你要的是百万级、极端天气全覆盖的工业数据集这份 7962 张的量级不够但作为快速验证和 baseline 搭建它的性价比很高。2. 拆开这个 7z目录结构、类别分布与格式对照2.1 解压后你会看到什么拿到一个.7z压缩包第一件事不是急着解压完就开训而是先看清楚里面装了什么。这类双格式数据集常见的组织方式是把图片放一个目录VOC 的 XML 和 YOLO 的 txt 分开放或者按 train/val 已经切好。解压前先确认磁盘空间7962 张图如果按 JPEG 平均 200KB 算加上两份标注解压后大概 2 到 3 个 GB留够空间再动手。# 安装 p7zip多数 Linux 发行版自带 7z 命令 sudo apt-get install p7zip-full -y # 先列出压缩包内容不解压确认目录结构 7z l TT100K_VOC_YOLO_7962_45.7z | head -50 # 确认无误后解压到指定目录 7z x TT100K_VOC_YOLO_7962_45.7z -o./tt100k_data # 统计图片数量验证是否与标题的 7962 一致 find ./tt100k_data -name *.jpg -o -name *.png | wc -l7z l只列目录不落盘这一步能帮你判断压缩包是不是分卷、有没有嵌套一层同名文件夹。-o参数指定输出目录避免解压到当前目录弄乱工作区。最后那条find是血泪经验很多打包版本标题写 7962实际因为去重或过滤图片数会有出入先数一遍心里有底别等训练报错才发现对不上。2.2 VOC 与 YOLO 标注的字段对照VOC 的 XML 和 YOLO 的 txt 描述的是同一批框但坐标系和存储方式完全不同。VOC 用绝对像素坐标左上角加右下角YOLO 用归一化后的中心点加宽高值都在 0 到 1 之间。理解这个差异是后面排查标注错位的根子。维度VOC (XML)YOLO (txt)坐标形式绝对像素xmin,ymin,xmax,ymax归一化cx,cy,w,h类别表示name文本标签行首数字类别索引一图多框多个object节点多行每行一个框依赖文件无需额外文件需要classes.txt定义索引顺序主要用途审计、可视化、跨框架转换直接训练关键坑在类别索引YOLO 的 txt 里第一列是数字这个数字对应哪个类别名完全由classes.txt或data.yaml里的顺序决定。如果这份数据集的classes.txt顺序和你自己写的data.yaml不一致训练不会报错但模型学到的全是错的类别。所以拿到数据第一件事就是核对类别映射。import os import xml.etree.ElementTree as ET from collections import Counter voc_dir ./tt100k_data/annotations # 按实际路径改 class_counter Counter() for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(voc_dir, xml_file)) root tree.getroot() for obj in root.findall(object): name obj.find(name).text class_counter[name] 1 # 打印每个类别的框数量按从多到少排序 for cls, cnt in class_counter.most_common(): print(f{cls}: {cnt}) print(f总类别数: {len(class_counter)})这段脚本遍历所有 XML统计每个类别出现的框数。跑完你就能看到 45 个类别的真实分布——交通标志数据集天然不均衡限速类可能几千个框某些罕见警告标志可能只有几十个。这个分布直接决定你后面要不要做重采样或者类别加权。class_counter.most_common()按数量降序输出方便你一眼看出长尾类别。2.3 类别不均衡有多严重先量化再决定交通标志的分布不均是常态但严重到什么程度得用数字说话。常见做法是算一下最多类别和最少类别的框数比值如果超过 50:1训练时就要考虑 focal loss 或者对稀有类做增强。下面这段代码把分布画成文本直方图不用 matplotlib 也能看。# 接上面的 class_counter max_cnt max(class_counter.values()) min_cnt min(class_counter.values()) print(f最多/最少类别框数比: {max_cnt / min_cnt:.1f} : 1) # 文本直方图每 40 个框一个字符宽度 for cls, cnt in class_counter.most_common(): bar # * max(1, int(cnt / max_cnt * 40)) print(f{cls:20} {bar} {cnt})比值算出来如果很夸张别急着上复杂方案先把稀有类别的图片单独抽出来看看标注质量——很多时候长尾类别框少是因为标注本身就漏了不是真的少。这一步能帮你避免在脏数据上白调参。3. 从 VOC 转 YOLO转换脚本、类别映射与三个必查项3.1 自己写转换脚本而不是用现成工具的理由网上有现成的 voc2yolo 脚本但这份数据集已经带了 YOLO 格式为什么还要自己写转换因为你要验证它自带的 YOLO 标注是不是真的和 VOC 对得上。打包数据集最常见的翻车点就是转换时类别索引错位、坐标归一化用了错误的图片尺寸。自己写一遍转换拿结果和自带的 txt 对比对不上就说明有一方是错的。import os import xml.etree.ElementTree as ET from PIL import Image voc_dir ./tt100k_data/annotations img_dir ./tt100k_data/images out_dir ./tt100k_data/labels_check os.makedirs(out_dir, exist_okTrue) # 类别名到索引的映射必须和 data.yaml 保持一致 classes sorted(os.listdir(voc_dir)) # 这里只是占位实际用你的类别列表 class_list [speed_limit, stop, yield] # 替换成真实的 45 类 class_to_id {name: i for i, name in enumerate(class_list)} for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(voc_dir, xml_file)) root tree.getroot() # 用图片真实尺寸做归一化不能硬编码 img_name root.find(filename).text img_path os.path.join(img_dir, img_name) with Image.open(img_path) as im: w, h im.size lines [] for obj in root.findall(object): cls_name obj.find(name).text 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 cls_id class_to_id[cls_name] lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_file os.path.join(out_dir, xml_file.replace(.xml, .txt)) with open(out_file, w) as f: f.write(\n.join(lines))核心逻辑就三步读图片真实宽高、把绝对坐标转成归一化中心点格式、按类别映射写索引。Image.open拿到的w, h必须是原图尺寸不能拿标注里的size节点凑合因为有些数据集的size字段是错的。:.6f保留六位小数是 YOLO 训练的常见精度太少会丢框太多没必要。3.2 转换后必须核对的三件事转完不是就完事了三个检查项一个都不能省。第一随机抽 20 张图把 VOC 框和 YOLO 框画在同一张图上叠着看位置对不上就是归一化错了。第二统计转换前后每个类别的框数数量必须完全一致少了说明有类别没映射上。第三检查有没有宽高为 0 或者超出 1 的异常框这种框会让训练直接 NaN。# 校验统计 YOLO 标注里的类别分布和 VOC 对比 yolo_dir ./tt100k_data/labels_check yolo_counter Counter() for txt_file in os.listdir(yolo_dir): if not txt_file.endswith(.txt): continue with open(os.path.join(yolo_dir, txt_file)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f格式异常: {txt_file} - {line}) continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) # 检查坐标合法性 if not (0 cx 1 and 0 cy 1 and 0 bw 1 and 0 bh 1): print(f坐标越界: {txt_file} - {line}) yolo_counter[cls_id] 1 print(YOLO 各类别框数:, dict(yolo_counter))这段校验脚本会揪出格式不对的行和坐标越界的框。如果 VOC 统计出来某类有 500 个框YOLO 这边只有 480那 20 个大概率是被continue跳过了回去查类别名是不是有拼写差异或者空格。3.3 划分训练集和验证集时别踩的坑7962 张图怎么切 train/val直接影响你评估指标的可信度。常见做法是 8:2 或者 9:1但交通标志数据集有个特殊性同一段路拍的连续帧高度相似如果随机切验证集里会出现和训练集几乎一样的图指标虚高。更稳的做法是按拍摄场景或时间分组切分同一组的图要么全在训练集要么全在验证集。import random import shutil random.seed(42) # 固定种子保证可复现 all_imgs [f for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(all_imgs) split int(len(all_imgs) * 0.9) train_imgs all_imgs[:split] val_imgs all_imgs[split:] for phase, files in [(train, train_imgs), (val, val_imgs)]: os.makedirs(f./dataset/images/{phase}, exist_okTrue) os.makedirs(f./dataset/labels/{phase}, exist_okTrue) for img in files: shutil.copy(os.path.join(img_dir, img), f./dataset/images/{phase}/{img}) txt img.replace(.jpg, .txt) shutil.copy(os.path.join(yolo_dir, txt), f./dataset/labels/{phase}/{txt}) print(f训练集 {len(train_imgs)} 张验证集 {len(val_imgs)} 张)random.seed(42)是后悔药保证你下次跑结果一样不然调参时连数据划分都变了根本分不清是模型改了还是数据变了。如果数据集本身带了场景分组信息把random.shuffle换成按组切分指标会更真实。4. 用这份数据跑通 YOLOv8 训练data.yaml、参数与首轮验证4.1 data.yaml 怎么写才不出错YOLOv8 训练入口就是一份data.yaml路径、类别数、类别名三样东西写错一样就开不了工。路径建议用绝对路径相对路径在不同工作目录下跑容易找不到文件。类别名顺序必须和转换时的class_to_id完全一致差一个位置全盘皆错。# data.yaml path: /home/user/dataset # 数据集根目录绝对路径 train: images/train val: images/val nc: 45 # 类别数必须和实际一致 names: # 顺序必须和标注索引对应 0: speed_limit_5 1: speed_limit_10 # ... 中间省略实际写满 45 个 44: warning_othernc写错是最隐蔽的坑写少了多出来的类别索引会被当成背景写多了模型输出维度对不上直接报错。写完data.yaml先别急着训用下面这条命令让 YOLO 自己检查一遍数据。# 用 YOLO 自带的数据检查不训练只看数据能不能读 yolo checks # 或者直接跑一个 epoch 看是否报错 yolo detect train datadata.yaml modelyolov8n.pt epochs1 imgsz640 batch8先跑 1 个 epoch 是低成本试错能在一分钟内暴露路径错误、标注格式错误、类别数不匹配这些致命问题比等半小时才发现强。4.2 关键训练参数怎么设交通标志检测有几个参数和通用目标检测不太一样。输入尺寸imgsz建议 640 起步因为标志在图中占比小分辨率太低标志就糊成一团如果显存够上 960 或 1280 对小目标召回提升明显。batch根据显存调8GB 显存跑 640 大概能上 16。学习率用默认的lr00.01配合余弦退火通常够用但类别不均衡严重时把cls损失权重适当调高。yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ warmup_epochs3 \ cos_lrTrue \ cls0.7 \ projectruns/tt100k \ nameexp1warmup_epochs3让学习率从低到高预热避免一开始就把预训练权重带偏。cos_lrTrue用余弦退火后期收敛更稳。cls0.7是分类损失权重默认 0.5类别不均衡时适当提高能让模型更关注稀有类。这些值不是金科玉律先按这套跑通再根据验证集指标微调。4.3 首轮训练后看什么指标训练跑完别只看mAP50一个数。打开runs/tt100k/exp1目录重点看三样混淆矩阵、每类 AP、以及训练曲线。混淆矩阵能告诉你哪些类别被互相认错——限速 30 和限速 40 混淆是高频问题因为数字区域太小。每类 AP 能暴露长尾类别是不是完全没学到。训练曲线看val/cls_loss有没有反弹反弹说明过拟合。# 用 pandas 快速看每类 APresults.csv 在训练输出目录 import pandas as pd df pd.read_csv(runs/tt100k/exp1/results.csv) # 列名带空格先 strip df.columns [c.strip() for c in df.columns] print(df[[epoch, metrics/mAP50(B), metrics/mAP50-95(B)]].tail(10))如果某些类别 AP 长期为 0回去查这个类别的框数是不是太少或者标注是不是有问题。别急着加数据增强先确认数据本身没毛病。5. 避坑与排查这份数据集最容易翻车的五个地方5.1 类别索引错位导致模型学错类别现象训练 loss 正常下降mAP 也有数值但推理时把停止标志识别成限速标志或者类别名和框完全对不上。原因data.yaml里的names顺序和 YOLO txt 里的数字索引不一致模型学的是「第 3 类」但你以为第 3 类是停止。解决写个脚本把data.yaml的 names 和classes.txt逐行对比或者随机抽图把预测结果和真实标签画一起看对不上就重排 names 顺序。5.2 图片尺寸和标注尺寸不一致现象训练时框的位置整体偏移或者框大小明显不对。原因转换时用了错误的图片宽高做归一化比如用了标注文件里size节点的值而那个值是错的。解决转换脚本里坚持用PIL.Image.open读真实尺寸转换后抽 20 张图叠框可视化位置对不上就重转。5.3 长尾类别被模型完全忽略现象训练完发现某些类别 AP 一直是 0混淆矩阵里这些类全被预测成背景。原因这些类别框数太少模型在分类损失里几乎收不到正样本梯度。解决先确认这些类别的框数如果只有几十个考虑对含稀有类的图片做过采样或者用cls权重加 focal loss 变体。别一上来就上复杂方案先确认标注没漏。5.4 验证集指标虚高现象验证集 mAP 很高但拿真实路测图一跑效果差很多。原因随机切分导致训练集和验证集有高度相似的连续帧验证集相当于在考训练集见过的题。解决按场景或时间分组切分同一段路的图只进一个集合。如果数据集没带分组信息至少用图片文件名前缀或 EXIF 时间做粗分组。5.5 解压后文件名乱码或路径带空格现象训练脚本报FileNotFoundError但文件明明在。原因压缩包在 Windows 下打包文件名编码和 Linux 不一致或者路径里有空格没转义。解决解压时加-mcp936指定编码或者解压后用rename批量改掉中文和空格。路径里永远不要留空格用下划线代替。6. 把 7962 张用到极致小目标增强与类别平衡的实操技巧数据量固定的时候提升效果靠的是把每一张图榨干。交通标志检测的核心矛盾是小目标加类别不均衡这两个问题在这份数据集上都能用相对轻量的手段缓解。先说小目标。TT100K 里很多标志在原图里只有二三十像素直接 resize 到 640 训练标志可能只剩十几个像素特征提取网络根本抓不住。我一般会做两件事一是训练时用imgsz960甚至1280让标志保留更多像素二是开启 YOLO 自带的mosaic和copy_paste增强mosaic 把四张图拼一张变相增加了小目标在不同位置的样本copy_paste 把标志抠出来贴到别的图上直接增加小目标密度。这两个增强在 YOLOv8 里默认就开但copy_paste默认是 0需要手动设。yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs150 \ imgsz960 \ batch8 \ copy_paste0.3 \ mosaic1.0 \ scale0.5 \ projectruns/tt100k \ nameexp_smallcopy_paste0.3表示 30% 的概率做粘贴增强太高会让背景不自然0.2 到 0.4 是常见区间。scale0.5允许图片随机缩放配合 mosaic 能覆盖更多尺度。imgsz960显存占用大概是 640 的两倍多batch 要相应降下来。再说类别平衡。前面统计出的长尾类别除了过采样还可以在损失层面做文章。YOLOv8 的分类损失用的是 BCE对不均衡不敏感可以换成带 focal 权重的版本或者简单粗暴地把稀有类图片复制几份进训练集。我一般先试复制因为改动最小、最容易验证。import shutil import os # 假设 rare_classes 是框数最少的几个类别名 rare_classes [warning_other, prohibit_rare] rare_imgs [] # 从 VOC 标注里找出含稀有类的图片 for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(voc_dir, xml_file)) for obj in tree.getroot().findall(object): if obj.find(name).text in rare_classes: rare_imgs.append(xml_file.replace(.xml, .jpg)) break # 每张稀有类图片复制 3 份进训练集 for img in rare_imgs: for i in range(3): new_name img.replace(.jpg, f_dup{i}.jpg) shutil.copy(f./dataset/images/train/{img}, f./dataset/images/train/{new_name}) shutil.copy(f./dataset/labels/train/{img.replace(.jpg, .txt)}, f./dataset/labels/train/{new_name.replace(.jpg, .txt)}) print(f稀有类图片 {len(rare_imgs)} 张复制后新增 {len(rare_imgs) * 3} 张)复制的时候标注文件要同步改名不然图片和标签对不上。复制完重新统计一下类别分布确认稀有类占比上来了再开训。这个操作会让验证集和训练集有重叠风险所以复制只对训练集做验证集保持原样。最后说一个验证技巧训练完别只看 mAP拿几张真实路测图跑推理把预测框和置信度打出来看。数据集里的图和真实场景有分布差异mAP 高不代表落地能用。我习惯留 50 张完全不参与训练的图做最终验收这批图的效果才是真正能说明问题的。这套流程我踩过类别索引错位和验证集虚高的坑后来养成习惯拿到任何数据集先统计分布、再核对映射、最后小样本试训三步走完再上全量。希望帮到你。本文还有配套的精品资源点击获取
返回列表