0


GIRAFFE: CVPR 2021 最佳论文介绍和代码解释

GIRAFFE是一个基于学习的、完全可微的渲染引擎,用于将场景合成为多个“特征域”的总和。

CVPR 2021年结束了,深度学习继续在计算机视觉领域占据主导地位,包括SLAM、姿态估计、深度估计、新的数据集、GANs,以及去年的神经辐射场[1]或NeRFs的许多改进。

到目前为止,你可能已经听说过一篇论文“GIRAFFE: representation Scenes as composition Generative Neural Feature Fields”。[2]“作为今年最佳论文奖的大奖,这篇论文结合了gan、nerf和可差异化渲染来生成新颖的图像。然而更重要的是,它提供了一个模块化框架,以完全可微和可学习的方式从对象构建和组成3D场景,让我们向神经三维设计的世界更近一步。在这篇文章中,我将进一步研究GIRAFFE源代码,并生成一些快速的可视化示例。

简单回顾一下nerf,它们是一种描述和渲染3D场景的方法,在3D体积中任何给定的点上它的密度和辐射。它与光场的概念密切相关,光场是表达光如何流经给定空间的函数。对于空间中给定的(x,y,z)视点,图像将方向(θ, φ)的射线投射到一个场景中。对于这条线上的每个点,我们收集其密度和视相关的发射辐射,并以类似于传统光线追踪的方式将这些光线合成为单个像素值。这些NeRF场景是从各种姿势拍摄的图像收集学习,你会使用在结构从运动应用程序。

GIRAFFE

本质上,GIRAFFE 是一种基于学习的、完全可微的渲染引擎,它允许您将场景组合成多个“特征场”的总和,这是 NeRF 中辐射场的概括。这些特征字段是 3D 体积,其中每个体素包含一个特征向量。特征域是通过合成 GAN 生成的学习表示来构建的,这些表示接受潜在代码作为 3D 场景的输入。由于特征字段应用于 3D 体积,因此您可以应用相似性变换,例如旋转、平移和缩放。您甚至可以将整个场景合成为各个特征字段的总和。该方法对 NeRF 进行了以下改进:

可以用独立的变换来表示多个对象(和一个背景)(原始 NeRF 只能支持一个“场景”,无法解开单个对象)。

可以对单个对象应用姿势和相似性变换,例如旋转、平移和缩放。

生成特征字段的 GAN 可以像组件一样独立学习和重用。

具有经过端到端训练的可微渲染引擎。

颜色值不仅支持 RGB,还可能扩展到其他材料属性。

使用像转换器这样的位置编码来编码位置,这也“引入了归纳偏差来学习规范方向的 3D 形状表示,否则将是任意的。”

GIRAFFE 项目包括源代码,您可以使用这些源代码来重现他们的人物,甚至创作您自己的场景。我简要介绍了他们的源代码,并展示了我们如何使用 GIRAFFE 来组成一些简单的神经 3D 场景。

GIRAFFE 源代码的结构考虑了配置。configs/default.yaml 文件指定应用程序的默认配置。其他配置文件,如 configs/256res/cars_256/pretrained.yaml 使用 inherit_from 键从该配置文件继承,并通过指定其他键值对覆盖默认值。这使我们能够使用 python render.py <CONFIG.yaml> 渲染图像,并通过自记录配置文件而不是组合输入参数使用 python train.py <CONFIG.yaml> 进行训练。

要自己尝试一些渲染,请首先运行 README.md 文件中的快速入门说明。这将下载一个预训练模型并将一系列输出可视化(如下所示)写入文件夹 out。

配置文件只是采用默认值并在 Cars 数据集上插入一个预训练模型。它生成了许多操作底层渲染的方式的可视化,例如外观插值、形状插值、背景插值、旋转和平移。这些可视化在 configs/default.yaml 中的 render_program 键下指定,其值是指定这些可视化的字符串列表。这些指定了 GIRAFFE 渲染器在调用 render.py 时将调用的“渲染程序”。在 im2scene.giraffe.rendering.Renderer 的 render_full_visualization 方法中,您将看到一系列 if 语句,用于查找更多渲染程序的名称,例如“object_translation_circle”、“render_camera_elevation”和“render_add_cars”。

我们可以进行自定义的测试,创建一个名为 cars_256_pretrained_more.yaml 的新配置文件,并添加以下内容:

这只是我们之前使用的配置文件,默认配置文件的 render_program 键被我们想要的新渲染程序覆盖。现在执行 python render.py configs/256res/cars_256_pretrained_more.yaml 以生成更多可视化。你应该得到这样的结果:

inherit_from: configs/256res/cars_256.yaml
training:
  out_dir:  out/cars256_pretrained
test:
  model_file: https://s3.eu-central-1.amazonaws.com/avg-projects/giraffe/models/checkpoint_cars256-d9ea5e11.pt
