4. 数据加载器实现:自定义Dataset类、BEV数据增强策略、多模态数据融合加载
数据加载器,说白了就是整个部署流程的「粮草官」。
我见过不少团队,模型结构设计得漂漂亮亮,结果一到训练就卡在数据读取上。要么内存爆炸,要么增强策略跟BEV空间不兼容,跑出来的结果一塌糊涂。嗯,今天咱们就把这块硬骨头啃下来。
4.1 自定义Dataset类:从原始数据到模型输入的最后一公里
写一个Dataset类,其实没那么玄乎。你只需要搞清楚三件事:数据从哪来、怎么读、怎么给。
核心原则:Dataset类只负责「单帧」数据的加载与预处理。多线程、批处理这些活儿,交给DataLoader去干。
我个人习惯把BEV的Dataset拆成三个模块:
- 数据索引模块:扫描数据集目录,建立样本列表
- 数据加载模块:读取图像、点云、标定参数
- 数据预处理模块:去畸变、坐标变换、裁剪填充
来看一个简化版的代码骨架:
class BEVDataset(Dataset):
def __init__(self, data_root, split='train'):
self.samples = self._build_index(data_root, split)
# 我习惯在这里预加载标定参数,避免每次__getitem__都读文件
self.calibs = self._load_calibrations(data_root)
def __len__(self):
return len(self.samples)
def __getitem__(self, idx):
sample = self.samples[idx]
# 多模态数据
imgs = self._load_images(sample['img_paths'])
points = self._load_lidar(sample['lidar_path'])
calib = self.calibs[sample['scene_id']]
# 坐标变换:从传感器坐标系到BEV网格
# 这里要注意,不同传感器的时间戳要对齐
return {
'images': imgs, # [N_cam, C, H, W]
'points': points, # [N_pts, 4] (x, y, z, intensity)
'calib_matrix': calib, # 投影矩阵
'gt_boxes': sample['gt'] # 标注框
}
避坑指南:我曾经在nuscenes数据集上踩过一个坑——不同相机的曝光时间不一样,导致同一时刻的图像亮度差异巨大。后来我在Dataset里加了一个自适应直方图均衡化,效果好了不少。
4.2 BEV数据增强策略:别把空间关系搞乱了
图像上的数据增强,比如随机裁剪、颜色抖动,直接搬到BEV上会出问题。为什么?因为BEV空间是物理世界的映射,你随便裁剪一下,物体的真实位置就变了。
我总结了一套「BEV安全增强三板斧」:
4.2.1 翻转(Flip)
水平翻转是最安全的。你想想看,一辆车在路左边,翻转后到了右边,这在现实中完全合理。
- 图像域:左右翻转所有相机图像
- 点云域:对x轴取反(假设y轴朝前)
- 标注域:框的中心x坐标取反,朝向角加π
4.2.2 旋转(Rotation)
旋转要小心。BEV网格是离散的,旋转后物体边缘会出现锯齿。我一般限制在±5°以内。
注意:旋转操作必须同时作用于所有模态。我曾经只旋转了点云没旋转图像,结果图像上的车道线和点云上的路沿差了半个车身位,模型直接学废了。
4.2.3 缩放(Scaling)
缩放模拟的是相机高度变化。比如公交车和轿车的视野高度不同。
- 缩放因子建议在[0.9, 1.1]之间
- 缩放后要重新计算BEV网格的分辨率
- 标注框的尺寸也要等比缩放
来看一个增强策略的代码片段:
def bev_augmentation(imgs, points, gt_boxes, config):
# 随机翻转
if random.random() < 0.5:
imgs = [cv2.flip(img, 1) for img in imgs]
points[:, 0] = -points[:, 0]
gt_boxes[:, 0] = -gt_boxes[:, 0]
gt_boxes[:, 6] = np.pi - gt_boxes[:, 6] # 朝向角调整
# 随机旋转(小角度)
angle = random.uniform(-5, 5) * np.pi / 180
rot_mat = np.array([[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
points[:, :2] = points[:, :2] @ rot_mat.T
# 别忘了旋转图像!这里用仿射变换
M = cv2.getRotationMatrix2D((W/2, H/2), angle*180/np.pi, 1.0)
imgs = [cv2.warpAffine(img, M, (W, H)) for img in imgs]
return imgs, points, gt_boxes
4.3 多模态数据融合加载:时间同步与空间对齐
多模态融合,说白了就是让摄像头和激光雷达「说同一种语言」。
我遇到过最头疼的问题就是时间戳对齐。摄像头是30fps,激光雷达是10fps,你拿到的数据天然就是错位的。
我的做法:以激光雷达的时间戳为基准,找到最近邻的相机帧。如果时间差超过50ms,就丢弃这帧数据。别心疼,错位的数据比没数据更可怕。
4.3.1 空间对齐:投影矩阵
把点云投影到图像上,需要三步:
- 点云从激光雷达坐标系转到相机坐标系(外参)
- 从相机坐标系转到图像坐标系(内参)
- 去畸变(径向畸变+切向畸变)
代码实现:
def project_lidar_to_image(points, calib):
# points: [N, 3] 在激光雷达坐标系下
# 1. 转到相机坐标系
pts_cam = points @ calib['lidar2cam_rot'].T + calib['lidar2cam_trans']
# 2. 投影到图像平面
pts_img = pts_cam @ calib['intrinsic'].T
pts_img[:, :2] /= pts_img[:, 2:3] # 归一化
# 3. 去畸变(这里用OpenCV的接口)
pts_undist = cv2.undistortPoints(pts_img[:, :2],
calib['K'], calib['dist'])
return pts_undist
4.3.2 数据融合策略
我常用的融合方式有三种:
| 融合方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 前融合(Early Fusion) | 信息损失少 | 计算量大,内存爆炸 | 小规模场景 |
| 后融合(Late Fusion) | 模块解耦,易调试 | 丢失了底层交互信息 | 工业部署 |
| 特征融合(Feature Fusion) | 平衡了性能和效果 | 实现复杂 | 大多数BEV模型 |
个人经验:在部署阶段,我倾向于用后融合。为什么?因为摄像头和激光雷达的故障模式不同,后融合可以单独降级。摄像头坏了,激光雷达还能撑一会儿。
4.4 本章知识体系
下面这张图,是我自己梳理的数据加载器整体流程。你照着这个结构去写代码,基本不会跑偏。
嗯,数据加载器这块,说白了就是「把原始数据变成模型能吃的格式」。你只要把时间同步、空间对齐、增强策略这三个点拿捏住,剩下的就是体力活了。
可可精品资料库