Nuscenes传感器数据可视化全攻略:用Python玩转激光雷达与多相机融合
NuScenes多传感器数据可视化实战:从零构建你的自动驾驶感知沙盘
如果你正在自动驾驶领域深耕,或者对多模态传感器融合技术抱有浓厚兴趣,那么NuScenes数据集绝对是你绕不开的一座宝库。它不仅仅是一个海量的数据集合,更像是一个精心设计的、高度结构化的物理世界数字孪生。面对6路环视相机、1个顶置激光雷达、5个毫米波雷达以及IMU/GPS产生的异构数据流,如何高效地“看见”并理解它们,是每个算法工程师面临的第一个挑战。数据可视化,正是打开这扇大门的钥匙。它不仅是调试模型、验证算法的前置步骤,更是深入理解数据分布、传感器特性乃至整个自动驾驶任务本质的绝佳途径。本文将带你超越简单的API调用,深入NuScenes数据结构的肌理,用Python亲手搭建一个交互式、可定制的多传感器可视化系统,让你真正“玩转”这些数据。
1. 环境搭建与数据准备:打好地基
在开始任何炫酷的可视化之前,一个稳定、高效的工作环境是必不可少的。与许多教程直接让你pip install不同,我强烈建议使用Conda来管理你的Python环境,这能有效避免不同项目间依赖包版本的冲突。
# 创建并激活一个专用于自动驾驶数据处理的Conda环境
conda create -n nuscenes_viz python=3.8 -y
conda activate nuscenes_viz
# 安装核心数据处理与可视化库
pip install nuscenes-devkit
pip install opencv-python pillow matplotlib
pip install plotly==5.14.1 # 用于交互式3D点云
pip install pyntcloud # 可选,用于高级点云处理
pip install ipywidgets # 用于Jupyter Notebook内的交互控件
注意:
nuscenes-devkit是官方工具包,它封装了数据加载、查询和基础渲染功能,是我们所有工作的起点。但我们的目标不是仅仅使用它,而是理解并扩展它。
接下来是数据。NuScenes提供了完整版(Full)和迷你版(Mini)数据集。对于学习和开发,v1.0-mini是完美的起点,它体积小,但包含了完整的数据结构。假设你已经从官网下载并解压了数据,你的目录结构应该如下所示:
/path/to/your/nuscenes_data/
├── maps/
├── samples/
├── sweeps/
├── v1.0-mini/
└── (可能还有其他元数据文件)
初始化NuScenes对象是第一步,但这里有个细节:verbose参数。在开发阶段,将其设为True可以打印出丰富的加载信息,帮助你确认一切正常;但在最终脚本或产品中,应设为False以避免控制台输出混乱。
from nuscenes.nuscenes import NuScenes
# 指定数据版本和根路径
DATA_ROOT = '/path/to/your/nuscenes_data'
VERSION = 'v1.0-mini'
nusc = NuScenes(version=VERSION, dataroot=DATA_ROOT, verbose=True)
当看到类似“Loading NuScenes tables for version v1.0-mini...”的输出时,恭喜你,数据桥梁已经架设完毕。此时,nusc对象就像是一个连接你和庞大数据世界的智能导航器。
2. 深入核心数据结构:理解数据组织的“语法”
很多人在使用NuScenes时,直接跳到渲染函数,却对底层数据关系一知半解。这就像不识字就想读小说。要真正自由地可视化,必须理解其核心的“关系型数据库”设计。
NuScenes的数据组织围绕几个核心表(Table)展开,它们通过token(一种全局唯一标识符)相互关联。下面这个表格清晰地展示了主要表之间的关系和用途:
| 表名 (Table) | 核心字段示例 | 描述与用途 |
|---|---|---|
scene |
token, name, description, first_sample_token, last_sample_token, nbr_samples |
描述一个连续的驾驶片段(约20秒)。是最高层级的组织单元。 |
sample |
token, timestamp, scene_token, data (dict), anns (list) |
数据查询的枢纽。代表某个特定时间戳(约0.5秒间隔)下,所有传感器数据的“快照”。data字段包含了该时刻各传感器数据的token。 |
sample_data |
token, sample_token, ego_pose_token, calibrated_sensor_token, filename, timestamp |
存储具体传感器数据(如图像文件、点云文件)的元数据。通过它才能找到实际的数据文件。 |
sample_annotation |
token, sample_token, instance_token, category_name, bbox (3D) |
存储标注信息,如3D边界框、物体类别。是感知算法训练和评估的基础。 |
instance |
token, category_token, first_annotation_token, last_annotation_token |
代表一个被跟踪的物体实例(如某辆特定的车),在多个sample中出现。 |
ego_pose |
token, timestamp, translation, rotation |
自车(采集车)在全局坐标系下的位姿(位置和朝向)。 |
calibrated_sensor |
token, sensor_token, translation, rotation, camera_intrinsic |
传感器标定的关键。描述传感器相对于自车坐标系(ego pose)的安装位置、朝向,以及相机的内参矩阵。 |
理解这些关系后,数据查询就变成了有章可循的“导航”。例如,要获取某个场景(scene)的第一个样本(sample)的前置相机(CAM_FRONT)图像数据,逻辑链如下:
- 获取场景的第一个
sample_token。 - 通过
sample_token和sensor_channel(如CAM_FRONT)从sample['data']字典中获取对应的sample_data_token。 - 用这个
sample_data_token从sample_data表中获取元数据,其中包含图像文件的路径(filename)。 - 使用
PIL或OpenCV加载该路径下的图像文件。
# 示例:获取第一个scene的第一个sample的CAM_FRONT图像路径
first_scene = nusc.scene[0]
first_sample_token = first_scene['first_sample_token']
first_sample = nusc.get('sample', first_sample_token)
# 从sample的data字典中获取CAM_FRONT对应的sample_data token
cam_front_token = first_sample['data']['CAM_FRONT']
cam_front_data = nusc.get('sample_data', cam_front_token)
# 拼接出完整的图像文件路径
image_path = os.path.join(DATA_ROOT, cam_front_data['filename'])
print(f"图像文件位于: {image_path}")
这种层层递进的查询方式,是灵活操作NuScenes数据的基石。
3. 单传感器数据可视化:从静态渲染到动态探索
官方nuscenes-devkit提供了基础的render_sample_data函数,但它更像一个“黑盒”。我们要做的是拆解它,并构建更灵活、更强大的可视化工具。
3.1 相机图像可视化:不仅仅是显示
对于6路环视相机,简单的显示图像只是第一步。我们更关心的是如何将其他模态的信息(如激光雷达点云、3D标注框)投影到图像上,进行跨模态验证。
首先,加载并显示图像:
import cv2
import matplotlib.pyplot as plt
from PIL import Image
def load_and_show_image(sample_data_token):
"""加载并显示指定sample_data_token对应的图像"""
sample_data = nusc.get('sample_data', sample_data_token)
img_path = os.path.join(nusc.dataroot, sample_data['filename'])
img = Image.open(img_path)
plt.figure(figsize=(13, 8))
plt.imshow(img)
plt.axis('off')
plt.title(f"Channel: {sample_data['channel']}")
plt.show()
return np.array(img) # 返回numpy数组供后续处理
但更有价值的是将激光雷达点云投影到图像上。这需要:
- 将激光雷达点从激光雷达坐标系转换到自车坐标系。
- 再从自车坐标系转换到相机坐标系。
- 最后利用相机内参矩阵,将3D点投影到2D图像像素坐标。
def map_pointcloud_to_image(points_lidar, cam_sample_data_token, lidar_sample_data_token):
"""
将激光雷达点云映射到指定相机图像上。
points_lidar: (N, 3) 或 (N, 4) 的numpy数组,激光雷达坐标系下的点云。
"""
# 1. 获取标定信息
cam_data = nusc.get('sample_data', cam_sample_data_token)
lidar_data = nusc.get('sample_data', lidar_sample_data_token)
# 获取传感器到自车坐标系的变换矩阵
cs_cam = nusc.get('calibrated_sensor', cam_data['calibrated_sensor_token'])
cs_lidar = nusc.get('calibrated_sensor', lidar_data['calibrated_sensor_token'])
# 获取自车位姿(从自车到全局坐标系)
ego_pose_cam = nusc.get('ego_pose', cam_data['ego_pose_token'])
ego_pose_lidar = nusc.get('ego_pose', lidar_data['ego_pose_token'])
# 2. 坐标变换链: Lidar -> Ego (at lidar time) -> Global -> Ego (at cam time) -> Camera
# 这里涉及时间戳对齐,简化起见,假设同一sample内位姿变化极小,可忽略。
# 实际严谨处理需考虑运动补偿,但NuScenes的sample频率较高,常作此近似。
from nuscenes.utils.geometry_utils import transform_matrix
# 计算从激光雷达到相机的变换矩阵
T_ego_to_cam = transform_matrix(cs_cam['translation'], Quaternion(cs_cam['rotation']), inverse=False)
T_lidar_to_ego = transform_matrix(cs_lidar['translation'], Quaternion(cs_lidar['rotation']), inverse=False)
T_ego_to_global = transform_matrix(ego_pose_lidar['translation'], Quaternion(ego_pose_lidar['rotation']), inverse=False)
T_global_to_ego = transform_matrix(ego_pose_cam['translation'], Quaternion(ego_pose_cam['rotation']), inverse=True)
# 组合变换: T_cam_from_lidar = T_ego_to_cam * T_global_to_ego * T_ego_to_global * T_lidar_to_ego
# 为简化,使用devkit提供的现成函数
from nuscenes.utils.data_classes import LidarPointCloud
from pyquaternion import Quaternion
# 使用LidarPointCloud类进行变换
pc = LidarPointCloud(points_lidar.T) # 输入需要是(3/4, N)形状
pc.rotate(Quaternion(cs_lidar['rotation']).rotation_matrix)
pc.translate(np.array(cs_lidar['translation']))
# 假设ego pose相同,直接变换到相机坐标系
pc.rotate(Quaternion(cs_cam['rotation']).rotation_matrix.T) # 相机到自车的逆
pc.translate(-np.array(cs_cam['translation']))
# 3. 透视投影
points_cam = pc.points[:3, :] # 取前三维 (X, Y, Z)
depths = points_cam[2, :]
# 使用相机内参
K = np.array(cs_cam['camera_intrinsic'])
points_2d = view_points(points_cam, K, normalize=True) # normalize=True 返回齐次坐标
# 过滤在相机前方的点 (Z > 0)
mask = depths > 0.1
points_2d = points_2d[:, mask]
depths = depths[mask]
return points_2d[:2, :].T, depths # 返回像素坐标(u,v)和深度值
得到投影点后,你可以根据深度信息,用颜色编码的方式将其绘制在图像上,直观地看到激光雷达点云与图像的对应关系。
3.2 激光雷达点云可视化:从2D俯瞰到3D沉浸
激光雷达点云是自动驾驶的“三维眼睛”。使用matplotlib进行2D俯瞰图(Bird‘s Eye View, BEV)渲染是最常见的需求,可以快速感知场景的平面布局。
def render_lidar_bev(points, save_path=None):
"""
生成激光雷达点云的鸟瞰图。
points: (N, 3+) numpy数组,至少包含x, y, z坐标。
"""
fig, ax = plt.subplots(figsize=(10, 10))
# 取x, y坐标,并用z轴高度或强度进行颜色映射
x = points[:, 0]
y = points[:, 1]
# 假设第四列是反射强度
if points.shape[1] >= 4:
intensity = points[:, 3]
scatter = ax.scatter(x, y, c=intensity, s=0.5, cmap='viridis', alpha=0.6)
plt.colorbar(scatter, label='Intensity')
else:
ax.scatter(x, y, s=0.5, c='blue', alpha=0.6)
# 设置坐标轴,使车辆前进方向为y轴正方向(常见设定)
ax.set_xlabel('X (左右)')
ax.set_ylabel('Y (前后)')
ax.set_title('LiDAR Point Cloud - Bird\'s Eye View')
ax.grid(True)
ax.axis('equal') # 保持横纵轴比例一致
if save_path:
plt.savefig(save_path, dpi=150, bbox_inches='tight')
plt.show()
然而,2D BEV损失了高度信息。对于理解立交桥、隧道、多层停车库等复杂结构,交互式3D可视化不可或缺。plotly库在这方面表现出色。
import plotly.graph_objects as go
def interactive_3d_pointcloud(points, annotations=None):
"""
创建交互式3D点云图,可选添加3D标注框。
points: (N, 3) numpy数组。
annotations: 该sample的标注列表。
"""
fig = go.Figure()
# 添加点云散点图
scatter = go.Scatter3d(
x=points[:, 0],
y=points[:, 1],
z=points[:, 2],
mode='markers',
marker=dict(
size=2,
color=points[:, 2], # 用Z轴坐标着色
colorscale='Viridis',
opacity=0.8
),
name='LiDAR Points'
)
fig.add_trace(scatter)
# 可选:添加3D标注框
if annotations:
from nuscenes.utils.data_classes import Box
for ann in annotations:
box = Box(ann['translation'], ann['size'], Quaternion(ann['rotation']),
label=ann['category_name'], score=1.0)
# 获取框的8个顶点
corners = box.corners().T # (8, 3)
# 定义绘制立方体12条边的索引
lines = [(0,1),(1,2),(2,3),(3,0), # 底面
(4,5),(5,6),(6,7),(7,4), # 顶面
(0,4),(1,5),(2,6),(3,7)] # 侧面
for start, end in lines:
fig.add_trace(go.Scatter3d(
x=[corners[start, 0], corners[end, 0]],
y=[corners[start, 1], corners[end, 1]],
z=[corners[start, 2], corners[end, 2]],
mode='lines',
line=dict(color='red', width=2),
showlegend=False
))
# 设置场景属性,使视角更符合驾驶习惯
camera = dict(
up=dict(x=0, y=0, z=1),
center=dict(x=0, y=0, z=0),
eye=dict(x=2, y=-2, z=1.5) # 调整视角
)
fig.update_layout(
scene=dict(
xaxis_title='X (右为正)',
yaxis_title='Y (前为正)',
zaxis_title='Z (上为正)',
aspectmode='data', # 保持坐标轴比例
camera=camera
),
title="Interactive 3D LiDAR Point Cloud with Annotations"
)
fig.show()
这段代码生成的图表允许你旋转、缩放,从任意角度审视点云和3D框,对于检查标注质量、理解障碍物空间关系至关重要。
3.3 毫米波雷达数据解析与可视化
毫米波雷达数据常被忽视,但它提供了宝贵的速度信息。NuScenes中的雷达数据以.pcd格式存储,包含点坐标、多普勒速度、反射强度等。
from nuscenes.utils.data_classes import RadarPointCloud
def load_and_visualize_radar(radar_sample_data_token):
"""加载并可视化雷达点云,重点展示速度信息"""
radar_data = nusc.get('sample_data', radar_sample_data_token)
radar_pc = RadarPointCloud.from_file(os.path.join(nusc.dataroot, radar_data['filename']))
points = radar_pc.points.T # (N, 18),包含x,y,z, dyn_prop, id, rcs, vx,vy,vx_comp,vy_comp, is_quality_valid, ambig_state, x_rms, y_rms, invalid_state, pdh0, vx_std, vy_std
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
# 子图1:BEV位置与速度矢量
ax1 = axes[0]
x, y = points[:, 0], points[:, 1]
vx, vy = points[:, 8], points[:, 9] # 补偿后的速度 vx_comp, vy_comp
# 计算径向速度大小(用于着色)
radial_vel = np.sqrt(vx**2 + vy**2)
scatter1 = ax1.scatter(x, y, c=radial_vel, s=20, cmap='coolwarm', alpha=0.7, vmin=-10, vmax=10)
# 绘制速度箭头
ax1.quiver(x, y, vx, vy, color='black', alpha=0.5, scale=50, width=0.002)
ax1.set_xlabel('X [m]')
ax1.set_ylabel('Y [m]')
ax1.set_title(f'Radar Points with Velocity Vectors ({radar_data["channel"]})')
ax1.grid(True)
ax1.axis('equal')
plt.colorbar(scatter1, ax=ax1, label='Radial Velocity [m/s]')
# 子图2:速度分布直方图
ax2 = axes[1]
ax2.hist(radial_vel, bins=50, color='skyblue', edgecolor='black', alpha=0.7)
ax2.set_xlabel('Radial Velocity [m/s]')
ax2.set_ylabel('Count')
ax2.set_title('Distribution of Radar Point Velocities')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return points
通过这种可视化,你可以清晰地区分静止物体(速度接近0)和运动物体,并观察其运动方向,这对于理解雷达数据的特性非常有帮助。
4. 多传感器融合可视化:构建统一的时空视图
单传感器可视化是基础,但自动驾驶的魅力在于融合。我们的目标是创建一个同步视图,将同一时刻的激光雷达点云、多相机图像和雷达数据在统一的坐标系下展示出来。
4.1 时间戳对齐与数据关联
NuScenes数据已经按sample进行了时间对齐。每个sample代表一个约0.5秒的时间切片,其中所有sample_data都共享一个sample_token。因此,关联数据变得直接:
def get_synchronized_data(sample_token):
"""获取指定sample下所有传感器的数据token"""
sample = nusc.get('sample', sample_token)
synchronized_tokens = {
'sample_token': sample_token,
'camera': {},
'lidar': None,
'radar': {}
}
for channel, data_token in sample['data'].items():
if channel.startswith('CAM'):
synchronized_tokens['camera'][channel] = data_token
elif channel.startswith('LIDAR'):
synchronized_tokens['lidar'] = data_token
elif channel.startswith('RADAR'):
synchronized_tokens['radar'][channel] = data_token
return synchronized_tokens
4.2 构建融合可视化面板
我们可以使用matplotlib的subplots功能,创建一个综合仪表盘。
def render_fusion_dashboard(sample_token, save_fig=False):
"""渲染一个包含多相机图像、激光雷达BEV和3D点云的综合面板"""
synced_data = get_synchronized_data(sample_token)
# 创建一个大画布
fig = plt.figure(figsize=(22, 12))
gs = fig.add_gridspec(3, 4, hspace=0.1, wspace=0.1) # 3行4列
# 第一行:6路相机图像 (2x3布局)
cam_channels = ['CAM_FRONT', 'CAM_FRONT_RIGHT', 'CAM_BACK_RIGHT',
'CAM_BACK', 'CAM_BACK_LEFT', 'CAM_FRONT_LEFT']
axes_cam = []
for idx, channel in enumerate(cam_channels):
row, col = divmod(idx, 3)
ax = fig.add_subplot(gs[row, col])
if channel in synced_data['camera']:
cam_data = nusc.get('sample_data', synced_data['camera'][channel])
img = Image.open(os.path.join(nusc.dataroot, cam_data['filename']))
ax.imshow(img)
# 尝试将激光雷达点云投影到此图像上(以前置相机为例)
if channel == 'CAM_FRONT' and synced_data['lidar']:
# 这里可以调用之前定义的 map_pointcloud_to_image 函数
# 并在图像上绘制投影点,代码略
pass
ax.set_title(channel, fontsize=9)
ax.axis('off')
axes_cam.append(ax)
# 第二行:激光雷达BEV图 (占据左侧两列)
ax_bev = fig.add_subplot(gs[1, 0:2])
if synced_data['lidar']:
lidar_data = nusc.get('sample_data', synced_data['lidar'])
lidar_pc = LidarPointCloud.from_file(os.path.join(nusc.dataroot, lidar_data['filename']))
points = lidar_pc.points.T
# 调用之前的render_lidar_bev逻辑,但绘制到指定ax上
ax_bev.scatter(points[:, 0], points[:, 1], s=0.5, c=points[:, 2], cmap='viridis', alpha=0.6)
ax_bev.set_xlabel('X [m]')
ax_bev.set_ylabel('Y [m]')
ax_bev.set_title('LiDAR BEV (Color by Height)')
ax_bev.grid(True)
ax_bev.axis('equal')
ax_bev.set_xlim([-50, 50])
ax_bev.set_ylim([-20, 80]) # 典型的前视范围
# 第二行:雷达速度图 (占据右侧两列)
ax_radar = fig.add_subplot(gs[1, 2:])
if 'RADAR_FRONT' in synced_data['radar']:
radar_points = load_and_visualize_radar(synced_data['radar']['RADAR_FRONT'])
# 这里简化,只绘制雷达点位置
ax_radar.scatter(radar_points[:, 0], radar_points[:, 1], s=20, c='red', alpha=0.7, marker='o')
ax_radar.set_xlabel('X [m]')
ax_radar.set_ylabel('Y [m]')
ax_radar.set_title('Front Radar Points')
ax_radar.grid(True)
ax_radar.axis('equal')
ax_radar.set_xlim([-50, 50])
ax_radar.set_ylim([-20, 80])
# 第三行:交互式3D点云截图或预留说明 (跨所有列)
ax_note = fig.add_subplot(gs[2, :])
ax_note.text(0.5, 0.5, 'Interactive 3D View Available in Notebook Environment\n(Using Plotly or Mayavi)',
horizontalalignment='center', verticalalignment='center', fontsize=12)
ax_note.axis('off')
plt.suptitle(f'Sample Token: {sample_token[:10]}...', fontsize=16, y=0.98)
if save_fig:
plt.savefig(f'fusion_dashboard_{sample_token[:8]}.png', dpi=150, bbox_inches='tight')
plt.show()
这个仪表盘一次性展示了所有关键传感器信息,让你对某一时刻的驾驶环境有一个全局的、融合的认知。在实际项目中,我经常将这种面板用于数据质量检查和算法输出验证。例如,检查激光雷达点云投影到图像上是否对齐,或者查看雷达检测到的运动目标在图像和点云中是否有对应。
4.3 序列化可视化与动画生成
自动驾驶是连续的时空过程。静态帧的分析还不够,我们常常需要将连续的多帧数据做成动画,观察动态变化。这可以通过matplotlib.animation或生成图像序列后用OpenCV合成视频来实现。
import imageio.v2 as imageio
from tqdm import tqdm
def create_scene_animation(scene_token, output_gif='scene_animation.gif', fps=5):
"""为整个scene生成传感器融合动画GIF"""
scene = nusc.get('scene', scene_token)
sample_token = scene['first_sample_token']
frames = []
pbar = tqdm(desc="Rendering frames")
while sample_token:
sample = nusc.get('sample', sample_token)
# 为当前sample生成融合仪表盘图像,并保存到临时文件
temp_img_path = f'temp_frame_{sample_token[:8]}.png'
render_fusion_dashboard(sample_token, save_fig=True) # 假设此函数已修改为保存至指定路径
# 读取临时图像
frame = imageio.imread(temp_img_path)
frames.append(frame)
os.remove(temp_img_path) # 清理临时文件
# 获取下一帧
sample_token = sample['next']
pbar.update(1)
pbar.close()
# 保存为GIF
imageio.mimsave(output_gif, frames, fps=fps)
print(f"动画已保存至: {output_gif}")
生成这样的动画,对于演示、汇报或者直观感受某个场景的演变过程(如车辆切入、行人横穿)非常有价值。它把静态的数据变成了生动的故事。
5. 高级技巧与实战应用
掌握了基础和多传感器融合可视化后,我们可以探索一些更高级、更贴近实际研发需求的技巧。
5.1 自定义渲染与数据增强预览
在模型训练中,数据增强(如随机旋转、平移、缩放点云)是提升泛化能力的关键。可视化增强前后的数据,能确保增强策略没有引入错误。
def visualize_augmentation(points, aug_points, aug_name="Rotation"):
"""对比显示原始点云与增强后的点云"""
fig, axes = plt.subplots(1, 2, figsize=(14, 6), subplot_kw={'projection': '3d'})
titles = ['Original Point Cloud', f'After {aug_name}']
point_sets = [points, aug_points]
for ax, title, pts in zip(axes, titles, point_sets):
ax.scatter(pts[:, 0], pts[:, 1], pts[:, 2], s=1, c=pts[:, 2], cmap='viridis', alpha=0.6)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title(title)
# 设置统一的视角
ax.view_init(elev=20., azim=-60)
ax.set_xlim([-50, 50])
ax.set_ylim([-50, 50])
ax.set_zlim([-5, 5])
plt.tight_layout()
plt.show()
# 示例:随机旋转增强
def random_rotation_augmentation(points, rotation_range=(-np.pi/4, np.pi/4)):
"""绕Z轴随机旋转点云"""
angle = np.random.uniform(*rotation_range)
cos_a, sin_a = np.cos(angle), np.sin(angle)
rotation_matrix = np.array([[cos_a, -sin_a, 0],
[sin_a, cos_a, 0],
[0, 0, 1]])
# 只旋转x, y坐标
rotated_xy = np.dot(points[:, :2], rotation_matrix[:2, :2].T)
aug_points = np.copy(points)
aug_points[:, :2] = rotated_xy
return aug_points
# 使用示例
sample_lidar_data = nusc.get('sample_data', some_lidar_token)
lidar_pc = LidarPointCloud.from_file(os.path.join(nusc.dataroot, sample_lidar_data['filename']))
points = lidar_pc.points[:3, :].T # 取x,y,z
augmented_points = random_rotation_augmentation(points)
visualize_augmentation(points, augmented_points, "Z-axis Rotation")
5.2 真值标注的可视化与分析
NuScenes提供了丰富的3D标注框。将这些框与原始数据一起可视化,是评估感知模型性能和理解标注规则的基础。
def render_annotations_on_bev(sample_token, ax=None):
"""在BEV图上渲染3D标注框"""
if ax is None:
fig, ax = plt.subplots(figsize=(10, 10))
sample = nusc.get('sample', sample_token)
# 获取激光雷达点云作为背景
lidar_token = sample['data']['LIDAR_TOP']
lidar_pc = LidarPointCloud.from_file(os.path.join(nusc.dataroot, nusc.get('sample_data', lidar_token)['filename']))
points = lidar_pc.points.T
ax.scatter(points[:, 0], points[:, 1], s=0.2, c='lightblue', alpha=0.3, label='LiDAR Points')
# 渲染每个标注框
for ann_token in sample['anns']:
ann = nusc.get('sample_annotation', ann_token)
box = Box(ann['translation'], ann['size'], Quaternion(ann['rotation']),
label=ann['category_name'])
# 获取框的底面四个角点 (忽略高度)
corners = box.bottom_corners() # (3, 4)
# 绘制底面矩形
rect_corners = np.c_[corners[:2, :], corners[:2, 0:1]] # 闭合图形
ax.plot(rect_corners[0, :], rect_corners[1, :], 'r-', linewidth=1.5)
# 在框中心标注类别
ax.text(box.center[0], box.center[1], ann['category_name'].split('.')[-1],
fontsize=8, color='darkred', ha='center', va='center',
bbox=dict(boxstyle="round,pad=0.2", facecolor='yellow', alpha=0.5))
ax.set_xlabel('X [m]')
ax.set_ylabel('Y [m]')
ax.set_title('BEV with 3D Annotations')
ax.legend(loc='upper right')
ax.grid(True)
ax.axis('equal')
ax.set_xlim([-50, 50])
ax.set_ylim([-20, 80])
return ax
通过这个视图,你可以快速检查标注框是否准确框住了点云,以及不同类别物体的分布情况。
5.3 性能优化与大规模数据浏览
当需要快速浏览整个数据集或大量样本时,渲染速度成为瓶颈。这里有几个优化策略:
- 降采样点云:对于预览性质的BEV图,不需要渲染全部点云。
def downsample_points(points, factor=10): """随机降采样点云""" indices = np.random.choice(points.shape[0], points.shape[0]//factor, replace=False) return points[indices] - 预加载与缓存:频繁访问的标定参数、地图信息可以加载到内存中,避免重复I/O。
- 使用更快的渲染后端:对于交互式3D,
pyvista或open3d可能比plotly在渲染大量点时更高效。 - 生成静态HTML报告:使用
plotly的write_html功能,可以生成包含交互式图表的HTML文件,方便分享和离线查看,而无需运行Python环境。
可视化不是目的,而是手段。在我处理NuScenes数据的实际经验中,一套好的可视化工具链,能让我在数据清洗、算法调试、结果分析阶段节省大量时间。它帮助我一眼就发现数据中的异常(比如错误的标定参数、时间戳未对齐的帧),也能直观地向团队展示算法的效果和问题所在。从读懂数据结构开始,到构建出融合多传感器信息的动态视图,这个过程本身也是对自动驾驶感知系统理解的深化。希望这些代码和思路,能成为你探索NuScenes乃至更广阔自动驾驶世界的一块坚实跳板。
更多推荐
所有评论(0)