从GTSDB到YOLO:手把手教你构建交通标志检测数据集
1. 从GTSDB到YOLO:为什么需要转换数据集格式
第一次接触交通标志检测项目时,我发现GTSDB(German Traffic Sign Detection Benchmark)是个不错的起点。这个数据集包含了900张德国交通标志的图片,格式为ppm,标签文件则是gt.txt。但问题来了:YOLO系列模型需要的是jpg图片和特定格式的txt标签文件。这就好比你想用微波炉加热食物,却发现包装上写着"仅适用于烤箱"——必须得先转换格式才能用。
我刚开始也犯过直接硬上的错误,结果训练时各种报错。后来才明白,YOLO需要的标签格式和GTSDB原始格式主要有三个关键区别:
- 坐标系统:GTSDB使用左上角和右下角坐标,YOLO需要中心点坐标+宽高
- 归一化处理:YOLO要求所有坐标值在0-1之间(相对于图片尺寸)
- 文件结构:每个图片需要单独的标签文件,而不是集中在一个gt.txt里
这就像把纸质文档扫描成电子版——内容没变,但存储方式完全不同。下面我就带你一步步完成这个"格式翻译"工作。
2. 实战第一步:获取并解压原始数据集
首先得拿到原始数据。GTSDB数据集可以在Public Archive: ff17dc924eba88d5d01a807357d6614c (erda.dk)找到,下载FullIJCNN2013.zip这个文件。我建议新建一个专门的项目文件夹,比如命名为GTSDB2YOLO,把下载的压缩包放进去解压。
解压后会看到:
- 900个.ppm格式的图片文件
- 一个gt.txt标签文件
- 一些readme文档(建议先看看)
这里有个小坑要注意:Windows系统默认可能无法预览ppm文件。别担心,这不是文件损坏,只是系统不支持这种格式的缩略图显示。用代码能正常读取就行。
3. 图片格式转换:从ppm到jpg
YOLO对jpg/png的支持最好,所以先处理图片格式。Python的PIL库就能搞定这个转换,我常用的转换脚本如下:
from PIL import Image import os def convert_ppm_to_jpg(input_dir, output_dir): if not os.path.exists(output_dir): os.makedirs(output_dir) for filename in os.listdir(input_dir): if filename.lower().endswith('.ppm'): img_path = os.path.join(input_dir, filename) try: img = Image.open(img_path) new_filename = os.path.splitext(filename)[0] + '.jpg' save_path = os.path.join(output_dir, new_filename) img.save(save_path, 'JPEG', quality=95) except Exception as e: print(f"Error processing {filename}: {str(e)}") # 使用示例 input_dir = './FullIJCNN2013' # 解压后的文件夹 output_dir = './images_jpg' # 输出文件夹 convert_ppm_to_jpg(input_dir, output_dir)运行后会生成900张jpg图片。有个细节要注意:quality参数控制压缩质量,建议设置在90-95之间。太低会影响图片质量,太高则文件体积会变大。
4. 标签格式转换:从gt.txt到YOLO格式
这是最关键的步骤。GTSDB的gt.txt文件内容是这样的格式:
00000.ppm;775;397;812;425;14 00001.ppm;399;393;427;421;14每行表示:图片名;x1;y1;x2;y2;类别编号
而YOLO需要的格式是:
- 每个图片对应一个同名的.txt文件
- 每行表示:class_id x_center y_center width height
- 所有坐标值都是归一化后的(0-1之间)
转换脚本如下:
import os def convert_gtsdb_to_yolo(gt_path, images_dir, labels_dir): if not os.path.exists(labels_dir): os.makedirs(labels_dir) with open(gt_path, 'r') as f: lines = f.readlines() for line in lines: parts = line.strip().split(';') if len(parts) < 6: continue img_name = parts[0] x1, y1, x2, y2 = map(int, parts[1:5]) class_id = int(parts[5]) - 1 # GTSDB类别从1开始,YOLO从0开始 # 获取图片尺寸用于归一化 img_path = os.path.join(images_dir, img_name.replace('.ppm', '.jpg')) if not os.path.exists(img_path): continue img = Image.open(img_path) img_w, img_h = img.size # 计算中心点和宽高 x_center = ((x1 + x2) / 2) / img_w y_center = ((y1 + y2) / 2) / img_h width = (x2 - x1) / img_w height = (y2 - y1) / img_h # 写入YOLO格式标签 label_name = os.path.splitext(img_name)[0] + '.txt' label_path = os.path.join(labels_dir, label_name) with open(label_path, 'w') as f: f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n") # 使用示例 gt_path = './FullIJCNN2013/gt.txt' images_dir = './images_jpg' labels_dir = './labels' convert_gtsdb_to_yolo(gt_path, images_dir, labels_dir)注意几个关键点:
- 类别ID减了1,因为GTSDB从1开始计数,而YOLO从0开始
- 坐标归一化是相对于图片宽高的比例
- 只生成有标注的图片对应的标签文件(共741个)
5. 数据集划分与YAML配置
现在我们有741张带标注的图片(原始900张中有159张没有标注)。接下来需要划分训练集、验证集和测试集。我推荐使用8:1:1的比例,这样既保证足够训练数据,又有合理的验证测试集。
import os import random from shutil import copyfile def split_dataset(images_dir, labels_dir, output_dir, ratios=(0.8, 0.1, 0.1)): # 创建输出目录结构 os.makedirs(os.path.join(output_dir, 'images', 'train'), exist_ok=True) os.makedirs(os.path.join(output_dir, 'images', 'val'), exist_ok=True) os.makedirs(os.path.join(output_dir, 'images', 'test'), exist_ok=True) os.makedirs(os.path.join(output_dir, 'labels', 'train'), exist_ok=True) os.makedirs(os.path.join(output_dir, 'labels', 'val'), exist_ok=True) os.makedirs(os.path.join(output_dir, 'labels', 'test'), exist_ok=True) # 获取所有有效图片名(有对应标签的) image_files = [f for f in os.listdir(images_dir) if f.endswith('.jpg')] label_files = [f.replace('.jpg', '.txt') for f in image_files] valid_pairs = [(img, lbl) for img, lbl in zip(image_files, label_files) if os.path.exists(os.path.join(labels_dir, lbl))] random.shuffle(valid_pairs) total = len(valid_pairs) train_end = int(total * ratios[0]) val_end = train_end + int(total * ratios[1]) # 复制文件到对应目录 for i, (img_file, lbl_file) in enumerate(valid_pairs): if i < train_end: subset = 'train' elif i < val_end: subset = 'val' else: subset = 'test' # 复制图片 src_img = os.path.join(images_dir, img_file) dst_img = os.path.join(output_dir, 'images', subset, img_file) copyfile(src_img, dst_img) # 复制标签 src_lbl = os.path.join(labels_dir, lbl_file) dst_lbl = os.path.join(output_dir, 'labels', subset, lbl_file) copyfile(src_lbl, dst_lbl) # 使用示例 split_dataset('./images_jpg', './labels', './dataset')最后需要创建YOLO的配置文件,比如gtsdb.yaml:
# 数据集路径 path: ./dataset train: images/train val: images/val test: images/test # 类别信息 names: 0: speed_limit_20 1: speed_limit_30 2: speed_limit_50 # ... 其他类别按GTSDB的实际顺序填写 42: end_of_no_passing6. 验证与训练技巧
完成上述步骤后,建议先用YOLO的命令行工具验证下数据集:
yolo detect train data=gtsdb.yaml model=yolov8n.pt epochs=10 --imgsz 640我在实际训练中发现几个优化点:
- 图片尺寸设置为640x640效果就不错,再大提升有限
- 数据增强很重要,特别是旋转和色彩变换
- 类别不平衡问题明显(比如限速标志特别多),可以尝试加权损失函数
如果遇到"Missing labels"警告,可能是有些图片没有对应的标签文件。这时需要检查数据集划分步骤是否正确处理了这些情况。
