从零到一:基于DDAD数据集构建自动驾驶感知模型的实战全记录

最近在尝试复现一些前沿的自动驾驶深度估计模型时,我遇到了一个绕不开的“硬骨头”——DDAD数据集。这个由丰田研究院(TRI-ML)发布的数据集,以其多传感器同步、稠密深度标注和丰富的3D边界框信息,在学术界和工业界都备受关注。然而,从数据集下载、环境配置到最终跑通训练流程,整个过程远非一句pip install那么简单。我花了将近一周的时间,踩遍了几乎所有能遇到的坑,从dgp库的版本地狱到Horovod分布式训练的兼容性问题。这篇文章,就是一份热气腾腾的“战地笔记”。我不会照搬任何官方教程,而是从一个实际开发者的视角,带你完整走一遍这个充满挑战但又极具价值的旅程,重点分享那些官方文档里没有、却能让你节省大量调试时间的“避坑”细节。

1. 理解DDAD:不止是一个数据集,更是一个生态系统

在动手写第一行代码之前,我们必须先搞清楚DDAD到底是什么,以及它为什么值得我们去折腾。DDAD的全称是Dense Depth for Autonomous Driving,其核心价值在于提供了高质量、稠密且跨传感器对齐的深度真值。这对于依赖视觉进行3D感知的自动驾驶模型(如单目/多目深度估计、3D目标检测)来说,是极其宝贵的训练资源。

与KITTI等经典数据集相比,DDAD有几个显著特点,也直接影响了我们后续的使用方式:

  • 多传感器同步与融合:每个样本(sample)都严格同步了6个环视RGB相机和1个激光雷达(LiDAR)的数据。这意味着你可以轻松地进行跨视角的视觉研究,或者探索视觉与激光雷达的融合算法。
  • 丰富的标注层次:除了原始的传感器数据,DDAD还提供了:
    • 相机与LiDAR的精确位姿(Pose)、内参和外参。
    • 2D和3D的物体边界框标注。
    • 2D语义分割标注。
    • 从LiDAR投影生成的稠密深度图,这是其命名的由来,也是许多深度估计模型的训练目标。
  • 基于dgp库的数据治理:这是第一个大坑的源头。DDAD并不直接提供一堆图片和标注文件让你读取,而是通过一个名为dgp(Dataset Governance Policy)的Python库来管理和访问。dgp定义了一套标准的数据结构、加载流程和预处理方法,好处是规范统一,坏处是环境配置变得复杂,且版本兼容性要求极高。

简单来说,想用DDAD,你必须先“搞定”dgp。这就像拿到了一把设计精妙的保险箱钥匙(你的模型代码),但必须先学会操作整个保险库的大门系统(dgp)。

2. 环境搭建:在dgp的版本迷宫中找到出路

几乎所有新手(包括我)遇到的第一个拦路虎,都是dgp库的安装与版本问题。官方提供了Docker和Conda两种方式,我强烈推荐使用Conda虚拟环境,因为它对宿主机环境破坏最小,且更容易进行后续的调试和定制。

2.1 Conda环境创建与基础依赖

首先,创建一个独立的Python环境,这是保证环境纯净的第一步。

# 创建一个名为ddad的新环境,指定Python版本(建议3.8,兼容性最好)
conda create -n ddad python=3.8 -y
conda activate ddad

接下来,安装PyTorch。这里需要特别注意CUDA版本与你本地GPU驱动版本的匹配。你可以通过nvidia-smi命令查看你的CUDA Driver版本,然后去PyTorch官网找到对应的安装命令。

# 示例:为CUDA 11.3安装PyTorch 1.12.1
pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 torchaudio==0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113

2.2 安装dgp库:避开版本冲突的深坑

这是最关键也最易出错的一步。官方仓库的README可能会直接让你pip install .,但这样安装的永远是最新的main分支代码,而很多社区模型(如packnet-sfm)是在特定历史版本上开发的,直接使用最新版大概率会因API变动而报错。

我遇到的具体错误是:AttributeError: 'SynchronizedSceneDataset' object has no attribute 'get_datum_index_for_datum_name'。这个函数在某个版本后被移除了。

我的解决方案是“时间旅行”安装法:

  1. 克隆仓库并切换到稳定标签:不要直接pip install,先克隆代码,查看有哪些发布版本(tag)。

    git clone https://github.com/TRI-ML/dgp.git
    cd dgp
    git tag -l | head -10  # 查看最近的版本标签
    

    我发现在packnet-sfm的代码中,其requirements.txt里锁定了dgp的版本。因此,最稳妥的办法是安装一个与你要跑的模型代码相匹配的dgp版本。如果模型代码没有明确指定,可以尝试一个较旧且广泛使用的标签,例如v1.0

    git checkout v1.0  # 切换到v1.0标签对应的代码状态
    
  2. 从源码安装指定版本:在切换后的代码目录下进行安装。

    pip install -e .  # 使用`-e`(可编辑模式)安装,方便后续可能的手动修改
    

    注意:安装过程中可能会提示缺少一些依赖,如open3dnumba等,根据错误提示用pip逐一安装即可。如果遇到编译错误,可能需要安装系统级的开发工具包(如build-essential)。

  3. 验证安装:安装完成后,在Python中简单导入测试,并尝试读取数据集元数据(先不下载数据)。

    import dgp
    from dgp.datasets.synchronized_dataset import SynchronizedSceneDataset
    print(dgp.__version__)  # 确认版本
    

