1. 项目概述

最近在做一个自动驾驶相关的目标检测项目,需要将NuImages数据集转换成YOLO格式。本以为是个简单的格式转换任务,结果在实际操作中踩了不少坑。今天就把完整的转换流程和解决方案整理出来,希望能帮到有同样需求的开发者。

NuImages是nuScenes数据集的一部分,包含了丰富的街景图像和标注信息。但它的标注格式与YOLO完全不同,直接转换会遇到坐标系统不一致、类别映射复杂等问题。经过多次尝试和调整,我终于找到了一套稳定可靠的转换方案。

2. YOLO格式详解

2.1 YOLO标注格式解析

YOLO格式的核心是每个图像对应一个.txt文件,文件中的每一行代表一个目标对象的标注信息。具体格式如下:

<class_id> <x_center> <y_center> <width> <height>
  • <class_id> :目标类别的整数ID(从0开始)
  • <x_center> <y_center> :边界框中心点的归一化坐标(0-1之间)
  • <width> <height> :边界框宽度和高度的归一化值

注意:所有坐标值都是相对于图像宽高的比例值,不是绝对像素值。这是YOLO格式与其他数据集最大的区别之一。

2.2 YOLO格式的优势

  1. 简洁高效 :纯文本格式,解析速度快
  2. 归一化处理 :不受原始图像尺寸影响
  3. 兼容性好 :适用于YOLO全系列模型
  4. 扩展性强 :可以轻松添加自定义类别

3. NuImages数据集解析

3.1 数据集结构

NuImages数据集包含约93,000张图像,涵盖23个物体类别。数据集目录结构如下:

nuimages/
├── samples/          # 原始图像
├── annotations/      # 标注文件
│   ├── samples.json  # 主要标注文件
│   └── ...           # 其他元数据
└── v1.0/             # 版本信息

3.2 标注格式特点

NuImages使用JSON格式存储标注,主要包含以下关键字段:

{
  "sample_data": [...],  // 图像信息
  "objects": [...],      // 物体标注
  "categories": [...],   // 类别定义
  "attributes": [...]    // 物体属性
}

与YOLO格式的主要差异:

  1. 使用绝对像素坐标而非归一化坐标
  2. 标注信息集中存储而非分散文件
  3. 包含丰富的元数据(如遮挡情况、天气条件等)

4. 转换流程详解

4.1 环境准备

首先需要安装必要的Python包:

pip install nuscenes-devkit pyyaml tqdm

建议使用Python 3.7+环境,并确保有足够的磁盘空间(完整数据集约200GB)。

4.2 数据下载与解压

  1. 从nuScenes官网下载NuImages数据集
  2. 解压后确保目录结构完整
  3. 记录数据集根路径(后续代码中需要)

提示:可以使用 7z x filename.7z -o/path/to/output 命令解压大型文件

4.3 核心转换代码实现

以下是完整的转换脚本框架:

import json
import os
from pathlib import Path
import shutil

def convert_nuimages_to_yolo(nuimages_dir, output_dir):
    # 1. 加载标注文件
    with open(os.path.join(nuimages_dir, 'annotations', 'samples.json')) as f:
        data = json.load(f)
    
    # 2. 创建类别映射
    category_map = {cat['token']: idx for idx, cat in enumerate(data['categories'])}
    
    # 3. 处理每张图像
    for sample in data['samples']:
        image_path = os.path.join(nuimages_dir, 'samples', sample['file_name'])
        txt_path = os.path.join(output_dir, 'labels', 
                               Path(sample['file_name']).stem + '.txt')
        
        # 4. 获取对应标注
        objects = [o for o in data['objects'] if o['sample_token'] == sample['token']]
        
        # 5. 转换坐标格式
        with open(txt_path, 'w') as f:
            for obj in objects:
                # 坐标转换逻辑...
                f.write(f"{class_id} {x_center} {y_center} {width} {height}\n")
        
        # 6. 复制图像文件
        shutil.copy(image_path, os.path.join(output_dir, 'images'))

4.4 坐标转换关键算法

NuImages使用[x1, y1, x2, y2]格式的绝对坐标,需要转换为YOLO的归一化中心坐标:

def convert_bbox(bbox, img_width, img_height):
    x1, y1, x2, y2 = bbox
    
    # 计算中心点
    x_center = (x1 + x2) / 2 / img_width
    y_center = (y1 + y2) / 2 / img_height
    
    # 计算宽高
    width = (x2 - x1) / img_width
    height = (y2 - y1) / img_height
    
    return x_center, y_center, width, height

5. 常见问题与解决方案

5.1 类别映射不一致

问题现象 :转换后的类别ID与预期不符

解决方案

  1. 显式定义类别映射关系
  2. 保存category_map到文件供后续参考
category_map = {
    'vehicle.car': 0,
    'human.pedestrian': 1,
    # 其他类别...
}

