python

Пристальный взгляд на код из лучшего доклада конференции по компьютерному зрению и распознаванию обр

  • понедельник, 28 июня 2021 г. в 00:36:52
https://habr.com/ru/company/skillfactory/blog/564896/
  • Блог компании SkillFactory
  • Python
  • Программирование
  • Работа с 3D-графикой
  • Машинное обучение


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

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


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

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

Вращение объектов из набора данных 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. Обратите внимание на то, как вместе с фоном и видом автомобиля в профиль меняется перспектива камеры: камера как будто вращается вокруг авто сверху вниз.

И вот так:

Добавление автомобилей с помощью набора данных 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. Обратите внимание, каким образом автомобиль вращается при движении слева направо.
Объект «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-разработке на этом языке.

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

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