25 июня завершилась конференция CVPR – 2021, и какая замечательная подборка докладов! Глубокое обучение продолжает доминировать в области компьютерного зрения: у нас есть новые методы для SLAM, оценки позы, оценки глубины, новые наборы данных, сети GAN, а также многочисленные доработки прошлогодних нейронных полей свечения[1] — NeRF, и это далеко не всё.

Возможно, вы уже слышали о работе GIRAFFE[2]. Получив главный приз за лучшую работу этого года, она объединяет сети GAN, NeRF и дифференцируемый рендеринг, чтобы генерировать новые изображения. Однако, что важнее, новый подход предоставляет модульный фреймворк конструирования и композиции трёхмерных сцен из объектов в полностью дифференцируемом и обучаемом стиле — и это на шаг приближает нас к миру нейронного 3D-дизайна. К старту курса о машинном и глубоком обучении делимся переводом статьи, автор которой подробно рассматривает исходный код GIRAFFE и создаёт несколько кратких примеров визуализаций. На КДПВ вы видите кадр из презентации GIRAFFE.


Вращение и перенос генерируемого GAN автомобиля с помощью GIRAFFE. Создано автором с использованием https://github.com/autonomousvision/giraffe, лицензия MIT

Нейронные поля свечения

Наглядное объяснение и демонстрация NeRF

Говоря коротко, NeRF представляет собой метод описания и визуализации трёхмерной сцены в терминах её плотности и излучения в любой заданной точке трёхмерного объёма. Она тесно связана с концепцией световых полей, то есть функций, выражающих, как свет проходит через данное пространство.

Для заданной точки (x,y,z) в пространстве изобразим луч с направлением (θ, φ) на сцену. Для каждой точки вдоль луча соберём её плотность и зависимое от вида излучаемое свечение в этой точке, затем объединим эти лучи в одно пиксельное значение, как при обычной трассировке лучей. При этом сцены NeRF обучаются на коллекции снятых в различных позах изображений объектов, похожих на те, что применяются в приложениях типа «структура исходя из движения».

GIRAFFE

Наглядное объяснение и демонстрация GIRAFFE

Обзор 

В сущности, GIRAFFE — это основанный на обучении, полностью дифференцируемый механизм рендеринга, позволяющий составлять сцену как совокупность нескольких "полей признаков", то есть обобщение полей сияния в NeRFs. Эти поля признаков представляют собой трёхмерные объёмы, где каждый воксел содержит вектор признака.

Поля признаков строятся путём композиции созданных GAN обученных представлений, принимающих латентные коды в качестве входа в трёхмерную сцену. Поля признаков применяются к 3D-объёму, поэтому можно применять преобразования подобия, такие как поворот, перенос и масштабирование. Можно даже составить целую сцену как совокупность отдельных полей характеристик. По сравнению с NeRF этот метод даёт такие преимущества:

  • Может представлять несколько объектов и один фон с независимыми преобразованиями (оригинальный NeRF поддерживает только одну «сцену» и не отделяет объекты друг от друга).

  • Может применять позы и преобразования подобия — поворот, перенос и масштабирование — к отдельным объектам.

  • Создающие поля признаков сети GAN могут независимо обучаться и повторно использоваться в качестве компонентов.

  • Имеет дифференцированный движок рендеринга со сквозным обучением.

  • Значения цвета не ограничиваются RGB и могут распространяться на другие свойства материала.

  • Для кодирования положения использует позиционное кодирование, как в трансформере, что также “вводит индуктивное смещение для изучения представлений трёхмерных форм в канонических ориентациях, которые иначе оказались бы произвольными”.

Проект GIRAFFE содержит исходный код, он может использоваться для воспроизведения фигур из проекта и даже для создания ваших сцен. Я дам краткое руководство по их исходному коду и покажу, как работать с GIRAFFE — создавать простые нейронные трёхмерные сцены.

Исходный код

Репозиторий GIRAFFE структурирован с учётом конфигурации. Файл configs/default.yaml определяет конфигурации приложения по умолчанию. Другие файлы конфигурации, например configs/256res/cars_256/pretrained.yaml, наследуют содержимое от этого файла при помощи ключа inherit_from и переопределяют значения по умолчанию, указывая другие пары «ключ — значение».

Этот подход позволяет не составлять входные параметры отдельно, а вместо этого выводить изображения, запустив скрипт с параметром render.py <CONFIG.yaml> и обучать сети запуском скрипта с параметром — train.py <CONFIG.yaml>.

Чтобы посмотреть на рендеры в деле, сначала выполните инструкции по быстрому запуску в файле README.md: так вы загрузите предварительно обученную модель и запишете ряд выходных визуализаций, они показаны ниже.

