1. 理解TT100K数据集与VOC格式

TT100K数据集是清华大学发布的交通标志识别数据集,包含超过10万张中国道路场景图像。这个数据集特别适合训练交通标志检测模型,但原始数据格式与常见的VOC格式不同,需要转换后才能用于大多数目标检测框架。

VOC格式是PASCAL VOC挑战赛采用的标准数据格式,主要包括:

  • JPEGImages文件夹存放原始图片
  • Annotations文件夹存放XML格式的标注文件
  • ImageSets/Main文件夹存放训练集和验证集划分文件

为什么要做格式转换?我遇到过不少新手直接拿TT100K原始数据训练模型,结果不是报错就是效果很差。VOC格式的优势在于:

  1. 主流框架(如Faster R-CNN、YOLO等)都内置支持
  2. XML标注结构清晰,方便人工检查
  3. 标准化的目录结构便于管理大型数据集

2. 环境准备与数据下载

2.1 安装必要依赖

建议使用Python 3.7+环境,安装以下包:

pip install lxml pillow tqdm

我测试过多个版本组合,发现Python 3.7与lxml 4.6.3的兼容性最好。如果遇到XML写入问题,可以尝试这个特定版本:

pip install lxml==4.6.3

2.2 获取数据集和工具

  1. 下载TT100K原始数据集(约19.2GB):
wget http://cg.cs.tsinghua.edu.cn/traffic-sign/data_model_code/data.zip
  1. 下载转换工具库:
git clone https://github.com/cqfdch/TT100K_to_VOC

解压后目录结构应该是这样的:

TT100K_to_VOC/
├── train/
├── test/ 
├── annotations.json
└── TT100K_VOC_classes.json

遇到过有同学把annotations.json放错位置导致脚本报错,建议对照检查。数据集解压后占用约45GB空间,确保磁盘有足够容量。

3. 构建VOC标准目录

先创建符合VOC2007规范的目录结构,这个步骤虽然简单但很重要。我习惯用Python脚本自动创建,避免手动建文件夹出错:

import os

def make_voc_dir():
    voc_dir = "VOC2007"
    os.makedirs(voc_dir, exist_ok=True)
    os.makedirs(f"{voc_dir}/Annotations", exist_ok=True)
    os.makedirs(f"{voc_dir}/JPEGImages", exist_ok=True) 
    os.makedirs(f"{voc_dir}/ImageSets/Main", exist_ok=True)
    
if __name__ == "__main__":
    make_voc_dir()

执行后会生成如下结构:

VOC2007/
├── Annotations/
├── JPEGImages/
└── ImageSets/
    └── Main/

注意JPEGImages文件夹名称必须完全一致,大小写敏感。曾经有Windows用户因为文件夹名大小写问题导致后续步骤失败。

4. 转换标注为XML格式

4.1 解析原始标注

TT100K的标注信息都存储在annotations.json中,我们需要将其转换为VOC格式的XML文件。关键是要处理以下几个字段的映射关系:

import json
from lxml import etree as ET

def parse_annotation(img_id, annos):
    img_info = annos["imgs"][img_id]
    objects = []
    for obj in img_info["objects"]:
        objects.append({
            "category": obj["category"],
            "bbox": {
                "xmin": obj["bbox"]["xmin"],
                "ymin": obj["bbox"]["ymin"],
                "xmax": obj["bbox"]["xmax"],
                "ymax": obj["bbox"]["ymax"]
            }
        })
    return objects

4.2 生成XML文件

VOC格式的XML需要包含完整的图片信息和标注框数据。这里有个细节要注意:TT100K的图片尺寸都是2048x2048,但实际使用时可能需要调整:

def create_xml(img_id, objects, output_dir):
    root = ET.Element("annotation")
    
    # 添加图片基本信息
    ET.SubElement(root, "folder").text = "VOC2007"
    ET.SubElement(root, "filename").text = f"{img_id}.jpg"
    
    # 图片尺寸信息
    size = ET.SubElement(root, "size")
    ET.SubElement(size, "width").text = "2048"
    ET.SubElement(size, "height").text = "2048"
    ET.SubElement(size, "depth").text = "3"
    
    # 每个目标的标注信息
    for obj in objects:
        obj_elem = ET.SubElement(root, "object")
        ET.SubElement(obj_elem, "name").text = obj["category"]
        ET.SubElement(obj_elem, "difficult").text = "0"
        
        bndbox = ET.SubElement(obj_elem, "bndbox")
        ET.SubElement(bndbox, "xmin").text = str(obj["bbox"]["xmin"])
        ET.SubElement(bndbox, "ymin").text = str(obj["bbox"]["ymin"])
        ET.SubElement(bndbox, "xmax").text = str(obj["bbox"]["xmax"])
        ET.SubElement(bndbox, "ymax").text = str(obj["bbox"]["ymax"])
    
    # 美化XML格式并保存
    tree = ET.ElementTree(root)
    xml_str = ET.tostring(root, encoding="UTF-8", pretty_print=True)
    with open(f"{output_dir}/{img_id}.xml", "wb") as f:
        f.write(xml_str)

5. 筛选45类关键图像

TT100K包含221类交通标志,但我们通常只需要其中45类常见标志。筛选逻辑需要注意以下几点:

  1. 只保留包含45类标志的图片
  2. 每类至少要有100个样本
  3. 排除包含其他类别标志的图片
