Глубина-Осознанная Вставка Объекта в Видео с Использованием Python

Вставка объекта в видео с использованием Python

Инструкции по размещению 3D моделей в видео с использованием метода, учитывающего глубину, с использованием Python

Изображение автора

В области компьютерного зрения, последовательная оценка глубины и позы камеры в видео положила основу для более продвинутых операций, таких как вставка объектов, учитывающая глубину, в видео. Отталкиваясь от моей предыдущей статьи, в которой я исследовал эти основные техники, эта статья сосредоточена на вставке объектов, учитывающих глубину. Используя вычислительные методы на основе Python, я опишу стратегию добавления объектов в существующие видеокадры в соответствии с данными о глубине и ориентации камеры. Этот подход не только повышает реализм отредактированного видео, но также имеет широкие применения в видео-постпроизводстве.

Вкратце, подход включает два основных шага: сначала оценку последовательной глубины и позиции камеры в видео, а затем наложение сетчатого объекта на видеокадры. Чтобы объект казался неподвижным в 3D пространстве видео, он перемещается в противоположном направлении движения камеры. Это противодвижение гарантирует, что объект будет выглядеть неподвижным на протяжении всего видео.

Вы можете проверить мой код на моей странице GitHub, на которую я буду ссылаться в течение этой статьи.

Шаг 1: Генерация матриц позиции камеры и последовательная оценка глубины видео

В моей предыдущей статье я подробно объяснил, как оценить последовательные кадры глубины видео и соответствующие матрицы позиции камеры.

Для этой статьи я выбрал видео с мужчиной, идущим по улице, специально выбрав его из-за ярко выраженного движения камеры вдоль оси. Это позволит ясно оценить, сохраняют ли вставленные объекты свои фиксированные позиции в 3D пространстве видео.

Я следовал всем шагам, которые я объяснил в своей предыдущей статье, чтобы получить кадры глубины и оцененные матрицы позиции камеры. Нам особенно понадобится сгенерированный файл “custom.matrices.txt”, созданный COLMAP.

Оригинальные кадры и оцененное видео глубины приведены ниже.

(Слева) Стоковое видео предоставлено Videvo, загружено с www.videvo.net | (Справа) Оцененное видео глубины, созданное автором

Представлена визуализация облака точек, соответствующая первому кадру.

Сгенерированное облако точек первого кадра видео

Шаг 2: Выбор сетчатых файлов, которые вы хотите вставить

Теперь мы выбираем сетчатые файлы, которые будут вставлены в видеопоследовательность. Различные платформы, такие как Sketchfab.com и GrabCAD.com, предлагают широкий выбор 3D моделей для выбора.

Для демонстрационного видео я выбрал две 3D модели, ссылки на которые предоставлены подписями к изображениям ниже:

(Слева) 3D модель, предоставленная Abby Gancz (CC BY 4.0), загружена с www.sketchfab.com | (Справа) 3D модель, предоставленная Renafox (CC BY 4.0), загружена с www.sketchfab.com

Я предварительно обработал 3D-модели с помощью CloudCompare, свободно распространяемого инструмента для манипулирования облаком точек в 3D. Конкретно, я удалил нижнюю часть объектов, чтобы улучшить их интеграцию в видео. Хотя этот шаг необязателен, если вы хотите изменить определенные аспекты вашей 3D-модели, CloudCompare настоятельно рекомендуется.

После предварительной обработки файлов сетки сохраните их в форматах .ply или .obj. (Обратите внимание, что не все расширения файлов 3D-моделей поддерживают цветные сетки, например, .stl).

Шаг 3: Повторная визуализация кадров с учетом внедрения объектов, зависящих от глубины

Теперь мы переходим к основной части проекта: обработке видео. В моем репозитории представлены два ключевых скрипта – video_processing_utils.py и depth_aware_object_insertion.py. Как следует из их названия, video_processing_utils.py содержит все необходимые функции для вставки объектов, а depth_aware_object_insertion.py служит основным скриптом, который выполняет эти функции для каждого кадра видео в цикле.

Ниже приведена усеченная версия основной части depth_aware_object_insertion.py. В цикле for, который выполняется столько раз, сколько кадров во входном видео, мы загружаем пакетную информацию из конвейера вычисления глубины, из которой получаем исходный RGB-кадр и его оценку глубины. Затем мы вычисляем обратную матрицу позы камеры. Затем мы передаем сетку, глубину и интринсики камеры в функцию с именем render_mesh_with_depth().