Вращение объектов из набора данных Cars (создано автором с использованием https://github.com/autonomousvision/giraffe, лицензия MIT)

Файл конфигурации просто берёт значения по умолчанию и вставляет предварительно обученную на наборе данных Cars модель. Этот файл создаёт довольно много визуализаций различных манипуляций с рендерингом, среди них — интерполяция внешнего вида, интерполяция формы, интерполяция фона, вращение и перенос. Эти визуализации задаются в файле configs/default.yaml ключом render_program, значение которого — список определяющих визуализации строк. Они определяют «программы рендеринга», которые рендер GIRAFFE будет вызывать, обращаясь к render.py.

В методе render_full_visualization метода im2scene.giraffe.rendering.Renderer вы увидите ряд операторов if, которые ищут имена ещё большего количества программ рендеринга: object_translation_circle, render_camera_elevation и render_add_cars. Давайте посмотрим на них в деле. Создадим новый файл конфигурации с именем cars_256_pretrained_more.yaml и добавим в него такие строки:

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
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. Обратите внимание на то, как вместе с фоном и видом автомобиля в профиль меняется перспектива камеры: камера как будто вращается вокруг авто сверху вниз.

И вот так:

Добавление автомобилей с помощью набора данных Cars

Как эти программы рендеринга на самом деле размещают, переносят и поворачивают эти автомобили? Чтобы ответить на этот вопрос, внимательнее посмотрим на класс Renderer. В примере с рендером object_rotation выше вызывается метод Renderer.render_object_rotation.

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
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))
    # ...

Этот метод генерирует диапазон матриц вращения r для членов заданного пакета. Затем он итеративно передаёт члены этого диапазона (и некоторые значения по умолчанию для масштабирования и переноса) в метод сети GAN — forward, который задаётся ключом generator в файле default.yaml. Если теперь вы посмотрите на im2scene.giraffe.models.__init__.py, то увидите, что этот ключ сопоставлен с im2scene.giraffe.models.generator.Generator.

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
from im2scene.giraffe.models import generator
# ...
generator_dict = {
    'simple': generator.Generator,
}

Потерпите меня, пока смотрите на Generator.forward. Он принимает различные необязательные входные аргументы, такие как transformations, bg_rotation и camera_matrices, а затем передаёт их в свой метод volume_render_image, где происходит магия композиции. Латентные коды всех объектов сцены, включая наш фон, разбиваются на составляющие их формы и внешнего вида.

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
z_shape_obj, z_app_obj, z_shape_bg, z_app_bg = latent_codes

Здесь латентный код генерируется случайным образом при помощи функции torch.randn:

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
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
    # ...

А здесь прямой проход декодера отображает точки трёхмерного пространства и направление обзора камеры в значения σ и RGB (признак) для каждого объекта. К фону применяется другой генератор (для удобства детали опущены).

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
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 либо среднего значения при помощи функции composite_function из этих отображений составляется композиция.

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
sigma_sum, feat_weighted = self.composite_function(sigma, feat)

Окончательное изображение создаётся путём взвешивания отображения признаков по объёму σ  вдоль вектора луча. Результат — один кадр в одном окне анимаций выше. Чтобы больше узнать о том, как построены di и ray_vector, смотрите generator.py,

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
weights = self.calc_volume_weights(di, ray_vector, sigma_sum)
feat_map = torch.sum(weights.unsqueeze(-1) * feat_weighted, dim=-2)

Подводя итоги, давайте попробуем создать собственную программу рендеринга: чтобы добиться эффекта вращения и скольжения автомобиля слева направо, просто комбинируем вращение и перенос глубины. Для этого напишем несколько простых дополнений к классу Renderer в rendering.py.

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
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)

Скопируйте эти дополнения в rendering.py и создайте файл конфигурации configs/256res/cars_256_pretrained_wipeout.yaml:

# adapted from https://github.com/autonomousvision/giraffe (MIT License)
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, вы должны получить примерно такой результат:

Объект «wipeout», полученный из набора Cars. Обратите внимание, каким образом автомобиль вращается при движении слева направо.

Заключение

GIRAFFE — это захватывающее пополнение в потоке последних исследований в области NeRF и GAN. Представление поля сияния описывает мощный, расширяемый фреймворк, с помощью которого возможно строить трёхмерные сцены в обучаемом и дифференцируемом стиле.

Ссылки

[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.

Статья открывает интересные возможности для экспериментов с изображениями и напоминает о том, насколько быстро развивается глубокое обучение. Но чтобы двигать область вперёд по-прежнему нужны люди. Если вам инетесна сфера машинного и глубокого обучения, то вы можете присмотреться к программе курса «Machine Learning и Deep Learning», где рассматривается множество различных нейронных сетей, включая сети GAN, а если вас интересует лаконичный Python, вы можете обратить внимание на наш курс о Fullstack-разработке на этом языке.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы