做自动驾驶视觉方向的人,几乎都绕不开KITTI数据集。它算是这个领域最经典的benchmark之一,哪怕现在nuScenes、Waymo Open Dataset这些更大规模的数据集陆续出来,KITTI依然是很多论文对比baseline时必跑的一项,也是不少人入坑目标检测、多传感器融合、SLAM时第一个拿来练手的数据集。这篇博文就围绕“KITTI数据集下载及解析”这个核心,把从官网获取数据到读懂标定文件、再到用代码解析点云和标注的完整流程,一次性讲清楚。
文章适合这两类人看:一类是刚接触自动驾驶视觉、想拿KITTI跑通检测或点云处理流程的同学,另一类是已经跑过开源代码,但被数据格式、标定矩阵、train/val划分这些细节坑过,想彻底把底层数据搞明白的从业者。你可以直接照着下面的步骤操作,也能把每一节的解析思路当作手册来查。
1. KITTI数据集离你有多远:先搞清楚它到底是什么
1.1 全景认识:KITTI不是一套数据,而是好几套
KITTI由德国卡尔斯鲁厄理工学院和丰田美国研究院联合发起,采集车是一辆大众旅行车,搭载了2部灰度相机、2部彩色相机、1个Velodyne HDL-64E激光雷达,以及GPS/IMU惯性导航系统。车在卡尔斯鲁厄及周边跑了多圈,覆盖市区、乡村、高速公路等多种道路场景。
很多人第一次接触KITTI时,会被官网那一堆下载链接搞晕。其实它主要包含几个相对独立的子集,彼此之间数据内容不同,用途也不同:
- Object Detection:最常见的目标检测子集。训练集一共7481张图,测试集7518张图,提供2D框、3D框、截断度、遮挡程度等标注,是无数检测论文的擂台。
- Tracking:目标跟踪子集。21个训练序列和29个测试序列,每帧都有关联到同一目标的track id,适合做多目标跟踪(MOT)验证。
- Odometry:视觉里程计和SLAM子集。包含11个带有真值轨迹的序列(00~10),其中00~06用于训练、07~10用于测试。很多SLAM论文里的KITTI轨迹图就来自这里。
- Depth Completion/Depth Prediction:深度补全与深度估计子集。提供稀疏激光雷达投影的深度图和稠密真值深度图(部分场景),适合做深度恢复任务。
- Raw Data:未经过裁剪处理的原始数据序列。每个序列包含图像、点云、GPS/IMU数据以及标定文件,OD和Tracking子集就是从Raw Data中挑选并标注得到的。
- Semantic Segmentation:语义分割子集,标注了路面、车辆、行人等类别,适合做驾驶场景分割。
不同子集对应不同任务,下载时不要一次性全下,否则几百GB就没了。实际使用中,做目标检测的人通常只下载Object Detection的左右彩图和label,做SLAM的人主要下载Odometry或Raw Data里的某几个序列。先想清楚自己要做什么任务,再决定下哪个子集,可以少走不少弯路。
1.2 目录结构:下载前先盘一盘数据长什么样
KITTI数据的一个重要特点是,不同子集的目录组织习惯不完全一致,解析代码一旦没适配就会读错文件。以Object Detection为例,官网解压后通常是这样的结构:
object/ ├── testing/ │ ├── calib/ │ ├── image_2/ │ └── velodyne/ ├── training/ │ ├── calib/ │ ├── image_2/ │ ├── image_3/ │ ├── label_2/ │ ├── velodyne/ │ └── planes/ └── train.txt / val.txt / trainval.txt几个关键目录的含义:
image_2:左侧彩色相机图像,PNG格式,分辨率一般是1242×375,是检测任务中的主图像。image_3:右侧彩色相机图像,格式相同,立体匹配任务中会用到。velodyne:激光雷达点云数据,一个bin文件对应一帧,每个点4个float32值:x、y、z坐标和反射强度。label_2:2D/3D检测标注文件,每个txt文件对应一张图的标注。calib:标定文件,记录了相机内参、激光雷达到相机的变换矩阵、IMU到激光雷达的变换矩阵等。planes:地面平面参数文件,有些3D检测方法(如基于地面的来选proposal)会用到。
Odometry子集的结构又不一样。它按序列存放:
dataset/ ├── poses/ ├── sequences/ │ ├── 00/ │ │ ├── image_0/ │ │ ├── image_1/ │ │ ├── image_2/ │ │ ├── image_3/ │ │ ├── velodyne/ │ │ └── calib.txt这里的image_0和image_1是灰度相机,image_2和image_3才是彩色相机;calib.txt里存放的是这个序列统一的标定参数;poses目录下是00~10序列的轨迹真值。新人在这个子集上最容易犯的错误,就是把灰度图当成彩色图送去训练,或者把image_0当成左图。
Raw Data的下载方式又不一样。官网不是让你直接下载整个包,而是按sequence逐一下载,一个zip里通常包含某个序列的完整数据,包括image_00到image_03、velodyne_points、oxts等。如果是跑SLAM,通常只需要某个序列的彩色图、点云和标定就够了,没必要下载全部序列。
2. 下载实操:从官网走到本地硬盘
2.1 选对下载方式,速度和可靠性差很多
KITTI官网的下载页面把各子集整理成表格,点击对应条目就能直接下载。对于国内用户来说,裸连官网的速度普遍一般,经常出现几KB/s甚至连接失败的情况。
这里需要先说明一点,我下面讲的都是“不借助任何额外网络工具”的合法合规下载方式。优先推荐的办法是去国内的合规学术数据平台和镜像站找,很多高校和开发者已经做过搬运。比如OpenDataLab上就有KITTI的完整镜像,这种平台在国内访问速度快,而且做了目录整理,下载起来比官网省心很多。另外一些大厂的AI开放平台也会不定期提供KITTI等数据集的下载入口,搜索“KITTI数据集 镜像下载”就能找到不少可用资源。
如果你坚持用官网,建议用支持断点续传的下载工具,不要直接用浏览器单线程下载。因为KITTI单个文件动辄几GB,比如Object Detection的velodyne压缩包就超过了8GB,网络稍有波动就会断,普通浏览器断了就得从头再来,非常折磨人。
2.2 我用下来的下载流程与断点续传操作
我自己常用的流程是这样的:
- 打开KITTI官网的数据集下载页面,找到目标子集。
- 复制对应文件的下载链接。这里有个小技巧,很多浏览器的下载管理里可以直接查看文件地址,或者右键链接复制链接地址。
- 启动下载工具创建新任务,粘贴链接。下载工具设置成多线程分段下载,同时开启断点续传和重试机制。
- 下载完成后立即校验大小。KITTI官网每个文件旁边会标明文件大小,如果本地文件与官方标称体积不一致,基本就是下载损坏了,建议重下。
- 解压前再确认一次。部分下载工具会在下载完成后自动校验哈希,如果没有这个功能,可以用md5sum一类的命令手动校验。
如果只是想先跑通一个小demo,我强烈建议不要下载完整数据集。KITTI OD完整训练集压缩包十几个GB,解压后更大。很多任务只需要几百帧数据就能验证流程跑得通。这时候可以去GitHub或一些教学项目里找“mini KITTI”或“KITTI sample”,通常只有几十MB,包含了完整的目录结构和标定文件,足够把解析和可视化的代码跑通。
2.3 下载避坑记录:这些坑我踩过
有以下几点值得单独提醒:
- 别用移动硬盘直接解压大压缩包。KITTI的OD velodyne包解压后约20GB,如果移动硬盘是FAT32格式,单文件超过4GB就会报错。解压前先确认磁盘格式是NTFS或exFAT。
- 下载别只下image_2。很多人做2D检测,觉得只要有图像和label就够了,结果后来想试试多模态融合或者画3D框投影,发现没有velodyne和calib,只能回头重新下载。建议核心文件一次下齐:image_2、label_2、velodyne、calib。
- train.txt和val.txt这两个小文件经常被忽略。它们是KITTI官方划分好的训练/验证集索引,里面每行是一个样本编号。很多开源代码默认读取这两个文件来划分数据集,没有它们程序直接就崩了。
- 文件损坏的事经常发生。官网直连下载时数据包损坏的概率并不低,表现是解压时报
CRC failed或者某一帧图像根本打不开。这时候不要硬着头皮用,要重新下载损坏的那一个文件。
3. 数据标注与标定文件全解析
3.1 Object Detection的label格式:每行数字都是什么
KITTI的目标检测标注文件label_2/xxx.txt看起来是一堆数字和字母,但它的格式非常固定,一行代表一个3D目标。标准字段依次是:
type truncation occlusion alpha bbox_left bbox_top bbox_right bbox_bottom dim_height dim_width dim_length location_x location_y location_z rotation_y其中:
type:物体类别,常见有Car、Pedestrian、Cyclist、Van、Truck等。不同代码对类别的处理习惯不同,有些会把Van和Truck都归为Car。truncation:截断程度,0.0表示完整可见,1.0表示物体大部分被截断或裁剪出画面。occlusion:遮挡状态,0表示完全可见,1表示部分遮挡,2表示大部分遮挡,3表示未知,一般推理时不会去回归occlusion。alpha:观察角度,指物体相对于相机光轴的朝向角度,注意它并不是物体在世界坐标系里的朝向角rotation_y,两者之间存在换算关系。bbox:2D框的左上角和右下角像素坐标,单位是像素。dim:3D尺寸,依次是高度、宽度、长度,单位是米。注意这里是“高宽长”的顺序,而不是常用的“长宽高”。location:3D框中心点在相机坐标系下的坐标,单位是米。其中x向右,y向下,z向前。rotation_y:物体在相机坐标系下绕y轴的朝向角,范围通常是-pi到pi。
很多人会在这里犯晕,分不清alpha和rotation_y。简单来说,rotation_y是物体朝向与相机z轴(正前方)之间的夹角,和物体在图像中的位置无关;alpha则是将物体朝向角减去物体相对相机光轴的水平角度后得到的结果,它决定了你从某个视角看过去,物体呈现的侧向姿态。3D检测的论文和代码里,计算投影、画3D框时几乎都用rotation_y,而alpha更多用于数据集统计和可视化。
3.2 标定文件:KITTI的灵魂
KITTI解析中最重要的就是标定文件。在Object Detection子集里,每个样本对应一个calib/xxx.txt,文件内容类似下面这样(数字只是示例):
P0: 7.070912e+02 0.000000e+00 6.018873e+02 0.000000e+00 0.000000e+00 7.070912e+02 1.831104e+02 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 P1: ... P2: 7.070912e+02 0.000000e+00 6.018873e+02 -4.688783e+01 0.000000e+00 7.070912e+02 1.831104e+02 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 P3: ... R0_rect: 9.999239e-01 9.837760e-03 -7.445048e-03 ... Tr_velo_to_cam: 7.533745e-03 -9.999714e-01 -6.166020e-04 ... Tr_imu_to_velo: 9.999976e-01 7.553071e-04 -2.035826e-03 ...这里的每个矩阵含义要分清:
P0、P1、P2、P3:分别是4个相机(0号到3号)的投影矩阵,尺寸3×4。P2是最常用的左彩色相机投影矩阵,它直接完成“世界/相机坐标 → 图像像素坐标”的映射。R0_rect:是0号相机的校正旋转矩阵,尺寸3×3。因为激光雷达和相机是外置的,两者坐标系不完全平行,激光点要先旋转到图像平面方向,才能和像素对应上。Tr_velo_to_cam:激光雷达到相机的平移旋转矩阵,尺寸3×4。它把激光雷达坐标系下的点变换到相机坐标系。Tr_imu_to_velo:IMU坐标系到激光雷达坐标系的变换矩阵,尺寸3×4。用GPS/IMU做轨迹融合时才常用到。
在文档和社区里,这三种矩阵一般写作:R0_rect是3×3旋转矩阵,Tr_velo_to_cam是3×4外参矩阵,P2是3×4投影矩阵。进行点云投影时,它们的组合方式是固定的,我在下一节直接给出公式。
3.3 把点云投影到图像:一次完整的坐标变换
激光雷达三维点投影到左彩色图像(image_2)的公式是:
[ u, v, 1 ]^T = P2 * R0_rect * Tr_velo_to_cam * [ x, y, z, 1 ]^T具体到实际代码里,我会对计算顺序和矩阵维度格外小心。步骤是:
- 把Tr_velo_to_cam补成4×4矩阵,底行是
[0,0,0,1]。 - 把R0_rect补成4×4矩阵,在右下填1,其它补0。实际上
R0_rect在上面的公式里是以4×4形式参与乘法。 - 先计算
R0_rect * Tr_velo_to_cam得到一个4×4矩阵,再用它变换点云。 - 把变换后的点云坐标(在相机坐标系下)左乘
P2,得到齐次像素坐标。 - 将像素坐标除以最后一个分量(深度),得到最终的uv像素坐标。
这里有个常见的坑:P2最后一位通常是一个非零的平移量(比如-4.688783e+01),这说明image_2相机的光心相对0号相机有偏移。如果你把点云投影结果整体偏了一个固定像素,一般就是漏了这一列的平移量。
如果你玩的是Odometry子集,calib.txt里的格式又不一样,它没有P0~P3,而是P0到P3合并成了4个3×4矩阵并按行存储,同时还有Tr矩阵。解析代码需要按不同子集分别处理,不能一套代码通用所有场景。
4. 核心代码实现:五步吃透KITTI解析
4.1 第一步:用Python读取点云bin文件
KITTI的velodyne点云文件本质上是一堆二进制float32数据,每4个float是一组(x、y、z、reflectivity),没有文件头,也没有分隔符。用numpy读取非常快:
import numpy as np def load_kitti_bin(bin_path): points = np.fromfile(bin_path, dtype=np.float32).reshape(-1, 4) return pointspoints的shape是(N, 4),第0到2列是空间坐标,第3列是反射强度。有些代码会进一步把点云裁剪到一定范围(比如只保留车前60米、左右20米以内的点),这是为了减少计算量,但对解析本身不是必须的。
如果你的点云文件读出来之后的点数量是奇数行,比如val_size对不上4的整数倍,那通常是文件下载损坏或者路径错了,而不是格式问题。正常一帧HDL-64E点云有大约12万个点,不同帧有浮动。
在C++里用PCL库读KITTI bin也很常见,核心就是先把文件读入内存再填充PCL点云结构:
std::fstream input(bin_path.c_str(), std::ios::in | std::ios::binary); for (int i = 0; input.good() && !input.eof(); ++i) { input.read((char *) &point, sizeof(point)); cloud->points.push_back(point); }4.2 第二步:可视化点云,确认数据没问题
解析完点云后最好先可视化一帧,确认数据方向、范围都正常。用Open3D比较简单:
import open3d as o3d def visualize_bin(bin_path): points = load_kitti_bin(bin_path) pcd = o3d.geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(points[:, :3]) o3d.visualization.draw_geometries([pcd])如果点云整体只出现在一个很小的扇形区域,先别急着怀疑数据,可能是没做intensity归一化导致的显示问题。KITTI的反射强度没有固定单位,直接用原始值画图,颜色会集中在某一段,视觉上就像只有一小块有数据。你可以在渲染时用intensity的百分比分位数做归一化,或者干脆只用坐标画几何结构。
Open3D里可能报“GLFW error”之类的窗口渲染错误,这在远程Linux服务器上尤其常见。这时候不要硬调显示,可以把点云存成ply或pcd文件下载到本地用CloudCompare等软件打开,效果一样。
4.3 第三步:画出2D标注框,熟悉label坐标
读取label并直接在图像上画框,是检验解析流程最简单的方式:
import cv2 def draw_kitti_label(image_path, label_path): img = cv2.imread(image_path) with open(label_path, 'r') as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) < 15: continue obj_type = parts[0] bbox = [float(x) for x in parts[4:8]] x1, y1, x2, y2 = map(int, bbox) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, obj_type, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img注意,KITTI的bbox坐标是float,但像素坐标必须是整数才能用于OpenCV绘图。有些论文代码在这里直接int()截断,会导致框线略微偏移1个像素,视觉上基本无感,但如果你要精确计算IoU,建议用float计算,只在最后画图时取整。
这个可视化过程非常有价值。你可以把几张典型的图拉出来看一眼,如果发现框明显偏离目标,往往是label文件与image不是同一帧,或者类别顺序踩到了不同的解析约定。如果能看到框稳定贴合目标,说明数据下载和解析链路都正常,接下来才值得继续做模型训练。
4.4 第四步:坐标变换与点云投影完整代码
把点云投影到2D图像的代码,我会按下面这样实现:
import numpy as np def project_velo_to_image(points, calib_dict): # points: (N, 4) in velodyne coordinates P2 = np.reshape(calib_dict['P2'], (3, 4)) R0 = np.reshape(calib_dict['R0_rect'], (3, 3)) Tr = np.reshape(calib_dict['Tr_velo_to_cam'], (3, 4)) # 补成4x4 R0_4 = np.eye(4) R0_4[:3, :3] = R0 Tr_4 = np.eye(4) Tr_4[:3, :] = Tr # 齐次坐标 pts = points[:, :3] pts_h = np.hstack([pts, np.ones((pts.shape[0], 1))]) # velo -> cam -> rect -> pixel cam = (Tr_4 @ pts_h.T).T rect = (R0_4 @ pts_h.T).T # 更严谨:rect部分应使用cam坐标,但很多KITTI代码直接用原始点云经过combined矩阵 # 常用思路:一次性求出4x4矩阵 combined = P2 @ R0_4 @ Tr_4 uv_h = (combined @ pts_h.T).T # 归一化 u = uv_h[:, 0] / uv_h[:, 2] v = uv_h[:, 1] / uv_h[:, 2] depth = uv_h[:, 2] return u, v, depth这里有一个比较“玄学”但实际影响很大的细节:标定文件里R0_rect是3×3,Tr_velo_to_cam是3×4,P2是3×4。很多开源代码在读取时没有补全4×4,直接做P2 @ R0_rect @ Tr_velo_to_cam也是能算的,因为numpy会自动广播维度,但结果可能错得悄无声息。我建议你始终手动补成4×4,并在关键维度上打印检查。
如果你只想投影到图像,不关心深度,那可以把公式简化为一次性求出3×4的proj_matrix = P2 @ R0_rect @ Tr_velo_to_cam,然后对点云坐标[x, y, z, 1]直接矩阵乘法。这样每一步都能对应到上一节公式里的矩阵,避免后续维护时混乱。
4.5 第五步:理解train/val划分与真值轨迹
KITTI Object Detection的train.txt、val.txt和trainval.txt分别对应官方划分好的训练集、验证集和训练验证集。很多人不知道,KITTI砍掉了部分带标注的样本数量,各个文件的行数也不同:
trainval.txt —— 7481 行 train.txt —— 3712 行 val.txt —— 3769 行在实际训练中,如果你用了某些开源Repo,它可能默认按trainval.txt训练、按train.txt做验证,这类约定与自己的实验设置不同时,模型评估结果会有明显差异。所以拿到别人的KITTI训练代码,第一件事就是看一下它读的是哪个split文件,训练集和验证集是否有重叠。
Odometry子集的poses/下存储的是每个序列每帧的位姿真值。每行12个数字,可以reshape成一个3×4矩阵,表示当前帧相机在全局坐标系下的位姿:
pose = np.array(line.strip().split(), dtype=np.float32).reshape(3, 4)注意这个pose是“世界到相机”还是“相机到世界”的方向,官方文档用的是T_w_cam,即从相机坐标系变换到世界坐标系的矩阵。视觉SLAM中经常需要把它取逆来得到相机在世界中的位置。如果轨迹画出来是倒着的或者镜像的,无非是矩阵转置方向搞反了,别慌,换个方向就能解决。
5. 常见问题与排查技巧实录
5.1 下载过程高频问题
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 官网下载非常慢 | 网络链路问题,与数据集本身无关 | 换国内合规学术平台或镜像站;用支持断点续传的工具分线程下载 |
| zip文件解压报CRC校验错误 | 文件下载不完整或传输损坏 | 对比本地文件与官网标称大小;重新下载损坏的文件,不要硬解 |
| 序列目录缺失部分传感器数据 | 某些数据包需要单独下载 | 检查官网目录结构,Raw Data按序列拆分下载,别漏了oxts或velodyne |
| 解压后某帧图像打不开 | 文件损坏,常见于点云和图像包 | 换源重新下载该文件,不要因为只有一个文件损坏就忽略 |
再补充一个容易混淆的点:官网不同版本的Object Detection数据,在标注内容上也有细微差别,比如早期版本可能少部分帧缺少planes文件。如果你的代码强制读取所有planes文件,可能在中途报错。解决办法是解析时先判空,或者跳过缺失项。
5.2 解析时最容易踩的坑
- 矩陈维度不对但没有报错:这是KITTI解析里最坑的一个问题。numpy里
(3,4)矩阵乘(4,3)矩阵不会直接引发异常,但结果完全错误。建议只用一个固定的封装函数处理标定文件,统一把3×4补齐成4×4再参与运算。 - 图像坐标系y轴方向忘记翻转:KITTI相机坐标系的y轴向下,所以标注的
location_y是负值,而点云坐标的y轴指向地面。投影时一旦用错符号,点云会垂直翻转,标签框和点云对不上。 - 类别名称不一致:有的代码把所有非车辆类别都归类为背景,有的代码只保留
Car。复现论文时,模型的mAP对类别映射非常敏感,跑之前一定要确认类别列表和原论文一致。 - 忽略反射强度通道:3D目标检测里,常见的PointPillars、SECOND等模型会把反射强度当成特征送入网络。解析时如果把intensity丢了或者归一化方式不同,训练出来的模型性能可能和论文差一大截。
5.3 训练与可视化中的心得
在训练前先做一个“数据体检”可以省很多时间:随机抽20帧,把2D标注框、点云投影框、标定文件全部可视化,肉眼确认过再进train loop。这一步听起来麻烦,实际只花十分钟,能提前暴露路径错误、split配置错误、标定矩阵读错等问题。
另外,KITTI虽然老,但它对数据格式的定义非常规范,等于给自动驾驶视觉立了一个“格式教科书”。你今天看懂了KITTI,再去看nuScenes的sample_data结构、Waymo的tfrecord序列化设计,很多概念都是相通的。我还是建议有条件的人把KITTI的OD子集完整下载下来跑一遍经典检测算法,完整地解析一遍标注和标定数据,这个过程中的收获比只看文档大得多。
最后分享一个实用习惯:我会在自己的代码仓库里留一个kitti_utils.py,里面只放读取标定、加载点云、投影点云、画框这四类函数,每个函数十几行,不引第三方深度学习库。这样不管以后换什么检测框架,解析层代码都不用重写,直接复制过来就能用。这一步节省的时间,后期会让你觉得特别值。