自動運転:BEVの先駆け LSS(リフト、スプラ、シュート)原理コードシリーズトーク
序文
現在、自動運転の分野では、自動運転の知覚に関連するタスクを完了するために、収集された周囲の画像情報に基づいて BEV の観点から特徴を構築することが一般的な研究の方向性です。したがって、カメラ視点からBEV視点への移行をいかに正確に完了させるかが非常に重要となる。現在主流となっている方法は大きく分けて2種類あります。
- 画像の奥行き情報を明示的に推定して、BEV パースペクティブの構築を完了します。これは、一部の記事ではボトムアップ構築法とも呼ばれます。
- トランスフォーマーのクエリ メカニズムを使用して、BEV クエリを使用して BEV 機能を構築します。このプロセスはトップダウン構築とも呼ばれます。
LSS の最大の貢献は、エンドツーエンドのトレーニング方法を提供し、複数のセンサー フュージョンの問題を解決することです。複数のセンサーを個別に検出して後処理を実行する従来の方法では、このプロセス損失を逆伝播してカメラ入力を調整することができませんが、LSS ではこの段階での後処理の必要性がなくなり、融合結果が直接出力されます。
リフト
パラメータ
まずいくつかのパラメータを紹介します:
検知範囲
: x 軸方向の検知範囲 -50m ~ 50m; y 軸方向の検知範囲 -50m ~ 50m; Z 軸方向の検知範囲 -10m ~ 10m; BEV
セル
x 軸方向のサイズ ユニット長 0.5 m、y 軸方向のユニット長 0.5 m、z 軸方向のユニット長 20 m、BEV
グリッド サイズ
200 x 200 x 1、
深度推定範囲
LSS は明示的に推定する必要があるため、論文では、ピクセルの離散深度が示されています。範囲は 4m ~ 45m、間隔は 1m です。つまり、アルゴリズムは 41 個の離散深度を推定します。これは以下の dbound です。
なぜ dbound :
2 次元ピクセルは、現実世界のある点からカメラの中心に向かう光線として理解できるため、カメラの内部パラメータと外部パラメータがわかれば、対応関係がわかりますが、光線上のどの点がわからない (つまり、深さがわからない) ため、作成者は、カメラから 5 m から 45 m までの視錐台内で 1 m ごとにモデルのオプションの深さの値を設定します (したがって、各ピクセルは 41 になります)オプションの離散深度値)。
コードは以下のように表示されます。
ogfH=128
ogfW=352
xbound=[-50.0, 50.0, 0.5]
ybound=[-50.0, 50.0, 0.5]
zbound=[-10.0, 10.0, 20.0]
dbound=[4.0, 45.0, 1.0]
fH, fW = ogfH // 16, ogfW // 16
視錐台を作成する
コード:
def create_frustum(self):
# make grid in image plane
ogfH, ogfW = self.data_aug_conf['final_dim']
fH, fW = ogfH // self.downsample, ogfW // self.downsample
ds = torch.arange(*self.grid_conf['dbound'], dtype=torch.float).view(-1, 1, 1).expand(-1, fH, fW)
D, _, _ = ds.shape
xs = torch.linspace(0, ogfW - 1, fW, dtype=torch.float).view(1, 1, fW).expand(D, fH, fW)
ys = torch.linspace(0, ogfH - 1, fH, dtype=torch.float).view(1, fH, 1).expand(D, fH, fW)
# D x H x W x 3
frustum = torch.stack((xs, ys, ds), -1)
return nn.Parameter(frustum, requires_grad=False)
コードによれば、そのサイズは 2dimage に基づいて構築され、そのサイズは D * H * W * 3 で、次元 3 は [x, y, 深さ] を表します。この視円錐は、長さ x、幅 y、高さ奥行きの直方体として理解でき、視円錐内の各点が直方体の座標になります。
カムエンコード
この部分では主に Efficient Net を使用して画像の特徴を抽出します。最初にコードを見てください。
class CamEncode(nn.Module):
def __init__(self, D, C, downsample):
super(CamEncode, self).__init__()
self.D = D
self.C = C
self.trunk = EfficientNet.from_pretrained("efficientnet-b0")
self.up1 = Up(320+112, 512)
# 输出通道数为D+C,D为可选深度值个数,C为特征通道数
self.depthnet = nn.Conv2d(512, self.D + self.C, kernel_size=1, padding=0)
def get_depth_dist(self, x, eps=1e-20):
return x.softmax(dim=1)
def get_depth_feat(self, x):
# 主干网络提取特征
x = self.get_eff_depth(x)
# 输出通道数为D+C
x = self.depthnet(x)
# softmax编码,相理解为每个可选深度的权重
depth = self.get_depth_dist(x[:, :self.D])
# 深度值 * 特征 = 2D特征转变为3D空间(俯视图)内的特征
new_x = depth.unsqueeze(1) * x[:, self.D:(self.D + self.C)].unsqueeze(2)
return depth, new_x
def forward(self, x):
depth, x = self.get_depth_feat(x)
return x
最初は前と同じです。init 関数の最後の文で、特徴チャネルは D + C にダウンサンプリングされます。D は上記の視錐台の D と一致し、深度特徴を保存するために使用されます。C は画像の意味特徴を抽出し、D の部分でチャネル Softmax を実行して奥行きの確率分布を予測し、D の部分と C の部分を別々に取り出して 2 つの外積を計算します。となり、BNDCHWの形状の特徴が得られます。
デモコードは次のとおりです。
a = torch.ones(36*4).resize(4,6,6)+1
demo1 = a.unsqueeze(1)
print(demo1.shape)
b = torch.ones(36*4).resize(4,6,6)+3
demo2 = b.unsqueeze(0)
print(demo2.shape)
c = demo1*demo2
print(c.shape)
torch.Size([4, 1, 6, 6])
torch.Size([1, 4, 6, 6])
torch.Size([4, 4, 6, 6])
右側のグリッド ダイアグラムを見てみましょう。まず、グリッド ダイアグラムの座標について説明します。ここで、a は特定の深さのソフトマックス確率 (サイズ H * W) を表し、c は意味的特徴の特定のチャネルの特徴を表します。次に、ac はこれらを表します。行列の対応する要素が乗算されるため、特徴の各点に深度確率が与えられ、その後、すべての AC がブロードキャストされ、異なる深さ (チャネル) での異なるチャネルの意味論的特徴の特徴マップが取得されます。 , 重要な特徴の色は (ソフトマックスの確率が高いため) どんどん暗くなり、逆にどんどん暗くなって 0 に近づきます。
スプラット
深度情報を含む特徴マップを取得した後、これらの特徴が 3D 空間内のどの点に対応するかを知りたいと思います。
私たちの視錐台は元の画像を 16 回ダウンサンプリングし、上で得られた特徴マップの受容野も 16 であるため、次の操作で特徴マップを視錐台座標にマッピングできます。
錐台座標系の変換
まず、以前に 2D ビュー錐台を取得し、カメラの内部パラメータと外部パラメータを通じてそれを車体の座標系 (車の中心を原点とする) にマッピングしました。
コードは以下のように表示されます。
def get_geometry(self, rots, trans, intrins, post_rots, post_trans):
B, N, _ = trans.shape # B: batch size N:环视相机个数
# undo post-transformation
# B x N x D x H x W x 3
# 抵消数据增强及预处理对像素的变化
points = self.frustum - 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(intrins))
points = combine.view(B, N, 1, 1, 1, 3, 3).matmul(points).squeeze(-1)
points += trans.view(B, N, 1, 1, 1, 3)
# (bs, N, depth, H, W, 3):其物理含义
# 每个batch中的每个环视相机图像特征点,其在不同深度下位置对应
# 在ego坐标系下的坐标
return points
ボクセルプーリング
コード:
def voxel_pooling(self, geom_feats, x):
# geom_feats;(B x N x D x H x W x 3):在ego坐标系下的坐标点;
# x;(B x N x D x fH x fW x C):图像点云特征
B, N, D, H, W, C = x.shape
Nprime = B*N*D*H*W
# 将特征点云展平,一共有 B*N*D*H*W 个点
x = x.reshape(Nprime, C)
# flatten indices
geom_feats = ((geom_feats - (self.bx - self.dx/2.)) / self.dx).long() # ego下的空间坐标转换到体素坐标(计算栅格坐标并取整)
geom_feats = geom_feats.view(Nprime, 3) # 将体素坐标同样展平,geom_feats: (B*N*D*H*W, 3)
batch_ix = torch.cat([torch.full([Nprime//B, 1], ix,
device=x.device, dtype=torch.long) for ix in range(B)]) # 每个点对应于哪个batch
geom_feats = torch.cat((geom_feats, batch_ix), 1) # geom_feats: (B*N*D*H*W, 4)
# filter out points that are outside box
# 过滤掉在边界线之外的点 x:0~199 y: 0~199 z: 0
kept = (geom_feats[:, 0] >= 0) & (geom_feats[:, 0] < self.nx[0])\
& (geom_feats[:, 1] >= 0) & (geom_feats[:, 1] < self.nx[1])\
& (geom_feats[:, 2] >= 0) & (geom_feats[:, 2] < self.nx[2])
x = x[kept]
geom_feats = geom_feats[kept]
# get tensors from the same voxel next to each other
ranks = geom_feats[:, 0] * (self.nx[1] * self.nx[2] * B)\
+ geom_feats[:, 1] * (self.nx[2] * B)\
+ geom_feats[:, 2] * B\
+ geom_feats[:, 3] # 给每一个点一个rank值,rank相等的点在同一个batch,并且在在同一个格子里面
sorts = ranks.argsort()
x, geom_feats, ranks = x[sorts], geom_feats[sorts], ranks[sorts] # 按照rank排序,这样rank相近的点就在一起了
# cumsum trick
if not self.use_quickcumsum:
x, geom_feats = cumsum_trick(x, geom_feats, ranks)
else:
x, geom_feats = QuickCumsum.apply(x, geom_feats, ranks)
# griddify (B x C x Z x X x Y)
final = torch.zeros((B, C, self.nx[2], self.nx[0], self.nx[1]), device=x.device) # final: bs x 64 x 1 x 200 x 200
final[geom_feats[:, 3], :, geom_feats[:, 2], geom_feats[:, 0], geom_feats[:, 1]] = x # 将x按照栅格坐标放到final中
# collapse Z
final = torch.cat(final.unbind(dim=2), 1) # 消除掉z维
return final # final: bs x 64 x 200 x 200
要約する
アドバンテージ:
1. LSS メソッドは、BEV の観点にうまく統合されたメソッドを提供します。この方法に基づいて、動的目標検出、静的道路構造認識、さらには信号機検出、前方車両方向指示器検出やその他の情報であっても、この方法を使用して BEV の特徴を抽出して出力することができ、自動統合が大幅に向上します。運転知覚フレームワークの。
2. LSS の本来の目的は、マルチビュー カメラの特性を統合し、「純粋な視覚」モデルとして機能することです。ただし、実際のアプリケーションでは、この方法は他のセンサーの機能融合と完全に互換性があります。超音波レーダー機能を融合したい場合は、試してみてください。
欠点:
1. 深度情報の精度に大きく依存するため、深度特徴は明示的に提供する必要があります。もちろん、これはほとんどの純粋に視覚的な方法の欠点です。この方法を勾配バックプロパゲーションを通じて深度ネットワークの最適化を促進するために直接使用する場合、深度ネットワーク設計が比較的複雑である場合、長いバックプロパゲーション チェーンにより深度の最適化方向がぼやけることが多く、良好な結果を達成することが困難になります。結果。もちろん、良い解決策は、LSS プロセスがより理想的な深度出力を持つように、最初により良い深度重みを事前トレーニングすることです。
2. 外積演算に時間がかかりすぎる。機械学習の場合、この計算量はそれほど重要ではありませんが、車に展開するモデルの場合、画像の特徴サイズが大きく、予測したい奥行き距離と精度が高い場合、外積は演算量が大幅に増加します。これはモデルの軽量展開には役立たないため、この点では Transformer メソッドの方がわずかに優れています。