def filter_tt45_classes(annos, min_samples=100):
    with open("TT100K_VOC_classes.json") as f:
        tt45_classes = set(json.load(f).keys())
    
    # 统计每类样本数
    class_counts = {cls: 0 for cls in tt45_classes}
    valid_img_ids = []
    
    for img_id in annos["imgs"]:
        objects = annos["imgs"][img_id]["objects"]
        if not objects:
            continue
            
        # 检查是否全部属于45类
        valid = True
        for obj in objects:
            if obj["category"] not in tt45_classes:
                valid = False
                break
                
        if valid:
            valid_img_ids.append(img_id)
            for obj in objects:
                class_counts[obj["category"]] += 1
    
    # 筛选满足最小样本量的类别
    final_classes = [cls for cls, cnt in class_counts.items() 
                    if cnt >= min_samples]
    
    # 二次筛选图片
    final_img_ids = []
    for img_id in valid_img_ids:
        objects = annos["imgs"][img_id]["objects"]
        categories = {obj["category"] for obj in objects}
        if categories.issubset(set(final_classes)):
            final_img_ids.append(img_id)
            
    return final_classes, final_img_ids

6. 数据集划分与验证

6.1 生成训练集和验证集

建议按8:2的比例划分数据集,可以使用以下脚本:

import random
from sklearn.model_selection import train_test_split

def split_dataset(img_ids, test_size=0.2):
    train_ids, val_ids = train_test_split(
        img_ids, 
        test_size=test_size,
        random_state=42
    )
    
    # 保存划分结果
    with open("VOC2007/ImageSets/Main/train.txt", "w") as f:
        f.write("\n".join(train_ids))
        
    with open("VOC2007/ImageSets/Main/val.txt", "w") as f:
        f.write("\n".join(val_ids))
    
    return train_ids, val_ids

6.2 验证数据集完整性

转换完成后,建议进行以下检查:

  1. 确认每个XML文件都有对应的图片
  2. 检查标注框是否超出图片边界
  3. 验证类别名称是否一致

这里提供一个简单的验证脚本:

from PIL import Image
import xml.etree.ElementTree as ET

def validate_annotation(xml_path, img_dir):
    try:
        tree = ET.parse(xml_path)
        root = tree.getroot()
        
        img_name = root.find("filename").text
        img_path = f"{img_dir}/{img_name}"
        
        # 检查图片是否存在
        if not os.path.exists(img_path):
            return False
            
        # 检查图片尺寸
        img = Image.open(img_path)
        width, height = img.size
        
        size = root.find("size")
        xml_width = int(size.find("width").text)
        xml_height = int(size.find("height").text)
        
        if width != xml_width or height != xml_height:
            return False
            
        # 检查标注框
        for obj in root.iter("object"):
            bndbox = obj.find("bndbox")
            xmin = float(bndbox.find("xmin").text)
            ymin = float(bndbox.find("ymin").text)
            xmax = float(bndbox.find("xmax").text)
            ymax = float(bndbox.find("ymax").text)
            
            if xmin < 0 or ymin < 0 or xmax > width or ymax > height:
                return False
                
        return True
        
    except Exception as e:
        print(f"Error validating {xml_path}: {str(e)}")
        return False

7. 完整流程整合

将上述步骤整合成一个完整的处理流程:

def main():
    # 1. 准备目录
    make_voc_dir()
    
    # 2. 加载原始标注
    with open("annotations.json") as f:
        annos = json.load(f)
    
    # 3. 筛选45类图像
    classes, img_ids = filter_tt45_classes(annos)
    print(f"Found {len(img_ids)} valid images with {len(classes)} classes")
    
    # 4. 转换标注格式
    for img_id in tqdm(img_ids):
        objects = parse_annotation(img_id, annos)
        create_xml(img_id, objects, "VOC2007/Annotations")
        
        # 复制图片到JPEGImages
        src = f"train/{img_id}.jpg" if img_id in train_ids else f"test/{img_id}.jpg"
        dst = f"VOC2007/JPEGImages/{img_id}.jpg"
        shutil.copy(src, dst)
    
    # 5. 划分数据集
    train_ids, val_ids = split_dataset(img_ids)
    
    # 6. 验证
    print("Validating annotations...")
    for xml_file in os.listdir("VOC2007/Annotations"):
        if not validate_annotation(f"VOC2007/Annotations/{xml_file}", "VOC2007/JPEGImages"):
            print(f"Invalid annotation: {xml_file}")

if __name__ == "__main__":
    main()

8. 常见问题解决

在实际操作中可能会遇到以下问题:

  1. 内存不足:处理大型数据集时,可以分批处理:
from itertools import islice

def batch_process(items, batch_size=1000):
    it = iter(items)
    while True:
        batch = list(islice(it, batch_size))
        if not batch:
            return
        yield batch
  1. 标注框越界:添加边界检查逻辑:
def clip_bbox(bbox, img_width, img_height):
    xmin = max(0, bbox["xmin"])
    ymin = max(0, bbox["ymin"])
    xmax = min(img_width, bbox["xmax"])
    ymax = min(img_height, bbox["ymax"])
    return {"xmin": xmin, "ymin": ymin, "xmax": xmax, "ymax": ymax}
  1. 类别不平衡:可以统计各类别样本数并适当调整:
def analyze_class_distribution(annos, img_ids):
    class_counts = defaultdict(int)
    for img_id in img_ids:
        for obj in annos["imgs"][img_id]["objects"]:
            class_counts[obj["category"]] += 1
    
    plt.bar(class_counts.keys(), class_counts.values())
    plt.xticks(rotation=90)
    plt.show()

处理完的数据集现在可以直接用于训练目标检测模型了。在实际项目中,我通常会先用几张小批量数据测试整个流程,确认无误后再处理完整数据集,这样可以节省大量调试时间。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