BEVDetモデルの構造と実装詳細

BEVDetモデルの構造解析

BEVDetは4段階の処理フローで3Dオブジェクト検出を実現しています: - 画像ビュー符号化器:周囲カメラ画像の特徴抽出 - ビュー変換器:画像ビュー特徴をBEV(Bird's Eye View)特徴に変換 - BEV符号化器:BEV特徴のエンコーディング強化 - 検出ヘッド:最終的なオブジェクト検出 この設計ではビュー変換器が明示的なBEV特徴表現を生成し、点群データのような形式を採用しています。性能向上のためにCUDAによるvoxel pooling操作の最適化が行われ、検出ヘッドでは改良されたNMSアルゴリズムが採用されています。

主要なコードフロー解析

  1. テストエントリポイント
    tools/test.py
    outputs = single_gpu_test(...)
    # -> mmdet3d/apis/test.py
    else:
        ...
    
  2. 推論スケジューリング
    mmdet3d/apis/test.py
    return self.forward_train(**kwargs)
    else:
        return self.forward_test(**kwargs)
        # -> mmdet3d/models/detectors/base.py
    
  3. 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;
}

タグ: BEVDet 3D目標検出 VoxelPooling CUDA 深度学習

9月15日 17:15 投稿