5.2 坐标超出边界

问题现象 :转换后的坐标值不在[0,1]范围内

解决方法

# 在convert_bbox函数中添加边界检查
x_center = max(0, min(1, x_center))
y_center = max(0, min(1, y_center))
width = max(0, min(1, width))
height = max(0, min(1, height))

5.3 图像尺寸获取

问题现象 :需要知道原始图像尺寸才能进行归一化

解决方案

  1. 使用OpenCV读取图像获取尺寸
  2. 或从sample_data中获取尺寸信息
import cv2
img = cv2.imread(image_path)
height, width = img.shape[:2]

6. 完整代码优化版

结合上述解决方案,这是优化后的完整代码:

import json
import os
from pathlib import Path
import shutil
import cv2

def convert_nuimages_to_yolo(nuimages_dir, output_dir):
    # 创建输出目录
    os.makedirs(os.path.join(output_dir, 'images'), exist_ok=True)
    os.makedirs(os.path.join(output_dir, 'labels'), exist_ok=True)
    
    # 加载标注数据
    ann_file = os.path.join(nuimages_dir, 'annotations', 'samples.json')
    with open(ann_file) as f:
        data = json.load(f)
    
    # 创建类别映射
    categories = sorted(set(cat['name'] for cat in data['categories']))
    category_map = {cat['name']: idx for idx, cat in enumerate(categories)}
    
    # 保存类别映射文件
    with open(os.path.join(output_dir, 'classes.txt'), 'w') as f:
        f.write('\n'.join(categories))
    
    # 处理每个样本
    for sample in data['samples']:
        img_path = os.path.join(nuimages_dir, 'samples', sample['file_name'])
        txt_path = os.path.join(output_dir, 'labels', 
                               Path(sample['file_name']).stem + '.txt')
        
        # 获取图像尺寸
        img = cv2.imread(img_path)
        img_height, img_width = img.shape[:2]
        
        # 获取对应标注
        objects = [o for o in data['objects'] 
                  if o['sample_token'] == sample['token']]
        
        # 写入YOLO格式标注
        with open(txt_path, 'w') as f:
            for obj in objects:
                bbox = obj['bbox']
                class_name = next(
                    c['name'] for c in data['categories'] 
                    if c['token'] == obj['category_token']
                )
                class_id = category_map[class_name]
                
                x_center, y_center, width, height = convert_bbox(
                    bbox, img_width, img_height
                )
                
                f.write(f"{class_id} {x_center:.6f} {y_center:.6f} "
                       f"{width:.6f} {height:.6f}\n")
        
        # 复制图像文件
        shutil.copy(img_path, os.path.join(output_dir, 'images'))

def convert_bbox(bbox, img_width, img_height):
    x1, y1, x2, y2 = bbox
    
    # 计算并限制边界
    x_center = max(0, min(1, (x1 + x2) / 2 / img_width))
    y_center = max(0, min(1, (y1 + y2) / 2 / img_height))
    width = max(0, min(1, (x2 - x1) / img_width))
    height = max(0, min(1, (y2 - y1) / img_height))
    
    return x_center, y_center, width, height

7. 验证与测试

转换完成后,建议进行以下验证步骤:

  1. 随机抽样检查 :选取部分图像和对应的标签文件,确认标注是否正确
  2. 可视化验证 :使用以下脚本绘制边界框进行视觉确认
import cv2

def visualize_yolo_label(img_path, label_path, class_names):
    img = cv2.imread(img_path)
    h, w = img.shape[:2]
    
    with open(label_path) as f:
        for line in f:
            class_id, xc, yc, bw, bh = map(float, line.strip().split())
            
            # 转换回绝对坐标
            x1 = int((xc - bw/2) * w)
            y1 = int((yc - bh/2) * h)
            x2 = int((xc + bw/2) * w)
            y2 = int((yc + bh/2) * h)
            
            # 绘制边界框和标签
            cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
            cv2.putText(img, class_names[int(class_id)], 
                       (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 
                       0.9, (0, 255, 0), 2)
    
    cv2.imshow('Preview', img)
    cv2.waitKey(0)

8. 性能优化建议

对于大规模数据集转换,可以考虑以下优化措施:

  1. 并行处理 :使用多进程加速图像处理
from multiprocessing import Pool

def process_sample(args):
    # 处理单个样本的函数
    pass

with Pool(processes=8) as pool:
    pool.map(process_sample, samples)
  1. 增量处理 :记录已处理的样本,支持断点续传

  2. 内存优化 :分批加载JSON数据,避免内存溢出

  3. 使用更快的图像库 :如Pillow-SIMD替代OpenCV

在实际项目中,我发现最耗时的部分是图像文件的复制操作。对于SSD存储,完整转换约需要2-3小时;如果是HDD,可能需要更长时间。

更多推荐