【三维重建】NeRF原理+代码讲解

news2024/12/23 23:37:46

文章目录

  • 一、技术原理
    • 1.概览
    • 2.基于神经辐射场(Neural Radiance Field)的体素渲染算法
    • 3.体素渲染算法
    • 4.位置信息编码(Positional encoding)
    • 5.多层级体素采样
  • 二、代码讲解
    • 1.数据读入
    • 2.创建nerf
      • 1.计算焦距focal与其他设置
      • 2.get_embedder 获取位置编码
      • 3.创建nerf
    • 3.渲染过程
      • 1.图像坐标->真实世界坐标
      • 2.渲染
    • 4.计算损失
  • 三、几何学原理


在这里插入图片描述

NeRF是2020年ECCV论文,任务是做新视角的合成,是借助深度学习技术的计算机图形学任务,实现了摄像机级别的逼真的新视图合成。仅仅2年时间,相关work和论文就已经大量涌现。

论文:https://arxiv.org/abs/2003.08934
TensorFlow代码:https://github.com/bmild/nerf
PyToch代码:https://github.com/yenchenlin/nerf-pytorch
官方数据;;https://drive.google.com/drive/folders/128yBriW1IG_3NJ5Rp7APSTZsJqdJdfc1

一、技术原理

以下为原理简述,更多物理公式推导请看最后一章

1.概览

NeRF可以简要概括为用一个MLP(全连层而非卷积,加上激活层)神经网络去隐式地学习一个静态3D场景,实现复杂场景的任意新视角合成(渲染)

为了训练网络,针对一个静态场景需要提供包含大量相机参数已知的图片的训练集,以及图片对应的相机所处3D坐标,相机朝向(2D,但实际使用3D单位向量表示方向)。

使用多视角的数据进行训练,空间中目标位置具有更高的密度和更准确的颜色,促使神经网络预测一个连续性更好的场景模型。

以任意的相机位置+朝向作为输入,经过训练好的神经网络进行体绘制(Volume Rendering),即可以从渲染出图片结果了。

在这里插入图片描述

2.基于神经辐射场(Neural Radiance Field)的体素渲染算法

NeRF函数是将一个连续的场景表示为一个输入为5D向量的函数,包括一个空间点的3D坐标位置x=(x,y,z),以及方向(θ,ϕ);

输出为视角相关的该3D点的颜色c=(r,g,b),和对应位置(体素)的密度σ。

实践中,用3D笛卡尔单位向量d来表示方向,因此这个神经网络可以写作:ϜΘ:(x,d)→(c,σ)。
在这里插入图片描述
在具体的实现中,x首先输入到MLP网络中,并输出σ和一个256维的中间特征,中间特征和d再一起输入到额外的全连接层(128维)中预测颜色,如下图所示。
在这里插入图片描述

因此,体素密度只和空间位置有关而颜色则与空间位置以及观察的视角有关。基于view dependent 的颜色预测,能够得到不同视角下不同的光照效果。输入数据x和d都先经过了位置信息编码(Position Encoding),即γ(∙)。

输入的位置 x,指的是由各个相机原点出发的,经过对应图像中每一像素引起的射线,所经过的采样点位置,方向指该射线的方向;输出的体素密度σ和方向相关的颜色值c=(r,g,b),共同决定了该位置在后续渲染时所提供的对渲染结果的数值;上述网络中的权重/参数,为所有的像素射线共享。

3.体素渲染算法

传统体渲染方法

体素密度σ(x)可以被理解为,一条穿过空间的射线,在x处被一个无穷小的粒子终止的概率,这个概率是可微分的,可以将其近似理解为该位置点的不透明度

相机沿着特定方向进行观测,其观测射线上的点是连续的,则该相机成像平面上对应的像素颜色,可以理解为由对应射线经过的点的颜色积分得到。

将一条射线的原点标记为o,射线方向(即相机视角)标记为d,则可将射线表示为r(t)=o+td,t的近端和远端边界分别为tn和tf。
在这里插入图片描述

可将这条射线的颜色,用积分的方式表示为:
在这里插入图片描述
其中,T(t)表示的是射线从tn到t这一段的累计透明度,即,该射线从tn到t都没有因击中任何粒子而被停下的概率,具体写作:
在这里插入图片描述
在连续的辐射场中,针对任意视角进行渲染,就需要对穿过目标虚拟相机的每个像素的射线,求取上述颜色积分,从而得到每个像素的颜色,渲染出该视角下的成像图片

分段近似渲染方法

用NeRF难以估计射线上的连续点,这就对其进行分段近似。作者提出了一种分层抽样(Stratified Sampling)的方法:首先将射线需要积分的区域[tn,tf]均匀分为N份,再在每个小区域进行均匀随机采样。则以上预测颜色C(r)的积分,可以简化为求和的形式:
在这里插入图片描述
其中,δi 为两个近邻采样点之间的距离,此处T(t)改写作:在这里插入图片描述
这种从所有采样点的(ci,σi)集合求和得到射线渲染颜色的方法也是可微分的,并且可以简化为传统的透明度混合算法,其中alpha值 αi=1−exp(−σiδi)。

4.位置信息编码(Positional encoding)