rendering:
  render_dir: rendering
  render_program: ['render_camera_elevation', 'render_add_cars']

这只是我们之前使用的配置文件,默认配置文件的 render_program 键被我们想要的新渲染程序覆盖。现在执行 python render.py configs/256res/cars_256_pretrained_more.yaml 以生成更多可视化。你应该得到这样的结果:

带有 Cars 数据集的相机高程。注意相机视角在背景和汽车轮廓上的变化,就好像相机从上方和下方围绕汽车旋转

使用其他数据集添加新的车

这些渲染程序实际上是如何放置、平移和旋转这些汽车的?为了回答这个问题,我们需要仔细看看 Renderer 类。对于上面的“object_rotation”示例,调用了 Renderer.render_object_rotation 方法。

class Renderer(object):
    # ...
    def render_object_rotation(self, img_out_path, batch_size=15, n_steps=32):
        gen = self.generator
        bbox_generator = gen.bounding_box_generator

        n_boxes = bbox_generator.n_boxes

        # Set rotation range
        is_full_rotation = (bbox_generator.rotation_range[0] == 0
                            and bbox_generator.rotation_range[1] == 1)
        n_steps = int(n_steps * 2) if is_full_rotation else n_steps
        r_scale = [0., 1.] if is_full_rotation else [0.1, 0.9]

        # Get Random codes and bg rotation
        latent_codes = gen.get_latent_codes(batch_size, tmp=self.sample_tmp)
        bg_rotation = gen.get_random_bg_rotation(batch_size)

        # Set Camera
        camera_matrices = gen.get_camera(batch_size=batch_size)
        s_val = [[0, 0, 0] for i in range(n_boxes)]
        t_val = [[0.5, 0.5, 0.5] for i in range(n_boxes)]
        r_val = [0. for i in range(n_boxes)]
        s, t, _ = gen.get_transformations(s_val, t_val, r_val, batch_size)

        out = []
        for step in range(n_steps):
            # Get rotation for this step
            r = [step * 1.0 / (n_steps - 1) for i in range(n_boxes)]
            r = [r_scale[0] + ri * (r_scale[1] - r_scale[0]) for ri in r]
            r = gen.get_rotation(r, batch_size)

            # define full transformation and evaluate model
            transformations = [s, t, r]
            with torch.no_grad():
                out_i = gen(batch_size, latent_codes, camera_matrices,
                            transformations, bg_rotation, mode='val')
            out.append(out_i.cpu())
        out = torch.stack(out)
        out_folder = join(img_out_path, 'rotation_object')
        makedirs(out_folder, exist_ok=True)
        self.save_video_and_images(
            out, out_folder, name='rotation_object',
            is_full_rotation=is_full_rotation,
            add_reverse=(not is_full_rotation))
    # ...

此函数为给定批次的成员生成一系列旋转矩阵。然后它迭代地将这个范围的成员(以及一些缩放和平移的默认值)传递给生成器网络的 forward 方法,该方法由 default.yaml 中的生成器键指定。如果您现在查看 im2scene.giraffe.models.init.py,您将看到此键映射到 im2scene.giraffe.models.generator.Generator。

from im2scene.giraffe.models import generator
# ...
generator_dict = {
    'simple': generator.Generator,
}

现在,查看 Generator.forward 。它接受各种可选的输入参数,例如transformations、bg_rotation 和camera_matrices,然后将它们传递给它的volume_render_image 方法。这就是合成魔法发生的地方。场景中所有对象的潜在代码,包括我们的背景,都被分为它们的形状和外观组件。

z_shape_obj, z_app_obj, z_shape_bg, z_app_bg = latent_codes

在这个例子中,这些潜在代码是使用 torch.randn 函数随机生成的:

class Generator(nn.Module):
    # ...
    def get_latent_codes(self, batch_size=32, tmp=1.):
        z_dim, z_dim_bg = self.z_dim, self.z_dim_bg
        n_boxes = self.get_n_boxes()
        def sample_z(x): return self.sample_z(x, tmp=tmp)
        z_shape_obj = sample_z((batch_size, n_boxes, z_dim))
        z_app_obj = sample_z((batch_size, n_boxes, z_dim))
        z_shape_bg = sample_z((batch_size, z_dim_bg))
        z_app_bg = sample_z((batch_size, z_dim_bg))
        return z_shape_obj, z_app_obj, z_shape_bg, z_app_bg

    def sample_z(self, size, to_device=True, tmp=1.):
        z = torch.randn(*size) * tmp
        if to_device:
            z = z.to(self.device)
        return z
    # ...

这是解码器前向传递将 3D 点和相机观察方向映射到每个对象的 σ 和 RGB(特征)值的地方。不同的生成器应用于背景(为了可读性省略了细节)。

