KITTI数据集详解:从2D目标检测到3D与SLAM评测的完整实操指南 1. 为什么KITTI至今还是自动驾驶算法评测绕不开的擂台聊到KITTI数据集大多数做自动驾驶视觉或者SLAM的人都不会陌生。这个由卡尔斯鲁厄理工学院和丰田美国研究院在2012年前后发布的基准数据集直到现在依然是各大论文里出现频率最高的评测平台之一。很多刚入门的同学会问现在NuScenes、Waymo Open Dataset这些新数据集规模更大、场景更丰富为什么还有这么多人拿KITTI测试算法的性能我的理解是KITTI的江湖地位不在于它数据量有多大而在于它把评测这件事定义得非常清晰而且整个数据组织方式非常适合算法快速迭代验证。我最早接触KITTI是在跑视觉SLAM的时候当时用的是odometry序列。后来转到目标检测方向又开始在KITTI Object Detection benchmark上做算法对比。前后用了很多年最大的感受是KITTI不是一个数据集而是一整套评测体系。它包含了传感器标定参数、真值标签格式、官方评测协议、不同难度分级甚至连数据怎么划分、结果怎么提交都有明确规定。这种把评测环境标准化的思路才是它最值钱的地方。如果你打算用KITTI测试不同算法的性能我建议先搞清楚几个问题KITTI到底包含哪些子数据集每个子数据集对应什么任务评测指标是什么数据怎么下载、目录怎么组织算法输出格式怎么转换成官方能识别的格式这些问题如果没想清楚很容易出现跑完了结果却没法横向对比的尴尬局面。这篇内容我会按我自己实操的流程把这些东西完整过一遍包括一些踩过的坑和值得注意的细节。2. 先搞清楚KITTI数据集的内部结构下载和解析才不会懵2.1 采集平台与传感器配置决定了数据形态KITTI的采集平台是一辆大众Passat旅行车车顶装了Velodyne HDL-64E激光雷达、两台PointGrey灰度相机和两台彩色相机车身还配备了GPS/IMU惯性导航系统。这套传感器配置直接决定了KITTI数据的形态图像有灰度也有彩色点云是64线旋转扫描同时还带有精确的位姿信息。所以KITTI能支撑的任务非常广2D/3D目标检测、语义分割、深度估计、光流与场景流、视觉里程计、SLAM等都有对应的benchmark。我之前带过一个实习生第一次下载KITTI时只下了相机图像没下calib标定文件后面做点云投影到图像实验时才发现缺了关键一环又回头补数据。所以如果你要测的目标是多种算法比如同时对比2D检测、3D检测、深度估计一开始就把raw data、calib、devkit这些全部下载好省得后面来回折腾。KITTI官网按benchmark分门别类提供下载包比如Object Detection的left color images、velodyne point clouds、training labels、calibration files等每个包都是独立压缩文件按需下载就行。2.2 目录结构与文件命名规则KITTI Object Detection数据集的核心目录是这样的KITTI/object/ ├── testing/ │ ├── calib/ │ ├── image_2/ │ └── velodyne/ └── training/ ├── calib/ ├── image_2/ ├── label_2/ └── velodyne/其中image_2存的是左侧彩色相机图像image_3是右侧彩色相机部分数据集也包含灰度图image_0和image_1velodyne存的是激光雷达点云每个文件是一个.bin二进制文件label_2存的是目标检测真值标签calib存的是标定参数。文件和文件之间靠前缀数字一一对应比如000000.png、000000.bin、000000.txt这组文件描述的是同一个时刻的同一场景。training目录下大约有7481个样本testing目录下大约有7518个样本。很多人第一次下载后会去核对文件数量这是好习惯。我自己会写一个简单的Python脚本把图像、点云、标签、标定文件的数量都扫一遍确认没缺没漏再做后续工作。别嫌这一步繁琐数据集不完整导致训练报错或者评测结果异常的案例我见得太多了。2.3 标签格式与标定文件解读标签文件每一行代表一个目标对象格式如下类型 截断率 遮挡状态 观察角度 bbox_left bbox_top bbox_right bbox_bottom 高度 宽度 长度 3D中心x 3D中心y 3D中心z 旋转角yaw以Car 0.00 0 -1.57 600.00 180.00 650.00 220.00 1.50 1.60 4.00 -2.00 1.50 10.00 -1.57为例前面的Car是类别第二个数字表示截断程度0到1之间第三个数字表示遮挡状态0、1、2、3分别表示完全可见、部分遮挡、大面积遮挡、未知第四个数字是观测角度后面四个数字是2D边界框再后面是3D物体的尺寸和中心点坐标最后一个数字是绕y轴的旋转角。如果某一行是DontCare表示这片区域里有目标但未标注训练时需要忽略评测时也不应该在这里产生检测结果。标定文件是另一种关键输入。KITTI的calib目录下每个.txt文件包含P0、P1灰度相机的投影矩阵P2、P3彩色相机的投影矩阵R0_rect相机0的校正旋转矩阵Tr_velo_to_cam激光雷达坐标系到相机坐标系的变换矩阵如果你要做点云和图像的融合或者做3D检测这几个矩阵是绕不开的。我自己在写坐标转换代码时踩过一次坑直接拿P2和Tr_velo_to_cam相乘去投影点云结果图像上全是乱的后来发现R0_rect这个校正矩阵需要在Tr_velo_to_cam之后、P2之前乘进去顺序反了结果完全不对。这个细节后面我会专门展开讲。3. 测试所有算法之前先统一评测协议和数据划分3.1 官方评估指标AP、mAP和难度分级很多人在KITTI上对比算法性能时只看一个mAP数字这其实是比较粗糙的。KITTI官方对2D目标检测的评估基于PASCAL VOC风格的AP计算但和COCO的评测方式有明显区别。官方devkit里使用的是11点插值AP也就是在召回率轴上取11个等间隔点做插值然后求平均。后来社区里很多评测工具扩展了40点、101点版本比如kitti-object-eval-python就支持R11和R40两种计算方式。不同实现算出来的AP数值是有差异的所以写论文或者对比算法时必须注明你用的是哪种AP计算方式。KITTI的检测难度分为三个等级难度级别最小边界框高度最大遮挡程度最大截断比例Easy40像素完全可见15%Moderate25像素部分遮挡30%Hard25像素难以辨认50%这个分级非常重要因为同一个算法在三个难度下的AP可能差异很大。很多模型在Easy上表现不错但到了Moderate和Hard上掉点严重。评测时如果只报告Easy的AP你很难判断算法在真实道路场景下的鲁棒性。我自己的习惯是三个难度都看重点比较Moderate——这个等级最接近实际道路中常见的遮挡情况也是论文里最常被引用的指标。还有一个容易忽略的点KITTI检测任务只评测Car、Pedestrian、Cyclist三个类别而且每个类别单独计算AP最后取平均得到mAP。标签里出现的Truck、Van、Tram等类别在评测时不在官方统计范围内。如果你自己训练模型时把所有标签类别都拿去算mAP那和官方数值之间会产生很大出入横向对比就没有意义了。3.2 数据划分训练集和验证集到底怎么切KITTI官方并没有像COCO那样给出固定的train/val划分文件而是把7481张训练图片放在一起。社区里比较常用的做法是用000000到006180的图片做训练006181到007480做验证这个划分源自早期一些论文后来沿用的人很多。也有的工作会自己随机划分或者用官方在挑战赛里提供的划分文件。这里要特别提醒一点如果你拿到的模型权重是在某个特定划分下训练的评测时就不要随意换验证集否则对比出来的性能差异可能来自数据分布差异而非算法本身。我自己在测试不同算法时会专门维护一个统一的数据划分文件所有模型都在同样的训练集和验证集上跑这样出来的结果才有可比性。另外标签里的DontCare区域需要在训练和评测时单独处理训练阶段一般直接忽略推理阶段如果模型在DontCare区域输出了高置信度检测框评测工具通常会按误检处理所以有些算法会在后处理时加一个抑制DontCare区域检测结果的逻辑这也会影响最终分数。3.3 评测工具选型KITTI官方提供的是基于C的devkit你需要自己编译并传入检测结果文件。这个流程对新手来说有点繁琐而且拿到的只是命令行输出可视化程度有限。现在社区里常用的替代工具是kitti-object-eval-python它是KITTI官方评估的Python实现接口清晰支持批量评估还能输出每个类别的详细AP。另一个方案是用mmdetection3d或者OpenPCDet这类工具箱自带的KITTI评估接口它们通常已经内置了指标计算和数据格式转换省不少事。我的建议是如果只是快速验证模型效果用kitti-object-eval-python就够了如果要做3D检测或者多模态融合直接用OpenPCDet这类框架更省心。不过无论用哪套评估工具都要确认工具的AP计算方式、难度分级、类别定义和官方一致否则结果还是会偏。4. 在KITTI上对比2D目标检测算法完整的实操流程4.1 环境配置与模型选型我自己做算法对比时通常会选三个不同技术路线的代表模型一个单阶段检测器比如YOLOv5或YOLOv8、一个双阶段检测器Faster R-CNN、一个基于Transformer的检测器DETR或Deformable DETR。这样对比出来的结果能覆盖不同设计思路的性能差异比单纯在同一类模型里调参更有参考价值。环境配置方面我习惯用conda创建独立环境避免依赖冲突conda create -n kitti-benchmark python3.9 conda activate kitti-benchmark pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install pykitti opencv-python pillow numpy pandas tqdm如果你要跑YOLOv5直接把仓库clone下来再装requirements即可。跑DETR等模型时注意PyTorch版本要和模型代码兼容我遇到过用新版PyTorch跑旧代码导致torchvision.ops.nms报错的情况这类问题排查起来比训练还耗时。我的习惯是每个模型用独立的conda环境虽然占点磁盘空间但省心。4.2 KITTI标签转YOLO格式的关键细节KITTI的标签格式和YOLO训练所需的格式并不一致需要转换。YOLO格式要求每行是类别id 中心x 中心y 宽度 高度其中坐标都是相对于图片宽高的归一化值。转换时要特别注意的是KITTI标签里的bbox是左上角和右下角坐标不是中心点加宽高类别id要和YOLO配置文件里的类别顺序一一对应。举个例子如果YOLO模型的类别顺序是[Car, Pedestrian, Cyclist]那KITTI标签里的Car就要映射成0Pedestrian映射成1。我写过一个转换脚本核心逻辑大概是import numpy as np def kitti_to_yolo(label_path, img_width, img_height): with open(label_path, r) as f: lines f.readlines() yolo_lines [] class_map {Car: 0, Pedestrian: 1, Cyclist: 2} for line in lines: parts line.strip().split() cls_name parts[0] if cls_name not in class_map: continue left, top, right, bottom map(float, parts[4:8]) x_center (left right) / 2 / img_width y_center (top bottom) / 2 / img_height width (right - left) / img_width height (bottom - top) / img_height yolo_lines.append(f{class_map[cls_name]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) return yolo_lines转换时有个容易忽视的点KITTI图像不是固定尺寸的大多是1242x375左右但不同样本可能略有差异。如果你用YOLO的默认训练配置它会做letterbox缩放这时候标签归一化坐标是按原始图尺寸算的训练时模型内部会把图像统一缩放到输入尺寸只要归一化坐标转换正确就不会有问题。但如果你在训练前手动resize了图片标签也必须按resize后的尺寸重新归一化这个顺序错了模型基本学不出来。4.3 训练与推理训练参数我一般不会照搬COCO的训练配置因为KITTI数据量小、类别少、场景相对单一。以YOLOv5为例我常用的配置是img_size640、batch_size16、epochs50关闭马赛克增强或者降级使用因为KITTI目标较小过强的数据增强反而容易让小目标学不好。训练完成后用detect.py跑验证集开启--save-txt保存检测结果用--save-conf保存置信度方便后续评估工具读取。推理阶段还有一个细节如果模型输入尺寸比较大比如1280FPS会明显下降评测时要把推理速度和精度放在一起看。只用AP衡量算法性能是不完整的尤其对于强调实时性的检测任务FPS和mAP是trade-off的关系。我的实测大致数据是YOLOv5s在KITTI验证集上mAP50能到75到80之间FPS在RTX 3090上能到100以上Faster R-CNN的AP略高一些但FPS掉到30以下DETR系列的AP在中小目标上往往不如前两者但对遮挡目标的鲁棒性有一定优势。具体数值因训练配置和输入尺寸不同会有波动但趋势基本一致。4.4 输出格式转换与官方评测YOLO输出的检测结果是YOLO格式的坐标要跑KITTI官方评测还需要转成类名 截断 遮挡 角度 x1 y1 x2 y2 3D信息...的文本格式。2D检测评测只关心2D框所以3D信息部分填-1即可。转换时要注意保留置信度但KITTI官方评测格式里并没有置信度这一列——它假设你已经做了阈值筛选和后处理。不同的置信度阈值会直接影响AP所以评测时不能随便用一个阈值最好遍历一组阈值找到最优值或者在保存结果时就按足够的阈值粒度输出多组结果。我自己在这一步上吃过亏一开始直接把YOLO的原始输出提交到官方devkitAP低得离谱后来才发现YOLO默认输出的是很多低置信度框官方评测器会把这些都算成误检。正确做法是先做NMS、再按置信度阈值过滤、再进行格式转换。把这个流程做成一个标准脚本后不管是YOLO还是其他模型都能用同一套流程转换和评测对比起来就公平了。5. 从2D检测扩展到3D检测与SLAM扩展测试的注意事项5.1 3D目标检测的评测差异如果你想更进一步用KITTI测试3D目标检测算法事情会复杂不少。2D检测的IoU是在图像平面上算的3D检测则需要计算3D边界框的IoU而且要处理旋转框。KITTI对3D目标检测的AP计算也按Easy/Moderate/Hard三个难度分级但要额外考虑目标深度、3D框中心和航向角估计的误差。除此之外还有一个BEV视角下的AP也就是把3D框投影到鸟瞰图再算IoU这个指标对车辆定位误差比较敏感。很多3D检测模型比如PointPillars、SECOND、VoxelNet在KITTI的3D检测benchmark上都有公开的论文数据。横向对比时要注意有些论文报告的是AP_4040点插值AP有些报告的是AP_11数值上有区别不统一就没有可比性。我自己对比时统一用官方最新评估代码或者OpenPCDet里的KITTI评估模块保证所有模型用同一套计算逻辑。5.2 点云坐标系与投影的常见坑做3D检测或多模态融合时点云坐标系是必须先搞清楚的。KITTI的Velodyne坐标系定义为x轴向前、y轴向左、z轴向上而相机坐标系是x轴向右、y轴向下、z轴向前。直接把点云的x坐标当成图像的x坐标投影出来的结果必然是乱的。正确的转换是import numpy as np def load_calib(calib_file): calib {} with open(calib_file, r) as f: for line in f.readlines(): key, value line.split(:) calib[key] np.array([float(x) for x in value.strip().split()]) P2 calib[P2].reshape(3, 4) R0_rect calib[R0_rect].reshape(3, 3) Tr_velo_to_cam calib[Tr_velo_to_cam].reshape(3, 4) return P2, R0_rect, Tr_velo_to_cam def project_velo_to_image(pts_velo, P2, R0_rect, Tr_velo_to_cam): R0_rect_h np.eye(4) R0_rect_h[:3, :3] R0_rect Tr_velo_to_cam_h np.eye(4) Tr_velo_to_cam_h[:3, :4] Tr_velo_to_cam pts_cam (R0_rect_h Tr_velo_to_cam_h pts_velo.T).T pts_cam pts_cam[pts_cam[:, 2] 0] # 去掉相机后方的点 pts_img (P2 pts_cam.T).T pts_img[:, 0] / pts_img[:, 2] 1e-6 pts_img[:, 1] / pts_img[:, 2] 1e-6 return pts_img这里最关键的是矩阵乘法顺序先把激光雷达坐标转到相机坐标再做镜头畸变校正对应的R0_rect旋转最后用投影矩阵P2得到像素坐标。我见过不少人在这一步把矩阵顺序搞反结果点云投影到图像上出现镜像效果或者错位。如果你只是快速验证也可以直接用pykitti库它会帮你封装好这些转换但理解原理仍然是必要的因为很多模型代码内部要求的输入格式各不相同你需要根据实际模型调整。5.3 里程计与SLAM算法评估KITTI的odometry数据集是SLAM领域最常用的评测数据之一。它包含22个序列其中00到10序列提供真值轨迹11到21序列只提供原始数据不提供真值用于挑战赛评估。评估指标主要是ATE和RPE常用的评测工具是evo。在odometry序列上测试SLAM算法时有一个关键细节不同序列对应不同场景有的以高速道路为主有的是城市街区和弯道环境差异很大。只在一个序列上评测算法性能没有说服力最好在00到10的多个序列上分别跑算出平均ATE和RPE再做对比。我自己在测视觉SLAM时发现有的算法在直线较多的序列上表现很好但到了弯道密集的序列误差迅速累积所以多序列评测是必须的。另外要注意时间戳对齐。KITTI的raw data有不同传感器的原始时间戳odometry序列则已经做了同步处理。如果用raw data测试图像的采集频率和点云的采集频率不一致融合时必须按时间戳插值或者最近邻匹配。这一点在测试多传感器融合SLAM算法时尤其重要时间戳没对齐的话系统输出的轨迹会直接发散。5.4 类别不均衡与遮挡目标是评测中的隐藏变量KITTI检测数据集里Car的数量远多于Pedestrian和Cyclist而且Pedestrian和Cyclist的小目标、遮挡目标占比更高。所以很多模型在Car上的AP能到90以上在Pedestrian上却只有60多。评测时如果只看Car或者只看mAP很难发现模型在小目标上的弱点。我个人会把每个类别分开报告另外单独统计一下Moderate和Hard难度下各个类别的表现。如果某个模型只在Easy难度下表现好而Moderate和Hard掉点严重说明它的泛化能力还有较大问题。还有一个常被忽视的点KITTI的图像采集于德国的城市道路光照条件相对理想天气大多是晴好天气和国内一些多雨多雾地区的真实路况有差异。所以KITTI上表现好的算法换一个场景不一定依然表现好。评价算法时我会把KITTI当作一个标准起点之后再用NuScenes或Waymo Open Dataset补充验证泛化性。KITTI的优势在于小而全、环境标准化、迭代快而这恰恰也是它和真实世界复杂度之间的差距所在。6. 那些我在KITTI测试过程中反复踩过的坑6.1 评测结果对不上论文数字大概率是评测协议不一致这是最常见的问题。很多初学者在KITTI上复现论文跑出来的AP和论文报告的差了十多个点第一反应是模型没训练好。实际上更大的原因往往是评测协议差异AP是R11还是R40、bbox是原始分辨率还是letterbox后的分辨率、置信度阈值是多少、验证集划分是哪一份、有没有过滤DontCare区域、有没有把Truncated目标排除这些因素都会影响最终数字。我自己在对比算法时会把所有模型的输出全部统一转成KITTI官方格式然后用同一套官方评估代码在同一个验证集上跑。这样至少保证了所有模型之间的相对对比是公平的。如果你要和论文数据做绝对对比那就要把论文里提到的评测细节全部对齐——很多论文并没有把细节说清楚这种时候强行对比没有意义不如只对比自己在统一环境下测出来的数值。6.2 数据加载细节图像尺寸不一致与点云文件读取KITTI图像尺寸在不同样本间存在细微差异常见的是1242x375但我也见过1241x375等其他尺寸。训练时如果代码里写死了图像尺寸某些样本可能会报错或变形。稳妥做法是在数据加载时每次都读取实际尺寸边读边处理。点云方面每个.bin文件存储的是N行4列的float32数据前3列是xyz坐标第4列是反射强度。读取时用np.fromfile配合reshape(-1, 4)即可。这个文件格式没有header如果读法不对轻则数据错乱重则内存越界崩溃。6.3 训练集与验证集切分不一致导致对比失效这个问题主要出现在多人协作或者使用第三方预训练模型时。我遇到过一种情况A同学用自己划分的验证集评测YOLOB同学用另一份划分评测Faster R-CNN两个人测出来的数字放在一起对比结果YOLO的AP居然比Faster R-CNN高三四个点但换到同一份验证集上差距又反转了。原因就是两份验证集里样本难度分布差异大。所以统一评测文件、统一验证集划分、统一预处理方式是做算法对比的基本前提。我现在的做法是把划分好的train/val文件直接提交到git仓库里每次实验都基于同一份划分执行杜绝这类问题。6.4 可视化验证评测数字之外必须做的步骤指标是统计结果但统计指标不能完全代替人工视觉检查。我会在评测完模型后把检测结果和真值框画在同一张图上重点看几类典型样本严重遮挡的行人、远处的小目标、与其他车辆紧邻的车辆。有些模型在指标上表现很好但可视化后发现检测框抖动严重、定位偏了半个车身这种问题在数字上是看不出来的。我自己养成的习惯是每个模型至少抽看50到100张典型可视化结果然后再下结论。这一步虽然费时间但能帮你发现很多指标之外的算法缺陷。另外一个实用技巧把整个评测流程脚本化、参数化。KITTI测试不是做一次就结束的后面换模型、调参数、换数据增强都会需要反复评测。我把从KITTI格式转YOLO格式、训练、推理、结果转回KITTI格式、官方评估、结果汇总这几步全部封装成了脚本每次实验只需要改配置文件里的模型路径和参数跑完自动输出一个汇总表格。这套流程我用了很久效率提升非常明显。KITTI的意义就在于它是标准化的既然标准化了评测流程也应该跟着标准化而不是每次手工操作。