一个重要技巧dgp库本身不包含数据,它只是一个数据加载器。安装成功后,你可以自由地git checkout到其他分支或标签查看代码,而无需重新安装,因为pip install -e .链接的是当前目录。

2.3 数据集下载与组织

DDAD数据集很大(约1TB),需要通过官方提供的脚本和AWS CLI进行下载。你需要先在TRI-ML DDAD页面申请数据访问权限。

下载后,数据集目录结构通常如下:

DDAD/
├── train/                # 训练集
│   ├── 00-00-0000/      # 场景文件夹
│   │   ├── rgb/         # 6个相机的RGB图像
│   │   ├── point_cloud/ # LiDAR点云
│   │   └── ...          # 标定文件等
│   └── ...
├── val/                  # 验证集
└── ddad_train_val.json   # 核心的索引和标注文件

你需要将数据集路径和这个JSON文件的路径记录下来,在代码中会用到。

3. 数据加载与可视化:窥探数据全貌

环境准备好后,第一件事就是验证我们能否正确加载和查看数据。这里我直接分享一段修改自官方Demo,且在我环境下验证通过的代码片段。

3.1 基础数据加载

import os
from dgp.datasets.synchronized_dataset import SynchronizedSceneDataset

# 配置你的路径
DDAD_ROOT = '/path/to/your/DDAD'
SPLIT_JSON = os.path.join(DDAD_ROOT, 'ddad_train_val.json')

# 定义需要加载的数据类型(6个相机 + 1个LiDAR)
DATUMS = [
    'CAMERA_01', 'CAMERA_02', 'CAMERA_03',
    'CAMERA_04', 'CAMERA_05', 'CAMERA_06',
    'lidar'
]

# 创建数据集对象
# 关键参数:generate_depth_from_datum='lidar',这将自动从LiDAR点云生成稠密深度图作为真值
ddad_dataset = SynchronizedSceneDataset(
    scene_dataset_json=SPLIT_JSON,
    split='train',  # 或 'val'
    datum_names=DATUMS,
    generate_depth_from_datum='lidar'
)

print(f'数据集共有 {len(ddad_dataset)} 个样本。')

3.2 理解数据样本结构

加载一个样本看看里面到底有什么:

sample_idx = 0
sample = ddad_dataset[sample_idx]

# sample是一个字典,键值对非常丰富
print("样本包含的键:", sample.keys())

# 查看RGB图像形状 (6个相机,3通道,H, W)
rgb_data = sample['rgb']
print(f"RGB图像形状: {rgb_data.shape}")  # 预期: torch.Size([6, 3, 384, 640])

# 查看生成的深度图形状 (6个相机对应的深度,1通道,H, W)
depth_data = sample['depth']
print(f"深度图形状: {depth_data.shape}")  # 预期: torch.Size([6, 1, 384, 640])

# 查看相机内参
intrinsics = sample['intrinsics']  # 是一个包含6个相机内参矩阵的列表
print(f"第一个相机的内参:\n{intrinsics[0]}")

你会发现,dgp已经帮你做好了大量繁琐的工作:传感器数据对齐、深度图生成、数据格式统一(转换为PyTorch Tensor)。这极大地简化了后续模型训练的DataLoader编写。

3.3 深度图可视化与理解

初次可视化从LiDAR生成的深度图时,你可能会感到困惑:为什么看起来是稀疏的、有空洞的点状图,而不是光滑连续的?

import matplotlib.pyplot as plt
import numpy as np

# 获取第一个相机视角的深度图,并转换为numpy数组
depth_np = depth_data[0, 0].numpy()  # 形状 (384, 640)

plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.imshow(depth_np, cmap='viridis')
plt.colorbar(label='深度值')
plt.title('LiDAR投影深度图 (原始)')
plt.axis('off')

# 为了更清晰,我们可以显示深度值的倒数(视差)或者进行对数缩放以看清细节
plt.subplot(1, 2, 2)
# 将无效深度(通常为0)掩码掉
valid_depth = depth_np.copy()
valid_depth[valid_depth == 0] = np.nan
plt.imshow(np.log(valid_depth + 1e-6), cmap='plasma')
plt.colorbar(label='log(深度)')
plt.title('LiDAR深度图 (对数缩放,突出细节)')
plt.axis('off')
plt.show()

