
做自动驾驶视觉方向的人几乎都绕不开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_22D/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两者之间存在换算关系。bbox2D框的左上角和右下角像素坐标单位是像素。dim3D尺寸依次是高度、宽度、长度单位是米。注意这里是“高宽长”的顺序而不是常用的“长宽高”。location3D框中心点在相机坐标系下的坐标单位是米。其中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.070912e02 0.000000e00 6.018873e02 0.000000e00 0.000000e00 7.070912e02 1.831104e02 0.000000e00 0.000000e00 0.000000e00 1.000000e00 0.000000e00 P1: ... P2: 7.070912e02 0.000000e00 6.018873e02 -4.688783e01 0.000000e00 7.070912e02 1.831104e02 0.000000e00 0.000000e00 0.000000e00 1.000000e00 0.000000e00 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_veloIMU坐标系到激光雷达坐标系的变换矩阵尺寸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.688783e01这说明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, dtypenp.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×3Tr_velo_to_cam是3×4P2是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(), dtypenp.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里面只放读取标定、加载点云、投影点云、画框这四类函数每个函数十几行不引第三方深度学习库。这样不管以后换什么检测框架解析层代码都不用重写直接复制过来就能用。这一步节省的时间后期会让你觉得特别值。