for i in tqdm(range(batch_count)):    batch = np.load(os.path.join(BATCH_DIRECTORY, file_names[i]))    # ... (усечено для краткости)    # преобразование сетки с обратными экструзиями камеры    frame_transformation = np.vstack(np.split(extrinsics_data[i],4))    inverse_frame_transformation = np.empty((4, 4))    inverse_frame_transformation[:3, :] = np.concatenate((np.linalg.inv(frame_transformation[:3,:3]),                                                            np.expand_dims(-1 * frame_transformation[:3,3],0).T), axi    inverse_frame_transformation[3, :] = [0.00, 0.00, 0.00, 1.00]    mesh.transform(inverse_frame_transformation)    # ... (усечено для краткости)    image = np.transpose(batch['img_1'], (2, 3, 1, 0))[:,:,:,0]        depth = np.transpose(batch['depth'], (2, 3, 1, 0))[:,:,0,0]    # ... (усечено для краткости)    # рендеринг цветного и глубинного буфера преобразованной сетки в области изображения    mesh_color_buffer, mesh_depth_buffer = render_mesh_with_depth(np.array(mesh.vertices),                                                                   np.array(mesh.vertex_colors),                                                                   np.array(mesh.triangles),                                                                   depth, intrinsics)            # глубинно-зависимое наложение сетки и исходного изображения    combined_frame, combined_depth = combine_frames(image, mesh_color_buffer, depth, mesh_depth_buffer)     # ... (усечено для краткости)

Функция render_mesh_with_depth принимает 3D-сетку, представленную ее вершинами, цветами вершин и треугольниками, и отображает ее на 2D-глубинный кадр. Функция начинает с инициализации глубинного и цветового буферов для хранения отображенного вывода. Затем она проецирует вершины 3D-сетки на 2D-кадр с использованием внутренних параметров камеры. Функция использует алгоритм растрирования по строке для перебора каждого треугольника в сетке, растризуя его на пиксели 2D-кадра. Во время этого процесса функция вычисляет барицентрические координаты для каждого пикселя для интерполяции глубины и цветовых значений. Эти интерполированные значения затем используются для обновления глубинного и цветового буферов, но только если интерполированная глубина пикселя ближе к камере, чем существующее значение в глубинном буфере. Наконец, функция возвращает цветовой и глубинный буферы в качестве отображенного вывода, при этом цветовой буфер преобразуется в формат uint8, подходящий для отображения изображения.

def render_mesh_with_depth(mesh_vertices, vertex_colors, triangles, depth_frame, intrinsic):    vertex_colors = np.asarray(vertex_colors)        # Инициализация глубинного и цветового буферов    buffer_width, buffer_height = depth_frame.shape[1], depth_frame.shape[0]    mesh_depth_buffer = np.ones((buffer_height, buffer_width)) * np.inf        # Проецирование 3D-вершин на 2D-координаты изображения    vertices_homogeneous = np.hstack((mesh_vertices, np.ones((mesh_vertices.shape[0], 1))))    camera_coords = vertices_homogeneous.T[:-1,:]    projected_vertices = intrinsic @ camera_coords    projected_vertices /= projected_vertices[2, :]    projected_vertices = projected_vertices[:2, :].T.astype(int)    depths = camera_coords[2, :]    mesh_color_buffer = np.zeros((buffer_height, buffer_width, 3), dtype=np.float32)        # Цикл по каждому треугольнику для его рендеринга    for triangle in triangles:        # Получение 2D-точек и глубины для вершин треугольника        points_2d = np.array([projected_vertices[v] for v in triangle])        triangle_depths = [depths[v] for v in triangle]        colors = np.array([vertex_colors[v] for v in triangle])                # Сортировка вершин по их y-координатам для растрирования по строке        order = np.argsort(points_2d[:, 1])        points_2d = points_2d[order]        triangle_depths = np.array(triangle_depths)[order]        colors = colors[order]        y_mid = points_2d[1, 1]        for y in range(points_2d[0, 1], points_2d[2, 1] + 1):            if y < 0 or y >= buffer_height:                continue                        # Определение начальных и конечных x-координат для текущей строки сканирования            if y < y_mid:                x_start = interpolate_values(y, points_2d[0, 1], points_2d[1, 1], points_2d[0, 0], points_2d[1, 0])                x_end = interpolate_values(y, points_2d[0, 1], points_2d[2, 1], points_2d[0, 0], points_2d[2, 0])            else:                x_start = interpolate_values(y, points_2d[1, 1], points_2d[2, 1], points_2d[1, 0], points_2d[2, 0])                x_end = interpolate_values(y, points_2d[0, 1], points_2d[2, 1], points_2d[0, 0], points_2d[2, 0])                        x_start, x_end = int(x_start), int(x_end)            # Цикл по каждому пикселю в строке сканирования            for x in range(x_start, x_end + 1):                if x < 0 or x >= buffer_width:                    continue                               

