NuImages转YOLO格式:自动驾驶目标检测实战指南
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格式的优势
- 简洁高效 :纯文本格式,解析速度快
- 归一化处理 :不受原始图像尺寸影响
- 兼容性好 :适用于YOLO全系列模型
- 扩展性强 :可以轻松添加自定义类别
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格式的主要差异:
- 使用绝对像素坐标而非归一化坐标
- 标注信息集中存储而非分散文件
- 包含丰富的元数据(如遮挡情况、天气条件等)
4. 转换流程详解
4.1 环境准备
首先需要安装必要的Python包:
pip install nuscenes-devkit pyyaml tqdm
建议使用Python 3.7+环境,并确保有足够的磁盘空间(完整数据集约200GB)。
4.2 数据下载与解压
- 从nuScenes官网下载NuImages数据集
- 解压后确保目录结构完整
- 记录数据集根路径(后续代码中需要)
提示:可以使用
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与预期不符
解决方案 :
- 显式定义类别映射关系
- 保存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 图像尺寸获取
问题现象 :需要知道原始图像尺寸才能进行归一化
解决方案 :
- 使用OpenCV读取图像获取尺寸
- 或从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. 验证与测试
转换完成后,建议进行以下验证步骤:
- 随机抽样检查 :选取部分图像和对应的标签文件,确认标注是否正确
- 可视化验证 :使用以下脚本绘制边界框进行视觉确认
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. 性能优化建议
对于大规模数据集转换,可以考虑以下优化措施:
- 并行处理 :使用多进程加速图像处理
from multiprocessing import Pool
def process_sample(args):
# 处理单个样本的函数
pass
with Pool(processes=8) as pool:
pool.map(process_sample, samples)
-
增量处理 :记录已处理的样本,支持断点续传
-
内存优化 :分批加载JSON数据,避免内存溢出
-
使用更快的图像库 :如Pillow-SIMD替代OpenCV
在实际项目中,我发现最耗时的部分是图像文件的复制操作。对于SSD存储,完整转换约需要2-3小时;如果是HDD,可能需要更长时间。
更多推荐
所有评论(0)