ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

KITTI数据集完全解析:下载、标定与点云投影实践

KITTI数据集完全解析:下载、标定与点云投影实践 很多做SLAM、自动驾驶目标检测和三维视觉的朋友第一次接触KITTI数据集时都会经历一段手忙脚乱官网页面入口多不知道下哪个链接好不容易选对了入口几十GB的数据下到一半断了下完了打开calib文件面对一堆P0、P1、R0_rect、Tr_velo_to_cam完全不知道哪个矩阵该乘在哪一步。这篇文章我把自己这几年用KITTI的经验完整梳理一遍围绕数据集的下载和解析展开从目录规划、下载方案、校准文件解读到点云投影、标签格式全部用可直接复现的方式讲清楚。无论你是准备复现论文还是要训练自己的检测模型这篇内容都能帮你少走弯路。1. 摸清KITTI的数据家底Raw Data与Benchmark怎么选很多人的第一个坑就是在官网首页直接点了Raw Data下载入口结果发现里面全是按日期和采集序列组织的文件夹和自己在论文里看到的odometry benchmark、object detection benchmark对不上。这是因为KITTI本身不是一个打包好的单一数据集而是按研究任务划分了多个独立评测项目各自有独立的下载入口和数据格式。1.1 采集平台上到底装了什么KITTI的数据采集车是一辆改装过的大众帕萨特B6车上固定安装了一套多传感器系统。搞清楚的这套配置后面理解数据格式会容易很多2个灰度相机分别叫image_00左灰度和image_01右灰度分辨率约1388x512采集帧率10Hz2个彩色相机分别叫image_02左彩色和image_03右彩色分辨率与灰度相机一致1个Velodyne HDL-64E激光雷达固定在车顶64线束10Hz旋转扫描输出点云和反射强度1套GPS/IMU组合导航系统输出车辆位姿、速度、加速度、角速度等信息这套传感器布局意味着KITTI的数据天然是多模态的图像、点云、位姿三种信息在时间上是同步的同步版数据空间上通过一系列校准矩阵关联。理解这种多模态组合是后面解析所有文件的基础。1.2 Raw Data、Odometry、Object Detection的数据差异KITTI官网的Datasets栏目下主要能看到以下几大类数据它们的差异不只是内容不同连目录结构和标签格式都完全不一样。数据模块包含内容典型体积主要用途Raw Data完整传感器原始数据含图像、点云、GPS/IMU、标定文件单个序列几GB到几十GB自定义任务、多模态研究、SLAMOdometry Benchmark灰度图、彩色图、校正后的激光雷达点云、位姿真值全部序列几十GB到上百GB视觉里程计、SLAM评估Object Detection左彩色图、3D点云、2D/3D标签、标定文件图片约12GB点云约50GB2D/3D目标检测、跟踪、YOLO类训练Tracking Benchmark连续帧图像与3D标签几十GB多目标跟踪Depth Prediction图像与稀疏/稠密深度图几十GB单目深度估计Semantic Segmentation图像与像素级语义标签约几十GB语义分割从下载解析的角度看最常用的是前三类。Raw Data里带完整的oxts位姿数据适合做SLAMOdometry Benchmark的位姿真值组织得非常规范适合直接评估轨迹精度Object Detection的标签文件是理解3D边界框格式的经典样本训练YOLO类检测器时也经常用来做迁移训练。1.3 不同研究目标对应的下载组合选数据模块之前先想清楚自己要做什么避免把不需要的几TB数据全拉下来。做视觉SLAM或激光SLAM验证算法下Raw Data里的一两个序列就够推荐2011_09_26_drive_0001或2011_09_26_drive_0002这两个序列采集自卡尔斯鲁厄市区包含红绿灯、行人、车辆、弯道场景复杂度覆盖得很好。做里程计精度评测直接下Odometry Benchmark的data_odometry_gray.zip、data_odometry_color.zip、data_odometry_poses.zip、data_odometry_calib.zip。如果不是专门研究点云算法data_odometry_velodyne.zip可以不下载省下大量空间。做目标检测训练和评估下Object Detection模块的data_object_image_2.zip左彩色图像、data_object_label_2.zip2D/3D标签、data_object_calib.zip相机标定。如果只需要2D检测连点云都不用下载。做多模态融合或行为预测下Raw Data的sync版数据因为sync版保证所有传感器帧对齐省去自己做时间同步的麻烦。这个选择过程看似简单但很多人上来就把所有模块全选下载结果硬盘直接爆掉。先明确研究目标再决定下载范围能省下大量时间和带宽。2. 下载前的准备与完整下载流程下载KITTI这件事本身不难难在网络不稳定、文件体积大、官网入口分散。这里分享一套我经过多次实践后确定下来的流程照着做基本不会出问题。2.1 空间规划与目录习惯下载之前先把目录结构和磁盘格式搞定。KITTI的zip解压后基本都保留多层目录结构比如Raw Data解压后是2011_09_26/2011_09_26_drive_0001_sync/image_00这样的路径。建议在根目录下建立专门的KITTI文件夹按数据模块分子目录KITTI/ ├── raw_data/ │ ├── 2011_09_26/ │ │ ├── 2011_09_26_drive_0001_sync/ │ │ └── 2011_09_26_calib/ ├── odometry/ │ ├── dataset/ │ │ ├── poses/ │ │ ├── sequences/ │ │ └── poses/ └── object/ ├── training/ │ ├── calib/ │ ├── image_2/ │ └── label_2/ └── testing/磁盘格式方面有个容易忽略的点如果下载到的分区是FAT32单文件不能超过4GB而KITTI的很多zip压缩包都超过这个大小下载工具会直接报错或者只能下载到一部分。务必确认目标分区的文件系统是NTFS或ext4。2.2 从官网按模块拿数据的两个入口KITTI官方的数据托管在AWS S3上这也是为什么很多链接看起来是s3.eu-central-1.amazonaws.com开头。进入官网后在Datasets子页面点进对应Benchmark页面里会列出各个zip的下载链接。以Odometry Benchmark为例你会发现下载链接非常有规律data_odometry_calib.zip、data_odometry_gray.zip、data_odometry_color.zip、data_odometry_poses.zip、data_odometry_velodyne.zip。这五个文件分别对应标定文件、灰度序列、彩色序列、位姿真值、激光点云。Raw Data的下载则不太一样。你需要先选日期比如2011_09_26页面会列出当天采集的所有drive点击某个drive后会看到多个可选模块包括unsyncedunrectified data和syncedrectified data。建议下载syncedrectified data也就是同步且去畸变后的数据对应链接通常是xxx_sync.zip。这个zip解压后直接包含image_00到image_03、velodyne_points、oxts等目录处理起来最省事。2.3 网络波动时的三个替代下载策略KITTI的S3服务器在国外国内直连速度时快时慢高峰期甚至可能几分钟就断开。遇到这种情况我试过几种办法效率差别很明显。第一个方法是用多线程下载工具替代浏览器直接下载。浏览器下载大文件在中途断掉后虽然支持续传但单线程速度很难跑满带宽。用aria2c加多线程参数能显著提高下载速度并支持断点续传aria2c -x 16 -s 16 -k 1M https://s3.eu-central-1.amazonaws.com/avg-kitti/data_odometry_color.zip参数-x 16表示对同一个文件最多启用16个连接-s 16表示先分16段下载再合并-k 1M设置每个分片的大小。这个组合在我实测中比普通下载快两到三倍。第二个方法是只用wget -c做断点续传。如果你只是想下载几个小文件比如calib zip或者单个序列wget -c就够用不需要额外装aria2c。第三个方法是最划算的仔细评估自己是否真的需要全部数据。比如做目标检测的2D部分只需要data_object_image_2.zip和data_object_label_2.zip二者加起来不到1GB根本不会遇到网络问题。很多人的下载焦虑其实是被不必要的大文件放大的。2.4 下载后的完整性检查下载完成后不要急着解压先做完整性检查。KITTI官方没有为每个zip提供MD5校验码我们能做的就是检查zip自身的完整性。unzip -t data_odometry_color.zip这个命令会把zip里所有文件解压测试一遍如果某个分片下载损坏它会明确报错。如果解压测试通过再解压到目标目录unzip -q data_odometry_color.zip -d odometry/解压完成后建议对比一下文件数量。以Odometry Benchmark为例彩色序列有image_02和image_03两个子目录每个序列下有对应帧数的PNG图像。如果解压后发现某些序列的帧数明显偏少多半是解压过程中文件系统出问题或者下载本身有缺陷。3. 解析第一步calib文件与oxts位姿坐标系转换的根KITTI解析里最劝退的就是calib文件。不少人在这一步选择直接调库但对坐标系变换没有理解后续换到自己的传感器平台上就会抓瞎。其实calib文件里的内容非常固定逐个搞清楚后会发现整个过程就是矩阵乘法的拼接。3.1 calib_cam_to_cam.txt里每个矩阵在干什么Raw Data里的校准文件通常叫calib_cam_to_cam.txt里面包含的字段如下字段名维度含义S_00、S_01、S_02、S_032相机原始图像尺寸K_00、K_01、K_02、K_033x3相机内参矩阵D_00、D_01、D_02、D_035畸变系数R_00、R_01、R_02、R_033x3各相机相对参考相机的旋转矩阵T_00、T_01、T_02、T_033x1各相机相对参考相机的平移向量S_rect_00、S_rect_01、S_rect_02、S_rect_032去畸变后图像尺寸R_rect_003x3参考相机的矫正旋转矩阵P_rect_00、P_01、P_02、P_033x4去畸变后各相机的投影矩阵大多数人只需要关注R_rect_00和P_rect_02。R_rect_00的作用是把相机坐标系做一个旋转矫正让两个相机的极线对齐这是立体匹配和深度估计的前提。P_rect_02则是把3D相机坐标点投影到左彩色图像像素坐标的最终投影矩阵。在Object Detection模块里calib_cam_to_cam.txt的P_rect_02字段是关键因为标签里的3D位置坐标就是相机坐标系下的值要投影到图像上直接乘这个矩阵就行。3.2 velodyne坐标到图像像素的一整套投影链路激光雷达到图像像素坐标的转换链路是整个KITTI解析里最经典的一段公式。理解了它你就能看懂所有坐标系之间的转换套路。链路是这样的激光雷达坐标系下的点先通过Tr_velo_to_cam在calib_velo_to_cam.txt里转到相机坐标系再通过R0_rect做矫正旋转最后通过P_rect_02投影到像素平面。import numpy as np def read_calib_file(path): data {} with open(path, r) as f: for line in f: line line.strip() if not line or line.startswith(#): continue key, value line.split(:, 1) try: data[key] np.array([float(x) for x in value.split()]) except ValueError: pass return data calib_cam read_calib_file(calib_cam_to_cam.txt) calib_velo read_calib_file(calib_velo_to_cam.txt) # 投影矩阵 P_rect_02 P2 calib_cam[P_rect_02].reshape(3, 4) # 矫正旋转矩阵 R_rect_00扩展为4x4 R0 np.eye(4) R0[:3, :3] calib_cam[R_rect_00].reshape(3, 3) # 激光雷达到相机坐标的变换扩展为4x4 Tr_velo_to_cam np.vstack((calib_velo[Tr_velo_to_cam].reshape(3, 4), [0, 0, 0, 1])) def project_velo_to_image(velo_points): # velo_points: N x 4前3列是xyz第四列是反射强度 pts np.hstack((velo_points[:, :3], np.ones((velo_points.shape[0], 1)))) pts_cam (Tr_velo_to_cam pts.T).T # 转到相机坐标系 pts_cam (R0 pts_cam.T).T # 矫正旋转 pts_cam pts_cam[pts_cam[:, 2] 0] # 剔除相机后方的点 pts_img (P2 pts_cam.T).T # 投影到像素坐标系 pts_img pts_img[:, :2] / pts_img[:, 2:3] # 齐次坐标归一化 return pts_img这个投影链路在Raw Data和Object Detection中基本通用唯一的区别是文件路径和文件名不同。在Odometry Benchmark里calib文件被拆成了单独的txt文件比如P0.txt、P1.txt、P2.txt、P3.txt、R0_rect.txt、Tr_velo_cam.txt读取时需要针对性调整。3.3 oxts位姿文件读法与使用边界Raw Data里的oxts目录存放的是GPS/IMU输出的位姿数据每个文件对应一帧文件名与图像帧号对应。每行有30个数值前几个是关键第1~3个纬度、经度、海拔第4~6个roll、pitch、yaw翻滚角、俯仰角、偏航角第7~15个速度、加速度第16~24个角速度等IMU原始量要真正转成车辆在世界坐标系下的位姿矩阵需要做两件事一是把经纬度转成UTM平面坐标二是把roll、pitch、yaw转成旋转矩阵。KITTI官方devkit里给了Matlab参考实现Python用户可以直接用pykitti库里的oxts模块或者参考下面的简化流程def oxts_to_pose(lat, lon, alt, roll, pitch, yaw): # 1. 经纬度转UTM坐标这里示意实际需要调用utm库 import utm easting, northing, zone_num, zone_letter utm.from_latlon(lat, lon) # 2. 欧拉角转旋转矩阵 cy, sy np.cos(yaw), np.sin(yaw) cp, sp np.cos(pitch), np.sin(pitch) cr, sr np.cos(roll), np.sin(roll) Rx np.array([[1, 0, 0], [0, cr, -sr], [0, sr, cr]]) Ry np.array([[cp, 0, sp], [0, 1, 0], [-sp, 0, cp]]) Rz np.array([[cy, -sy, 0], [sy, cy, 0], [0, 0, 1]]) R Rz Ry Rx # 3. 拼成4x4位姿矩阵 pose np.eye(4) pose[:3, :3] R pose[0, 3] easting pose[1, 3] northing pose[2, 3] alt return pose需要注意的是oxts得到的位姿是世界坐标系原点是第一帧的位置实际是UTM坐标下的要把它和激光雷达坐标系、相机坐标系对齐还需要串联IMU到激光雷达的外参calib_imu_to_velo.txt以及激光雷达到相机的外参。这也是Raw Data比Odometry Benchmark复杂的一个原因。如果你的SLAM算法只需要评估轨迹精度直接使用Odometry Benchmark的poses目录会更省事每个序列一个txt文件每行12个数字按行展开就是一个3x4的位姿矩阵这个位姿直接表示从世界坐标系到相机坐标系的变换。这就是为什么很多论文里用Odometry数据而不是Raw Data做里程计评测。4. 从文件到可视化点云、标签与图像的融合解析解析KITTI数据的最终目的通常是把多模态数据在同一个坐标系下可视化出来或者送入网络训练。这一步需要把前面提到的calib、点云、标签串起来。4.1 点云bin文件的正确读取姿势KITTI的激光雷达点云存储在velodyne_points/data目录下文件名是帧号没有扩展名信息实际是.bin后缀每帧点数不固定因为HDL-64E在旋转过程中有一些点会被遮挡或超出量程。读取方式非常直接因为bin文件就是按float32连续存储的每4个float为一个点def load_velo_scan(filename): scan np.fromfile(filename, dtypenp.float32) scan scan.reshape(-1, 4) return scan这个reshape依赖一个前提bin文件里的数据确实是N行4列四列分别是x、y、z和反射强度。如果你的数据集来源不正或者经过手动裁剪这个前提可能不成立读出来的点数会是奇数或者parse失败。遇到这种情况先检查文件的字节大小确认是4的倍数再reshape。4.2 标签文件格式与3D信息解读Object Detection模块的label_2目录是理解KITTI标签的黄金样本。每个txt文件对应一张图像每一行描述一个目标物体格式固定def read_label(path): objects [] with open(path) as f: for line in f: parts line.strip().split() if len(parts) 15: continue obj { type: parts[0], # 物体类别如Car、Pedestrian、Cyclist truncated: float(parts[1]), # 截断程度0~1 occluded: int(parts[2]), # 遮挡状态0~3 alpha: float(parts[3]), # 观察角度 bbox: [float(x) for x in parts[4:8]], # 2D框 x1 y1 x2 y2 dimensions: [float(x) for x in parts[8:11]], # 高 宽 长 location: [float(x) for x in parts[11:14]], # 相机坐标x y z rotation_y: float(parts[14]), # 绕y轴旋转角 } objects.append(obj) return objects很多初学者容易把alpha和rotation_y搞混。rotation_y是物体在相机坐标系下绕y轴的旋转角表示物体的朝向alpha是观察角度和物体在图像中的位置有关两者之间存在换算关系。画3D框时应该用rotation_y。location是物体底部中心在相机坐标系下的坐标单位是米。构建3D边界框时要在底部中心的基础上根据dimensions的长宽高和rotation_y计算8个顶点的相对偏移再叠加到底部中心位置上。4.3 一版可直接跑通的投影与画框代码结合前面的calib解析下面这段代码可以把3D点云投影到图像上并把标签里的3D框角点也投影过去同时画出2D框。import cv2 import numpy as np def project_3d_box_to_image(calib_cam, calib_velo, label, velo_points, image): P2 calib_cam[P_rect_02].reshape(3, 4) R0 np.eye(4) R0[:3, :3] calib_cam[R_rect_00].reshape(3, 3) Tr_velo_to_cam np.vstack((calib_velo[Tr_velo_to_cam].reshape(3, 4), [0, 0, 0, 1])) # 投影点云到图像 pts np.hstack((velo_points[:, :3], np.ones((velo_points.shape[0], 1)))) pts_cam (R0 (Tr_velo_to_cam pts.T)).T pts_cam pts_cam[pts_cam[:, 2] 0] pts_img (P2 pts_cam.T).T pts_img pts_img[:, :2] / pts_img[:, 2:3] for obj in label: # 画2D框 x1, y1, x2, y2 [int(v) for v in obj[bbox]] cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2) # 根据3D信息计算8个角点的偏移并投影 h, w, l obj[dimensions] x, y, z obj[location] ry obj[rotation_y] # 8个角点相对底部中心的偏移相机坐标系 cx, cy, cz 0, -h / 2, 0 # 底部中心上方h/2处为质心 corners_3d np.array([ [l/2, 0, w/2], [l/2, 0, -w/2], [-l/2, 0, -w/2], [-l/2, 0, w/2], [l/2, h, w/2], [l/2, h, -w/2], [-l/2, h, -w/2], [-l/2, h, w/2] ]) # 旋转矩阵 cos_t, sin_t np.cos(ry), np.sin(ry) rot np.array([[cos_t, 0, sin_t], [0, 1, 0], [-sin_t, 0, cos_t]]) corners_3d (rot corners_3d.T).T # 平移到实际位置 corners_3d[:, 0] x corners_3d[:, 1] y corners_3d[:, 2] z ones np.ones((8, 1)) corners_homo np.hstack((corners_3d, ones)) corners_img (P2 corners_homo.T).T corners_img corners_img[:, :2] / corners_img[:, 2:3] corners_img corners_img.astype(int) # 画3D框边线 edges [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4), (0,4),(1,5),(2,6),(3,7)] for e in edges: cv2.line(image, tuple(corners_img[e[0]]), tuple(corners_img[e[1]]), (0, 0, 255), 1) return image需要提醒几个细节dimensions的顺序是高、宽、长不是长、宽、高rotation_y的旋转轴是y轴方向朝下角点偏移都是以底部中心为原点。如果投影出来发现3D框在地面以下或者方向不对多半是这三个细节里至少有一个搞错了。4.4 时间戳对齐sync版为什么省心Raw Data下载页会提供syncedrectified data和unsyncedunrectified data两种版本。前者在采集时做了时间同步所有传感器帧率统一到10Hz并且图像已经做过去畸变帧号直接一一对应后者则保留原始时间戳各传感器的帧不对齐需要自己根据时间戳做最近邻匹配。我强烈建议下载sync版。自己做时间同步这件事看起来不难实际上非常繁琐。KITTI原始数据里不同传感器的时间戳文件格式虽然统一但某些传感器偶尔会丢帧或者延迟累积最近邻匹配出来的结果经常出现细微的错位导致点云和图像的边缘对不齐。sync版相当于官方帮你做完了这一步。使用sync版时直接按帧号取数据即可image_02/data/0000000000.png对应velodyne_points/data/0000000000.bin对应label_2/0000000000.txt如果有。命名规则是所有传感器统一编号从0开始递增。5. 高频问题排查下载中断、投影飞点与标签错位这一节把实际使用中最高频的几类问题单独拿出来讲讲它们出现的根本原因和排查思路。5.1 官网下载老失败核对这些先有很多人私信问我为什么官网下载或者S3链接下载到一半就断。首先检查网络环境KITTI服务器在国外确实有波动。但更常见的原因有两个一是下载工具不支持断点续传二是没有开启多线程下载。浏览器的默认下载在连接中断后有时候能续传但大文件成功率不高所以更推荐aria2c这种支持多线程和断点的工具。其次是文件大小问题。如果你下的是FAT32分区单个文件超过4GB就会报file too large。检查一下分区格式换到NTFS或ext4即可。最后是文件数量问题。KITTI某些zip解压后包含几万个文件如果存储设备是格式比较老的移动硬盘或者网络挂载盘解压速度会极慢看起来像卡死了。建议直接解压到本地NVMe盘确认完整后再拷贝。5.2 投影后点云满天飞问题出在哪点云投影到图像上出现散乱的飞点大多数人第一反应是calib文件读错了。但根据我的观察最常犯的错反而是矩阵维度没有对齐。例如把3x3的R_rect_00直接当4x4用或者忘了把Tr_velo_to_cam补成4x4齐次矩阵导致矩阵乘法报错。另一个常见错误是忘记剔除相机后方的点。激光雷达是360度扫描车尾方向的点转到相机坐标系后z坐标是负值这些点在投影数学上没有任何问题但会把图像坐标系算到错误位置画出来就是乱七八糟的点。所以投影前一定要先过滤pts_cam[:, 2] 0。还有一个隐蔽问题P_rect_02里已经包含了R0_rect的矫正效果。如果你在代码里既用了矫正后的点又手动额外乘一次R0_rect点云坐标会整体偏转一个角度。正确的做法是严格按官方的链路P2 * R0_rect * Tr_velo_to_cam * X_velo不要多做一次。5.3 标签里的数字读出来不成立体框格式坑标签文件看起来是纯数字但格式坑不少。很多人把truncated和occluded的顺序读反导致数值明显异常。另外要注意的是bbox的坐标是原始图像分辨率下的像素坐标而KITTI图像在sync版里已经去畸变并裁剪过通常是1242x375左右如果你拿原始1368x512的分辨率去画框2D框会错位。3D框的问题集中在两个点一是rotation_y和alpha混用二是location的y坐标。location是物体底部中心的y值通常接近地面高度约0~-0.5m之间如果你把它当作物体质心3D框会整体偏移半个车高。记得在画框时把每个角点相对底部中心的偏移算对。如果你是要把KITTI标签转到YOLO格式训练2D检测器注意标签里的bbox需要转换成归一化的中心点坐标和宽高即x_center (x1 x2) / 2 / width y_center (y1 y2) / 2 / height w (x2 - x1) / width h (y2 - y1) / heightKITTI图像宽度和高度在训练时需要保持一致很多开源工具会自动读取图像尺寸来换算但如果你手动处理务必确认宽高是图像的实际分辨率而不是原始传感器的分辨率。5.4 磁盘、跨平台与后续扩展最后说几个跨平台使用的细节。如果你在Windows下解压了KITTI数据再通过内置Linux子系统或者WSL使用时要注意文件路径和权限。KITTI的bin文件用C和Python读取都没问题但有些旧代码里用fopen或者ifstream在Windows下对long long类型的文件偏移支持不太好读取大体积bin文件时可能只读到一部分。解决方法是统一用Python的numpy.fromfile读取。磁盘空间管理上建议保留zip压缩包至少一个星期确认数据解析无误再删除。我之前有过一次急于释放空间、解压后立刻删除zip后来发现有个序列的图像帧有缺失重新下载浪费了整整一个晚上。如果后续要做更复杂的多模态融合建议关注KITTI-360扩展数据集它在KITTI基础上增加了全景相机、更高线束的激光雷达和更精细的标注坐标系和校准文件组织方式与KITTI一脉相承解析逻辑学一次能用在两处。个人体会是解析KITTI时最重要的不是抄代码而是建立坐标系意识。你自己需要清楚每一个矩阵做什么变换、数据当前在哪个坐标系下、下一步要转到哪里去。这套思路不仅适用于KITTI换到nuScenes、Waymo Open Dataset这些同样有多传感器校准文件的数据集核心逻辑完全一致。即使是做纯2D目标检测、只打算用KITTI训练模型的朋友也建议抽时间把点云投影链路手动跑一遍这个经验在你日后部署到实际机器人平台时会派上大用场。
返回列表