Ⅵ. Instant-NGP

7hu95b發表於2024-05-28

NGP 改用了 Grid based 的方法。NeRF 採用了一個很大的神經網路(8層、每層256個神經元),直接計算每個取樣點的 \(\sigma\)\(c\) 就會很慢;NGP則是先找到所有包含這個點的Voxel,利用Voxel的頂點進行“三維內插”(trilinear)得到"特徵值",把這個特徵值丟到一個2層神經網路中;每個Voxel都會計算得到一個特徵值,多個Voxel的做法在論文中稱為 Multiresolution,所以特徵值也不止一個。

一、NGP實驗中的影響因素

(一)ablation study

ed15b9f69d5d236902f3b5db172fe3ed.png

(二)Dense Grid: single resolution

下面我們詳細講一下 Grid Based 的具體做法。

bcb5cffd1db4552816c896274270fba5.png
581b7f075c8a00569b5cb8aaf40c2597.png

輸入座標 xyz 找到對應的voxel(只找一個 voxel 即可),利用它周圍八個頂點進行 trilinear 內插得到特徵值,輸入到神經網路中

如果這個點並不是處在 Voxel 的中心,比如極端一些,處在某個面上,那麼“三維內插”時,非所在面的其他四個頂點對最後的特徵值沒有影響(不參與計算);那麼在反向傳播時,這四個點是沒有梯度的,導致它們也沒辦法被最佳化(感覺也不用最佳化,本來這四個點對結果也沒啥影響)。

下面是博主提出來的一個思考題:一條直線最多可以透過多少個立方體?比如\(2\times2\times2\)的立方體,最多可以透過4個,規律如下圖所示。
6680e4d7c5e1f7cbf989ddb468080d6a.png

(三)Dense Grid: multi resolution

對於目標所在的區域,在第一次切分(single resolution)的基礎上,繼續細分(比如8→16),那麼 xyz 也一定在某個更細化的 voxel 內,那麼就能利用這個小 voxel 的頂點求得 \(f'\)

\(f, f'\) 這兩個特徵值都需要輸入到 MLP 中,並且因為可能不止分一次,細分多少次就需要把多少個 \(f'\) concat 到一起作為輸入,如下圖所示。
52b1a17fc02cfe9dcd09892685ef0795.png
d5f4908933cbdb368611f85c8b21d3d7.png
這樣做的引數總數並沒有增加,因為我們只對感興趣的區域進行細分,得到的效果反而更好~

(四)Hash Table: 創新點!

multiresolution 的切分越多、頂點越多、對GPU記憶體的需求越大。比如論文中Dense Grid切分了128塊,那麼共有\(128^3\)個Voxel塊。
c88b21ee0f57a013b11cdb43eb627fdc.png
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 快得多。如下圖所示:
37207e0216accfbc90517f49182e4dcd.png

但是Voxel並不能這樣做:如果固定 step 大小,每條射線的取樣點數是不同的,如果再用矩陣的方式就需要 Padding 補零,這會造成大量浪費。
f57f6955254dda583f672acde39e95c7.png

(一)density bitfield

首先把空間進行128等分,得到\(128^3\)個Voxel,每個Voxel的值為0或1,分別代表該方塊是否“有東西”,此時我們把它稱為“Density bitfield”。

a4e22281aa2994741caff884f8ee0ded.png

那麼射線經過為0的Voxel不會放置任何取樣點(如上右圖所示),只有經過為1的Voxel才會等間隔放置取樣點。

這裡有個問題:這個density-bitfield是如何得到的呢?見ngp_pl的訓練部分。

博主提供了一個測試程式,形象化地展示了取樣的整個過程,如下圖所示:
820845582a04e4f55f9deeaee0771360.png
觀察可以看到:只有物體所在的Voxel才會放置取樣點,這個和上面的構想是一致的。

不同場景的bitfield大小是不同的,NGP會進行調整。預設是1(內部進行128等分),當調整 aabb_scale後(2的冪次),仍會進行128等分,剩下的步驟和上面是相同的。
2962bb46adcff11dad18b5a4a2232f70.png

(二)inference階段的加速技巧

推論時有些畫素所在的射線其實時沒有用的(沒有任何交點),NGP的做法是先“允許最多使用”1個取樣點,那些沒有任何交點的射線最後真正使用的只有0個取樣點,那就可以把這些射線從渲染的物件中排除,減少了需要渲染的射線數目;然後再增加取樣點,比如+2,再重複上述步驟,排除已經收斂的射線,把計算資源分配給還沒收斂的射線。

fca11b7973a5fb808fc6cd8761ef34f2.png

取樣間隔的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 的計算。

  1. NSVF(2020年)
  2. instant NGP;
  3. plenoxels;
  4. plenoctree;
  5. direct voxel grid optimization;
  6. ReLU Fields: 把四周頂點的 RGB 和 \(\sigma\) 內插得到目標值 \(x\),然後套用 \(Relu(x)\) 並透過 clip(Relu(x), 0, 1) 把值限定在 0 到 1 之間。
  7. Volumetric Bundle Adjustment for online Photorealistic Scene Capture.不斷細分的方式,如下圖所示。

dfe1462b5819ef5941c908db28a5ef67.png