基本概念

相关链接

论文地址

基础版NeRF实现(PyTorch)

基础版NeRF实现(TensonFlow)

广义理解

NeRF通过神经网络(MLP)来隐式的存储3D信息。

  • 显式的3D信息:有明确的x,y,z值。
  • 隐式的3D信息:无明确的x,y,z的值,只能输出指定角度的2D图片。

也因此,模型并不具备泛化能力,一个模型只能存储一个3D信息。

模型结构

论文中模型的输入是一个5D的向量(x,y,z,theta,phi),分别是坐标值、方位角、仰角。输出是4D向量,分别是密度和颜色(RGB)。模型的结构是一个8层的MLP

NeRF的模型结构比较简单,重点在于是前处理和后处理。对于输入,需要有一个图片转5D的前处理;对于输出,需要有一个4D转图片的后处理。

实际上,输入的5D向量是粒子的空间位姿,输出的4D向量是粒子对应的颜色以及密度。

真实场景及相机模型

真实场景

现实生活中有多个光源,同时会有物体的折射和反射。

相机模型

相机模型连接了3D世界与2D图片,主要分为四个坐标系:

  • 世界坐标系:现实世界的三维坐标系,相当于地球的经纬度。
  • 相机坐标系:以相机镜头中心为原点的三维坐标系,通过相机的位置+朝向将世界坐标转换过来。
  • 归一化相机坐标系:把相机前方的空间压缩成一个固定边长的立方体(一般为2),统一不同距离的物体缩放比例。
  • 像素坐标系:最终照片上的像素网格位置

体渲染

定义

  • 属于渲染技术的分支
  • 目的是解决云/烟/果冻等非刚性物体的渲染建模
  • 将物质抽象成一团飘忽不定的粒子群
  • 光线在穿过时,是光子在跟粒子发生碰撞的过程

光子与粒子发生作用的过程

  • 吸收:光子被粒子吸收
  • 放射:粒子本身发光
  • 外射光:光子在冲击后,被弹射
  • 内射光:其他方向弹射来的粒子。

NeRF假设

  • 物体是一团自发光的粒子
  • 粒子有密度和颜色
  • 外射光和内射光抵消
  • 多个粒子被渲染成指定角度的图片

模型的输入输出

模型的输入:将物体进行稀疏表示的单个粒子位姿

模型的输出:该粒子密度和颜色

粒子的采集——光线原理

射线推导像素点

对于空间中的一个发光粒子:

  • 空间坐标(x, y, z)
  • 发射的光线通过相机模型
  • 成为图片上的像素坐标(u, v)
  • 粒子颜色即为像素颜色

其中(u, v)(x, y, z)的公式如下:

转换公式

其中R为旋转矩阵(3x3)T为平移向量(3x1)

反之,对于图片上的某一个像素(u, v)的颜色。可以看作是沿着某一条射线上的无数个发光点的“和”,利用相机模型,反推射线,那么这个射线表示为:
$$
r(t)=o+td
$$
其中o为射线原点,d为方向,t为距离,使用极坐标的方法表示。理论上来讲,t的取值范围为$(0,+\infty)$。

对于一张大小为(H, W)的图片而言,其射线数量为$H\times W$。

像素点推导射线

粒子的采集——光线原理

由像素点$P(u, v)$反推射线基本过程如下。

像素平面$\rightarrow$物理成像平面:
$$
(x_n,y_n)=(-(u-\frac{w}{2}),v-\frac{h}{2})=(\frac{w}{2}-u,v-\frac{h}{2})
$$
物理成像平面$\rightarrow$相机坐标系:
$$
(x_c,y_c,z_c)=(x_n,y_n,-f)
$$
其中$f$为相机焦距。

归一化:
$$
(x_c,y_c,z_c)=(\frac{x_c}{f},\frac{y_c}{f},-1)
$$
相机坐标系$\rightarrow$世界坐标系:
$$
(x_w,y_w,z_w)=c2w\times(x_c,y_c,z_c)
$$

代码

1
2
3
4
5
6
7
8
9
10
11
# Ray helpers
def get_rays(H, W, K, c2w):
i, j = torch.meshgrid(torch.linspace(0, W-1, W), torch.linspace(0, H-1, H)) # pytorch's meshgrid has indexing='ij'
i = i.t()
j = j.t()
dirs = torch.stack([(i-K[0][2])/K[0][0], -(j-K[1][2])/K[1][1], -torch.ones_like(i)], -1)
# Rotate ray directions from camera frame to the world frame
rays_d = torch.sum(dirs[..., np.newaxis, :] * c2w[:3,:3], -1) # dot product, equals to: [c2w.dot(dir) for dir in dirs]
# Translate camera frame's origin to the world frame. It is the origin of all rays.
rays_o = c2w[:3,-1].expand(rays_d.shape)
return rays_o, rays_d

这个方法一共有四个参数,HW表示图像的大小,K表示相机的内参,c2w表示旋转矩阵和平移向量。

相机内参

其中,$K[0][2]$和$K[1][2]$分别表示光心在图像中的$x$坐标和$y$坐标,可以通过这两个值确定成像中心的位置。

因此有如下公式:
$$
K[0][2]=\frac{W}{2}
$$

$$
K[1][2]=\frac{H}{2}
$$

$f_x$和$f_y$分别表示$x$轴的焦距和$y$轴的焦距,可以控制水平方向和垂直方向的成像尺度。

如果这两个值相等,那么就是正方形像素,如果二者不等,那么图像会被拉伸变形。

也就是说,对于NeRF而言,有如下关系:
$$
f_x=f_y=f
$$

1
2
3
4
i, j = torch.meshgrid(torch.linspace(0, W-1, W), torch.linspace(0, H-1, H))  # pytorch's meshgrid has indexing='ij'
i = i.t()
j = j.t()
dirs = torch.stack([(i-K[0][2])/K[0][0], -(j-K[1][2])/K[1][1], -torch.ones_like(i)], -1)

这段代码的主要功能是把像素平面转换为相机坐标系并进行归一化处理。

相对于像素点推导射线的公式而言,代码中与其有一定区别。上述$u$与$x_n$方向相反,$v$与$y_n$方向相同;而在代码中,$u$与$x_n$方向相同,$v$与$y_n$方向相反。

$i$和$j$分别表示图像的$u$方向和$v$方向,根据之前的公式推导,我们可以得出,转换后的坐标如下所示:
$$
(x_c,y_c,z_c)=(\frac{x_c}{f},\frac{y_c}{f},-1)=(u-\frac{w}{2},-(v-\frac{h}{2}),-1)
$$
该公式与上述公式相反的原因是坐标轴方向的变化。

那么推导一下就可以得出:
$$
(x_c,y_c,z_c)=(\frac{x_c}{f},\frac{y_c}{f},-1)=(i-\frac{W}{2},-(j-\frac{H}{2}),-1)
$$
再通过代码torch.stack(tensors, dim=0)就可以将多个张量沿着一个新维度拼接起来。

1
2
3
4
# Rotate ray directions from camera frame to the world frame
rays_d = torch.sum(dirs[..., np.newaxis, :] * c2w[:3,:3], -1) # dot product, equals to: [c2w.dot(dir) for dir in dirs]
# Translate camera frame's origin to the world frame. It is the origin of all rays.
rays_o = c2w[:3,-1].expand(rays_d.shape)

dirs是相机坐标系的方向,通过与c2w矩阵的旋转矩阵部分相乘,可以得到世界坐标系的方向,也就是rays_d

同理,rays_o是平移光线原点,取的是c2w矩阵中的平移向量部分。