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.不断细分的方式,如下图所示。