由于神经网络难以学习到高频信息,直接将位置视角作为网络的输入,渲染效果分辨率低;使用位置信息编码的方式将输入先映射到高频可以有效地解决这个问题。

γ用于将输入映射到高维空间中,论文中使用的是正余弦周期函数的形式:
在这里插入图片描述
位置和视角先进行归一化,到[-1,1]之间。对3D位置, γ(x)设置 L=10;视角信息γ(d)设置 L=4。

5.多层级体素采样

NeRF的渲染策略是对相机出发的每条射线都进行N个采样,将颜色加权的求和,得到该射线颜色。由于大量的对渲染没有贡献的空的或被遮挡的区域仍要进行采样计算,这样占用了过多的计算量

作者设计了一种“coarse to fine”的多层级体素采样方法,同时优化coarse和fine两个网络:首先,使用分层采样的方法均匀采集较为稀疏的Nc个点,在这些采样点上计算coarse网络的渲染结果,改写前述的离散求和函数:
在这里插入图片描述
wi=Ti*(1-exp(−σiδi)),对wi进行归一化:在这里插入图片描述
归一化后的 ωi 可以看作是沿着射线方向的概率密度函数,如下左图所示。通过这个概率密度函数,我们可以粗略地得到射线方向上物体的分布情况:
在这里插入图片描述
随后,基于粗采样得到的概率密度函数,使用逆变换采样(inverse transform sampling)方法,再采样出Nf个密集点,如上右图。这个方法可以从包含更多可见内容的区域中得到更多的采样点,然后在Nc+Nf的采样点集合上,计算refine网络的渲染结果。

针对不同的场景,需要进行独立训练一个NeRF。训练损失直接定义为:渲染结果的颜色,与图像真实像素值dL2损失。同时优化coarse和fine网络

二、代码讲解

项目提供了8个数据集,这里介绍其中两个:。
nerf_synthetic 其中包含lego小车不同角度的图片和Camera 的位姿

在这里插入图片描述

第二个是 nerf_llff_data
这个数据集是由真实照片制作的,Camera的位姿是由colmap生成的,位姿保存在poses_bound.npy的文件中
在这里插入图片描述

1.数据读入

load_blender_data函数:将所有图片和相机的位姿读入imgs和pos数组中。

if args.dataset_type == 'blender':
        # 一般使用合成数据集
        images, poses, render_poses, hwf, i_split = load_blender_data(args.datadir, args.half_res, args.testskip)
            # images:(138,400,400,4) poses每帧的位姿(138,4,4)  render_poses 40个新视角的内参(40,4,4)  
            #hwf 宽高焦距[400,400,555]               train[0,1,2,...99], val[100,101,...112],,test[113,114,..137]

