
1. 从零开始为什么说数据集是3D检测的“地基”最近在折腾mmdetection3D准备复现几个经典的3D目标检测模型。说实话模型代码、配置文件看了一大堆各种SOTA论文也读了不少但真正卡住我的第一步往往不是模型本身而是数据集。我相信很多刚接触这个领域的朋友都有同感好不容易配好了环境跑通了demo兴致勃勃地想用自己的数据或者跑一个标准数据集来训练结果在数据准备这一步就栽了跟头。报错信息千奇百怪从路径不对到数据格式不匹配再到标签文件解析失败每一步都可能让你怀疑人生。这其实引出了一个核心问题在3D目标检测乃至整个深度学习领域数据集到底扮演着什么角色我的体会是它远不止是“喂给模型的粮食”那么简单。一个规范、清晰、预处理得当的数据集是整个项目的地基。地基没打牢后面无论搭建多么精妙的模型上层建筑都可能是空中楼阁训练过程会充满各种难以调试的玄学问题。mmdetection3D作为一个优秀的开源框架其强大之处在于提供了统一的模型接口和训练流程但这也意味着它对输入数据的格式有着严格的要求。框架本身就像一台精密的机床而你的数据集就是待加工的原材料。原材料尺寸、规格不达标机床再先进也加工不出合格的产品。因此这篇内容我们不谈复杂的模型结构也不讲花哨的训练技巧就扎扎实实地聊一聊在mmdetection3D中如何为你的3D检测任务准备好那份“合格的原材料”。我们会覆盖从理解标准数据集如KITTI、nuScenes的结构到数据转换、自定义数据集制作的完整链路并穿插大量我在实际操作中踩过的坑和总结的经验。无论你是想跑通经典数据集的基准测试还是打算将自己的点云数据用于训练希望这篇内容都能帮你扫清障碍。2. 深入解剖主流3D检测数据集格式与mmdetection3D的期望在动手处理数据之前我们必须先搞清楚两个关键对象主流开源数据集本身是如何组织的以及mmdetection3D框架期望的数据格式是什么。这两者之间通常存在一个“鸿沟”而我们的工作就是搭建一座桥梁。2.1 标杆案例KITTI数据集的经典结构KITTI数据集无疑是3D目标检测领域的“MNIST”结构清晰文档齐全是入门的最佳选择。理解它的结构对理解其他数据集大有裨益。通常下载解压后的KITTI数据集以3D Object Detection任务为例会包含如下关键目录和文件KITTI/ ├── training/ │ ├── image_2/ # 左侧RGB相机图像 (P0) │ ├── velodyne/ # 点云数据.bin文件来自激光雷达 │ ├── label_2/ # 标注文件.txt文件每个物体一行 │ └── calib/ # 校准文件.txt文件包含所有相机的内外参和雷达到相机的变换矩阵 └── testing/ ├── image_2/ ├── velodyne/ └── calib/这里有几个核心要点需要吃透点云数据.bin文件每个文件是一个N x 4的二进制数组float32其中N是点的数量4个维度分别是 (x, y, z, intensity)。这里的坐标是在激光雷达坐标系下的。x指向前方y指向左方z指向上方。标注文件.txt文件每一行代表一个物体。其字段顺序为类型 截断 遮挡 角度 alpha 2D框左上x 2D框左上y 2D框右下x 2D框右下y 3D框高 3D框宽 3D框长 3D框中心x 3D框中心y 3D框中心z 3D框朝向角。其中3D框的尺寸和中心坐标也是在激光雷达坐标系下定义的。角度和朝向角的关系需要特别注意alpha是观察角而标注中的朝向角是物体在鸟瞰图下的偏航角rotation_y。校准文件.txt文件这是实现多传感器融合和坐标转换的钥匙。它包含了P0到P3四个相机的投影矩阵、雷达到相机0的变换矩阵Tr_velo_to_cam、以及相机0的基准矩阵R0_rect。我们要将激光雷达坐标系下的3D框投影到图像上或者反过来都依赖于这些矩阵。2.2 mmdetection3D的“中间格式”Info文件mmdetection3D并不直接读取原始的KITTI.bin和.txt文件。它设计了一种通用的中间数据格式通常以.pklPickle文件或.json文件的形式存在我们称之为info文件。这个文件包含了单个样本或整个数据集的所有元信息。对于KITTI通过官方提供的转换脚本会生成kitti_infos_train.pkl和kitti_infos_val.pkl等文件。我们来看一下一个典型的info字典里包含什么# 这是一个简化示例实际字段更多 sample_info { point_cloud: { num_features: 4, # x, y, z, intensity velodyne_path: training/velodyne/000000.bin, # 点云文件相对路径 }, annos: { name: np.array([Car, Pedestrian, ...]), # 物体类别 truncated: np.array([0.0, 0.5, ...]), # 截断程度 occluded: np.array([0, 2, ...]), # 遮挡等级 alpha: np.array([1.2, -0.5, ...]), # 观察角 bbox: np.array([[712, 143, 810, 307], ...]), # 2D图像框 [x1, y1, x2, y2] dimensions: np.array([[1.8, 0.6, 0.5], ...]), # 3D框尺寸 [高宽长] (h, w, l) location: np.array([[10.5, 2.1, 30.8], ...]), # 3D框中心坐标 [x, y, z] (相机坐标系下注意) rotation_y: np.array([0.1, -1.5, ...]), # 3D框朝向角 (绕Y轴) score: np.array([...]), # 通常用于评估训练时没有 }, calib: { P0: np.array(3x4), # 相机0投影矩阵 P1: np.array(3x4), P2: np.array(3x4), P3: np.array(3x4), R0_rect: np.array(4x4), # 修正旋转矩阵 Tr_velo_to_cam: np.array(4x4), # 雷达到相机的变换矩阵 }, image: { image_idx: 0, # 样本索引 image_path: training/image_2/000000.png, # 图像路径 image_shape: np.array([370, 1224]), # 图像高宽 (H, W) } }注意一个关键细节在原始的KITTI标注中location和dimensions是在激光雷达坐标系下的。但在mmdetection3D的info文件里location被转换到了相机坐标系通常是相机0下。这是为了后续数据增强如图像和点云的同步增强以及一些需要图像坐标的模型如MVXNet更方便。dimensions的顺序也固定为(h, w, l)。这个转换是在数据准备阶段由转换脚本完成的。如果你自己制作数据集必须明确你的标注是在哪个坐标系并考虑是否需要转换。2.3 更复杂的现实nuScenes数据集如果说KITTI是“单帧静态”的典范那么nuScenes就是“多帧动态”和“多传感器”的集大成者。它包含了雷达、6个相机、GPS/IMU等数据并且是连续的序列。它的原始数据格式是数据库文件.db和大量的传感器数据文件。mmdetection3D为nuScenes提供了完善的转换工具。其核心过程是解析nuScenes的官方SDK提取出每个关键帧通常每秒2帧的信息然后构建类似于KITTI info但更复杂的结构。nuScenes的info文件会包含多传感器路径6个相机的图像路径、雷达点云路径。时序信息前后帧的样本token用于支持时序融合模型如CenterPoint。更丰富的标注属性如物体速度、属性车辆是否停放、可见性token等。标定信息每个传感器到自车坐标系的变换矩阵。理解这两种典型数据集的处理流程就掌握了处理大多数其他数据集如Waymo Open Dataset Lyft Level 5的钥匙。它们的共同思路都是将原始数据集的复杂结构解构、提取并重组为mmdetection3D定义的标准化info文件。3. 实战演练手把手完成KITTI数据集的准备与转换理论说得再多不如动手做一遍。我们以准备KITTI数据集用于训练PointPillars模型为例走一遍完整流程。假设你已经从KITTI官网下载了data_object_image_2.zip,data_object_velodyne.zip,data_object_label_2.zip和data_object_calib.zip并解压到了同一个目录下结构如2.1节所示。3.1 环境与代码准备首先确保你已经克隆了mmdetection3D的官方仓库并完成了基础安装。git clone https://github.com/open-mmlab/mmdetection3d.git cd mmdetection3d pip install -v -e .数据集转换工具位于tools/dataset_converters/目录下。我们需要使用的是kitti_data_converter.py。3.2 执行数据转换转换命令的核心是指定原始数据路径和输出路径。通常我们会建立一个data目录来存放所有数据集。# 假设你的原始KITTI数据放在 /path/to/kitti/ 下即该目录下有 training/ 和 testing/ 文件夹 # 我们在mmdetection3d根目录下创建data目录并建立软链接或直接移动数据 mkdir -p ./data/kitti # 将原始数据移动到对应位置或者创建软链接推荐节省空间 ln -s /path/to/kitti/training ./data/kitti/ ln -s /path/to/kitti/testing ./data/kitti/ # 运行转换脚本 python tools/dataset_converters/kitti_data_converter.py --data-root ./data/kitti --out-dir ./data/kitti --split training关键参数解析--data-root: 指向包含training和testing文件夹的根目录。--out-dir: 生成的info文件等中间数据的输出目录。通常和--data-root设为相同这样所有数据都在一个地方。--split: 指定转换训练集还是测试集。第一次需要分别运行training和testing或val。执行过程与输出脚本运行后它会遍历training/image_2下的所有图像为每一帧生成对应的info条目并收集所有标定和标注信息。最终它会在--out-dir下生成以下关键文件kitti_infos_train.pkl: 训练集的info文件。kitti_infos_val.pkl: 验证集的info文件如果你有验证集划分文件train.txt,val.txt需要提前放置好脚本会自动读取。如果没有则需要手动划分或使用后续命令。kitti_infos_trainval.pkl: 训练验证集的info文件。kitti_infos_test.pkl: 测试集的info文件。kitti_dbinfos_train.pkl: 用于数据增强中GT采样如Copy-Paste的数据库信息。gt_database/: 一个目录里面存储了每个物体的点云和标注信息以.bin文件形式保存同样用于GT采样。3.3 创建自定义的训练/验证集划分很多时候我们不想用官方测试集做验证因为没标签而是想从训练集中划出一部分作为验证集。KITTI官方没有提供标准划分社区常用两种方式按照原始论文划分例如PointPillars论文将3712个训练样本分为train3682和val3769这里数字可能不对需要查证。实际上常见的划分是约3769个样本用于训练3769个用于验证总共7518个训练样本。我们需要一个划分文件。随机划分自己按比例随机分割。mmdetection3D的脚本支持通过--split-file参数指定划分。你可以创建一个train.txt和val.txt里面每行写一个样本ID如000000,000001...然后运行# 首先生成包含所有训练样本的info文件假设叫all_train python tools/dataset_converters/kitti_data_converter.py --data-root ./data/kitti --out-dir ./data/kitti --split training # 然后使用create_data.py脚本并指定划分文件来生成最终的train和val的info文件 # 这个步骤通常被集成在后续的“创建数据”步骤中但理解原理很重要。 # 更常见的做法是直接修改mmdet3d/datasets/kitti_dataset.py中关于数据划分的代码或者准备好在对应路径下的划分文件。一个更实用的方法是直接使用社区已经准备好的划分文件。你可以在mmdetection3d的data/kitti/目录下寻找或从其他开源项目复制ImageSets文件夹里面包含train.txt,val.txt,trainval.txt,test.txt。我踩过的一个坑划分文件里的ID不需要后缀。例如应该是000000而不是000000.png或000000.txt。脚本是根据这个ID去拼接文件路径的。3.4 运行create_data.py生成最终数据生成.pkl的info文件后还需要一步才能被dataloader直接使用。这一步会根据配置文件对点云进行预处理如体素化、数据范围过滤等生成.bin格式的中间文件加速训练时的数据读取。python tools/create_data.py kitti --root-path ./data/kitti --out-dir ./data/kitti --extra-tag kittikitti: 指定数据集类型。--root-path: 数据根目录即包含training文件夹和kitti_infos_*.pkl的目录。--out-dir: 处理后的数据输出目录通常和--root-path一致。--extra-tag: 给生成的文件加个标签防止不同配置生成的文件冲突。运行成功后你会在--out-dir下看到一个以--extra-tag命名的文件夹例如kitti_gt_database和一系列.bin文件如points,image_2等可能被重新组织。更重要的是它会更新kitti_infos_train.pkl等文件在里面添加预处理后的数据路径等信息。至此KITTI数据集的准备工作才算真正完成。你可以通过运行一个简单的测试脚本来验证python tools/misc/browse_dataset.py configs/pointpillars/hv_pointpillars_secfpn_6x8_160e_kitti-3d-3class.py --show-interval 1如果配置正确这个脚本会可视化数据集中的样本检查标注框是否对齐。4. 进阶挑战处理自定义点云数据集使用公开数据集是学习和验证的第一步但真正的价值往往在于解决自己的问题。将自定义的点云数据适配到mmdetection3D是更常见的需求。这个过程本质上是模仿KITTI或nuScenes的info文件结构为你自己的数据生成一套对应的.pkl文件。4.1 定义你的数据“协议”在写代码之前你必须明确以下几点并最好写成文档传感器坐标系你的点云数据是在哪个坐标系下的激光雷达车体定义好X前、Y左、Z上的方向。强烈建议与KITTI保持一致X前Y左Z上可以省去大量后续坐标转换的麻烦。标注格式你如何标注3D框你需要记录每个物体的类别字符串或整数ID。尺寸长、宽、高l, w, h。注意KITTI顺序是(h, w, l)mmdetection3D内部也常用(h, w, l)。建议统一。位置3D框中心点在你的坐标系下的(x, y, z)。朝向偏航角yaw。定义0度朝向通常是X轴正方向和旋转正方向通常是逆时针。数据组织如何存储点云和图像是每个样本一个.bin/.pcd/.ply文件和一个图像文件吗路径结构如何标定信息如果需要多模态如果有点云和图像的融合需求必须有精确的相机内参焦距、主点和外参雷达到相机的变换矩阵。4.2 编写转换脚本你需要编写一个Python脚本遍历你的所有数据样本为每个样本构建一个字典最后将所有字典列表保存为.pkl文件。这个字典的结构要尽可能贴近3.2节中描述的info字典。下面是一个高度简化的示例框架import numpy as np import pickle from pathlib import Path def create_custom_info(): data_root Path(/path/to/your/custom_data) train_info [] val_info [] # 假设你有一个train_list.txt和val_list.txt里面是样本ID with open(data_root / train_list.txt, r) as f: train_ids [line.strip() for line in f] with open(data_root / val_list.txt, r) as f: val_ids [line.strip() for line in f] def process_sample(sample_id, split): info {} # 1. 点云信息 pc_path data_root / pointcloud / f{sample_id}.bin # 确保点云文件存在并可以读取。例如如果是4维(x,y,z,i)的bin文件 # points np.fromfile(pc_path, dtypenp.float32).reshape(-1, 4) info[point_cloud] { num_features: 4, # 你的点云特征维度 velodyne_path: str(pc_path.relative_to(data_root)), # 相对路径 } # 2. 图像信息如果有 img_path data_root / image / f{sample_id}.jpg if img_path.exists(): # 可以使用PIL或OpenCV读取图像获取宽高 # img Image.open(img_path) # width, height img.size info[image] { image_idx: sample_id, image_path: str(img_path.relative_to(data_root)), image_shape: np.array([height, width], dtypenp.int32), # (H, W) } # 3. 标注信息 label_path data_root / label / f{sample_id}.txt annos {} names [] dimensions [] # 存储 (h, w, l) locations [] # 存储 (x, y, z) 在你的坐标系下 rotation_y [] if label_path.exists(): with open(label_path, r) as f: lines f.readlines() for line in lines: parts line.strip().split() cls_name parts[0] # 假设你的标注格式是: cls l w h x y z yaw l, w, h float(parts[1]), float(parts[2]), float(parts[3]) x, y, z float(parts[4]), float(parts[5]), float(parts[6]) yaw float(parts[7]) names.append(cls_name) # 注意mmdet3d常用维度顺序是 (h, w, l) dimensions.append([h, w, l]) locations.append([x, y, z]) rotation_y.append(yaw) annos[name] np.array(names) annos[dimensions] np.array(dimensions, dtypenp.float32) annos[location] np.array(locations, dtypenp.float32) annos[rotation_y] np.array(rotation_y, dtypenp.float32) # 其他字段如truncated, occluded, alpha, bbox等如果你的数据没有可以赋默认值或空数组 annos[truncated] np.zeros(len(names), dtypenp.float32) annos[occluded] np.zeros(len(names), dtypenp.int32) annos[alpha] np.zeros(len(names), dtypenp.float32) annos[bbox] np.zeros((len(names), 4), dtypenp.float32) # 如果没有2D框 info[annos] annos # 4. 标定信息如果有图像且需要融合 # 这里需要根据你的标定文件格式来解析 # info[calib] {...} # 如果没有可以留空或赋None但后续配置文件中的管道不能使用需要calib的步骤 return info for sample_id in train_ids: train_info.append(process_sample(sample_id, train)) for sample_id in val_ids: val_info.append(process_sample(sample_id, val)) # 保存为pkl文件 with open(data_root / custom_infos_train.pkl, wb) as f: pickle.dump(train_info, f) with open(data_root / custom_infos_val.pkl, wb) as f: pickle.dump(val_info, f) print(fCreated {len(train_info)} training samples and {len(val_info)} validation samples.) if __name__ __main__: create_custom_info()4.3 注册自定义数据集并修改配置生成info文件后你需要在mmdetection3D中注册你的数据集。最简单的方式是继承现有的数据集类如Custom3DDataset或KittiDataset并重写相关方法。创建数据集类在mmdet3d/datasets/目录下新建一个文件例如custom_dataset.py。from mmdet3d.datasets.custom_3d import Custom3DDataset from mmdet3d.core.bbox import LiDARInstance3DBoxes DATASETS.register_module() class CustomDataset(Custom3DDataset): CLASSES (Pedestrian, Cyclist, Car) # 修改为你的类别 def get_data_info(self, index): info self.data_infos[index] # 这里可以做一些自定义的数据信息提取或转换 # 例如确保你的标注框被正确加载并转换为LiDARInstance3DBoxes格式 input_dict super().get_data_info(index) return input_dict # 可选重写评估函数如果你的评估方式与KITTI不同修改配置文件在你模型的配置文件中例如configs/pointpillars/xxx.py修改数据集相关部分。# 修改数据集类型和路径 dataset_type CustomDataset # 你刚注册的类名 data_root /path/to/your/custom_data/ # 修改训练和验证数据配置 data dict( traindict( typedataset_type, data_rootdata_root, ann_filecustom_infos_train.pkl, # 你生成的info文件 pipelinetrain_pipeline, classes(Pedestrian, Cyclist, Car), # 必须和数据集类中一致 test_modeFalse), valdict( typedataset_type, data_rootdata_root, ann_filecustom_infos_val.pkl, pipelinetest_pipeline, classes(Pedestrian, Cyclist, Car), test_modeTrue), testdict(...) # 类似val )修改数据预处理管道Pipeline这是最容易出错的地方。你的train_pipeline和test_pipeline必须与你的数据特性匹配。点云范围(PointCloudRange): 必须根据你的传感器视野和场景定义。例如KITTI常用[0, -40, -3, 70.4, 40, 1]格式通常是[x_min, y_min, z_min, x_max, y_max, z_max]。你需要统计你的点云数据确定一个合理的范围过滤掉范围外的点。体素大小(voxel_size): 对于体素化方法如PointPillars, VoxelNet这个参数至关重要。需要根据点云密度和物体大小调整。例如[0.16, 0.16, 4]意味着在X和Y轴上每0.16米一个体素Z轴不分割或高度为4米。数据增强旋转、缩放、翻转等增强参数也需要根据你的场景调整。在室内场景的增强幅度通常比自动驾驶室外场景要小。4.4 调试与验证完成以上步骤后不要急于开始长时间训练。务必进行充分调试使用browse_dataset.py可视化这是最直观的检查方法。确保3D框和点云对齐类别颜色正确。python tools/misc/browse_dataset.py your_config.py --show-interval 1运行一个极短的训练设置runner.max_epochs 1data.samples_per_gpu 1跑一个epoch。观察是否有错误损失是否从合理值开始下降。检查数据加载速度使用dataloader的调试模式看是否存在某个样本读取特别慢可能是数据格式问题。核对类别数量在日志开头框架会打印数据集中每个类别的实例数量。检查是否与你的标注统计一致。处理自定义数据集是一个迭代过程几乎一定会遇到各种坐标不对齐、尺寸错误、管道不匹配的问题。耐心地通过可视化工具定位问题逐一修正你的转换脚本和配置文件是成功的关键。