大纲
Nerf系列论文详细解读(1)
前言
在2021年末,本人曾经对Nerf系列的文章进行了通读。我们知道,Nerf系列是近年来大火并且比较庞大的一个主题,其中包括Nerf、Nerf++、Nerf wild、GRAF、pi-gan、Giraffe等等,想要对后续的文章有较深层次理解,那么对Nerf的精读是极为必要的,曾经我以为已经很好的掌握这篇文章了,但是回过头来逐字逐句甚至于对每一个矩阵和向量都进行研究之后才发现,之前的很多理解,很多观点,只不过是自以为是罢了。
接下来我会对Nerf进行深层细致的解剖,希望可以带给大家帮助。文章末尾附复现结果8 hours in 2080Ti==
Nerf???
什么是Nerf?(Neural Radiance Field)神经辐射场是他的全称。通俗来讲,就是构造一个隐式的渲染流程,其输入是某个视角下发射的光线的位置o,方向d以及对应的坐标(x,y,z)。通过神经辐射场Fθ,得到体密度和颜色,最后再通过渲染得到最终的图像。
**光线是怎么得到的?怎么从2D的图片投影到3D的世界中并得到对应的光线?**相信大家首先会想到这样的问题,接下来我来对其进行讲解。
神奇的2D到3D
从世界到相机
从世界矩阵到相机矩阵的变换,只需要物体进行旋转和平移。由下图1可知:
若以x为轴进行旋转,其中:(同理可知绕y轴和绕z轴所得到的旋转矩阵)
Xc=X;
Yc=cosθ·Y+sinθ·Z;
Zc=-sinθ·Y+cosθ·Z;我们可以得到矩阵如下所示:
我们将旋转矩阵假定为R,平移矩阵为T,就可以得到完整的坐标变化公式:(当中的系数矩阵也被称作为相机的外参矩阵)
从相机到图像
从相机矩阵到图像矩阵的变换就是从三维立体空间到二维平面空间的变换,所反映的是空间中任一点P与图像上任一点P之间的关系,如下图2所示:
由三角形相似我们可以得到以下等式:
从而得到以下变换矩阵==(齐次坐标表示)== (其中p代表picture;c代表camera;s代表点p在相机矩阵中z方向的坐标)
从图像到像素
这部分比较简单,如下图3所示:
我们可以得到矩阵变换如下:(dx 为每个像素的物理尺寸)
第一步完成!
在我们可以实现从像素矩阵到世界矩阵的变换之后,我们就可以得到想要的origins;directions;lengths和xy_gird。
1、根据H,W计算得到n_rays_per_image=H*W
2、uniform的生成n_pts_per_ray(64)个depth(在min(2)和max(6)之间的均匀采样)
class GridRaysampler(torch.nn.Module):
"""
Samples a fixed number of points along rays which are regularly distributed
in a batch of rectangular image grids. Points along each ray
have uniformly-spaced z-coordinates between a predefined
minimum and maximum depth.
The raysampler first generates a 3D coordinate grid of the following form:
```
/ min_x, min_y, max_depth -------------- / max_x, min_y, max_depth
/ /|
/ / | ^
/ min_depth min_depth / | |
min_x ----------------------------- max_x | | image
min_y min_y | | height
| | | |
| | | v
| | |
| | / max_x, max_y, ^
| | / max_depth /
min_x max_y / / n_pts_per_ray
max_y ----------------------------- max_x/ min_depth v
< --- image_width --- >
```
In order to generate ray points, `GridRaysampler` takes each 3D point of
the grid (with coordinates `[x, y, depth]`) and unprojects it
with `cameras.unproject_points([x, y, depth])`, where `cameras` are an
additional input to the `forward` function.
3、从NDC空间设两个平面(图2空间中)通过将其映射回world/camera坐标系,得到光线的方向(ray_directions_world)和原点位置(rays_origins_world)
4、output:
origins(batchsize,H,W,3)
directions(batchsize,H,W,3)
lengths(batchsize,H,W,64)
xy_gird
注意第二步中在深度上取64个点是均匀采样,但是实际上物体在世界坐标系中并不是均匀的,应该变成间隔不一致的结构,以下函数用来完成此步骤:
def _stratify_ray_bundle(self, ray_bundle: RayBundle):
"""
Stratifies the lengths of the input `ray_bundle`.
More specifically, the stratification replaces each ray points' depth `z`
with a sample from a uniform random distribution on
`[z - delta_depth, z+delta_depth]`, where `delta_depth` is the difference
of depths of the consecutive ray depth values.
Args:
`ray_bundle`: The input `RayBundle`.
Returns:
`stratified_ray_bundle`: `ray_bundle` whose `lengths` field is replaced
with the stratified samples.
"""
z_vals = ray_bundle.lengths
# Get intervals between samples.
mids = 0.5 * (z_vals[..., 1:] + z_vals[..., :-1])
upper = torch.cat((mids, z_vals[..., -1:]), dim=-1)
lower = torch.cat((z_vals[..., :1], mids), dim=-1)
# Stratified samples in those intervals.
z_vals = lower + (upper - lower) * torch.rand_like(lower)
return ray_bundle._replace(lengths=z_vals)
神经辐射场
对第一步得到的rays调用volumetric函数,得到rays_densities和rays_features(也就是体密度和RGB)
def forward(
self,
ray_bundle: RayBundle,
**kwargs,
):
"""
The forward function accepts the parametrizations of
3D points sampled along projection rays. The forward
pass is responsible for attaching a 3D vector
and a 1D scalar representing the point's
RGB color and opacity respectively.
Args:
ray_bundle: A RayBundle object containing the following variables:
origins: A tensor of shape `(minibatch, ..., 3)` denoting the
origins of the sampling rays in world coords.
directions: A tensor of shape `(minibatch, ..., 3)`
containing the direction vectors of sampling rays in world coords.
lengths: A tensor of shape `(minibatch, ..., num_points_per_ray)`
containing the lengths at which the rays are sampled.
Returns:
rays_densities: A tensor of shape `(minibatch, ..., num_points_per_ray, 1)`
denoting the opacity of each ray point.
rays_colors: A tensor of shape `(minibatch, ..., num_points_per_ray, 3)`
denoting the color of each ray point.
"""
# We first convert the ray parametrizations to world
# coordinates with `ray_bundle_to_ray_points`.
rays_points_world = ray_bundle_to_ray_points(ray_bundle)
# rays_points_world.shape = [minibatch x ... x 3]
# For each 3D world coordinate, we obtain its harmonic embedding.
embeds = self.harmonic_embedding(
rays_points_world
)
# embeds.shape = [minibatch x ... x self.n_harmonic_functions*6]
# self.mlp maps each harmonic embedding to a latent feature space.
features = self.mlp(embeds)
# features.shape = [minibatch x ... x n_hidden_neurons]
# Finally, given the per-point features,
# execute the density and color branches.
rays_densities = self._get_densities(features)
# rays_densities.shape = [minibatch x ... x 1]
rays_colors = self._get_colors(features, ray_bundle.directions)
# rays_colors.shape = [minibatch x ... x 3]
return rays_densities, rays_colors
位置编码
为了让MLP更好的捕捉高频信息,我们对p点的坐标(x,y,z)和direction(方向)进行了位置编码(维度提升),如下图4所示:
对(x,y,z)的编码长度L为10;对direction的编码长度为4
得到体密度
对应成员函数:self._get_densities
具体步骤如下:
1、我们将第一步中得到的origins和direction进行处理,带入到camera ray r(t)=o+td,可以得到ray_points_world(batchsize,1024,64,3) 其中1024是对HW个像素点的采样,64就是第一步中ray中depth上不均匀采样的64个点。
2、对ray_points_world进行位置编码,得到(batchsize,1024,64,63),其中63=102*3+3。
10指编码长度L;2是xy;3是方向向量,+3保留自己
3、丢到MLP中,input的维度是63,经过若干中间层(self.intermediate_layer)的自定义参数256,最终输入(256,1)得到output_dim,
得到[0,1]范围内的densities (batchsize,1024,64,1)。
def _get_densities(self, features):
"""
This function takes `features` predicted by `self.mlp`
and converts them to `raw_densities` with `self.density_layer`.
`raw_densities` are later mapped to [0-1] range with
1 - inverse exponential of `raw_densities`.
"""
raw_densities = self.density_layer(features)
return 1 - (-raw_densities).exp()
得到color
对应成员函数self._get_colors
由于体密度与方向无关,所以并不需要对direction进行特征提取,但是对于color来说,不同的视角会造成不同的效果,所以对color来说,要使用同一个FC层,来同时对xyz的feature和经过位置编码后的ray_direction进行处理,之所以要使用同一个FC层,是为了将体密度和方向进行联系。
具体步骤如下:
1、input:经过self.intermediate_layer得到的结果(batchsize,1024,64,256)
2、input:经过位置编码后的direction**(batchsize,1024,27);27同63
3、output:(batchsize,1024,64,128)
4、output:(batchsize,1024,1,128)
5、最终该结果将output1和output2进行相加,得到每个ray上(1024)的每个点(64)上面的特征(128),并加上该ray方向上的编码特征,然后进行RELU,FC(128,3)处理后(Sigmoid),最终得到[0,1]范围内的rays_rgb(batchsize,1024,64,3)。
def _get_colors(self, features, rays_directions):
"""
This function takes per-point `features` predicted by `self.mlp`
and evaluates the color model in order to attach to each
point a 3D vector of its RGB color.
In order to represent viewpoint dependent effects,
before evaluating `self.color_layer`, `NeuralRadianceField`
concatenates to the `features` a harmonic embedding
of `ray_directions`, which are per-point directions
of point rays expressed as 3D l2-normalized vectors
in world coordinates.
"""
spatial_size = features.shape[:-1]
# Normalize the ray_directions to unit l2 norm.
rays_directions_normed = torch.nn.functional.normalize(
rays_directions, dim=-1
)
# Obtain the harmonic embedding of the normalized ray directions.
rays_embedding = self.harmonic_embedding(
rays_directions_normed
)
# Expand the ray directions tensor so that its spatial size
# is equal to the size of features.
rays_embedding_expand = rays_embedding[..., None, :].expand(
*spatial_size, rays_embedding.shape[-1]
)
# Concatenate ray direction embeddings with
# features and evaluate the color model.
color_layer_input = torch.cat(
(features, rays_embedding_expand),
dim=-1
)
return self.color_layer(color_layer_input)
好的,现在我们得到了我们想要的rays_densities和rays_colors,那么我们又可以开始愉快的下一步啦
体渲染
如何对前面得到的rays_densities和rays_features进行融合?使之成为人眼可以接受的图像呢?这就要提到raymarcher函数
images=self.reymarcher(
rays_denstites=rays_densities,
rays_features=rays_features,
ray_bundle=ray_bundle,
**kwargs
)
#images - minibatch x ... x (feature_dim+opacity_dim)
return images,ray_bundle
经典体渲染
体渲染又称作volume rendering,对于一条线的颜色,我们可以用积分的方式表达如下:
这么看起来好像有点不知所云,我来具体解释一下这个公式的含义
C®:表示每条光线ray的颜色
σ(x):体素密度,可以理解为一条射线r在经过x处的一个无穷小的粒子时被终止的概率,也就是这个点的不透明度
r(t):o+td,这里o是射线原点,d是direction,也就是相机的一条射线
tf,tn:t的近端和远端边界分别为tn以及tf
T(t):是射线从tn和tf这一段路径上的累计透明度,可以被理解为一路上没有集中任何粒子的概率,具体形式为
但是实际中并不能对Nerf进行连续的点的估计,所以上述的T(t)也就不服存在,转而替换成为将射线需要积分的区域分为N份,然后在每一个小区域中进行均匀随机采样。这样的方式能够在只采样离散点的前提下,保证采样位置的连续性。第i个采样点可以表示为:
于是我们可以将颜色的积分转化为求和公式如下
多层级体素采样
Nerf的渲染过程计算量很大,每条射线都要采样很多店。但是实际上,一条射线上大部分是空白区域,或者是被遮挡的区域,因此作者采用了一种”coarse to fine"的形式,同时优化coarse网络和fine网络。
对于coarse网络,我们可以采样较为记住的Nc个点,并将前述的离散求和函数重新表示为:
其中
接下来可以对w做归一化
此处的wi (weights)可以看作是沿着射线的概率密度函数
具体来说就是rays_densities*(batchsize,1024,64)得到的概率密度函数,当weights比较小时就表示对当前颜色的贡献比例小。
流程的输入输出如下
1、input:体密度(ray_densities) (batchsize,1024,64,1)
2、input:颜色(rays_colors) (batchsize,1024,64,3)
3、output:features(batchsize,1024,3)
4、output:weights(batchsize,1024,64)
最终得到的features=weights*ray_features(ray_colors)的和
结果复现
渲染图片对比
50000个iter:
10000个iter:
15000个iter:
20000个iter:
简化结果复现,简化包括:
射线采样:不执行分层射线采样,而是执行等距深度的射线采样。
渲染:对单个渲染通道进行训练,而不是原始实现进行粗略和精细渲染通道。
架构:使用更浅的网络架构,这允许以表面细节为代价进行更快的优化。
(学校的船,很帅是吧)
注:本两次实验训练共花费14h使用2080Ti,不过毕竟时代在进步,接下来讲解分析的文章会逐渐缩短时间直到5s,真是令人惊讶