json文件中记录位姿部分的内容:

    "camera_angle_x": 0.6911112070083618,
    "frames": [
        {
            "file_path": "./test/r_0",
            "rotation": 0.031415926535897934,
            "transform_matrix": [
                [
                    -0.9999999403953552,
                    0.0,
                    0.0,
                    0.0
                ],
                [
                    0.0,
                    -0.7341099977493286,
                    0.6790305972099304,
                    2.737260103225708
                ],
                [
                    0.0,
                    0.6790306568145752,
                    0.7341098785400391,
                    2.959291696548462
                ],
                [
                    0.0,
                    0.0,
                    0.0,
                    1.0
                ]
            ]
        },

函数 load_blender_data:

def load_blender_data(basedir, half_res=False, testskip=1):
    splits = ['train', 'val', 'test']
    metas = {}
    for s in splits:
        with open(os.path.join(basedir, 'transforms_{}.json'.format(s)), 'r') as fp:
            metas[s] = json.load(fp)
    # meta包含训练,验证,测试三个key值。以val为例,metas['val']包含'camera_angle_x', 'frames'两个key;
   # camera_angle_x为一个角度(0.691),frames包含若干帧,每个帧内含有文件名、rotation、transform_matrix(4*4矩阵)三个key


# 1.循环训练、验证、测试
for s in splits:
        meta = metas[s]

        for frame in meta['frames'][::skip]:                         # train时skip=1,否则为8
            fname = os.path.join(basedir, frame['file_path'] + '.png')
            imgs.append(imageio.imread(fname))                                                # 0~255
            poses.append(np.array(frame['transform_matrix']))                    # 4*4
        imgs = (np.array(imgs) / 255.).astype(np.float32) #  (RGBA)            #(1008008004)
        poses = np.array(poses).astype(np.float32)                                           # (100,4,4)
        counts.append(counts[-1] + imgs.shape[0])                                          # [0,100]
        all_imgs.append(imgs)
        all_poses.append(poses)

    i_split = [np.arange(counts[i], counts[i+1]) for i in range(3)]     # train[0,1,2,...99], val[100,101,...112],,test[113,114,..137]
    
    imgs = np.concatenate(all_imgs, 0)                 # (138,800,800,4)
    poses = np.concatenate(all_poses, 0)              # (138,4,4)
    
    H, W = imgs[0].shape[:2]
    camera_angle_x = float(meta['camera_angle_x'])                   # test 中的 0.694
    focal = .5 * W / np.tan(.5 * camera_angle_x)                                # camera焦距
    
    render_poses = torch.stack([pose_spherical(angle, -30.0, 4.0) for angle in np.linspace(-180,180,40+1)[:-1]], 0)    
    # 将360度分成40等份,每个角度一个(4*4)内参矩阵

# 2.  根据性能决定是否降低分辨率:True 时,宽、高、焦距减半
    if half_res:                       
        H = H//2  ,W = W//2,  focal = focal/2.

        imgs_half_res = np.zeros((imgs.shape[0], H, W, 4))
        for i, img in enumerate(imgs):
            imgs_half_res[i] = cv2.resize(img, (W, H), interpolation=cv2.INTER_AREA)
        imgs = imgs_half_res

    return imgs, poses, render_poses, [H, W, focal], i_split        
     # (138,400,400,4)138,4,4) (40,4,4)  [400,400,555]               train[0,1,2,...99], val[100,101,...112],,test[113,114,..137]

2.创建nerf

1.计算焦距focal与其他设置

focal = .5 * W / np.tan(.5 * camera_angle_x)

NeRF渲染过程中需要在一条 Camera Ray 上采集采样点,累计获得像素点的RGB值。采样时要确定图片的远近位置,near和far的值为距离相机中心的距离(由于blender是合成资料集,near和far是在制作资料集时设定好的):

# 1.设置参数
near = 2.
far = 6.
K = np.array([  [focal, 0, 0.5*W],    [0, focal, 0.5*H],      [0, 0, 1] ])           # 根据焦距,创建相机内参矩阵

背景设为白色 white_bkgd

imges 维度:(n * h * w * 4), n 为帧数,wh宽高,RGB做了0~1归一化,第四维透明度(0代表有物体,1代表没有物体)。

parser.add_argument("--white_bkgd", action='store_true',
                    help='set to render synthetic data on a white bkgd (always use for dvoxels)')
                    
# True时把透明的背景变成白色,返回一个 RGB 图像:
if args.white_bkgd:
    images = images[...,:3]*images[...,-1:] + (1.-images[...,-1:])

2.get_embedder 获取位置编码

论文讲到PE的必要性:直接输入5D坐标,网路uo难以捕捉高频信息,使重建图像模糊。通过 γ 函数将数据升至高维后再输入网络,可以增加输出的细节(输入升至高维可以增大两个点的距离)。
论文中对3D位置坐标和两个视角坐标的编码方式不同:对于3D位置坐标,论文中使用频率为10的PE,对于2D坐标,论文中使用频率为4的PE。

下面是参数配置的代码,以及生成PE过程:

parser.add_argument("--multires", type=int, default=10,
                    help='log2 of max freq for positional encoding (3D location)')
parser.add_argument("--multires_views", type=int, default=4,
                    help='log2 of max freq for positional encoding (2D direction)')

 freq_bands = 2.**torch.linspace(0., max_freq=9, steps=10)                              # [2^0,  2^1,  2^2,  2^3,  ... 2^9] =  [1, 2, 4, 8,... 256, 512]
 freq_bands_angle =     2.**torch.linspace(0., max_freq=3, steps=4)               # [1,2,4,8]
 #  freq_bands:  [2^0,  2^1,  2^2,  2^3,  ... 2^9]  =  [1, 2, 4, 8,... 256, 512]

3.创建nerf

output_ch = 5 
skips = [4]
model = NeRF(D=args.netdepth=8,      W=args.netwidth=256,   input_ch=63,       output_ch=5,  
                              skips=4,   input_ch_views=27, use_viewdirs=True).to(device)        # 输入5维坐标经过位置编码成63

model共分为5部分,网络如下。forward过程在后面渲染部分
在这里插入图片描述

  (pts_linears): ModuleList(
    (0): Linear(in_features=63, out_features=256, bias=True)
    (1): Linear(in_features=256, out_features=256, bias=True)
    (2): Linear(in_features=256, out_features=256, bias=True)
    (3): Linear(in_features=256, out_features=256, bias=True)
    (4): Linear(in_features=256, out_features=256, bias=True)
    (5): Linear(in_features=319, out_features=256, bias=True)
    (6): Linear(in_features=256, out_features=256, bias=True)
    (7): Linear(in_features=256, out_features=256, bias=True)
  )
  (views_linears): ModuleList(
    (0): Linear(in_features=283, out_features=128, bias=True)
  )
  (feature_linear): Linear(in_features=256, out_features=256, bias=True)
  (alpha_linear): Linear(in_features=256, out_features=1, bias=True)
  (rgb_linear): Linear(in_features=128, out_features=3, bias=True)
)

由于args.N_importance=64 > 0,则网络需要第二次精细采样,需创建一个相同NeRF结构,作为精细网络。

N_rand = args.N_rand                                              # 射线数,即采样点数量 512
use_batching = not args.no_batching              # False,每次训练都会从同一帧中获取不同的Batchsize个像素

3.渲染过程

1.图像坐标->真实世界坐标

  1. 迭代循环20000个iter
