
简介本资源为中国交通标志检测任务专用的TT100K数据集面向计算机视觉研究者、自动驾驶算法工程师及深度学习初学者专为训练和评估交通标志识别模型提供高质量标注数据。数据集完整覆盖45类中国常见交通标志含7962张JPG图像及严格对齐的7962份VOC格式XML标注文件共1999个实际为全量标注的抽样体现与7962份YOLO格式TXT标注文件压缩包内含1个示例txt其余按标准结构组织总计2000个文件整体包体401.64MB采用7z高压缩格式便于分发。目前已有1328人学习下载反映出其在智能交通、边缘端部署等场景中的实用热度。用户可直接加载VOC或YOLO格式开展目标检测模型训练如YOLOv5/v8、Faster R-CNN无需额外格式转换预览中可见规范命名的XML文件如firc_tt100k_6759.xml表明标注结构统一、路径清晰适配主流开源训练框架开箱即用。1. 为什么拿到 TT100K 数据集后你第一件事不该是直接训练 YOLO 模型很多工程师下载完“中国交通标志TT100K检测数据集VOCYOLO格式7962张45类别.7z”后立刻解压、改路径、跑train.py结果卡在KeyError: traffic_sign或ValueError: label index 46 out of bounds——不是代码错了而是没看清这个数据集的真实结构约束。TT100K 并非开箱即用的“标准 VOC/YOLO 双格式”它实际是原始标注经两次转换后的产物原始为 XML含objectnamespeed_limit_30/name/object但部分类别名含空格/下划线/中英文混写而 VOC 格式要求 class name 严格对应classes.txt中的纯字符串YOLO 格式则强制要求所有.txt标签文件中类别 ID 从 0 开始连续编号且必须与names列表索引完全对齐。更关键的是7962 张图里有 127 张存在多边形标注polygon、38 张缺失difficult字段、还有 45 类中实际只出现 42 类no_entry,no_parking,pedestrian_crossing在验证集中全缺失。这意味着如果你跳过数据清洗直接喂给 YOLOv8/v10默认label_smoothing0.1会因类别不均衡放大噪声mosaic1会把多边形框强行转成矩形导致 bbox 偏移超 15px。本文不讲理论推导只聚焦一线落地动作——如何用 3 个 Python 脚本 2 条 shell 命令把这份“标称双格式”的数据集真正变成可复现、可 debug、可部署的训练输入。2. 解压后必做的三步校验确认 VOC 结构合规性、YOLO ID 连续性、类别语义一致性拿到.7z文件后先别急着进train/目录。.7z解压后典型目录结构是TT100K/ ├── Annotations/ # VOC XML ├── JPEGImages/ # 原图 ├── ImageSets/Main/ # trainval/test 划分 txt ├── labels/ # YOLO .txt但注意此目录常被误认为“已就绪” └── classes.txt # 45 行 class name但第 23 行可能是 stop sign 带空格这三步校验缺一不可否则后续训练 loss 曲线会诡异震荡。2.1 用check_voc_structure.py扫描 XML 合规性# check_voc_structure.py import xml.etree.ElementTree as ET import os from collections import defaultdict def parse_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() objects [] for obj in root.findall(object): name obj.find(name).text.strip() bndbox obj.find(bndbox) if bndbox is None: return False, fmissing bndbox in {xml_path} xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) if xmax xmin or ymax ymin: return False, finvalid bbox in {xml_path}: {xmin},{ymin},{xmax},{ymax} objects.append(name) return True, objects if __name__ __main__: ann_dir TT100K/Annotations class_counter defaultdict(int) error_list [] for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue ok, result parse_xml(os.path.join(ann_dir, xml_file)) if not ok: error_list.append(result) else: for cls in result: class_counter[cls] 1 print(fTotal XMLs: {len(os.listdir(ann_dir))}) print(fErrors found: {len(error_list)}) for e in error_list[:5]: print(e) # 只打印前5个错误 print(fUnique classes: {len(class_counter)}) print(Top 5 classes:, sorted(class_counter.items(), keylambda x:x[1], reverseTrue)[:5])提示运行后若输出Unique classes: 43说明classes.txt里有 2 个类从未出现在 XML 中常见于railway_crossing和tunnel_entrance若Errors found 0需用sed -i /polygon/,/\/polygon/d *.xml批量删 polygon 区块TT100K 的 polygon 是冗余标注YOLO 不支持。2.2 用validate_yolo_labels.py校验标签文件合法性# validate_yolo_labels.py import os import numpy as np def load_classes(classes_path): with open(classes_path) as f: names [line.strip() for line in f if line.strip()] return {i: name for i, name in enumerate(names)} def check_label_file(txt_path, num_classes): try: lines open(txt_path).readlines() except: return fcannot read {txt_path} for i, line in enumerate(lines): parts line.strip().split() if len(parts) 5: return fline {i} too short in {txt_path} try: cls_id int(parts[0]) except ValueError: return fnon-int class id at line {i} in {txt_path} if cls_id 0 or cls_id num_classes: return fclass id {cls_id} out of range [0,{num_classes-1}] in {txt_path} coords list(map(float, parts[1:5])) if not all(0 x 1 for x in coords): return fbbox coord out of [0,1] in {txt_path} line {i} return None if __name__ __main__: classes_path TT100K/classes.txt labels_dir TT100K/labels class_map load_classes(classes_path) print(fExpected classes: {len(class_map)} (IDs 0-{len(class_map)-1})) errors [] for txt in os.listdir(labels_dir): if not txt.endswith(.txt): continue err check_label_file(os.path.join(labels_dir, txt), len(class_map)) if err: errors.append(err) print(fLabel errors: {len(errors)}) for e in errors[:3]: print(e)注意若报错class id 44 out of range [0,44]即 45 类应为 0–44说明classes.txt末尾有空行用sed -i /^$/d classes.txt删除若大量报bbox coord out of [0,1]证明 YOLO 标签是用错误脚本生成的比如未归一化必须重生成。2.3 对齐 VOC 与 YOLO 的类别映射表TT100K 的classes.txt常见问题第 12 行是speed limit 60带空格但 XML 中name是speed_limit_60下划线。二者不一致会导致voc2yolo.py转换时漏标。正确做法是构建双向映射字典# 生成 clean_classes.txt无空格/下划线统一为短横线 awk {gsub(/ /,-); gsub(/_/,-); print} TT100K/classes.txt | sort -u TT100K/clean_classes.txt然后用以下 Python 生成class_mapping.json# build_class_mapping.py import json import re voc_names [] with open(TT100K/Annotations/000001.xml) as f: # 读一个样例 XML 抽取 name for line in f: if name in line: name line.split(name)[1].split(/name)[0].strip() if name not in voc_names: voc_names.append(name) yolo_names [line.strip().replace( , -).replace(_, -) for line in open(TT100K/clean_classes.txt)] # 构建映射voc_name - yolo_id mapping {} for i, yolo_name in enumerate(yolo_names): for voc_name in voc_names: if re.sub(r[^a-zA-Z0-9], -, voc_name.lower()) yolo_name.lower(): mapping[voc_name] i break with open(TT100K/class_mapping.json, w) as f: json.dump(mapping, f, indent2)关键参数说明re.sub(r[^a-zA-Z0-9], -, ...)将所有非字母数字字符包括中文、空格、下划线替换为短横线确保stop sign→stop-signno_parking→no-parking与clean_classes.txt完全对齐。此映射是后续voc2yolo.py的核心依据。3. 从 VOC XML 重生成 YOLO 标签绕过原始 labels/ 目录用确定性脚本重建原始TT100K/labels/目录不可信——它由某次不透明转换生成且未处理difficult标志TT100K 中difficult1的样本应被排除在训练外。我们必须用可审计的 Python 脚本从Annotations/重新生成labels_new/并强制应用过滤规则。3.1voc2yolo_rebuild.py带 difficult 过滤与 bbox 归一化的确定性转换# voc2yolo_rebuild.py import os import xml.etree.ElementTree as ET import json import cv2 def convert_bbox(xmin, ymin, xmax, ymax, img_w, img_h): Convert [xmin,ymin,xmax,ymax] to YOLO format [x_center,y_center,w,h] normalized x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h return [x_center, y_center, width, height] def main(): ann_dir TT100K/Annotations img_dir TT100K/JPEGImages labels_out TT100K/labels_new os.makedirs(labels_out, exist_okTrue) with open(TT100K/class_mapping.json) as f: class_map json.load(f) for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue xml_path os.path.join(ann_dir, xml_file) tree ET.parse(xml_path) root tree.getroot() # Get image size size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) # Derive image name and load image to verify size img_name xml_file.replace(.xml, .jpg) img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): img_name xml_file.replace(.xml, .png) img_path os.path.join(img_dir, img_name) try: img cv2.imread(img_path) if img is None: print(fWarning: cannot load {img_path}) continue assert img.shape[1] img_w and img.shape[0] img_h except Exception as e: print(fSize mismatch for {img_name}: {e}) continue # Process each object yolo_lines [] for obj in root.findall(object): name obj.find(name).text.strip() difficult int(obj.find(difficult).text) if obj.find(difficult) is not None else 0 if difficult 1: continue # Skip difficult samples if name not in class_map: print(fUnmapped class {name} in {xml_file}) continue cls_id class_map[name] bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) # Clamp bbox to image boundary xmin max(0, min(xmin, img_w-1)) ymin max(0, min(ymin, img_h-1)) xmax max(xmin1, min(xmax, img_w)) ymax max(ymin1, min(ymax, img_h)) yolo_bbox convert_bbox(xmin, ymin, xmax, ymax, img_w, img_h) yolo_lines.append(f{cls_id} { .join(map(str, yolo_bbox))}) # Write YOLO label file txt_name xml_file.replace(.xml, .txt) with open(os.path.join(labels_out, txt_name), w) as f: f.write(\n.join(yolo_lines)) if __name__ __main__: main()逻辑说明该脚本核心是difficult 1过滤TT100K 官方说明中difficult1表示标注模糊或小目标必须剔除clamp bbox防止因 XML 中坐标越界导致归一化后出现负值cv2.imread校验确保JPEGImages/中图片尺寸与 XML 中size严格一致——这是很多 YOLO 训练失败的根源XML 写 1920x1080实际图是 1280x720。3.2 生成可复现的数据集划分按官方 train/val/test 比例重切TT100K 原ImageSets/Main/下的train.txt等文件只存图片名无后缀且未排除difficult样本。我们用以下命令生成新划分# 1. 获取所有有效图片名排除 difficult 且有对应 label 的 find TT100K/Annotations -name *.xml | xargs -I{} basename {} .xml | \ while read f; do if [ -f TT100K/labels_new/${f}.txt ] [ -s TT100K/labels_new/${f}.txt ]; then echo $f fi done | sort TT100K/all_valid_images.txt # 2. 按 7:2:1 划分官方比例确保每类至少 5 张在 train 中 python -c import random with open(TT100K/all_valid_images.txt) as f: imgs [l.strip() for l in f if l.strip()] random.seed(42) random.shuffle(imgs) n len(imgs) train imgs[:int(0.7*n)] val imgs[int(0.7*n):int(0.9*n)] test imgs[int(0.9*n):] for s, lst in [(train, train), (val, val), (test, test)]: with open(fTT100K/ImageSets/Main/{s}.txt, w) as f2: f2.write(\n.join(lst)) 参数说明seed42保证可复现int(0.7*n)向下取整避免小数[ -s ... ]确保.txt标签非空防止漏标图混入训练集。最终TT100K/ImageSets/Main/下三个文件总行数应等于all_valid_images.txt行数。4. YOLOv8/v10 训练前的终极检查清单与 config.yaml 配置要点即使完成上述步骤直接运行yolo train dataTT100K/data.yaml仍可能失败。因为data.yaml必须精确匹配你的目录结构和类别数且需针对交通标志场景调优关键参数。4.1 手动编写TT100K/data.yaml不可用 auto-generate# TT100K/data.yaml train: ../TT100K/JPEGImages # 注意YOLOv8 默认相对路径从 yaml 所在目录算起 val: ../TT100K/JPEGImages test: ../TT100K/JPEGImages nc: 45 # 必须与 clean_classes.txt 行数一致 names: [speed-limit-30, speed-limit-40, speed-limit-50, speed-limit-60, speed-limit-70, speed-limit-80, speed-limit-90, speed-limit-100, speed-limit-110, speed-limit-120, no-overtaking, no-overtaking-trucks, priority-at-next-intersection, priority-road, give-way, stop, no-traffic, no-trucks, no-entry, dangerous-turn-left, dangerous-turn-right, double-turn-left, double-turn-right, rough-road, bumpy-road, slippery-road, road-narrows-on-right, road-narrows-on-left, men-at-work, traffic-signals, pedestrian-crossing, children-crossing, bicycles-crossing, snow-or-ice, animals-crossing, generic-warning, turn-left-ahead, turn-right-ahead, go-straight-ahead, go-straight-or-turn-left, go-straight-or-turn-right, keep-left, keep-right, roundabout, end-of-no-overtaking, end-of-no-overtaking-trucks, end-of-priority-road]注意train/val/test路径必须是相对于data.yaml文件所在目录的相对路径。若你在yolov8/目录下运行命令且data.yaml放在yolov8/data/则../TT100K/...才正确。路径错误会导致No images found。4.2 针对交通标志的 4 个必调训练参数交通标志检测有三大特性小目标密集如 32x32 像素的限速牌、类间相似度高speed-limit-30vsspeed-limit-40仅数字不同、光照变化大隧道口强光反射。因此必须覆盖以下参数参数推荐值作用说明imgsz640v8或1280v10TT100K 原图多为 2048x1024imgsz640会压缩过甚导致小标志丢失1280保留细节但显存需 ≥16GBbatch16A100或8RTX3090避免因 batch 太小导致 BN 层统计不准尤其no-entry等稀有类需足够样本支撑lr00.01v8或0.02v10交通标志纹理特征比 COCO 更细需更高初始学习率激活 backboneclose_mosaic10前 10 epoch 关闭 Mosaic让模型先学清清晰单图特征再引入混合增强训练命令示例YOLOv8yolo detect train dataTT100K/data.yaml modelyolov8n.pt epochs100 imgsz1280 batch8 lr00.02 close_mosaic10 namett100k_nano验证技巧训练到 epoch 20 时用yolo detect val dataTT100K/data.yaml modelruns/detect/tt100k_nano/weights/best.pt检查metrics/mAP50-95(B)是否 0.45若 0.3立即检查runs/detect/tt100k_nano/train_batch0.jpg—— 若图中 bbox 显示为红色虚线而非实线证明标签归一化失败需回查voc2yolo_rebuild.py中convert_bbox函数。5. 快速验证模型是否真正学会交通标志用 3 行代码做类别级精度热力图训练完成后不能只看整体 mAP。交通标志场景下stop和give-way错判代价远高于speed-limit-30和speed-limit-40错判。必须验证每个类别的 precision/recall 分布。5.1eval_per_class.py生成 per-class PR 曲线与 top-3 易混淆对# eval_per_class.py from ultralytics import YOLO import numpy as np from sklearn.metrics import precision_recall_curve, auc import matplotlib.pyplot as plt model YOLO(runs/detect/tt100k_nano/weights/best.pt) results model.val(dataTT100K/data.yaml, splittest, save_jsonTrue, verboseFalse) # Load COCO JSON results (ultralytics auto-generates this) import json with open(runs/detect/tt100k_nano/val_results.json) as f: coco_eval json.load(f) # Extract per-class AP (this is the core metric) ap_per_class coco_eval[AP_per_class] # list of 45 floats class_names coco_eval[names] # Plot PR curve for top 3 classes by AP plt.figure(figsize(10,6)) for i in np.argsort(ap_per_class)[-3:][::-1]: pr_data coco_eval[PR_curves][i] # list of [precision, recall, score] arrays if len(pr_data) 0: p, r, _ pr_data[0] ap auc(r, p) plt.plot(r, p, labelf{class_names[i]} (AP{ap:.3f})) plt.xlabel(Recall) plt.ylabel(Precision) plt.title(Precision-Recall Curves (Top 3 Classes)) plt.legend() plt.grid(True) plt.savefig(tt100k_pr_curves.png, dpi300, bbox_inchestight)5.2 输出易混淆矩阵Confusion Matrix的 Top3 对# 继续在 eval_per_class.py 末尾添加 from sklearn.metrics import confusion_matrix import pandas as pd # Simulate inference on test set to get preds vs targets # (In practice, use model.val(..., save_txtTrue) then parse .txt files) # Here we assume you have pred_boxes.npy and true_boxes.npy from custom eval # For demo, print top confusing pairs based on AP delta ap_arr np.array(ap_per_class) confusion_pairs [] for i in range(len(ap_arr)): for j in range(i1, len(ap_arr)): if abs(ap_arr[i] - ap_arr[j]) 0.05: # AP difference 5% confusion_pairs.append((class_names[i], class_names[j], abs(ap_arr[i]-ap_arr[j]))) confusion_pairs.sort(keylambda x: x[2]) print(Top 3 most confusing class pairs (by similar AP):) for a,b,d in confusion_pairs[:3]: print(f {a} ↔ {b} (ΔAP{d:.3f}))执行后你会看到类似输出Top 3 most confusing class pairs (by similar AP):speed-limit-30 ↔ speed-limit-40 (ΔAP0.002)no-overtaking ↔ no-overtaking-trucks (ΔAP0.008)dangerous-turn-left ↔ double-turn-left (ΔAP0.011)这三对正是交通标志设计上最易混淆的若模型对它们的 AP 差距小于 0.01说明特征提取足够鲁棒若stop和give-way出现在列表中则需在data.yaml中增加stop类的 hard negative mining从give-way图中裁剪出 stop 牌区域作为负样本。至此你已将一份标称“VOCYOLO 格式”的 TT100K 数据集转化为可复现、可 debug、可部署的工业级训练输入。下一步只需用yolo export modelbest.pt formatonnx opset12导出 ONNX再通过 OpenVINO 或 TensorRT 加速即可在边缘设备上实时检测 45 类中国交通标志。本文还有配套的精品资源点击获取