Python实现NuScenes数据集可视化:从3D边界框到2D图像的投影原理与实践
Python实现NuScenes数据集可视化:从3D边界框到2D图像的投影原理与实践
- 一、 背景介绍
- 为什么需要可视化?
- 技术挑战
- 二、准备工作
- 2.1 下载数据集
- 2.2 数据集目录结构
- 三、 技术流程
- 3.1 整体可视化流程
- 3.2 核心原理:3D到2D投影
- 坐标系转换流程
- 投影公式
- 投影过程图解
- 四、 代码实现详解
- 4.1 环境安装
- 4.2 完整代码
- 3D坐标如何通过相机内参矩阵变成2D图像点
- 详细步骤分解:
- 1. 3D空间中的点 (现实世界)
- 2. 透视投影原理 (光线投射)
- 3. 内参矩阵的作用 (投影公式)
- 4. 公式分解说明
- (1) 透视除法 (X/Z 和 Y/Z)
- (2) 焦距参数 (f_x 和 f_y)
- (3) 主点偏移 (c_x 和 c_y)
- 5. 实际例子说明
- 6. 为什么需要齐次坐标?
- 7. 为什么需要深度信息?
- 8. 内参矩阵的物理意义
- 五、运行与结果
- 5.1 运行代码
- 5.2 输出示例
一、 背景介绍
NuScenes数据集是自动驾驶领域的重要数据集之一,包含多个场景的多传感器数据(摄像头、激光雷达等),以及详细的3D物体标注(车辆、行人、障碍物等)。数据可视化是理解和分析自动驾驶数据集的关键第一步。
为什么需要可视化?
- 理解数据分布:直观查看不同场景中的物体分布
- 验证标注质量:检查3D标注框是否准确贴合物体
- 算法调试:验证感知算法的输出结果
- 研究起点:为后续的目标检测、语义分割等任务提供基础
技术挑战
将3D空间中的物体位置投影到2D图像上涉及复杂的坐标变换,需要理解:
- 世界坐标系 → 车辆坐标系
- 车辆坐标系 → 相机坐标系
- 相机坐标系 → 图像坐标系
本文将详细解释这些变换的原理,并提供完整的Python实现代码。
二、准备工作
2.1 下载数据集
# 下载mini数据集(3.9GB)
wget -O v1.0-mini.tgz https://www.nuscenes.org/data/v1.0-mini.tgz# 创建目录并解压
mkdir nuscenes
tar -xf v1.0-mini.tgz -C nuscenes
2.2 数据集目录结构
解压后的目录结构如下:
root@in-dev-docker:/apollo# tree -L 2 nuscenes/
nuscenes/
|-- maps # 高清地图文件
| |-- 36092f0b03a857c6a3403e25b4b7aab3.png
| |-- 37819e65e09e5547b8a3ceaefba56bb2.png
| |-- 53992ee3023e5494b90c316c183be829.png
| `-- 93406b464a165eaba6d9de76ca09f5da.png
|-- samples # 关键帧传感器数据
| |-- CAM_BACK
| |-- CAM_BACK_LEFT
| |-- CAM_BACK_RIGHT
| |-- CAM_FRONT
| |-- CAM_FRONT_LEFT
| |-- CAM_FRONT_RIGHT
| |-- LIDAR_TOP # 激光雷达点云
| |-- RADAR_BACK_LEFT
| |-- RADAR_BACK_RIGHT
| |-- RADAR_FRONT
| |-- RADAR_FRONT_LEFT
| `-- RADAR_FRONT_RIGHT
|-- sweeps # 非关键帧传感器数据
| |-- CAM_BACK
| |-- CAM_BACK_LEFT
| |-- CAM_BACK_RIGHT
| |-- CAM_FRONT
| |-- CAM_FRONT_LEFT
| |-- CAM_FRONT_RIGHT
| |-- LIDAR_TOP
| |-- RADAR_BACK_LEFT
| |-- RADAR_BACK_RIGHT
| |-- RADAR_FRONT
| |-- RADAR_FRONT_LEFT
| `-- RADAR_FRONT_RIGHT
`-- v1.0-mini # 标注文件|-- attribute.json|-- calibrated_sensor.json # 传感器标定参数|-- category.json|-- ego_pose.json|-- instance.json|-- log.json|-- map.json|-- sample.json # 样本信息|-- sample_annotation.json # 3D标注|-- sample_data.json|-- scene.json|-- sensor.json`-- visibility.json28 directories, 17 files
三、 技术流程
3.1 整体可视化流程
3.2 核心原理:3D到2D投影
坐标系转换流程
实际场景示例
假设:
- 车辆位置:(10, 5, 0) 世界坐标
- 相机安装位置:(1.5, 0, 1.7) 车辆坐标
- 物体位置:(12, 6, 0) 世界坐标
转换过程:
- 世界→车辆:
(12,6,0) - (10,5,0) = (2,1,0) // 物体在车辆前方2米,右侧1米
- 车辆→相机:
(2,1,0) - (1.5,0,1.7) = (0.5,1,-1.7) // 物体在相机右前方0.5米,右侧1米,下方1.7米
投影公式
3D点 (X, Y, Z) 投影到2D图像点 (u, v) 的公式为:
u = f_x * (X/Z) + c_x
v = f_y * (Y/Z) + c_y
其中:
f_x
,f_y
:焦距参数,决定放大倍数c_x
,c_y
:主点偏移,将坐标系中心移到图像中心X/Z
,Y/Z
:透视除法,实现"近大远小"效果
投影过程图解
四、 代码实现详解
4.1 环境安装
pip install opencv-python==4.5.5.64
pip install nuscenes-devkit matplotlib
4.2 完整代码
cat > visualize_nuscenes.py <<-'EOF'
import os
import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf-8')
import cv2
import numpy as np
from nuscenes.nuscenes import NuScenes
from nuscenes.utils.data_classes import Box
from nuscenes.utils.geometry_utils import view_points
from pyquaternion import Quaternion
import time# 初始化设置
DATASET_PATH = 'nuscenes' # 数据集路径
VERSION = 'v1.0-mini' # 使用mini数据集
SAMPLE_INDEX = 10 # 选择要可视化的样本索引
CAMERA_NAME = 'CAM_FRONT' # 要使用的摄像头名称
OUTPUT_PATH = 'nuscenes_3d_boxes.jpg' # 输出图像路径def main():# 记录开始时间start_time = time.time()# 初始化NuScenes对象print(f"加载NuScenes数据集: {VERSION}...")nusc = NuScenes(version=VERSION, dataroot=DATASET_PATH, verbose=True)print(f"数据集加载完成,耗时: {time.time() - start_time:.2f}秒")# 获取样本数据print(f"\n处理样本索引: {SAMPLE_INDEX}...")sample_token = nusc.sample[SAMPLE_INDEX]['token']sample = nusc.get('sample', sample_token)# 获取相机数据print(f"获取相机数据: {CAMERA_NAME}...")cam_data = nusc.get('sample_data', sample['data'][CAMERA_NAME])image_path = os.path.join(nusc.dataroot, cam_data['filename'])# 加载图像print(f"加载图像: {image_path}...")image = cv2.imread(image_path)if image is None:print(f"错误: 无法加载图像 {image_path}")return# 获取相机内参和变换矩阵print("获取相机校准信息...")camera_calib = nusc.get('calibrated_sensor', cam_data['calibrated_sensor_token'])intrinsic = np.array(camera_calib['camera_intrinsic'])print(f"相机内参矩阵:\n",intrinsic)# 获取车辆位姿print("获取车辆位姿信息...")ego_pose = nusc.get('ego_pose', cam_data['ego_pose_token'])rotation = Quaternion(ego_pose['rotation']).rotation_matrixtranslation = np.array(ego_pose['translation'])print(f"车辆位姿(rotation):\n",rotation)print(f"车辆位姿(translation):\n",translation)# 获取所有标注print("获取3D标注信息...")ann_tokens = sample['anns']annotations = [nusc.get('sample_annotation', token) for token in ann_tokens]# 可视化3D标注框print("可视化3D边界框...")visualize_3d_boxes(image, annotations, intrinsic, rotation, translation, camera_calib)# 添加标题title = f"NuScenes 3D Boxes: {CAMERA_NAME} - Sample {SAMPLE_INDEX}"cv2.putText(image, title, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)# 保存结果print(f"\n保存结果到: {OUTPUT_PATH}")cv2.imwrite(OUTPUT_PATH, image)# 显示图像#cv2.imshow(title, image)#cv2.waitKey(0)#cv2.destroyAllWindows()# 打印总耗时total_time = time.time() - start_timeprint(f"\n处理完成! 总耗时: {total_time:.2f}秒")def visualize_3d_boxes(image, annotations, intrinsic, rotation, translation, camera_calib):"""在图像上可视化3D边界框:param image: 输入图像:param annotations: 标注列表:param intrinsic: 相机内参矩阵:param rotation: 车辆旋转矩阵:param translation: 车辆平移向量:param camera_calib: 相机校准信息"""# 设置类别颜色映射color_map = {'vehicle': (0, 0, 255), # 红色: 车辆'human': (0, 255, 0), # 绿色: 行人'movable': (255, 0, 0), # 蓝色: 可移动物体'static': (255, 255, 0) # 青色: 静态物体}# 统计不同类别的数量category_count = {}# 绘制所有3D边界框for i, ann in enumerate(annotations):# 确定类别颜色box_color = (200, 200, 200) # 默认灰色for cat in color_map:if cat in ann['category_name']:box_color = color_map[cat]break# 创建3D边界框box = Box(ann['translation'], ann['size'], Quaternion(ann['rotation']))# 将3D框投影到图像平面points_2d, visible = project_3d_box_to_image(i,box, intrinsic, rotation, translation, camera_calib)# 绘制3D框if visible:draw_3d_box(image, points_2d, box_color, ann['category_name'])# 更新类别计数category = ann['category_name'].split('.')[-1]category_count[category] = category_count.get(category, 0) + 1# 打印统计信息print(f"\n检测到 {len(annotations)} 个标注:")for category, count in category_count.items():print(f" {category}: {count}")def project_3d_box_to_image(index,box, intrinsic, rotation, translation, camera_calib):# 获取3D框的8个角点 (3x8)corners = box.corners()# 变换到车辆坐标系corners = corners - translation.reshape(3, 1)corners = np.dot(rotation.T, corners)# 变换到相机坐标系cam_translation = np.array(camera_calib['translation'])cam_rotation = Quaternion(camera_calib['rotation']).rotation_matrixif index==0:print(f"cam_translation:\n{cam_translation}")print(f"cam_rotation:\n{cam_rotation}")corners = corners - cam_translation.reshape(3, 1)corners = np.dot(cam_rotation.T, corners)# 使用NumPy和OpenCV实现投影# 1. 将3D点转换为齐次坐标 (4x8)homogeneous_points = np.vstack((corners, np.ones((1, corners.shape[1]))))# 2. 应用相机内参矩阵# 将内参矩阵扩展为3x4矩阵(添加零列)intrinsic_3x4 = np.hstack((intrinsic, np.zeros((3, 1))))# 3. 投影到图像平面projected = np.dot(intrinsic_3x4, homogeneous_points)if index==0:print(f"corners:\n{corners}")print(f"homogeneous_points:\n{homogeneous_points}")print(f"intrinsic_3x4:\n{intrinsic_3x4}")print(f"projected:\n{projected}")# 4. 归一化坐标(除以深度)# 提取深度值(Z坐标)z_coords = projected[2, :]# 避免除以零z_coords[z_coords == 0] = 1e-10# 计算2D坐标u = projected[0, :] / z_coordsv = projected[1, :] / z_coords# 组合成2D点数组 (8x2)points_2d = np.column_stack((u, v))# 检查可见性(至少2个点在相机前方)visible = np.sum(corners[2, :] > 0.1) >= 2return points_2d, visibledef draw_3d_box(image, points, color, label):"""在图像上绘制3D边界框:param image: 输入图像:param points: 8个角点的2D坐标 (8x2):param color: 框的颜色 (B, G, R):param label: 类别标签"""# 定义框的线连接顺序 (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:pt1 = tuple(points[start].astype(int))pt2 = tuple(points[end].astype(int))# 检查点是否在图像边界内if (0 <= pt1[0] < image.shape[1] and 0 <= pt1[1] < image.shape[0] and0 <= pt2[0] < image.shape[1] and 0 <= pt2[1] < image.shape[0]):cv2.line(image, pt1, pt2, color, 2)# 绘制类别标签if len(points) > 0:# 找到最底部的点作为标签位置min_y_idx = np.argmax(points[:, 1])label_pos = tuple(points[min_y_idx].astype(int))cv2.putText(image, label.split('.')[-1], (label_pos[0], label_pos[1] + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)if __name__ == '__main__':main()
EOF
3D坐标如何通过相机内参矩阵变成2D图像点
想象你站在一个黑暗的房间里,手里拿着一个手电筒(代表相机)。你面前有一个漂浮在空中的透明立方体(代表3D物体)。当手电筒的光线照射到这个立方体上时,立方体背后的墙上会出现一个影子(代表2D图像)。内参矩阵就是这个"光线投影"的数学描述。
详细步骤分解:
1. 3D空间中的点 (现实世界)
假设我们有一个3D点,比如立方体的一个角点,它在相机坐标系中的位置是 (X, Y, Z):
- X:左右方向(左负右正)
- Y:上下方向(下负上正)
- Z:前后方向(相机前方为正)
2. 透视投影原理 (光线投射)
想象从相机镜头中心发出一条光线,穿过这个3D点:
相机镜头 → 3D点 → 成像平面
这条光线最终会打在相机内部的成像传感器(CCD/CMOS)上,形成图像中的一个点。
3. 内参矩阵的作用 (投影公式)
内参矩阵是描述这个"光线投射"过程的数学公式:
[ u ] [ f_x 0 c_x ] [ X/Z ] [ v ] = [ 0 f_y c_y ] × [ Y/Z ] [ 1 ] [ 0 0 1 ] [ 1 ]
这个公式可以简化为:
u = f_x * (X/Z) + c_x v = f_y * (Y/Z) + c_y
4. 公式分解说明
(1) 透视除法 (X/Z 和 Y/Z)
- 为什么除以 Z?因为物体越远(Z越大),在图像上看起来越小
- 例如:距离相机2米的物体,在图像上的大小是距离1米时的一半
- 这模拟了人眼的透视效果:近大远小
(2) 焦距参数 (f_x 和 f_y)
- f_x 和 f_y 代表相机的焦距
- 焦距越大,视野越窄,物体在图像上越大(相当于望远镜效果)
- 焦距越小,视野越宽,物体在图像上越小(相当于广角效果)
(3) 主点偏移 (c_x 和 c_y)
- c_x 和 c_y 代表图像中心点的位置
- 因为图像坐标系的原点通常在左上角,而光学中心在图像中心
- 这个偏移量校正了坐标系差异
5. 实际例子说明
假设:
- 一个3D点位于 (2米, 1米, 4米)
- 相机参数:f_x = 800, f_y = 800, c_x = 640, c_y = 360
计算过程:
- 透视除法:
- X/Z = 2/4 = 0.5
- Y/Z = 1/4 = 0.25
- 应用焦距:
- f_x * (X/Z) = 800 * 0.5 = 400
- f_y * (Y/Z) = 800 * 0.25 = 200
- 主点偏移:
- u = 400 + 640 = 1040
- v = 200 + 360 = 560
所以这个3D点 (2,1,4) 在1280×720图像上的位置是 (1040, 560)
6. 为什么需要齐次坐标?
在代码中我们看到:
homogeneous_points = np.vstack((corners, np.ones((1, corners.shape[1]))))
- 齐次坐标是在普通坐标基础上增加一个维度(通常是1)
- 3D点 (X,Y,Z) 变成 (X,Y,Z,1)
- 这样做是为了能用单个矩阵乘法完成所有变换:
- 平移(通过矩阵的最后一列)
- 旋转(通过矩阵的前3×3部分)
- 投影(通过整个矩阵)
7. 为什么需要深度信息?
- 深度Z值决定了物体在图像中的大小
- 多个3D点可能投影到同一个2D位置(物体遮挡)
- 深度信息让我们能处理遮挡关系
8. 内参矩阵的物理意义
内参矩阵实际上包含了相机的"指纹"信息:
[ f_x 0 c_x ] [ 0 f_y c_y ] [ 0 0 1 ]
- 对角线上的f_x, f_y, 1:缩放因子
- 右上角的c_x, c_y:平移量
- 零元素:表示没有倾斜(现代相机通常没有倾斜)
3D坐标通过内参矩阵变成2D点的过程,本质上是模拟了相机成像的物理过程:
- 确定3D点在相机坐标系中的位置
- 应用透视原理(近大远小)
- 通过焦距缩放
- 通过主点偏移校正坐标系
五、运行与结果
5.1 运行代码
python visualize_nuscenes.py
5.2 输出示例
处理样本索引: 10...
获取相机数据: CAM_FRONT...
加载图像: nuscenes/samples/CAM_FRONT/n015-2018-07-24-11-22-45+0800__CAM_FRONT__1532402932612460.jpg...
获取相机校准信息...
相机内参矩阵:[[1.26641720e+03 0.00000000e+00 8.16267020e+02][0.00000000e+00 1.26641720e+03 4.91507066e+02][0.00000000e+00 0.00000000e+00 1.00000000e+00]]
获取车辆位姿信息...
车辆位姿(rotation):[[-0.27486103 0.96139551 0.01304215][-0.96142562 -0.27466963 -0.01474352][-0.01059207 -0.01659148 0.99980625]]
车辆位姿(translation):[ 399.08211592 1144.61525027 0. ]
获取3D标注信息...
可视化3D边界框...
cam_translation:
[1.70079119 0.01594563 1.51095764]
cam_rotation:
[[ 5.68477868e-03 -5.63666773e-03 9.99967955e-01][-9.99983517e-01 -8.37115272e-04 5.68014846e-03][ 8.05071338e-04 -9.99983763e-01 -5.64133364e-03]]
corners:
[[20.71362317 20.77283946 20.7443739 20.68515761 21.37947368 21.4386899721.41022441 21.35100812][ 0.17626201 0.16719472 1.80872908 1.81779638 0.18884715 0.179779851.82131422 1.83038151][18.63646596 19.25456968 19.28137732 18.6632736 18.57286 19.1909637219.21777136 18.59966764]]
homogeneous_points:
[[20.71362317 20.77283946 20.7443739 20.68515761 21.37947368 21.4386899721.41022441 21.35100812][ 0.17626201 0.16719472 1.80872908 1.81779638 0.18884715 0.179779851.82131422 1.83038151][18.63646596 19.25456968 19.28137732 18.6632736 18.57286 19.1909637219.21777136 18.59966764][ 1. 1. 1. 1. 1. 1.1. 1. ]]
intrinsic_3x4:
[[1.26641720e+03 0.00000000e+00 8.16267020e+02 0.00000000e+00][0.00000000e+00 1.26641720e+03 4.91507066e+02 0.00000000e+00][0.00000000e+00 0.00000000e+00 1.00000000e+00 0.00000000e+00]]
projected:
[[4.14444213e+04 4.20239515e+04 4.20097844e+04 4.14302542e+044.22357463e+04 4.28152765e+04 4.28011095e+04 4.22215793e+04][9.38317595e+03 9.67549532e+03 1.17675388e+04 1.14752194e+049.36785120e+03 9.66017057e+03 1.17522141e+04 1.14598947e+04][1.86364660e+01 1.92545697e+01 1.92813773e+01 1.86632736e+011.85728600e+01 1.91909637e+01 1.92177714e+01 1.85996676e+01]]检测到 127 个标注:adult: 26car: 11bicycle: 1construction_worker: 4truck: 4motorcycle: 2barrier: 27trafficcone: 12construction: 2rigid: 1保存结果到: nuscenes_3d_boxes.jpg处理完成! 总耗时: 0.53秒