3D目标检测实战:从KITTI到自定义数据集,详解mmdetection3D数据准备全流程
2026/8/23 3:52:06 网站建设 项目流程

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/

这里有几个核心要点需要吃透:

  1. 点云数据(.bin文件):每个文件是一个N x 4的二进制数组(float32),其中N是点的数量,4个维度分别是 (x, y, z, intensity)。这里的坐标是在激光雷达坐标系下的。x指向前方,y指向左方,z指向上方。
  2. 标注文件(.txt文件):每一行代表一个物体。其字段顺序为:类型 截断 遮挡 角度 alpha 2D框左上x 2D框左上y 2D框右下x 2D框右下y 3D框高 3D框宽 3D框长 3D框中心x 3D框中心y 3D框中心z 3D框朝向角。其中,3D框的尺寸和中心坐标也是在激光雷达坐标系下定义的角度朝向角的关系需要特别注意,alpha是观察角,而标注中的朝向角是物体在鸟瞰图下的偏航角(rotation_y)。
  3. 校准文件(.txt文件):这是实现多传感器融合和坐标转换的钥匙。它包含了P0P3(四个相机)的投影矩阵、雷达到相机0的变换矩阵Tr_velo_to_cam、以及相机0的基准矩阵R0_rect。我们要将激光雷达坐标系下的3D框投影到图像上,或者反过来,都依赖于这些矩阵。

2.2 mmdetection3D的“中间格式”:Info文件

mmdetection3D并不直接读取原始的KITTI.bin.txt文件。它设计了一种通用的中间数据格式,通常以.pkl(Pickle)文件或.json文件的形式存在,我们称之为info文件。这个文件包含了单个样本(或整个数据集)的所有元信息。

对于KITTI,通过官方提供的转换脚本,会生成kitti_infos_train.pklkitti_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标注中,locationdimensions是在激光雷达坐标系下的。但在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.zipdata_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: 指向包含trainingtesting文件夹的根目录。
  • --out-dir: 生成的info文件等中间数据的输出目录。通常和--data-root设为相同,这样所有数据都在一个地方。
  • --split: 指定转换训练集还是测试集。第一次需要分别运行trainingtesting(或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官方没有提供标准划分,社区常用两种方式:

  1. 按照原始论文划分:例如,PointPillars论文将3712个训练样本分为train(3682)和val(3769)?这里数字可能不对,需要查证。实际上常见的划分是约3769个样本用于训练,3769个用于验证(总共7518个训练样本)。我们需要一个划分文件。
  2. 随机划分:自己按比例随机分割。

mmdetection3D的脚本支持通过--split-file参数指定划分。你可以创建一个train.txtval.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.png000000.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 kitti
  • kitti: 指定数据集类型。
  • --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 定义你的数据“协议”

在写代码之前,你必须明确以下几点,并最好写成文档:

  1. 传感器坐标系:你的点云数据是在哪个坐标系下的?激光雷达?车体?定义好X(前)、Y(左)、Z(上)的方向。强烈建议与KITTI保持一致(X前,Y左,Z上),可以省去大量后续坐标转换的麻烦。
  2. 标注格式:你如何标注3D框?你需要记录每个物体的:
    • 类别:字符串或整数ID。
    • 尺寸:长、宽、高(l, w, h)。注意KITTI顺序是(h, w, l),mmdetection3D内部也常用(h, w, l)。建议统一。
    • 位置:3D框中心点在你的坐标系下的(x, y, z)
    • 朝向:偏航角(yaw)。定义0度朝向(通常是X轴正方向)和旋转正方向(通常是逆时针)。
  3. 数据组织:如何存储点云和图像?是每个样本一个.bin/.pcd/.ply文件和一个图像文件吗?路径结构如何?
  4. 标定信息(如果需要多模态):如果有点云和图像的融合需求,必须有精确的相机内参(焦距、主点)和外参(雷达到相机的变换矩阵)。

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, dtype=np.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], dtype=np.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, dtype=np.float32) annos['location'] = np.array(locations, dtype=np.float32) annos['rotation_y'] = np.array(rotation_y, dtype=np.float32) # 其他字段如truncated, occluded, alpha, bbox等,如果你的数据没有,可以赋默认值或空数组 annos['truncated'] = np.zeros(len(names), dtype=np.float32) annos['occluded'] = np.zeros(len(names), dtype=np.int32) annos['alpha'] = np.zeros(len(names), dtype=np.float32) annos['bbox'] = np.zeros((len(names), 4), dtype=np.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(f'Created {len(train_info)} training samples and {len(val_info)} validation samples.') if __name__ == '__main__': create_custom_info()

4.3 注册自定义数据集并修改配置

生成info文件后,你需要在mmdetection3D中注册你的数据集。最简单的方式是继承现有的数据集类(如Custom3DDatasetKittiDataset),并重写相关方法。

  1. 创建数据集类:在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不同
  1. 修改配置文件:在你模型的配置文件中(例如configs/pointpillars/xxx.py),修改数据集相关部分。
# 修改数据集类型和路径 dataset_type = 'CustomDataset' # 你刚注册的类名 data_root = '/path/to/your/custom_data/' # 修改训练和验证数据配置 data = dict( train=dict( type=dataset_type, data_root=data_root, ann_file='custom_infos_train.pkl', # 你生成的info文件 pipeline=train_pipeline, classes=('Pedestrian', 'Cyclist', 'Car'), # 必须和数据集类中一致 test_mode=False), val=dict( type=dataset_type, data_root=data_root, ann_file='custom_infos_val.pkl', pipeline=test_pipeline, classes=('Pedestrian', 'Cyclist', 'Car'), test_mode=True), test=dict(...) # 类似val )
  1. 修改数据预处理管道(Pipeline):这是最容易出错的地方。你的train_pipelinetest_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 调试与验证

完成以上步骤后,不要急于开始长时间训练。务必进行充分调试:

  1. 使用browse_dataset.py可视化:这是最直观的检查方法。确保3D框和点云对齐,类别颜色正确。
    python tools/misc/browse_dataset.py your_config.py --show-interval 1
  2. 运行一个极短的训练:设置runner.max_epochs = 1data.samples_per_gpu = 1,跑一个epoch。观察是否有错误,损失是否从合理值开始下降。
  3. 检查数据加载速度:使用dataloader的调试模式,看是否存在某个样本读取特别慢(可能是数据格式问题)。
  4. 核对类别数量:在日志开头,框架会打印数据集中每个类别的实例数量。检查是否与你的标注统计一致。

处理自定义数据集是一个迭代过程,几乎一定会遇到各种坐标不对齐、尺寸错误、管道不匹配的问题。耐心地通过可视化工具定位问题,逐一修正你的转换脚本和配置文件,是成功的关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询