Цветовой и глубинные буферы преобразованной сетки, а также исходное изображение RGB и его оцененная глубина затем передаются в функцию combine_frames() вместе с оригинальным RGB-изображением и его оцененной картой глубины. Эта функция предназначена для объединения двух наборов изображений и глубины. Она использует информацию о глубине, чтобы определить, какие пиксели в оригинальном кадре должны быть заменены соответствующими пикселями в отрисованном кадре сетки. В частности, для каждого пикселя функция проверяет, является ли значение глубины отрисованной сетки меньше значения глубины оригинальной сцены. Если это так, то этот пиксель считается "ближе" камере в отрисованном кадре сетки, и значения пикселей как в цветовом, так и в глубинном кадрах заменяются соответствующим образом. Функция возвращает объединенные цветовой и глубинные кадры, эффективно наложив отрисованную сетку на оригинальную сцену на основе информации о глубине.

# Комбинируем исходный и отрисованный кадры на основе информации о глубине
def combine_frames(original_frame, rendered_mesh_img, original_depth_frame, mesh_depth_buffer):
    # Создаем маску, где сетка находится ближе, чем исходная глубина
    mesh_mask = mesh_depth_buffer < original_depth_frame
    
    # Инициализируем объединенные кадры
    combined_frame = original_frame.copy()
    combined_depth = original_depth_frame.copy()
    
    # Обновляем объединенные кадры информацией о сетке, где маска истинна
    combined_frame[mesh_mask] = rendered_mesh_img[mesh_mask]
    combined_depth[mesh_mask] = mesh_depth_buffer[mesh_mask]
    
    return combined_frame, combined_depth

Вот как выглядят mesh_color_buffer, mesh_depth_buffer и combined_frame для первого объекта, слона. Поскольку объект слона не перекрывается другими элементами внутри кадра, он остается полностью видимым. При разных размещениях могут возникать перекрытия.

(слева) Вычисленный цветовой буфер сетки слона | (справа) Вычисленный глубинный буфер сетки слона | (внизу) Объединенный кадр

Соответственно, я поместил вторую сетку, автомобиль, на тротуар у дороги. Я также настроил его начальную ориентацию так, чтобы казалось, что он там припаркован. Ниже приведены соответствующие mesh_color_buffer, mesh_depth_buffer и combined_frame для этой сетки.

(слева) Вычисленный цветовой буфер сетки автомобиля | (справа) Вычисленный глубинный буфер сетки автомобиля | (внизу) Объединенный кадр

Визуализация облака точек с вставленными объектами представлена ниже. В результате появляются новые белые промежутки из-за новых областей перекрытия.

Сгенерированное облако точек первого кадра с вставленными объектами

После вычисления наложенных изображений для каждого кадра видео мы готовы визуализировать наше видео.

Шаг 4: Визуализация видео из обработанных кадров

В последнем разделе файла depth_aware_object_insertion.py мы просто визуализируем видео из кадров с вставленными объектами, используя функцию render_video_from_frames. Вы также можете настроить частоту кадров выходного видео на этом шаге. Код представлен ниже:

video_name = 'depth_aware_object_insertion_demo.mp4'
save_directory = "depth_aware_object_insertion_demo/"
frame_directory = "depth_aware_object_insertion_demo/"
image_extension = ".png"
fps = 15 # визуализация видео с наложенными кадрами
render_video_from_frames(frame_directory, image_extension, save_directory, video_name, fps)

Вот мое демонстрационное видео:

(Слева) Видеоматериал, предоставленный Videvo, загружен с сайта www.videvo.net | (Справа) Видеоматериал с двумя вставленными объектами

Более высокого разрешения версия этой анимации загружена на YouTube.

В целом, целостность объекта, по всей видимости, хорошо сохраняется; например, объект автомобиля убедительно скрыт столбом уличного фонаря в сцене. Хотя в течение всего видео можно заметить небольшое вибрирование положения автомобиля — скорее всего из-за несовершенства оценки положения камеры — механизм фиксации мира в целом работает в соответствии с ожиданиями в демонстрационном видео.

Хотя концепция вставки объектов в видео далеко не нова, существующие инструменты, такие как After Effects, предлагают методы на основе отслеживания особенностей, эти традиционные подходы часто могут быть очень сложными и затратными для тех, кто не знаком с инструментами видеомонтажа. Именно здесь возможности алгоритмов на основе Python становятся актуальными. Используя машинное обучение и основные программные конструкции, эти алгоритмы могут демократизировать сложные задачи видеомонтажа, делая их доступными даже для людей с ограниченным опытом в этой области. Таким образом, по мере развития технологий я предвижу, что программные подходы станут мощными инструментами, снижающими барьеры и открывающими новые возможности для творческого выражения в видеомонтаже.

Желаю вам отличного дня!