KITTI数据集可视化:点云投影、3D框绘制与坐标系避坑指南

发布时间:2026/10/12 16:15:59
KITTI数据集可视化:点云投影、3D框绘制与坐标系避坑指南 简介面向自动驾驶与三维视觉学习者这份资源聚焦KITTI数据集的解析与可视化封装了点云显示、BEV鸟瞰图生成及基于kitti_object_vis的9类可视化操作适合用来快速上手KITTI的bin格式数据、标注框与传感器融合结果也便于在项目调试中直观验证算法效果。压缩包共42个文件、9.81MB以Python脚本为核心辅以示例图片、notebook演示、配置文件与说明文档py脚本完成点云读取与绘制png图展示典型可视化效果ipynb便于交互式运行txt/xml等用于环境与工程配置整体目录划分清晰便于按需调用。目前已有2849人学习下载属于轻量实用型工具包。通过该压缩包用户可直接复用三套可视化脚本省去重复造轮子的时间结合查看BEV视角和9种标注可视化操作能更直观理解KITTI数据中3D框、RGB图与点云间的对应关系对开展目标检测、多传感器融合相关实验会很有帮助。1. KITTI数据集的可视化项目为什么先画出来比先跑模型更重要我刚拿到KITTI数据集的时候第一件事不是赶紧训练目标检测模型而是先把数据画出来。原因很简单这个数据集是一个黑匣子里面每一步都可能有坑——图像和点云的时间戳、雷达坐标系、相机内参、标注框的遮挡和截断任何一个环节理解错了模型训得再好也是白搭。KITTI数据集的可视化项目就是把这些数据用图像、点云和标注框的形式摊开在你面前让你一眼看出数据读得对不对、坐标变换对不对、标注对齐不对齐。它适合所有做自动驾驶感知、目标检测和点云处理的人尤其是刚入门、对着.bin文件和.txt标注发愁的初学者。把数据先画出来比先跑模型更重要。2. 读懂KITTI的目录与坐标系可视化前的数据准备2.1 KITTI对象检测数据集目录image_2、velodyne、label_2与calib各存什么KITTI数据集有raw data和object detection两套组织方式。可视化项目通常用object detection的training部分因为它自带标注目录结构非常规整training/ ├── image_2/ # 左侧彩色相机图像PNG格式 ├── velodyne/ # 激光雷达点云bin格式每帧一个文件 ├── label_2/ # 3D目标标注txt格式每帧一个文件 └── calib/ # 相机与激光雷达标定参数txt格式我第一次拿到这个目录时最大的困惑是文件名编号的对应关系。image_2、velodyne、label_2和calib四个目录下的文件名是相同的数字编号比如000001.png、000001.bin、000001.txt所以按编号一一对应读取即可。这里说的编号是帧号对应着KITTI官网提供的时间戳序列。图像是PNG点云是二进制的float32数组标注是纯文本标定文件也是纯文本。calib目录里的文件内容长这样它决定了一个点云点怎么投影到图像像素上P0: 7.070912e04 0.000000e00 6.018873e02 0.000000e00 ... P2: 7.215377e02 0.000000e00 6.095593e02 4.485728e01 ... R0_rect: 9.999239e-01 9.837760e-03 -9.445081e-03 ... Tr_velo_to_cam: 7.499165e-03 -9.999777e-01 -6.159957e-03 -4.107190e-03 ...P2是左侧彩色相机的投影矩阵R0_rect是相机0的矫正旋转矩阵Tr_velo_to_cam是激光雷达到相机的平移旋转外参。很多可视化脚本翻车都是因为这三组矩阵的相乘顺序写反了。具体顺序我在第3章里给出可复现的写法这里先记住一个结论点云要先乘Tr_velo_to_cam转到相机坐标系再乘R0_rect做矫正最后乘P2投影到像素平面。2.2 label_2标注格式与坐标定义类别、截断、遮挡和rotation_ylabel_2里的每个txt文件一行标注一个目标。KITTI对象检测的标注一共15个字段我用一个实际例子拆开讲Car 0.00 0 -1.56 599.41 156.40 629.75 201.01 1.85 1.63 2.53 -0.05 1.59 1.63 15.99按字段顺序含义如下表所示字段位置名称含义1type目标类别常见Car、Pedestrian、Cyclist等2truncation截断程度0到1的小数0表示完整可见3occlusion遮挡状态0到3的整数0完全可见4alpha观察角度影响3D框朝向显示5-8bbox2D框左上角和右下角坐标单位像素9-11dimensions3D框的高、宽、长单位米12-14location3D框中心点在相机坐标系的x、y、z15rotation_y绕y轴的旋转角决定车辆的朝向类别名称常见的有Car、Pedestrian、Cyclist、Van、Truck等。截断程度truncation是0到1的小数表示目标被图像边缘截断的比例0表示完整可见遮挡状态occlusion是0到3的整数0表示完全可见1表示部分遮挡2表示大部分遮挡3表示未知。这两个字段在可视化时如果直接忽略就会画出很多半截的车框看着就像模型预测错了其实是标注本身的状态。3D框的尺寸和位置是在相机坐标系里定义的。相机坐标系是x向右、y向下、z向前这一点和图像坐标系很接近但和激光雷达坐标系不一样。雷达坐标系是x向前、y向左、z向上。很多做点云BEV鸟瞰图的人第一版都会把这两个坐标系的轴搞混导致画出来的框和点云对不上。我一般会在可视化脚本里把坐标轴方向写死在注释里避免看代码时又忘了。2.3 二维可视化最快路径用OpenCV画2D框与类别标签读KITTI标注画2D框是最快能看到数据集长什么样子的方式。核心代码只需要两步解析txt行用OpenCV的rectangle画框、putText写字。下面这个函数可以直接跑import cv2 def draw_2d_boxes(img_path, label_path): img cv2.imread(img_path) with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) 15: continue cls parts[0] x1, y1, x2, y2 map(float, parts[4:8]) x1, y1, x2, y2 int(x1), int(y1), int(x2), int(y2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, cls, (x1, max(y1 - 4, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) return img这个函数的逻辑很直接先读图像再逐行解析标注取第5到第8个字段作为2D框的左上角和右下角坐标。解析时我做了两个保护字段数少于15就跳过防止读到空行或损坏行坐标取整前先转float避免某些标注里出现科学计数法导致int()直接报错。参数上要注意putText的坐标如果直接写y1-4在框贴到图像顶部时可能出现负坐标OpenCV会直接抛异常所以要用max(y1-4, 0)做一下裁剪。这个细节是很多初学可视化翻车的第一个坑。2.4 三维可视化形态点云、BEV鸟瞰图与3D框二维框只是KITTI数据集的表层。真正让这个数据集区别于普通图像数据集的是velodyne点云。点云可视化一般有三种形态我建议一个项目里全都做出来第一是3D点云在图像上的投影把雷达点映射回图像像素用点的深度信息染色第二是BEV鸟瞰图把所有点压到地面平面从上方俯视这种视角做目标检测和轨迹分析特别直观第三是3D包围框把label里的高宽长和中心点位置还原成空间中的8个角点既可以在3D查看器里显示也可以投影到图像上画成立体的线框。这三种形态对应的坐标系不一样容易搞混。点云投影用雷达坐标转相机坐标再转图像坐标BEV鸟瞰图直接取点云雷达坐标的x和y画散点图但要注意x对应前方、y对应左方画出来时y轴的方向可能需要翻转3D框的尺寸和位置是相机坐标如果要放进点云里看还得先转回雷达坐标。我见过不少人把三种坐标系混在一个脚本里结果图像上的3D框和点云永远对不齐。下一章我把坐标变换的完整步骤展开写给出一套能直接复用的代码。3. 写一个可视化脚本点云投影、标注框与序列播放3.1 依赖选型OpenCV、NumPy、Matplotlib还是Open3D做KITTI可视化依赖选型决定了后面代码的复杂度。我的常用组合是NumPy加OpenCV点云3D展示用Open3DBEV图用纯NumPy自己画。选型原因是OpenCV负责图像读写和画框Matplotlib虽然也能画但逐帧更新性能太差Open3D是看3D点云和3D框最方便的工具PyPI直接装不需要自己写点云渲染的底层逻辑。我一般不推荐一开始就上PCL或VTK配置成本太高而且KITTI的bin文件格式非常规整Open3D的read_point_cloud配合NumPy转换就够用。依赖清单就四个包numpy、opencv-python、open3d、Pillow。值得注意的是Open3D版本对Python版本的约束比较强装之前先看官方支持矩阵否则容易装不上。我一般用Python 3.8或3.10这两个版本踩坑最少。3.2 把velodyne点云投影到图像外参、内参和矩阵乘法点云投影是KITTI可视化里最核心的坐标变换。把上一章的calib文件读进来然后用一段代码完成投影import numpy as np def load_calib(calib_path): calib {} with open(calib_path) as f: for line in f: if line.startswith((P0, P1, P2, P3, R0_rect, Tr_velo_to_cam)): key, *vals line.strip().split() calib[key] np.array([float(v) for v in vals]).reshape(3, -1) return calib def project_velo_to_image(velo_points, calib): # velo_points: N x 3 的雷达坐标点云 N velo_points.shape[0] velo_hom np.hstack([velo_points, np.ones((N, 1))]) # 雷达 - 相机坐标 cam velo_hom calib[Tr_velo_to_cam].T # N x 3 # 补齐次再过矫正矩阵 R0_rect cam np.hstack([cam, np.ones((N, 1))]) # N x 4 rect np.eye(4) rect[:3, :3] calib[R0_rect] cam_rect cam rect.T # N x 4 # 相机坐标 - 图像像素 uv cam_rect calib[P2].T # N x 3 depth uv[:, 2] uv uv[:, :2] / depth[:, None] return uv, depth这段代码的逻辑分三步先把点云坐标补成齐次形式乘以Tr_velo_to_cam转到相机坐标系再乘R0_rect矫正矩阵最后乘P2投影矩阵并除以z分量得到像素坐标。这里最容易写错的是矩阵乘法的方向calib里的矩阵是按行存储的点云行向量要放在左边矩阵要转置后再乘也就是velo_hom Tr.T。如果用列向量写法结果会完全不同。参数说明里最关键的检查项是投影后depth的符号。投影后depth应该是正值代表点在相机前方凡是depth小于等于0的点都必须丢弃否则画到图像上就是镜像错乱的噪点。我一般会加上mask过滤valid depth 0 # 只保留相机前方的点3.3 画3D框在图像上8个角点投影的完整流程label里的3D框信息是中心点坐标x, y, z、尺寸高h、宽w、长l和rotation_y。把这五个量还原成8个角点再投影到图像是验证数据对齐程度的硬核操作import numpy as np def compute_3d_box_corners(h, w, l, x, y, z, ry): # 旋转矩阵KITTI的rotation_y绕y轴旋转 R np.array([[np.cos(ry), 0, np.sin(ry)], [0, 1, 0], [-np.sin(ry), 0, np.cos(ry)]]) # 角点坐标x方向对应长ly方向对应高hz方向对应宽w x_corners [l/2, l/2, -l/2, -l/2, l/2, l/2, -l/2, -l/2] y_corners [h/2, h/2, h/2, h/2, -h/2, -h/2, -h/2, -h/2] z_corners [w/2, -w/2, -w/2, w/2, w/2, -w/2, -w/2, w/2] corners np.stack([x_corners, y_corners, z_corners], axis1) corners corners R.T corners corners np.array([x, y, z]) return corners这里有一个KITTI特有的坑label里的h、w、l虽然是按高、宽、长给出的但三维框的8个角点对应关系里z方向变化对应的是宽度wx方向变化对应的是长度l。也就是说车辆如果正向朝前它的长度沿x轴宽度沿z轴高度沿y轴。这个三维尺寸和坐标轴的对齐关系写错画出来的框就会像一个斜着插进地里的长方体怎么调都不对。拿到8个角点后把它们当作普通的3D点用上一节的投影函数投到图像上再用OpenCV依次连线就能在图像上画出立体的3D框。连线规则是顶部4个点连一圈底部4个点连一圈再把上下对应的4条竖直棱连起来。我习惯把面向相机的棱画成实线背面的棱画成虚线这样一眼能看出车辆的朝向。3.4 序列播放器用滑动窗口回放KITTI原始数据单张图的静态可视化只能检查单帧数据。想看数据集的时间连续性比如车辆如何运动、点云如何变化需要写一个简单的序列播放器。我用的方案是OpenCV的waitKey控制帧率加一个滑动窗口的逻辑import cv2 def play_sequence(image_dir, label_dir, start0, fps10): frame_id start while True: img cv2.imread(f{image_dir}/{frame_id:06d}.png) if img is None: break # 复用draw_2d_boxes的逻辑这里省略具体画框代码 cv2.imshow(KITTI Viewer, img) key cv2.waitKey(int(1000 / fps)) 0xFF if key ord(q): break elif key ord(n): frame_id 1 cv2.destroyAllWindows()这里的关键不是播放本身而是两个细节第一KITTI文件名是六位零填充数字格式化时用:06d少一位都会读不到文件第二waitKey的返回值要做与0xFF否则在某些平台上按键判断会失败播放器按下q没反应这属于典型的玄学问题。播放器对数据体检非常有用。我遇到过一次数据集文件缺失静态看单张图完全没有问题一播放到中间某帧突然黑屏才发现下载时漏了一批bin文件。这种问题用滑动窗口一眼就能定位到断点。4. 可视化避坑5个坐标系与标注对齐的翻车现场4.1 现象点云投影后与图像错位车框漂到天上点云投影到图像上轮廓和图像里的车完全对不上点云散成一团甚至有一部分跑到天空上。这是所有KITTI可视化项目最常见的翻车现场。原因通常是矩阵乘法顺序写错。我在第3章给了Tr_velo_to_cam、R0_rect、P2依次相乘的正确顺序实际项目里有人把R0_rect乘在了Tr_velo_to_cam前面还有人忘了给R0_rect补成4x4齐次矩阵导致矩阵维度对不上。另一个常见原因是投影后没有剔除z小于0的点相机后方的点被错误地投影到图像上形成错位的残影。解决方法是把三个矩阵的shape和数值打印出来逐步核对尤其是R0_rect它本身是3x3矩阵要在左上角补成4x4才能参与齐次坐标乘法。我把这个检查写成assert每次跑投影前先过一遍能省下大量排查时间。4.2 现象BEV鸟瞰图里目标位置左右颠倒画BEV鸟瞰图时点云里的车明明在图像左侧鸟瞰图里却出现在右侧整个场景像是照了镜子。原因是雷达坐标系的y轴方向和BEV图显示坐标的y轴方向相反。KITTI的velodyne坐标系里x轴朝前y轴朝左z轴朝上。大多数绘图库画散点图时默认的y轴是朝上的直接拿x和y画图视野方向就反了。解决方法是把y轴取反也就是velo_y -velo_y或者旋转绘图坐标系的视角。我一般习惯在绘图前统一做一次坐标变换x对应图像水平方向y取反后再做归一化。还有一个附带问题BEV图里目标朝向会跟着取反判断朝向时要对rotation_y做符号修正这个细节不做的话画出来的箭头方向会和真实运动方向差180度。4.3 现象3D框画出后与点云不贴合框内点不该有的也有3D框投影到图像上看起来和图像里的车重合但放到点云里看框和点云总是错开半米有的点明明在框外却显示在框内。原因是location字段的含义理解偏差。KITTI的location是3D框中心点在相机坐标系下的坐标单位是米但x、y、z分别对应右、下、前。有的人把location里的y当成了向上的高度画出来框就会整体下沉到地面以下。另外尺寸h、w、l的顺序是固定高宽长但KITTI的宽是指车的宽对应相机坐标的z方向长对应x方向如果不做映射框的长度和宽度就对调了。解决方法是写一个坐标系映射表相机坐标的x对应3D框角点计算里的l方向z对应w方向y对应h方向。画之前先在坐标系里验证一下框的中心点是否落在点云目标的几何中心附近如果偏差超过阈值优先检查location和rotation_y不要上来就调可视化参数。4.4 现象截断与遮挡参数没处理边界框挤在图像边缘图像边缘的目标2D框画出来后一半在画面外看起来像是标注错了和图像内容对不上。原因是这类目标的truncation字段本来就大于0表示目标有一部分在图像外。KITTI的标注框坐标可以超出图像边界画图时OpenCV能画但显示很怪初学者很容易误判成数据问题。解决方法是画框前先读truncation和occlusion字段把截断目标用不同的颜色画并且把框坐标clamp到图像范围内。我一般用橙色画截断目标绿色画正常目标灰色画遮挡严重的这样一眼就能区分是标注状态还是真实错误。4.5 现象Velodyne点云与图像时间戳没对齐播放序列闪烁抖动序列播放时图像和点云错开了一帧车辆位置跳变严重点云和图像的内容总是差一拍。原因是下载的KITTI原始数据有时会出现帧号不连续的情况而object detection的training数据是按帧号对齐的如果混用了raw data和object data的目录文件编号的对应关系就会乱掉。解决方法是写一个文件清单检查读取image_2和velodyne目录下的所有文件名求交集后再排序播放器只遍历两边的共有编号。我遇到过某个序列的velodyne目录缺了十几帧如果播放器不做交集画面就会黑屏卡顿做了交集后播放就连续了。这个检查在项目里已经成了我的固定习惯。5. 把可视化做成调试工具交互选点、轨迹比较与数据体检5.1 交互式点选鼠标点击像素反查点云坐标与label静态画框只能单向看数据交互式点选可以让可视化变成调试工具。我常做的一个功能是鼠标点击图像上的一个点反查出对应的雷达点坐标、最近距离和所属目标类别。实现不复杂给OpenCV窗口挂一个setMouseCallback回调在回调里取像素坐标然后遍历已经投影好的点云找距离最近的点回显它的雷达坐标和深度值。def on_mouse(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: dist np.linalg.norm(proj_pts - np.array([x, y]), axis1) idx np.argmin(dist) print(f像素({x},{y}) - 雷达坐标 {velo[idx]} 深度 {velo_depth[idx]:.2f}m)这段代码里最值得关注的是proj_pts必须和velo数组保持下标一致。如果投影时做过mask过滤这里同样要过滤否则查出来的点不会对应真实的雷达点。这个bug我踩过查了半天才发现是投影点集和原始点集长度对不上。5.2 轨迹对齐用可视化比较SLAM轨迹与KITTI真值KITTI可视化项目还有一个高频需求把ORB-SLAM2这类SLAM算法跑在KITTI数据集上输出轨迹再用可视化去和KITTI自带的地面真值对比。做法是先把SLAM输出的轨迹读到内存再叠加到KITTI的俯视图上用线条把两条轨迹分别画出来。这个需求在KITTI数据集相关检索里排得很靠前说明做SLAM轨迹评估的人特别多。对比时最核心的坑是真值的坐标基准。KITTI的ground truth轨迹用的是UTM坐标系SLAM输出通常是相对起始位置的局部坐标两者直接画在一起会错位得离谱。我一般先算一个初始位姿对齐把SLAM轨迹的第一帧旋转平移到真值的第一帧上再看两条轨迹的偏差。这个步骤如果做得不严谨后面的ATE/RPE指标全是白算的。5.3 从可视化项目到数据体检工作台做得再多之后可视化项目就不再是“画给你看”的玩具代码了而是我处理KITTI数据的工作台。我会把2D框、3D框、点云投影、BEV视图、交互选点和轨迹对齐都封装成一个类每次拿到一批新数据先跑一遍可视化体检图像能不能打开、点云尺寸是否异常、标定矩阵是否有缺失、标注框和点云是否大致吻合。这些检查能提前拦截很多下游模型训练的问题比盯着loss曲线猜原因靠谱得多。做这个项目我最大的血泪经验是坐标系变换的每一步都要留校验接口。宁可多画一张调试图也不要等到模型精度上不去才开始怀疑数据读错了。KITTI可视化项目是自动驾驶感知入门性价比最高的一个方向它逼着你把每个数据字段、每个矩阵乘法的物理含义彻底搞懂。希望这篇笔记帮到你少踩我当年踩过的那些坑。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询