n_iter = n_boxes if not_render_background else n_boxes + 1
# ...
for i in range(n_iter):
    if i < n_boxes:  # Object
        p_i, r_i = self.get_evaluation_points(pixels_world,
            camera_world, di, transformations, i)
        z_shape_i, z_app_i = z_shape_obj[:, i], z_app_obj[:, i]
        feat_i, sigma_i = self.decoder(p_i, r_i, z_shape_i, z_app_i)
        # ...
    else:  # Background
        p_bg, r_bg = self.get_evaluation_points_bg(pixels_world,
            camera_world, di, bg_rotation)
        feat_i, sigma_i = self.background_generator(
            p_bg, r_bg, z_shape_bg, z_app_bg)
        # ...
    feat.append(feat_i)
    sigma.append(sigma_i)
# ...

然后使用复合函数通过 σ max 或平均值合成这些地图。

sigma_sum, feat_weighted = self.composite_function(sigma, feat)

最后,通过沿射线向量通过 σ 体积对特征图进行加权来创建最终图像。最终结果是您在上面看到的动画之一的单个窗口中的单个帧(有关如何构造 di 和 ray_vector 的详细信息,请参阅 generator.py)。

weights = self.calc_volume_weights(di, ray_vector, sigma_sum)
feat_map = torch.sum(weights.unsqueeze(-1) * feat_weighted, dim=-2)

现在总结一下,让我们尝试创建自己的渲染程序。这将简单地结合深度平移和旋转来创建汽车从左到右旋转和滑动的效果。为此,我们对 renderer.py 中的 Renderer 类进行了一些简单的添加。

class Renderer(object):
    # ...
    def render_full_visualization(self, img_out_path,
            render_program=['object_rotation']):
        for rp in render_program:
            # ...
            # APPEND THIS TO THE END OF render_full_visualization
            if rp == 'object_wipeout':
                self.set_random_seed()
                self.render_object_wipeout(img_out_path)
    # ...
    # APPEND THIS TO THE END OF rendering.py
    def render_object_wipeout(self, img_out_path, batch_size=15,
            n_steps=32):
        gen = self.generator

        # Get values
        latent_codes = gen.get_latent_codes(batch_size, tmp=self.sample_tmp)
        bg_rotation = gen.get_random_bg_rotation(batch_size)
        camera_matrices = gen.get_camera(batch_size=batch_size)
        n_boxes = gen.bounding_box_generator.n_boxes
        s = [[0., 0., 0.]
             for i in range(n_boxes)]
        n_steps = int(n_steps * 2)
        r_scale = [0., 1.]

        if n_boxes == 1:
            t = []
            x_val = 0.5
        elif n_boxes == 2:
            t = [[0.5, 0.5, 0.]]
            x_val = 1.0

        out = []
        for step in range(n_steps):
            # translation
            i = step * 1.0 / (n_steps - 1)
            ti = t + [[0.1, i, 0.]]
            # rotation
            r = [step * 1.0 / (n_steps - 1) for i in range(n_boxes)]
            r = [r_scale[0] + ri * (r_scale[1] - r_scale[0]) for ri in r]

            transformations = gen.get_transformations(s, ti, r, batch_size)
            with torch.no_grad():
                out_i = gen(batch_size, latent_codes, camera_matrices,
                            transformations, bg_rotation, mode='val')
            out.append(out_i.cpu())
        out = torch.stack(out)

        out_folder = join(img_out_path, 'object_wipeout')
        makedirs(out_folder, exist_ok=True)
        self.save_video_and_images(
            out, out_folder, name='object_wipeout',
            add_reverse=True)

将这些添加内容复制粘贴到 render.py 中,然后创建以下配置文件作为 configs/256res/cars_256_pretrained_wipeout.yaml:

inherit_from: configs/256res/cars_256.yaml
training:
  out_dir:  out/cars256_pretrained
test:
  model_file: https://s3.eu-central-1.amazonaws.com/avg-projects/giraffe/models/checkpoint_cars256-d9ea5e11.pt
rendering:
  render_dir: rendering
  render_program: ['object_wipeout']

现在,如果您执行 python render.py configs/256res/cars_256_pretrained_wipeout.yaml就能看到结果

GIRAFFE 是对 NeRF 和 GAN 研究最近大量研究的一个令人兴奋的补充。辐射场表示描述了一个强大且可扩展的框架,我们可以用它以可区分和可学习的方式构建 3D 场景。我希望您发现深入了解代码很有用。如果是这样,我鼓励您自己查看作者的来源和论文。

参考

[1] Ben Mildenhall, Pratul P. Srinivasan, Matthew Tancik, Jonathan T. Barron, Ravi Ramamoorthi, Ren Ng — NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis (2020), ECCV 2020

[2] Michael Niemeyer, Andreas Geiger — GIRAFFE: Representing Scenes as Compositional Generative Neural Feature Fields (2021), CVPR 2021

标签:

“GIRAFFE: CVPR 2021 最佳论文介绍和代码解释”的评论:

还没有评论