NGP 改用了 Grid based 的方法。NeRF 採用了一個很大的神經網路(8層、每層256個神經元),直接計算每個取樣點的 \(\sigma\) 和 \(c\) 就會很慢;NGP則是先找到所有包含這個點的Voxel,利用Voxel的頂點進行“三維內插”(trilinear)得到"特徵值",把這個特徵值丟到一個2層神經網路中;每個Voxel都會計算得到一個特徵值,多個Voxel的做法在論文中稱為 Multiresolution,所以特徵值也不止一個。
一、NGP實驗中的影響因素
(一)ablation study
(二)Dense Grid: single resolution
下面我們詳細講一下 Grid Based 的具體做法。
輸入座標 xyz 找到對應的voxel(只找一個 voxel 即可),利用它周圍八個頂點進行 trilinear 內插得到特徵值,輸入到神經網路中。
如果這個點並不是處在 Voxel 的中心,比如極端一些,處在某個面上,那麼“三維內插”時,非所在面的其他四個頂點對最後的特徵值沒有影響(不參與計算);那麼在反向傳播時,這四個點是沒有梯度的,導致它們也沒辦法被最佳化(感覺也不用最佳化,本來這四個點對結果也沒啥影響)。
下面是博主提出來的一個思考題:一條直線最多可以透過多少個立方體?比如\(2\times2\times2\)的立方體,最多可以透過4個,規律如下圖所示。
(三)Dense Grid: multi resolution
對於目標所在的區域,在第一次切分(single resolution)的基礎上,繼續細分(比如8→16),那麼 xyz 也一定在某個更細化的 voxel 內,那麼就能利用這個小 voxel 的頂點求得 \(f'\) 。
\(f, f'\) 這兩個特徵值都需要輸入到 MLP 中,並且因為可能不止分一次,細分多少次就需要把多少個 \(f'\) concat 到一起作為輸入,如下圖所示。
這樣做的引數總數並沒有增加,因為我們只對感興趣的區域進行細分,得到的效果反而更好~
(四)Hash Table: 創新點!
multiresolution 的切分越多、頂點越多、對GPU記憶體的需求越大。比如論文中Dense Grid切分了128塊,那麼共有\(128^3\)個Voxel塊。
HashTable 的做法是:計算這些 voxel 編號的雜湊值,放到 \(2^{14}\) 個buckets中。這樣帶來的優勢是:從原來的 \(128^3=2^{21}\) ** 縮減到 \(2^{14}\)** ,減少了 \(2^7\) **** 倍。
但是,\(2^{21}\)放到\(2^{14}\)個位置肯定存在雜湊衝突;但是實驗中也發現:只有2.57%的voxel是存在實際物體的“有用”voxel,其他的大部分都是空的“無用”voxel。因此即便存在雜湊衝突,大機率造成影響的也只是對那些無用的voxel。
輸入 xyz 後,就能知道 voxel 的編號,從而透過雜湊函式計算雜湊值,到雜湊表中查詢到所有 voxel(可能存在雜湊衝突的其他 voxel 我們認為其影響不大),然後計算特徵值,輸入到 MLP 中。
二、Accelerated NeRF Ray Marching
Pytorch的“平行計算”的基礎資料結構是“矩陣”,NeRF每條光線的取樣點數是固定的,所以透過構建一個:“光線數x取樣點數”的矩陣,就能按照行方向,計算矩陣的每一列,達到加速的目的,比 for-loop 快得多。如下圖所示:
但是Voxel並不能這樣做:如果固定 step 大小,每條射線的取樣點數是不同的,如果再用矩陣的方式就需要 Padding 補零,這會造成大量浪費。
(一)density bitfield
首先把空間進行128等分,得到\(128^3\)個Voxel,每個Voxel的值為0或1,分別代表該方塊是否“有東西”,此時我們把它稱為“Density bitfield”。
那麼射線經過為0的Voxel不會放置任何取樣點(如上右圖所示),只有經過為1的Voxel才會等間隔放置取樣點。
這裡有個問題:這個density-bitfield是如何得到的呢?見ngp_pl的訓練部分。
博主提供了一個測試程式,形象化地展示了取樣的整個過程,如下圖所示:
觀察可以看到:只有物體所在的Voxel才會放置取樣點,這個和上面的構想是一致的。
不同場景的bitfield大小是不同的,NGP會進行調整。預設是1(內部進行128等分),當調整 aabb_scale
後(2的冪次),仍會進行128等分,剩下的步驟和上面是相同的。
(二)inference階段的加速技巧
推論時有些畫素所在的射線其實時沒有用的(沒有任何交點),NGP的做法是先“允許最多使用”1個取樣點,那些沒有任何交點的射線最後真正使用的只有0個取樣點,那就可以把這些射線從渲染的物件中排除,減少了需要渲染的射線數目;然後再增加取樣點,比如+2,再重複上述步驟,排除已經收斂的射線,把計算資源分配給還沒收斂的射線。
取樣間隔的step大小也是不固定的,而是採用了 exponential stepping 的方式:離光心越遠,間隔越大(因為它們對最後的結果影響也小)。預設邊長為1時,取樣間隔為 \(\frac{\sqrt{3}}{1024}\) ,其中 \(\sqrt{3}\) 為正方體對角線的長度;對於其他邊長,下一步的取樣邊長 \(dt=\frac{t}{256}\),其中 \(t\) 是當前點距光心的距離,並且要把這個值透過 clip 操作控制在 \(\frac{\sqrt{3}}{1024}\) 到 \(\frac{\sqrt{3}}{128}*aabb\_scale\)之內。
二、其他 Grid Based 演算法
這種演算法都需要自己實現 cuda 的 Kernel 來進行 Voxel 的計算。
- NSVF(2020年)
- instant NGP;
- plenoxels;
- plenoctree;
- direct voxel grid optimization;
- ReLU Fields: 把四周頂點的 RGB 和 \(\sigma\) 內插得到目標值 \(x\),然後套用 \(Relu(x)\) 並透過
clip(Relu(x), 0, 1)
把值限定在 0 到 1 之間。 - Volumetric Bundle Adjustment for online Photorealistic Scene Capture.不斷細分的方式,如下圖所示。