如何理解自動駕駛,SLAM,BEV,訓練數據源常見術語?(4)
圖29實際上是在照片坐標系(uv)上拓展了一個深度Z構成的新坐標系。由于LSS默認是5路攝像頭,把5個Frustum送到get_geometry函數里,會輸出5路Frustum構成的一個組合籠子,其張量尺寸變為:B x N x D x H x W x 3,其中B是batch_size,默認是4組訓練數據,N是相機數量5。get_geometry里一開始要做一個
#undo post-transformation
這玩意是干啥的?這跟訓練集有關,在深度學習里里,有一種增強現有訓練樣本的方法,一般叫做Augmentation(其實AR技術里這個A就是Augmentation,增強的意思),通過把現有的訓練數據做一些隨機的:翻轉/平移/縮放/裁減,給樣本添加一些隨機噪音(Noise)。比如,在不做樣本增強前,相機的角度是不變的,訓練后的模型只認這個角度的照片,而隨機增強后再訓練,模型可以學習出一定角度范圍變化內的適應性,也就是Robustness。
圖30Augmentation技術也是有相關理論和方法的,這里就貼個圖不贅述了。數據增強的代碼一般都是位于DataLoader內:
class NuscData(torch.utils.data.Dataset):
def sample_augmentation(self):
回到剛才的get_geometry,數據增強會給照片增加一些隨機變化,但相機本身是必須固定的,這樣才能讓DNN模型學習這些隨機變化的規律并去適應它們。所以將5路Frustum的安置到車身坐標系時候要先去掉(undo)這些隨機變化。然后通過:
# cam_to_ego
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)
將各路Frustum從相機坐標系轉入車輛自身坐標系,注意這里的intrins是相機內參,rots和trans是相機外參,這些都是nuScenes訓練集提供的,這里只有intrincs用了逆矩陣,而外參沒有,因為nuScenes是先把每個相機放在車身原點,然后按照各路相機的位姿先做偏移trans再做旋轉rots,這里就不用做逆運算了。如果換個數據集或者自己架設相機采集數據,要搞清楚這些變換矩陣的定義和計算順序。四視圖大概就是這個樣子:
圖31
LSS中推理深度和相片特征的模塊位于:
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)
self.depthnet = nn.Conv2d(512, self.D + self.C, kernel_size=1, padding=0)
trunk用于同時推理原始的深度和圖片特征,depthnet用于將trunk輸出的原始數據解釋成LSS所需的信息,depthnet雖是卷積網但卷積核(Kernel)尺寸只有1個像素,功能接近一個全連接網FC(Full Connected),FC日常的工作是:分類或者擬合,對圖片特征而言,它這里類似分類,對深度特征而言,它這里類似擬合一個深度概率分布。EfficientNet是一種優化過的ResNet,就當做一個高級的卷積網(CNN)看吧。對于這個卷積網而言,圖片特征和深度特征在邏輯上沒有區別,兩者都位于trunk上的同一個維度,只是區分了channel而已。這就引出了另外一個話題:從單張2D圖片上是如何推理/提取深度特征的。這類問題一般叫做:Monocular Depth Estimation,單目深度估計。一般這類系統內部分兩個階段:粗加工(Coarse Prediction)和精加工(Refine Prediction),粗加工對整個畫面做一個場景級別的簡單深度推測,精加工是在這個基礎上識別更細小的物體并推測出更精細的深度。這類似畫家先用簡筆畫出場景輪廓,然后再細致勾勒局部畫面。除了用卷積網來解決這類深度估計問題,還有用圖卷積網(GCN)和Transformer來做的,還有依賴測距設備(RangeFinder)輔助的DNN模型,這個話題先不展開了,龐雜程度不亞于BEV本身。那么LSS這里僅僅采用了一個trunk就搞定深度特征是不是太兒戲了,事實上確實如此。LSS估計出的深度準頭和分辨率極差,參看BEVDepth項目里對LSS深度問題的各種測試報告:https://github.com/Megvii-BaseDetection/BEVDepthBEVDepth的測試里發現:如果把LSS深度估計部分的參數換成一個隨機數,并且不參與學習過程(Back Propagation),其BEV的總體測試效果只有很小幅度的降低。但必須要說明,Lift的機制本身是很強的,這個突破性的方法本身沒問題,只是深度估計這個環節可以再加強。
LSS的訓練過程還有另外一個問題:相片上大約有1半的數據對訓練的貢獻度為0,其實這個問題是大部分BEV算法都存在的:
圖32右邊的標注數據實際上只描述了照片紅線以下的區域,紅線上半部都浪費了,你要問LSS里的模型對上半部都計算了些什么,我也不知道,因為沒有標注數據可以對應上,而大部分的BEV都是這么訓練的,所以這是一個普遍現象。訓練時,BEV都會選擇一個固定面積范圍的周遭標注數據,而照片一般會拍攝到更遠的景物,這兩者在范圍上天生就是不匹配的,另一方面部分訓練集只關注路面標注,缺乏建筑,因為眼下BEV主要解決的是駕駛問題,不關心建筑/植被。這也是為什么圖17哪里的深度圖和LSS內部真實的深度圖是不一致的,真實深度圖只有接近路面這部分才有有效數據:
圖33所以整個BEV的DNN模型勢必有部分算力被浪費了。目前沒看到任何論文關于這方面的研究。
接著繼續深入LSS的Lift-Splat計算過程:
def get_depth_feat(self, x):
x = self.get_eff_depth(x)
# Depth
x = self.depthnet(x)
depth = self.get_depth_dist(x[:, :self.D])
new_x = depth.unsqueeze(1) * x[:, self.D:(self.D + self.C)].unsqueeze(2)
return depth, new_x
def get_voxels(self, x, rots, trans, intrins, post_rots, post_trans):
geom = self.get_geometry(rots, trans, intrins, post_rots, post_trans)
x = self.get_cam_feats(x)
x = self.voxel_pooling(geom, x)
return x
這里的new_x是把深度概率分布直接乘上了圖片紋理特征,為了便于直觀理解,我們假設圖片特征有3個channel:c1,c2,c3,深度只有3格:d1,d2,d3。我們從圖片上取某個像素,那么它們分別代表的意義是:c1:這個像素點有70%的可能性是車子,c2:有20%的可能性是路,c3:有10%的可能性是信號燈, d1:這個像素有80%的可能是在深度1,d2:有15%的可能性是在深度2,d3:有%5的可能性是在深度3上。如果把它們相乘的到:
那么這個像素最大的概率是:位于深度1的一輛車子。這也就是LSS里:
公式的意義,注意它這里把圖像特征叫做c(Context), a_d的意義是深度沿視線格子的概率分布,d是深度。new_x就是這個計算結果。前面說過,由于圖像特征和深度都是通過trunk訓練出來的,它們位于同一維度,只是占用channel不同,深度占用了前self.D(41)個channel,Context占用了后面self.C(64)個channel。由于new_x是分別按照每路相機的Frustum單獨計算的,而5個Frustum有重疊區域,須要做作數據融合,所以在voxel_pooling里計算好格子的索引和對應的空間位置,通過這個對應關系,把new_x的內容一一裝入指定索引的格子。LSS在voxel_pooling的計算力引入了cumsum這個機制,雖然有很多文章在解釋它,但這里不建議花太多功夫,它只是一個計算上的小技巧,對整個LSS是錦上添花的事,不是必要的。
*博客內容為網友個人發布,僅代表博主個人觀點,如有侵權請聯系工作人員刪除。