BEVDetモデルの構造解析
BEVDetは4段階の処理フローで3Dオブジェクト検出を実現しています: - 画像ビュー符号化器:周囲カメラ画像の特徴抽出 - ビュー変換器:画像ビュー特徴をBEV(Bird's Eye View)特徴に変換 - BEV符号化器:BEV特徴のエンコーディング強化 - 検出ヘッド:最終的なオブジェクト検出 この設計ではビュー変換器が明示的なBEV特徴表現を生成し、点群データのような形式を採用しています。性能向上のためにCUDAによるvoxel pooling操作の最適化が行われ、検出ヘッドでは改良されたNMSアルゴリズムが採用されています。
主要なコードフロー解析
- テストエントリポイント
tools/test.py
outputs = single_gpu_test(...) # -> mmdet3d/apis/test.py else: ... - 推論スケジューリング
mmdet3d/apis/test.py
return self.forward_train(**kwargs) else: return self.forward_test(**kwargs) # -> mmdet3d/models/detectors/base.py - BEVDet本体実装
mmdet3d/models/detectors/bevdet.py
def __init__(...): ... def forward_test(...): if not isinstance(img_inputs[0][0], list): return self.simple_test(...) def simple_test(...): img_feats, _, _ = self.extract_feat(...) # 参考centerpoint bbox_pts = self.simple_test_pts(img_feats, img_metas, rescale=rescale) def extract_feat(...): img_feats, depth = self.extract_img_feat(...) pts_feats = None return (img_feats, pts_feats, depth) def extract_img_feat(...): # 周囲カメラ画像の特徴抽出 x = self.image_encoder(img[0]) # BEV特徴 x, depth = self.img_view_transformer([x] + img[1:7]) # -> mmdet3d/models/necks/view_transformer.py x = self.bev_encoder(x) return [x],depth
ビュー変換核心実装
3.1.mmdet3d/models/necks/view_transformer.py
Voxel poolingの核となるvoxel_pooling_prepare_v2関数の処理を視覚化して説明しています。
def create_frustum(...):
...
def forward(self, input):
"""画像ビュー特徴をBEV特徴に変換
Args:
input: [画像ビュー特徴, 回転行列, 平移ベクトル, 内部パラメータ, 後処理回転, 後処理平移]
"""
# LIFT, x:[6, 139, 16, 44]
# 前self.Dは予測された離散距離、後self.out_channelsは深度特徴
x = self.depth_net(x)
# 深度
depth_digit = x[:, :self.D, ...]
# 特徴
tran_feat = x[:, self.D:self.D + self.out_channels, ...]
# 深度確率分布
depth = depth_digit.softmax(dim=1)
# BEV空間へ変換
return self.view_transform(input, depth, tran_feat)
def view_transform(...):
return self.view_transform_core(input, depth, tran_feat)
def view_transform_core(...):
'''
Args:
input:[1, 6, 512, 16, 44],周囲カメラ特徴
depth:[6, 59, 16, 44],# 深度確率分布
tran_feat: [6, 80, 16, 44],深度特徴
'''
if ...:
...
else:
# 点群取得
coor = self.get_lidar_coor(*input[1:7])
# BEV空間への投影
# 詳細はこちら https://zhuanlan.zhihu.com/p/586637783
bev_feat = self.voxel_pooling_v2(...)
# bev_feat:[1, 80, 128, 128] depth:[6, 59, 16, 44]
return bev_feat,depth
def get_lidar_coor(...):
# self.frustum ビューフrustum
# データ拡張の平移を差し引く
points = self.frustum.to(rots) - post_trans.view(B, N, 1, 1, 1, 3)
# 画像前処理の回転行列の逆行列をかける
points = torch.inverse(post_rots).view(B, N, 1, 1, 1, 3, 3).matmul(points.unsqueeze(-1))
# ピクセル座標系 -> 正規化カメラ座標系 -> カメラ座標系 -> 車両座標系
points = torch.cat((points[..., :2, :] * points[..., 2:3, :], points[..., 2:3, :]), 5)
# カメラ内蔵パラメータ
combine = rots.matmul(torch.inverse(cam2imgs))
# カメラ座標系から車両座標系への変換
points = combine.view(B, N, 1, 1, 1, 3, 3).matmul(points).squeeze(-1)
points += trans.view(B, N, 1, 1, 1, 3)
# BEV特徴の拡張行列、ここでは単位行列
points = bda.view(B, 1, 1, 1, 1, 3,3).matmul(points.unsqueeze(-1)).squeeze(-1)
return points
def voxel_pooling_v2(self, coor, depth, feat):
"""
Args:
coor:車両座標系でのビューフrustum点座標
depth:離散深度確率分布
feat:深度特徴
"""
ranks_bev, ranks_depth, ranks_feat, interval_starts, interval_lengths = self.voxel_pooling_prepare_v2(coor)
def voxel_pooling_prepare_v2(...):
"""voxel poolingのデータ準備
"""
B, N, D, H, W, _ = coor.shape
num_points = B * N * D * H * W # ビューフrustum点の総数
ranks_depth = torch.range(0, num_points - 1, dtype=torch.int, device=coor.device) # 0~249215
# 各層の特徴位置インデックス [0,1,2,3..4223,0,1,2...,4223,...,0,1,2...,4223]
ranks_feat = ...
# 座標系をBEV空間に調整
coor = ((coor - self.grid_lower_bound.to(coor)) / self.grid_interval.to(coor))
coor = coor.long().view(num_points, 3
# バッチ番号を追加
batch_idx = torch.range(0, B - 1).reshape(B, 1). expand(B, num_points // B).reshape(num_points, 1).to(coor)
coor = torch.cat((coor, batch_idx), 1)
# BEV空間外の点をフィルタリング
kept = (coor[:, 0] >= 0) & (coor[:, 0] < self.grid_size[0]) & \
(coor[:, 1] >= 0) & (coor[:, 1] < self.grid_size[1]) & \
(coor[:, 2] >= 0) & (coor[:, 2] < self.grid_size[2])
if len(kept) == 0:
return None, None, None, None, None
# BEV空間内の点を選択
coor, ranks_depth, ranks_feat = coor[kept], ranks_depth[kept], ranks_feat[kept]
# BEV特徴マップのグローバルインデックス計算
ranks_bev = coor[:, 3] * (self.grid_size[2] * self.grid_size[1] * self.grid_size[0])
ranks_bev += coor[:, 2] * (self.grid_size[1] * self.grid_size[0])
ranks_bev += coor[:, 1] * self.grid_size[0] + coor[:, 0]
# ソート
order = ranks_bev.argsort()
ranks_bev, ranks_depth, ranks_feat = ranks_bev[order], ranks_depth[order], ranks_feat[order]
kept = torch.ones(ranks_bev.shape[0], device=ranks_bev.device, dtype=torch.bool)
# 階層比較によりインデックス位置を特定
kept[1:] = ranks_bev[1:] != ranks_bev[:-1]
interval_starts = torch.where(kept)[0].int()
if len(interval_starts) == 0:
return None, None, None, None, None
interval_lengths = torch.zeros_like(interval_starts)
# 各区間長を計算
interval_lengths[:-1] = interval_starts[1:] - interval_starts[:-1]
interval_lengths[-1] = ranks_bev.shape[0] - interval_starts[-1]
return ranks_bev.int().contiguous(), ranks_depth.int().contiguous(
), ranks_feat.int().contiguous(), interval_starts.int().contiguous(
), interval_lengths.int().contiguous()
Voxel Pooling 図解
3.2.mmdet3d/ops/bev_pool_v2/src/bev_pool_cuda.cu
"""
Args:
c:80,BEV特徴チャンネル次元
n_intervals:Nd,trueのインデックスを持つ集合
その他は上記voxel_pooling_prepare_v2関数参照
"""
# trueのインデックスを持つビューフrustum点、各点の特徴深度80個分を確保
# block数はceil(((double)n_intervals * c / 256))、各blockに256スレッドを割り当て
bev_pool_v2_kernel<<<(int)ceil(((double)n_intervals * c / 256)), 256>>>(...);
}
__global__ void bev_pool_v2_kernel(...) {
// out:BEV特徴出力 [1,1,128,128,80]
int idx = blockIdx.x * blockDim.x + threadIdx.x; // グローバルスレッドインデックス
int index = idx / c; // 处理中のビューフrustum点
int cur_c = idx % c; // 处理中の特徴チャネル
if (index >= n_intervals) return;
int interval_start = interval_starts[index]; // trueのインデックス
int interval_length = interval_lengths[index]; // 区間長
float psum = 0; // 特徴チャネルごとの累積和
const float* cur_depth;
const float* cur_feat;
// 累積計算
for(int i = 0; i < interval_length; i++){
cur_depth = depth + ranks_depth[interval_start+i]; # ビューフrustum点の深度
cur_feat = feat + ranks_feat[interval_start+i] * c + cur_c; # 特徴データ
psum += *cur_feat * *cur_depth; // 累積
}
const int* cur_rank = ranks_bev + interval_start; // BEV特徴マップの位置
float* cur_out = out + *cur_rank * c + cur_c; // BEV特徴マップの位置
*cur_out = psum;
}