本文记录学习 ECCV 2020 的论文 NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis 对应的开源 tensorflow 代码。
对NeRF的原始论文的解读请见博文 原始 NeRF 论文主要点细致介绍。
if N_importance > 0: # sample points for fine model
z_vals_mid = 0.5 * (z_vals[: ,:-1] + z_vals[: ,1:]) # (N_rays, N_samples-1) interval mid points
z_vals_ = sample_pdf(z_vals_mid, weights_coarse[:, 1:-1],
N_importance, det=(perturb==0)).detach()
xyz_coarse_sampled = rays_o.unsqueeze(1) + \
rays_d.unsqueeze(1) * z_vals.unsqueeze(2)
在这里插入代码片
NeRF 方法的数据集是同一场景下不同视角的若干张图像。
设一共有 N 张图像,每张图像的大小是 H × W H \times W H×W。相当于有 N × H × W N\times H\times W N×H×W个像素作为数据集。
from load_blender import load_blender_data
images, poses, render_poses, hwf, i_split = load_blender_data(
args.datadir, args.half_res, args.testskip)
use_batching,它是一个 flag 参数。
假如 use_batching == true,则一次从 N × H × W N\times H\times W N×H×W个像素中随机选取 B 个像素来训练。这暗示了它们可能来自不同的图像。
假如use_batching == false,则一次先随机取1张图像出来,进而在 此张图像的 H × W H\times W H×W 个像素中选 B 个作为数据。
下面代码中 i_split 的含义是 每个 split 中各自有多少张图像,因为要把所有的数据集划分为 train / val / test。
elif args.dataset_type == 'blender':
images, poses, render_poses, hwf, i_split = load_blender_data(
args.datadir, args.half_res, args.testskip)
print('Loaded blender', images.shape,
render_poses.shape, hwf, args.datadir)
i_train, i_val, i_test = i_split
near = 2.
far = 6.
if args.white_bkgd:
images = images[..., :3]*images[..., -1:] + (1.-images[..., -1:])
else:
images = images[..., :3]
如果 images[…,-1] = 0,则
i m a g e s [ . . . , : 3 ] ∗ i m a g e s [ . . . , − 1 : ] : = i m a g e s [ . . . , : 3 ] ∗ 0 images[..., :3]*images[..., -1:]:= images[..., :3]*0 images[...,:3]∗images[...,−1:]:=images[...,:3]∗0又
1. − i m a g e s [ . . . , − 1 : ] : = 1 − 0 : = 1 1.-images[..., -1:] := 1-0:=1 1.−images[...,−1:]:=1−0:=1
则 i m a g e s [ . . . , : 3 ] : = 1 images[..., :3] :=1 images[...,:3]:=1因为像素强度经过归一化,所以 1 即 255,此时为白色。
如果 images[…,-1] = 1,则 images[…, :3] 的值无变化。
def batchify_rays(rays_flat, chunk=1024*32, **kwargs):
"""Render rays in smaller minibatches to avoid OOM."""
all_ret = {}
for i in range(0, rays_flat.shape[0], chunk):
ret = render_rays(rays_flat[i:i+chunk], **kwargs)
for k in ret:
if k not in all_ret:
all_ret[k] = []
all_ret[k].append(ret[k])
all_ret = {k: tf.concat(all_ret[k], 0) for k in all_ret}
return all_ret
H, W = imgs[0].shape[:2]
camera_angle_x = float(meta['camera_angle_x'])
focal = .5 * W / np.tan(.5 * camera_angle_x)
如下图所示, f , W f,W f,W分别代表 焦距和图像宽度,fov 代表 field of view。
则 ( W 2 ) / f = tan ( f o v 2 ) (\frac{W}{2})/f=\tan(\frac{fov}{2}) (2W)/f=tan(2fov)
即 f = W 2 / tan ( f o v 2 ) f=\frac{W}{2}/\tan(\frac{fov}{2}) f=2W/tan(2fov)
这和第三行代码吻合。
if half_res:
imgs = tf.image.resize_area(imgs, [400, 400]).numpy()
H = H//2
W = W//2
focal = focal/2.
这部分代码更好的写法是指定训练图像的分辨率,比如用一个 target_wh变量指定训练图像的宽度和高度, 而不是只有让它的长宽分别缩小到原来一半的可能性。
def init_nerf_model(D=8, W=256, input_ch=3, input_ch_views=3, output_ch=4, skips=[4], use_viewdirs=False):
def get_rays_np(H, W, focal, c2w):
"""Get ray origins, directions from a pinhole camera."""
i, j = np.meshgrid(np.arange(W, dtype=np.float32),
np.arange(H, dtype=np.float32), indexing='xy')
dirs = np.stack([(i-W*.5)/focal, -(j-H*.5)/focal, -np.ones_like(i)], -1)
rays_d = np.sum(dirs[..., np.newaxis, :] * c2w[:3, :3], -1)
rays_o = np.broadcast_to(c2w[:3, -1], np.shape(rays_d))
return rays_o, rays_d
注意,上图
( H 2 , W 2 ) (\frac{H}{2},\frac{W}{2}) (2H,2W)
的索引方式是把图像看成矩阵,先索引行,再索引列。
但是上述代码中 的 i , j i,j i,j分别对应索引列和索引行。
——————注意求 dirc 的代码是在相机坐标系下的——————
所以,求 dirs的代码的含义是:
矩阵中第 i 行第 j 列的位置,先减掉图像中心位置对应的水平位置( W 2 \frac{W}{2} 2W)再除以焦距,
然后是减掉图像中心位置对应的竖直位置( H 2 \frac{H}{2} 2H)再除以焦距,
direction 向量的 z 分量始终是 -1。
——————注意求 dirc 的代码是在相机坐标系下的——————
而求 rays_d 的代码其实就是
将相机坐标系中的 视角向量 direction 转换到 世界坐标系下
如果代码用 C2W @ direction
(其中@是python中矩阵相乘操作符),会更好理解,但是NeRF中的代码和这个diamante是等价的。
z_vals[: ,:-1],所有 rays 的 从near 开始的点,最后一个点的前一个点的 z 值
除了最后一个点没取,其他都取了
z_vals[: ,1:],所有 rays 的 从 near后面一个点开始,到最后一个点的 z 值
除了第一个点没取,其他都取了
z_vals_mid = 0.5 * (z_vals[: ,:-1] + z_vals[: ,1:]) # (N_rays, N_samples-1) interval mid points
相邻两个点的中点