for i in trange(start, N_iters):
       img_i = np.random.choice(i_train)                              # 从1100序列中,随机选一帧图像
       target = images[img_i]
       target = torch.Tensor(target).to(device)                     # (4004001)
       pose = poses[img_i, :3,:4]                                                 # (34)相机位姿
  1. get_rays函数,将 camera 坐标和 c2w(camera to world) 矩阵相乘,得到真实世界下的坐标(c2w就是每一帧的位姿pose):
    在这里插入图片描述
rays_o, rays_d = get_rays(H, W, K, torch.Tensor(pose))      # rays_o: (3) --> (H=400,400,3)  rays_d: (400,400,3)
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'    c2w位姿
    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)                # (400,400,3) :   [ (-200-200)/555,-1 ]
    # Rotate ray directions from camera frame to the world frame
    rays_d = torch.sum(dirs[..., np.newaxis, :] * c2w[:3,:3], -1)  # dot product: [c2w.dot(dir) for dir in dirs]            (400,400,3,3)->(400,400,3)
    # 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)                # 所有射线的原点相同,只需要广播:(3) --> (400,400,3)
    return rays_o, rays_d
  1. 图像上选取射线( (400,400,3) -> (512,3) )

400*400个点中,随机选512个点的射线和原点(上步已计算出来,都是三维坐标)

    if i < args.precrop_iters:
        # 迭代次数少于250,只从图像中心的一部分采样射线
        dH = int(H//2 * args.precrop_frac)            # 400/4 = 100
        dW = int(W//2 * args.precrop_frac)           # 400/4 = 100
        coords = torch.stack(    torch.meshgrid(   torch.linspace(H//2 - dH, H//2 + dH - 1, 2*dH), 
                                                                                            torch.linspace(W//2 - dW, W//2 + dW - 1, 2*dW) ), -1)                                                                     
        # 维度 (200,200,2) : 100~299的密集坐标

    else:
            coords = torch.stack(torch.meshgrid(torch.linspace(0, H-1, H), torch.linspace(0, W-1, W)), -1)  # (H, W, 2)


    coords = torch.reshape(coords, [-1,2])                                                   # (H * W, 2)    (40000,2)
    select_inds = np.random.choice(coords.shape[0], size=[N_rand], replace=False)         # (N_rand=512) 40000个密集点,采样512根射线
    select_coords = coords[select_inds].long()  # (N_rand, 2)
    rays_o = rays_o[select_coords[:, 0], select_coords[:, 1]]                # (N_rand, 3)
    rays_d = rays_d[select_coords[:, 0], select_coords[:, 1]]                # (N_rand, 3)
    batch_rays = torch.stack([rays_o, rays_d], 0)                                      # (2, N_rand=512, 3)
    target_s = target[select_coords[:, 0], select_coords[:, 1]]               #   (400,400,3)中选出)5123

2.渲染

下面是主函数:

rgb, disp, acc, extras = render(H, W, K,  chunk=32768 ,   rays=batch_rays,    verbose=i < 10,
                                                             retraw=True,    **render_kwargs_train)
def render(H, W, K, chunk=1024*32, rays=None, c2w=None, ndc=True,   near=0., far=1.,
                        use_viewdirs=False, c2w_staticcam=None,  **kwargs):

        rays_o, rays_d = rays

    if use_viewdirs:
        viewdirs = rays_d
        viewdirs = viewdirs / torch.norm(viewdirs, dim=-1, keepdim=True)          # 射线方向做归一化 (5123)
        viewdirs = torch.reshape(viewdirs, [-1,3]).float()

    sh = rays_d.shape # [..., 3]

    near, far = near * torch.ones_like(rays_d[...,:1]), far * torch.ones_like(rays_d[...,:1])         # (512,1):[2]   (512,1):[6]
    rays = torch.cat([rays_o, rays_d, near, far], -1)                                                           #  (512,8)
    if use_viewdirs:
        rays = torch.cat([rays, viewdirs], -1)                                                                            #  (512,11)

    all_ret = batchify_rays(rays, chunk, **kwargs)
    # 内部循环若干次,每次渲染一定数量的ray。具体展开函数为:
               ret = render_rays(rays_flat[i:i+chunk], **kwargs)
               

01. 512条射线上采样32个点,并得到其真实坐标。

def render_rays(ray_batch,    network_fn,  network_query_fn,  N_samples, retraw=False,
                                                 lindisp=False,    perturb=0.,  N_importance=0,  network_fine=None,
                                                  white_bkgd=False,   raw_noise_std=0.,  verbose=False,  pytest=False):                    
                     
rays_o, rays_d = ray_batch[:,0:3], ray_batch[:,3:6]           # [N_rays, 3] each
viewdirs = ray_batch[:,-3:] if ray_batch.shape[-1] > 8 else None
bounds = torch.reshape(ray_batch[...,6:8], [-1,1,2])         #  near/far  (512,1,2)
near, far = bounds[...,0], bounds[...,1] # [-1,1]                      # 2, 6

t_vals = torch.linspace(0., 1., steps=N_samples)                # 0~1之间,间隔采样32个点
z_vals = near * (1.-t_vals) + far * (t_vals)                                # (512, 32)  26之间,间隔取32个点
z_vals = z_vals.expand([N_rays, N_samples])                      # (512, 32)

 if perturb > 0.:                                                                                    # perturb= 1.0
        mids = .5 * (z_vals[...,1:] + z_vals[...,:-1])                            # (512, 31)              [2.06, ...5.9]
        upper = torch.cat([mids, z_vals[...,-1:]], -1)                      # (512, 32)              [2.06, ...6.0]
        lower = torch.cat([z_vals[...,:1], mids], -1)                        # (512, 32)              [2.00, ...5.9]
        t_rand = torch.rand(z_vals.shape)                                      # (512,32)0~1之间随机数

z_vals = lower + (upper - lower) * t_rand                        # (512, 32)  添加扰动后:每条ray扰动不同,例如 [2.03, 2.12,... 5.97] 

pts = rays_o[...,None,:] + rays_d[...,None,:] * z_vals[...,:,None] # [N_rays=512 , N_samples=32 , 3]      得到 32个采样点的3维坐标
#  (512,1,3)*(512,32,1) = (512,32,3)

02. 分别对坐标xyz、角度θα做位置编码。

raw = network_query_fn(pts, viewdirs, network_fn)
def run_network(inputs, viewdirs, fn, embed_fn, embeddirs_fn, netchunk=1024*64):

inputs_flat = torch.reshape(inputs, [-1, inputs.shape[-1]])      # (512,32,3) --> (16384,3)
embedded = embed_fn(inputs_flat)                                                  #(16384,3) --> (16384,63)
if viewdirs is not None:
    input_dirs = viewdirs[:,None].expand(inputs.shape)             # (512,32,3) 来源于ray_d(512,3)
    input_dirs_flat = torch.reshape(input_dirs, [-1, input_dirs.shape[-1]])     # (16384,3)
    embedded_dirs = embeddirs_fn(input_dirs_flat)                   # (16384,27)
    embedded = torch.cat([embedded, embedded_dirs], -1)

03. NeRF 网络的前向过程

在这里插入图片描述

batchify_rays:按照Batchsize的大小进行训练需要可能会out of memory。
每次训练1024个像素点,训练网络输入1024*256(192个fine采样点,64个coarse采样点),需要分成批次。

outputs_flat = batchify(fn, netchunk)(embedded)
# 1024 * 256的数据切成1024 * 32子块进行训练,返回值也是按子块返回

forward:
# 网络输入:位置input_pts(16384, 63, 角度input_views(16384, 2716384=512*32

h = input_pts
 for i, l in enumerate(self.pts_linears):
        h = self.pts_linears[i](h)
        h = F.relu(h)
        if i in self.skips:         
            h = torch.cat([input_pts, h], -1)        # 网络在第四层时,输出与原始输入拼接为319if self.use_viewdirs:
    alpha = self.alpha_linear(h)
    feature = self.feature_linear(h)
    h = torch.cat([feature, input_views], -1)
        
    for i, l in enumerate(self.views_linears):
        h = self.views_linears[i](h)
        h = F.relu(h)

    rgb = self.rgb_linear(h)
    outputs = torch.cat([rgb, alpha], -1)              # (16384, 4)
    outputs = torch.reshape(outputs, list(inputs.shape[:-1]) + [outputs_flat.shape[-1]])
    # (512, 32, 4)

04. 网络输出结果后处理:raw2outputs

网络输出为每条射线上,每个点的RGB值与强度值,需要将其转化为每条射线的颜色值,和深度图。

rgb_map, disp_map, acc_map, weights, depth_map = raw2outputs(raw, z_vals, rays_d, raw_noise_std, white_bkgd, pytest=pytest)

def raw2outputs(raw, z_vals, rays_d, raw_noise_std=0, white_bkgd=False, pytest=False):
    """
    参数:
        raw: [射线数, 线上采样点数, 4]. 模型输出结果
        z_vals: [射线数, 线上采样点数 ]. 射线上采样距离
        rays_d: [射线数, 3]. 每条射线方向
    返回值:
        rgb_map: [射线数, 3]. Estimated RGB color of a ray.
        disp_map: [射线数]. Disparity map视差图. Inverse of depth map.
        acc_map: [射线数]. 射线上权重之和.
        weights: [射线数, 线上采样点数]. 每个采样点权重.
        depth_map: [射线数]. 估计的到目标距离.
    """
    raw2alpha = lambda raw, dists, act_fn=F.relu: 1.-torch.exp(-act_fn(raw)*dists)       # 预定义求解α的公式

    dists = z_vals[...,1:] - z_vals[...,:-1]                                                                                                  # 每个采样点之间的距离 (51231)
    dists = torch.cat([dists, torch.Tensor([1e10]).expand(dists[...,:1].shape)], -1)             # [N_rays, N_samples] 最后补一维

    dists = dists * torch.norm(rays_d[...,None,:], dim=-1)                                                            # (512,32)

    rgb = torch.sigmoid(raw[...,:3])  # [N_rays, N_samples, 3]
    noise = 0.
    alpha = raw2alpha(raw[...,3] + noise, dists)  # [N_rays, N_samples]
    # weights = alpha * tf.math.cumprod(1.-alpha + 1e-10, -1, exclusive=True)
    weights = alpha * torch.cumprod(torch.cat([torch.ones((alpha.shape[0], 1)), 1.-alpha + 1e-10], -1), -1)[:, :-1]     # (51232)
    rgb_map = torch.sum(weights[...,None] * rgb, -2)  # [N_rays, 3]

    depth_map = torch.sum(weights * z_vals, -1)                     # (51232--> (512)
    disp_map = 1./torch.max(1e-10 * torch.ones_like(depth_map), depth_map / torch.sum(weights, -1))
    acc_map = torch.sum(weights, -1)                                            # (512)

    if white_bkgd:
        rgb_map = rgb_map + (1.-acc_map[...,None])

    return rgb_map, disp_map, acc_map, weights, depth_map

05. 精细采样
这步是对射线上weights比较大的区域,进行二次精细采样。
在这里插入图片描述

采样数N_importance=64,z_vals_mid是采样间隔:[2.02,2.12,…5.89]

根据上步计算的weights累乘,得到新的z_vals(从[2.01,…5.98]到[2.48,4.48]).非重要部分,可以掠过以下代码

z_samples = sample_pdf(z_vals_mid, weights[...,1:-1], N_importance, det=(perturb==0.), pytest=pytest)
#  返回值z_samples 维度 (512,64)

def sample_pdf(bins, weights, N_samples, det=False, pytest=False):
    # Get pdf
    weights = weights + 1e-5                                                                   # (512,30 )           
    pdf = weights / torch.sum(weights, -1, keepdim=True)       # (512,30)
    cdf = torch.cumsum(pdf, -1)                                                            # (512,30 ) 
    cdf = torch.cat([torch.zeros_like(cdf[...,:1]), cdf], -1)  # (batch, len(bins))

    # 随机采样
    u = torch.rand(list(cdf.shape[:-1]) + [N_samples])             # (512,64)
    u = u.contiguous()
    inds = torch.searchsorted(cdf, u, right=True)                              # (512,64) 在累乘的weithts中,找到比u(512,64)大的值的索引
    below = torch.max(torch.zeros_like(inds-1), inds-1)
    above = torch.min((cdf.shape[-1]-1) * torch.ones_like(inds), inds)
    inds_g = torch.stack([below, above], -1)  # (batch, N_samples, 2)

    # cdf_g = tf.gather(cdf, inds_g, axis=-1, batch_dims=len(inds_g.shape)-2)
    # bins_g = tf.gather(bins, inds_g, axis=-1, batch_dims=len(inds_g.shape)-2)
    matched_shape = [inds_g.shape[0], inds_g.shape[1], cdf.shape[-1]]           # (512,64,31)
    cdf_g = torch.gather(cdf.unsqueeze(1).expand(matched_shape), 2, inds_g)          # (512,64,2)31个采样点累乘weights中,找到inds_g的那个weitht
    bins_g = torch.gather(bins.unsqueeze(1).expand(matched_shape), 2, inds_g)     # (512,64,2)  

    denom = (cdf_g[...,1]-cdf_g[...,0])                                                                                                # (51264)
    denom = torch.where(denom<1e-5, torch.ones_like(denom), denom)                     # (51264) 小于0.0001就选1,大于则取denom
    t = (u-cdf_g[...,0])/denom
    samples = bins_g[...,0] + t * (bins_g[...,1]-bins_g[...,0])                                                      # (512,64)

    return samples

重新对采样点进行渲染,步骤同上(可忽略)

z_vals, _ = torch.sort(torch.cat([z_vals, z_samples], -1), -1)
pts = rays_o[...,None,:] + rays_d[...,None,:] * z_vals[...,:,None] # [N_rays, N_samples + N_importance, 3]

run_fn = network_fn if network_fine is None else network_fine
raw = network_query_fn(pts, viewdirs, run_fn)

rgb_map, disp_map, acc_map, weights, depth_map = raw2outputs(raw, z_vals, rays_d, raw_noise_std, white_bkgd, pytest=pytest)

4.计算损失

        img_loss = img2mse(rgb, target_s)    # mse loss
        trans = extras['raw'][...,-1]
        loss = img_loss
        psnr = mse2psnr(img_loss)                   # 5.2564

三、几何学原理

神经辐射场采用简单的体渲染作为一种方法,通过利用可见性的概率概念来使得通过射线-三角形
交叉点变得可微分。这是通过假设场景由一团发光粒子组成的来实现的,这些粒子的密度在空间中发
生变化 (在基于物理的渲染的术语中,这将被描述为具有吸收和发射但没有散射的体积。
在下文中,为了说明简单,并且不失一般性,我们假设发射的光不作为观察方向的函数而改变

透射率. 设密度场为 σ(x),其中 x∈R3 表示射线撞击粒子的微分似然度 (即在行进无穷小的距离时撞
击粒子的概率)。我们重新参数化沿给定射线 r=(o, d) 的密度作为标量函数 σ(t),因为沿射线的任何点
x 都可以写成 r(t)=o+td。密度与透射率函数 T (t) 密切相关,它表示光线在区间 [0, t) 上传播而没有击
中任何粒子的概率。那么当走差分步 dt 时 没有撞击粒子的概率 T (t+dt) 等于 T (t),即射线到达 t 的
可能性,乘以 (1 − dt · σ(t)),在该步骤中没有击中任何东西的概率:
在这里插入图片描述
这是一个经典的微分方程,可以如下求解:
在这里插入图片描述

其中我们将 T (a → b) 定义为光线从距离 a 到 b 而没有碰到粒子的概率,这与前面的符号 T (t) =
T (0 → t)。

概率解释. 请注意,我们也可以将函数 1 − T (t)(通常称为 不透明度)解释为累积分布函数 (CDF),
表示射线确实在某个时间之前到达距离 t 并击中粒子的概率。那么 T (t) · σ(t) 是相应的概率密度函数
(PDF),给出了射线在距离 t 处正好停止的可能性。

体积渲染. 我们现在可以计算当光线从 t=0 传播到 D 时体积中的粒子发出的光的预期值,合成在背景
颜色之上。由于在 t 处停止的概率密度为 T (t) · σ(t),因此预期颜色为
在这里插入图片描述
其中 cbg 是根据残差透射率 T (D) 与前景场景合成的背景色。不失一般性,我们在下文中省略了背景
术语。

同质媒体. 我们可以通过积分计算一些具有恒定颜色 ca 和密度 σa 在射线段 [a, b] 上的均匀体积介质的
颜色:

在这里插入图片描述
透射率是乘法. 请注意,透射率分解如下:

在这里插入图片描述
这也来自 T 的概率解释,因为射线没有击中 [a, c] 内的任何粒子的概率是它没有击中任何粒子的两个
独立事件的概率的乘积在 [a, b] 或 [b, c] 内。

分段常数数据的透射率.给定一组区间在这里插入图片描述
在第 n 段内具有恒定密度 σn,并且 t1=0 和 δn=tn+1−tn,透射率等于

在这里插入图片描述
分段常数数据的体积渲染. 结合以上,我们可以通过具有分段常数颜色和密度的介质来评估体绘制积
分:
在这里插入图片描述
这导致来自 NeRF [3, Eq.3] 的体绘制方程:

在这里插入图片描述

最后,我们可以用 alpha 合成权重 αn ≡ 1 − exp (−σnδn) 来重新表达它们,而不是用体积密度来写这
些表达式,并注意到 Q i
exp xi = exp (
P
i
xi) in (19):

结合恒定的每间隔密度,该恒等式产生与 (24) 相同的表达式.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/464208.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1690_Python中的复数数据类型

全部学习汇总&#xff1a;GreyZhang/python_basic: My learning notes about python. (github.com) 之前总结的知识中设计的数据类型有整形、浮点、字符串等&#xff0c;这些类型表示的都是一个单独的独立数据对象。在Python有也有表示复数改变的数据类型&#xff0c;也就是下…

Gradio入门到进阶全网最详细教程[二]:快速搭建AI算法可视化部署演示(侧重参数详解和案例实践)

常用的两款AI可视化交互应用比较&#xff1a; Gradio Gradio的优势在于易用性&#xff0c;代码结构相比Streamlit简单&#xff0c;只需简单定义输入和输出接口即可快速构建简单的交互页面&#xff0c;更轻松部署模型。适合场景相对简单&#xff0c;想要快速部署应用的开发者。 …

千云物流 -测试服务器准备 -iotdb,redis

服务器准备 准备CentOS-7-x86_64-DVD-2009.iso镜像 链接&#xff1a;https://pan.baidu.com/s/1rNkfoeHOuYv0OmitWVDNsQ?pwdjanl 提取码&#xff1a;janl 安装服务器需要的命令yum update yum install net-tools.x86_64 -y yum install zip unzip -y ## 安装jdk到当前机器&am…

MySQL查看索引语句:SHOW INDEX 详细讲解

概述&#xff1a; SHOW INDEX语句是MySQL中用于查看表索引信息的语句。它提供了有关表中索引的详细信息&#xff0c;包括索引名称、索引类型、关联的列等。以下是SHOW INDEX的详细说明&#xff1a; 语法&#xff1a; SHOW INDEX FROM table_name [FROM db_name] [WHERE cond…

python海龟库教学

海龟库&#xff1a; 海龟绘图 “小海龟”turtle是Python语言中一个很流行的绘制图像的函数库&#xff0c;想象一个小乌龟&#xff0c;在一个横轴为x、纵轴为y的坐标系原点&#xff0c;(0,0)位置开始&#xff0c;它根据一组函数指令的控制&#xff0c;在这个平面坐标系中移动&…

Visual Studio调试的10个技巧

https://www.cnblogs.com/darrenji/p/3900023.html#e 本篇体验Visual Studio的10个调试技巧&#xff0c;包括&#xff1a; 1、插入断点和断点管理2、查看变量信息3、逐语句F11&#xff0c;逐过程F10&#xff0c;跳出ShiftF114、查看堆栈信息5、设置下一条执行语句6、调试时修改…

API接口的自我阐述

API&#xff08;Application Programming Interface&#xff09;&#xff0c;翻译为应用程序接口&#xff0c;是一套定义程序之间如何通讯的接口。API可以实现软件的可重用性、可维护性和互操作性&#xff0c;同时也可以提升软件的性能和安全性。API接口是一个软件系统中的重要…

案例2:Java图书商城系统设计与实现开题报告

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

XPM_CDC_HANDSHAKE(UG974)

Parameterized Macro: Bus Synchronizer with Full Handshake&#xff08;参数化宏&#xff1a;具有完全握手的总线同步器&#xff09; MACRO_GROUP: XPMMACRO_SUBGROUP: XPM_CDCFamilies: UltraScale, UltraScale 1、 Introduction&#xff08;介绍&#xff09; 此…

ChatGPT国内可用版-国内chatGPT哪个软件好用

国内chatGPT哪个软件最好用 国内对接ChatGPT软件&#xff0c;让智能的对话变得更加简单便捷&#xff01;ChatGPT是由OpenAI公司开发的最新一代自然语言处理技术&#xff0c;为聊天机器人赋予了更加真实、流畅、智能的语言表达能力。 我们是国内一家专注于人工智能和自然语言处…

手撕源码(一)HashMap(JDK8)

目录 1.使用示例2.new HashMap<>() 解析2.1 加载因子2.2 构造方法 3.put() 解析3.1 原始put(k, v)3.2 计算哈希1&#xff09;为什么要进行二次hash&#xff1f;2&#xff09;二次hash计算示例&#xff1a;3&#xff09;为什么使用 (length-1)&hash 而不是 hash%lengt…

Centos 搭建共享数据发布服务器

Centos 搭建共享数据发布服务器 1. 下载系统镜像2. 制作系统盘3. 制作系统3.1 BIOS设置3.2 安装系统3.3 重做系统 4 配置服务器4.1 挂载硬盘4.2 配置账号4.3 配置samba4.4 配置ftp1. 安装ftp2. 配置ftp 5. 验证5.1 验证ftp5.2 验证samba 共享服务器策略简述&#xff1a; smb提…

耐腐蚀高速电动针阀在半导体硅片清洗机化学药液流量控制中的应用

摘要&#xff1a;化学药液流量的精密控制是半导体湿法清洗工艺中的一项关键技术&#xff0c;流量控制要求所用调节针阀一是开度电动可调、二是具有不同的口径型号、三是高的响应速度&#xff0c;四是具有很好的耐腐蚀性&#xff0c;这些都是目前提升半导体清洗设备性能需要解决…

PXI 24位动态信号数据采集模块软硬件设计方案,支持国产

【IEPE传感器&#xff08;音频测试&#xff0c;噪音测试&#xff0c;振动分析&#xff09;】 符合PXI规范2.2版 24位Sigma-Delta ADC与DAC 采样率最高达432 KS/s&#xff0c;可软件编程 可编程输入范围&#xff1a;40 V&#xff0c;10 V&#xff0c;3.16 V&#xff0c; 1 V&…

Spring Bean的顺序

之前的文章已经讲过&#xff0c;Spring Bean的创建是通过动态代理实现的&#xff0c;防止浪费篇幅&#xff0c;我们直接看Bean的循环创建代码&#xff1b; 这里我们可以看到 Bean 的创建是通过: List<String> beanNames new ArrayList<>(this.beanDefinitionName…

Linux服务使用宝塔面板搭建网站,并发布公网访问 - 内网穿透(1)

文章目录 前言1. 环境安装2. 安装cpolar内网穿透3. 内网穿透4. 固定http地址5. 配置二级子域名6. 创建一个测试页面 转载自远程内网穿透的文章&#xff1a;Linux使用宝塔面板搭建网站&#xff0c;并内网穿透实现公网访问 前言 宝塔面板作为简单好用的服务器运维管理面板&#…

el-input 只能输入整数(包括正数、负数、0)或者只能输入整数(包括正数、负数、0)和小数

使用el-input-number标签 也可以使用typenumbe和v-model.number属性&#xff0c;两者结合使用&#xff0c;能满足大多数需求&#xff0c;如果还不满足&#xff0c;可以再结合正则表达式过滤 <el-input v-model.number"value" type"number" /> el-i…

孙溟㠭先生篆刻欣赏——“数”

孙溟㠭篆刻作品《数》 孙溟㠭篆刻作品《数》 线条之美可见一斑。游龙戏凤&#xff0c;嬉戏又雅趣。此时溟㠭先生之心境又如何&#xff1f; 人生几多戏谑&#xff0c;世事几多无常&#xff1b;趣心对待&#xff0c;过而无痕。何必拘束&#xff1f;何必强求规矩&#xff1f;突…

测量射频器件噪声系数的三种方法盘点

本文介绍了测量噪声系数的三种方法&#xff1a;增益法、Y系数法和噪声系数测试仪法。这三种方法的比较以表格的形式给出。 在无线通信系统中&#xff0c;噪声系数&#xff08;NF&#xff09;或者相对应的噪声因数(F)定义了噪声性能和对接收机灵敏度的贡献。本篇应用笔记详细阐…

P1043 [NOIP2003 普及组] 数字游戏

题目描述 丁丁最近沉迷于一个数字游戏之中。这个游戏看似简单&#xff0c;但丁丁在研究了许多天之后却发觉原来在简单的规则下想要赢得这个游戏并不那么容易。游戏是这样的&#xff0c;在你面前有一圈整数&#xff08;一共 &#xfffd;n 个&#xff09;&#xff0c;你要按顺序…