这是因为LiDAR点云本身是稀疏的,投影到图像平面上自然会有大量像素没有对应的深度值(被设为0)。这正是DDAD提供“稠密”深度挑战的地方:模型需要从稀疏的监督信号中,学习预测出稠密、合理的深度图。在训练时,我们通常需要创建一个深度有效掩码,只在有真值的像素上计算损失。

# 创建深度有效掩码的示例
depth_mask = (depth_data > 0).float()  # 有效深度位置为1,无效为0
print(f"深度掩码中有效像素的比例: {depth_mask.mean().item():.2%}")

4. 集成现有模型训练:以PackNet-SFM为例

理解了数据加载,下一步就是将其接入一个真实的模型进行训练。这里我选择TRI-ML的PackNet-SFM作为例子,这是一个经典的自监督单目深度估计模型,官方也提供了对DDAD的支持。

4.1 克隆与依赖安装

git clone https://github.com/TRI-ML/packnet-sfm.git
cd packnet-sfm
pip install -r requirements.txt

注意:这里可能遇到最大的坑——Horovod。 PackNet-SFM默认使用Horovod进行多GPU分布式训练。如果你的环境没有NCCL2或者网络权限受限,安装Horovod会失败。

我的避坑方案:改为单GPU训练。 对于个人研究者或快速验证,单GPU完全足够。你需要做两件事:

  1. 跳过Horovod安装:注释掉requirements.txthorovod那一行,或者安装时不带分布式支持。
  2. 修改训练脚本:找到启动训练的脚本(通常是train.py),找到与Horovod相关的代码(如hvd.init(), hvd.rank()等),将其注释或修改。一个更简单粗暴但有效的方法是,直接使用代码库中通常也提供的单GPU训练入口,或者将--num_gpus参数设为1。

4.2 配置文件适配

PackNet-SFM使用YAML配置文件管理所有参数。你需要创建一个针对DDAD的配置文件,或修改现有的示例。

关键配置项包括:

# configs/ddad_example.yaml
model:
  name: 'PackNet'  # 模型名称

datasets:
  train:
    dataset: ['DDAD']  # 数据集名称
    path: ['/path/to/your/DDAD']  # DDAD根路径
    split: ['train']   # 使用训练集
    depth_type: ['lidar']  # 深度真值来源
    ...
  validation:
    dataset: ['DDAD']
    path: ['/path/to/your/DDAD']
    split: ['val']
    depth_type: ['lidar']
    ...

training:
  batch_size: 4  # 根据你的GPU显存调整
  num_workers: 4  # 数据加载线程数
  epochs: 20
  ...

你需要仔细检查配置文件中数据路径、图像尺寸、深度处理方式等是否与DDAD的特性匹配。

4.3 启动训练与监控

在解决所有依赖和配置问题后,终于可以启动训练了:

python scripts/train.py --config configs/ddad_example.yaml

训练开始后,重点关注以下几点:

  • 损失下降曲线:初期损失应快速下降,后续趋于平缓。
  • GPU利用率:使用nvidia-smi查看,确保GPU没有被闲置。如果利用率低,可能是数据加载(num_workers)或批处理大小(batch_size)设置不合理。
  • 验证集指标:关注在未见过的验证集上的深度估计误差(如Abs Rel, Sq Rel, RMSE等),这是衡量模型泛化能力的核心。

4.4 性能优化技巧

在单GPU环境下,如果训练速度过慢,可以尝试以下优化:

  1. 数据加载优化
    • 适当增加num_workers,但不要超过CPU核心数。
    • 使用pin_memory=True(在DataLoader中设置),可以加速数据从CPU到GPU的传输。
  2. 混合精度训练(AMP):现代PyTorch支持自动混合精度训练,能显著减少显存占用并加速计算。在训练脚本中通常只需添加几行代码即可启用。
    from torch.cuda.amp import autocast, GradScaler
    scaler = GradScaler()
    # 在前向传播和损失计算时
    with autocast():
        pred_depth = model(rgb)
        loss = criterion(pred_depth, gt_depth, mask)
    # 反向传播时
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()
    
  3. 梯度累积:当GPU显存不足以支撑大的batch_size时,可以使用梯度累积。例如,设置batch_size=2,但每4个批次才更新一次梯度,其效果近似于batch_size=8
  4. 检查点与恢复:一定要配置模型保存策略(如每N个epoch保存一次)。这样在训练中断时,可以从最近的检查点恢复,避免前功尽弃。

整个流程走下来,最深的体会就是:使用像DDAD这样的大型工业级数据集,环境隔离、版本控制和耐心调试比算法本身更重要。它迫使你更深入地理解模型依赖的数据流水线,而这恰恰是迈向成熟工程实践的关键一步。当你终于看到自己的模型在DDAD的复杂场景下开始输出合理的深度图时,那种成就感远大于在简单数据集上刷出一个新高的指标。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