从点云到像素:手把手用MMDetection3D搞定KITTI数据集的坐标对齐与可视化
从点云到像素手把手用MMDetection3D搞定KITTI数据集的坐标对齐与可视化在自动驾驶的3D感知任务中如何将LiDAR点云、3D检测框与相机图像进行精确对齐和可视化是算法调试和效果验证的关键环节。KITTI作为自动驾驶领域最经典的数据集之一提供了丰富的LiDAR点云和相机图像数据但原始数据中的坐标系差异和复杂的变换关系常常让初学者感到困惑。本文将带你一步步使用MMDetection3D框架从数据读取开始通过box_camera_to_lidar、points_cam2img等核心函数最终实现点云、3D框与图像的完美叠加可视化。1. 理解KITTI数据集的多传感器坐标系KITTI数据集同时包含了Velodyne HDL-64E激光雷达采集的点云数据以及多个摄像头拍摄的图像。这些传感器安装在车辆的不同位置拥有各自独立的坐标系激光雷达坐标系原点在激光雷达中心X轴指向车辆前进方向Y轴指向车辆左侧Z轴指向车辆上方相机坐标系以相机光心为原点X轴指向图像右侧Y轴指向图像下方Z轴指向相机前方关键点两个坐标系的Y轴和Z轴方向完全不同这是后续坐标变换需要特别注意的地方。2. 环境配置与数据准备2.1 安装MMDetection3Dconda create -n openmmlab python3.8 -y conda activate openmmlab pip install torch torchvision torchaudio pip install openmim mim install mmdet3d2.2 下载并组织KITTI数据集KITTI数据集的标准目录结构如下kitti/ ├── training/ │ ├── image_2/ # 左摄像头图像 │ ├── velodyne/ # LiDAR点云数据 │ ├── label_2/ # 3D标注框 │ └── calib/ # 标定文件 └── testing/ ├── image_2/ ├── velodyne/ └── calib/注意标定文件包含了相机内参和传感器间的外参矩阵是坐标变换的基础。3. 核心坐标变换原理与实现3.1 从激光雷达到相机的坐标变换MMDetection3D提供了LiDARInstance3DBoxes.convert_to方法处理坐标系转换from mmdet3d.core.bbox import LiDARInstance3DBoxes from mmdet3d.core.bbox.structures import Box3DMode # 假设lidar_boxes是激光雷达坐标系下的3D边界框 lidar_boxes LiDARInstance3DBoxes(lidar_boxes_tensor) camera_boxes lidar_boxes.convert_to(Box3DMode.CAM, rt_matTr_velo_to_cam)其中Tr_velo_to_cam是从KITTI标定文件中读取的4x4变换矩阵。3.2 相机坐标系到图像平面的投影使用points_cam2img函数将3D点投影到2D图像from mmdet3d.core.bbox.structures.utils import points_cam2img # 相机坐标系下的点云坐标 points_cam [...] # (N, 3) # 从标定文件获取的内参矩阵 intrinsic [...] # (3, 3) # 投影到图像平面 points_img points_cam2img(points_cam, intrinsic)3.3 边界框在不同坐标系间的转换box_camera_to_lidar函数专门处理相机到激光雷达的边界框转换from mmdet3d.core.bbox.box_np_ops import box_camera_to_lidar camera_boxes [...] # 相机坐标系下的边界框 calib [...] # 标定信息 lidar_boxes box_camera_to_lidar( camera_boxes, calib[rect], calib[Tr_velo_to_cam] )4. 完整可视化流程实战4.1 数据加载与预处理import numpy as np from mmdet3d.datasets import KittiDataset # 加载单帧数据示例 data_root path/to/kitti sample_idx 000001 points np.fromfile(f{data_root}/velodyne/{sample_idx}.bin, dtypenp.float32).reshape(-1, 4) image cv2.imread(f{data_root}/image_2/{sample_idx}.png) calib parse_kitti_calib(f{data_root}/calib/{sample_idx}.txt)4.2 坐标变换与投影# 将激光雷达点云转换到相机坐标系 points_lidar points[:, :3] # 取xyz坐标 points_cam calib[Tr_velo_to_cam] np.hstack([points_lidar, np.ones(len(points_lidar))]).T points_cam points_cam[:3].T # 去掉齐次坐标 # 投影到图像平面 points_img points_cam2img(points_cam, calib[P2])4.3 3D边界框可视化# 加载3D标注框 labels parse_kitti_label(f{data_root}/label_2/{sample_idx}.txt) # 过滤并转换3D框 valid_labels [l for l in labels if l.type ! DontCare] boxes_3d np.array([l.box3d for l in valid_labels]) boxes_cam CameraInstance3DBoxes(boxes_3d) boxes_lidar boxes_cam.convert_to(Box3DMode.LIDAR, calib[Tr_velo_to_cam]) # 获取2D投影框 boxes_corners boxes_cam.corners.numpy() # (N,8,3) boxes_img [] for box in boxes_corners: box_img points_cam2img(box, calib[P2]) boxes_img.append(box_img)4.4 使用OpenCV绘制叠加效果# 绘制点云投影 for x, y in points_img: if 0 x image.shape[1] and 0 y image.shape[0]: cv2.circle(image, (int(x), int(y)), 1, (0,255,0), -1) # 绘制3D框投影 for box in boxes_img: # 绘制底面四边形 for i in range(4): x1, y1 box[i] x2, y2 box[(i1)%4] cv2.line(image, (int(x1), int(y1)), (int(x2), int(y2)), (0,0,255), 2) # 绘制顶面四边形 for i in range(4,8): x1, y1 box[i] x2, y2 box[(i-3) if i7 else 4] cv2.line(image, (int(x1), int(y1)), (int(x2), int(y2)), (255,0,0), 2) # 绘制连接线 for i in range(4): x1, y1 box[i] x2, y2 box[i4] cv2.line(image, (int(x1), int(y1)), (int(x2), int(y2)), (0,255,255), 1) cv2.imshow(Projection, image) cv2.waitKey(0)5. 常见问题与调试技巧5.1 坐标轴方向不一致导致的问题现象投影后的3D框方向错误检查确认激光雷达和相机坐标系的定义验证Tr_velo_to_cam矩阵是否正确检查边界框的origin参数设置5.2 点云投影位置偏差可能原因未正确处理点云的反射强度维度标定矩阵应用顺序错误解决方案# 正确的点云处理流程 points np.fromfile(bin_path, dtypenp.float32).reshape(-1, 4) # x,y,z,intensity points_xyz points[:, :3] # 只取坐标 points_xyz_hom np.hstack([points_xyz, np.ones(len(points_xyz))]) # 齐次坐标 points_cam (calib[rect] calib[Tr_velo_to_cam] points_xyz_hom.T).T points_cam points_cam[:, :3] # 去掉齐次坐标5.3 性能优化建议对于大规模点云可视化先进行视锥剔除只处理可能在图像范围内的点使用OpenGL等加速渲染技术对点云进行下采样处理from mmdet3d.core.bbox.box_np_ops import remove_outside_points # 视锥剔除 points_img points_cam2img(points_cam, calib[P2]) keep remove_outside_points( points_cam, calib[rect], calib[Tr_velo_to_cam], calib[P2], (image.shape[1], image.shape[0]) ) points_cam points_cam[keep]