1. 为什么需要转换TT100K数据集格式
第一次接触TT100K数据集时,我完全被它复杂的目录结构和标注格式搞懵了。这个由清华大学和腾讯联合发布的交通标志数据集,包含了10万张图片和3万多个标注实例,但它的JSON标注格式和YOLO完全不兼容。当时为了训练YOLOv5模型,我不得不花了两周时间研究格式转换的问题。
TT100K数据集最大的特点是采用层级式JSON标注,所有标注信息都存放在一个巨大的annotations_all.json文件里。这种设计虽然节省了存储空间,但在实际使用时非常不方便。相比之下,YOLO需要的格式就简单多了——每个图片对应一个.txt文件,每行记录一个目标的类别和归一化坐标。
举个具体例子,TT100K中一个停车标志的标注可能是这样的:
{ "objects": [{ "category": "p12", "bbox": {"xmin": 100, "ymin": 200, "xmax": 150, "ymax": 250} }] }而YOLO需要的格式则是:
0 0.325 0.417 0.104 0.083这种转换不仅仅是格式变化,还涉及到坐标系的转换(从绝对坐标到相对坐标)、类别ID的重新映射(从字符串标签到数字索引)以及数据集划分策略的调整。我在第一次尝试时,就因为忽略了图像尺寸的读取顺序,导致所有检测框都偏移了位置。
2. 数据预处理的关键步骤
2.1 筛选有效类别
TT100K原始数据集包含221个交通标志类别,但实际项目中我们往往只需要其中的一部分。我的经验是先用以下代码统计各类别的实例数量:
with open('annotations_all.json') as f: data = json.load(f) class_count = {} for img_id, img_info in data['imgs'].items(): for obj in img_info['objects']: class_count[obj['category']] = class_count.get(obj['category'], 0) + 1 # 按数量排序 sorted_classes = sorted(class_count.items(), key=lambda x: x[1], reverse=True)建议保留实例数超过100的类别,这样可以保证每个类别都有足够的训练样本。在我的实践中,最终筛选出了45个主要类别,包括:
- 禁令标志(如p1、p10)
- 警告标志(如ph4、pl5)
- 指示标志(如i2、i4)
2.2 处理破损和无效数据
数据集里总会有些"问题儿童"需要特别处理:
- 标注错误:有些边界框完全超出图像范围,需要用clamp函数限制坐标
- 图像损坏:约0.3%的JPEG文件无法正常读取,建议用OpenCV的imread检查
- 长宽比异常:极少数图片的宽高比超过10:1,这类样本最好剔除
这里分享一个实用的图像验证代码片段:
def validate_image(img_path): try: img = cv2.imread(img_path) if img is None: return False h, w = img.shape[:2] return w > 16 and h > 16 # 过滤掉过小的图像 except: return False3. 从TT100K到COCO的格式转换
3.1 构建COCO格式的中间层
为什么需要COCO格式作为中转?因为直接从TT100K转到YOLO格式会丢失很多结构化信息。我的转换脚本主要处理以下几个部分:
- 类别信息:将筛选后的类别建立数字ID映射
- 图像信息:记录每张图片的路径、尺寸和唯一ID
- 标注信息:转换边界框格式并关联到对应图像
核心的数据结构如下:
coco_format = { "info": {...}, "licenses": [...], "categories": [ {"id": 0, "name": "p1", "supercategory": "prohibitory"}, ... ], "images": [ {"id": 0, "file_name": "test/100.jpg", "width": 640, "height": 480}, ... ], "annotations": [ { "id": 0, "image_id": 0, "category_id": 0, "bbox": [100, 200, 50, 50], "area": 2500, "iscrowd": 0 }, ... ] }3.2 数据集划分策略
TT100K原本的划分方式不适合目标检测任务。我采用了分层抽样的方法,确保每个类别在训练集、验证集和测试集中都有代表:
- 按7:2:1的比例划分
- 对样本数较少的类别,适当增加其在训练集中的比例
- 确保同一张图片不会出现在多个子集中
实现代码的关键部分:
for cls in class_list: cls_images = get_images_by_class(cls) random.shuffle(cls_images) train_end = int(0.7 * len(cls_images)) val_end = train_end + int(0.2 * len(cls_images)) train_set.update(cls_images[:train_end]) val_set.update(cls_images[train_end:val_end]) test_set.update(cls_images[val_end:])4. COCO到YOLO格式的终极转换
4.1 坐标归一化处理
这是最容易出错的环节。YOLO要求的是中心坐标+宽高的归一化格式,计算时需要特别注意:
def coco_to_yolo_bbox(bbox, img_width, img_height): # bbox格式:[x_min, y_min, width, height] x_center = bbox[0] + bbox[2] / 2 y_center = bbox[1] + bbox[3] / 2 # 归一化 x_center /= img_width y_center /= img_height width = bbox[2] / img_width height = bbox[3] / img_height return [x_center, y_center, width, height]4.2 生成YOLO格式的标签文件
每个图片对应一个.txt文件,格式要求非常严格:
- 每行一个目标
- 空格分隔的五个数值:类别ID、中心x、中心y、宽度、高度
- 数值精度保留6位小数
实际操作时我建议使用这个模板:
def save_yolo_label(file_path, class_id, bbox): with open(file_path, 'a') as f: line = f"{class_id} {bbox[0]:.6f} {bbox[1]:.6f} {bbox[2]:.6f} {bbox[3]:.6f}\n" f.write(line)4.3 创建数据集配置文件
最后需要准备YOLO模型训练必需的data.yaml文件:
train: ../images/train val: ../images/val test: ../images/test nc: 45 # 类别数量 names: ['p1', 'p10', 'ph4', ...] # 按类别ID顺序排列5. 实战中的常见问题与解决方案
5.1 标签错位问题
第一次训练时,模型完全学不会任何东西。排查后发现是因为:
- 图像尺寸读取错误(OpenCV的shape返回的是高度在前)
- 归一化时混淆了宽高顺序
解决方法是在转换时统一添加尺寸检查:
height, width = img.shape[:2] assert width > 0 and height > 0, f"Invalid image size: {img_path}"5.2 类别不平衡处理
某些常见标志(如限速标志)的样本数是不常见标志的100倍以上。我采用了这些策略:
- 过采样少数类别
- 使用带权重的损失函数
- 在数据增强时对少数类别使用更强的变换
5.3 数据增强技巧
针对交通标志的特点,这些增强方式特别有效:
- 模拟天气变化:添加雾、雨、雪效果
- 透视变换:模拟不同拍摄角度
- 色彩抖动:考虑白平衡变化的影响
示例增强代码:
import albumentations as A transform = A.Compose([ A.RandomBrightnessContrast(p=0.5), A.HueSaturationValue(p=0.5), A.RandomFog(fog_coef_lower=0.1, fog_coef_upper=0.3, p=0.1), A.RandomRain(p=0.1) ])6. 完整代码结构与使用指南
我的项目目录结构是这样的:
tt100k2yolo/ ├── configs/ │ ├── classes.txt # 筛选后的类别列表 │ └── data.yaml # YOLO数据集配置 ├── datasets/ │ ├── images/ # 图片文件夹 │ │ ├── train/ │ │ ├── val/ │ │ └── test/ │ └── labels/ # 标签文件夹 │ ├── train/ │ ├── val/ │ └── test/ └── scripts/ ├── convert.py # 主转换脚本 └── utils.py # 工具函数转换流程只需三步:
- 修改configs/classes.txt定义需要的类别
- 运行转换脚本:
python scripts/convert.py \ --input_dir /path/to/tt100k \ --output_dir ./datasets \ --config ./configs/classes.txt- 检查生成的data.yaml文件
7. 模型训练与效果验证
使用转换后的数据集训练YOLOv8,关键配置参数:
model = YOLO('yolov8n.yaml') results = model.train( data='configs/data.yaml', epochs=300, imgsz=640, batch=16, optimizer='AdamW' )在我的RTX 3090上训练300个epoch大约需要8小时。最终在测试集上的指标:
- mAP@0.5: 0.87
- mAP@0.5:0.95: 0.63
- 推理速度:8ms/张(640x640)
特别要注意的是,交通标志检测需要更高的召回率。我通过调整置信度阈值和NMS参数,将漏检率控制在5%以下:
results = model.predict( source='test.jpg', conf=0.3, # 降低置信度阈值 iou=0.4, # 放宽IoU阈值 augment=True )经过三个版本迭代,这套转换流程已经稳定支持各种YOLO系列模型。最大的收获是认识到数据质量比模型结构更重要——合理的格式转换和数据处理能让普通模型也表现出色。