Inserción de objetos con conciencia de profundidad en videos usando Python

Inserción de objetos con conciencia de profundidad en videos en Python

Instrucciones para colocar modelos 3D en videos con un método consciente de la profundidad utilizando Python

Imagen del autor

En el campo de la visión por computadora, la estimación coherente de la profundidad y la posición de la cámara en videos ha sentado las bases para operaciones más avanzadas, como la inserción de objetos conscientes de la profundidad en videos. Basándome en mi artículo anterior que exploró estas técnicas fundamentales, este artículo se centra en la inserción de objetos conscientes de la profundidad. Utilizando métodos computacionales basados en Python, describiré una estrategia para agregar objetos a cuadros de video preexistentes de acuerdo con los datos de profundidad y orientación de la cámara. Esta metodología no solo eleva el realismo del contenido de video editado, sino que también tiene amplias aplicaciones en la postproducción de video.

En resumen, el enfoque implica dos pasos principales: primero, estimar la profundidad y la posición de la cámara de manera coherente en un video, y segundo, superponer un objeto de malla en los cuadros de video. Para que el objeto aparezca estacionario en el espacio 3D del video, se mueve en la dirección opuesta a cualquier movimiento de la cámara. Este contramovimiento garantiza que el objeto parezca fijo en su lugar a lo largo del video.

Puedes verificar mi código en mi página de GitHub, al cual me referiré a lo largo de este artículo.

Paso 1: Generación de matrices de posición de cámara y estimación coherente de la profundidad del video

En mi artículo anterior, expliqué detalladamente cómo estimar cuadros de profundidad coherentes de videos y las correspondientes matrices de posición de la cámara a lo largo de los videos.

Para este artículo, seleccioné un video que muestra a un hombre caminando por una calle, elegido específicamente por su pronunciado movimiento de la cámara a lo largo de un eje. Esto permitirá una evaluación clara de si los objetos insertados mantienen sus posiciones fijas dentro del espacio 3D del video.

Seguí todos los pasos que expliqué en mi artículo anterior para obtener los cuadros de profundidad y las matrices de posición de la cámara estimadas. Especialmente necesitaremos el archivo “custom.matrices.txt” generado por COLMAP.

A continuación se muestra el metraje original y su video de profundidad estimada.

(Izquierda) Metraje de archivo proporcionado por Videvo, descargado de www.videvo.net | (Derecha) Video de profundidad estimada creado por el autor

La visualización de la nube de puntos correspondiente al primer cuadro se presenta a continuación. Los espacios en blanco indican regiones de sombra que están ocultas a la vista de la cámara debido a la presencia de objetos en primer plano.

Nube de puntos generada del primer cuadro del video

Paso 2: Selección de archivos de malla que deseas insertar

Ahora seleccionamos los archivos de malla que se insertarán en la secuencia de video. Varias plataformas como Sketchfab.com y GrabCAD.com ofrecen una amplia gama de modelos 3D para elegir.

Para mi video de demostración, he elegido dos modelos 3D, cuyos enlaces se proporcionan en las leyendas de las imágenes a continuación:

(Izquierda) Modelo 3D proporcionado por Abby Gancz (CC BY 4.0), descargado de www.sketchfab.com | (Derecha) Modelo 3D proporcionado por Renafox (CC BY 4.0), descargado de www.sketchfab.com

Preprocesé los modelos 3D utilizando CloudCompare, una herramienta de código abierto para la manipulación de nubes de puntos 3D. Específicamente, eliminé las porciones del suelo de los objetos para mejorar su integración en el video. Si bien este paso es opcional, si desea modificar ciertos aspectos de su modelo 3D, se recomienda encarecidamente el uso de CloudCompare.

Después de preprocesar los archivos de malla, guárdelos como archivos .ply o .obj. (Tenga en cuenta que no todas las extensiones de archivo de modelos 3D admiten mallas coloreadas, como .stl).

Paso 3: Volver a renderizar los fotogramas con inserción de objetos sensibles a la profundidad

Ahora llegamos al componente principal del proyecto: el procesamiento de video. En mi repositorio, se proporcionan dos scripts clave: video_processing_utils.py y depth_aware_object_insertion.py. Como se deduce por sus nombres, video_processing_utils.py alberga todas las funciones esenciales para la inserción de objetos, mientras que depth_aware_object_insertion.py sirve como el script principal que ejecuta estas funciones en cada fotograma de video dentro de un bucle.

A continuación se muestra una versión recortada de la sección principal de depth_aware_object_insertion.py. En un bucle for que se ejecuta tantas veces como el recuento de fotogramas en el video de entrada, cargamos información agrupada del canal de cálculo de profundidad, a partir de la cual obtenemos el fotograma RGB original y su estimación de profundidad. Luego calculamos la inversa de la matriz de posición de la cámara. A continuación, alimentamos la malla, la profundidad y las intrínsecas de la cámara en una función llamada render_mesh_with_depth().

for i in tqdm(range(batch_count)):    batch = np.load(os.path.join(BATCH_DIRECTORY, file_names[i]))    # ... (recortado por brevedad)    # transformación de la malla con las extrínsecas de la cámara inversa    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)    # ... (recortado por brevedad)    image = np.transpose(batch['img_1'], (2, 3, 1, 0))[:,:,:,0]        depth = np.transpose(batch['depth'], (2, 3, 1, 0))[:,:,0,0]    # ... (recortado por brevedad)    # renderizando el búfer de color y profundidad de la malla transformada en el dominio de la imagen    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)            # superposición sensible a la profundidad de la malla y la imagen original    combined_frame, combined_depth = combine_frames(image, mesh_color_buffer, depth, mesh_depth_buffer)     # ... (recortado por brevedad)

La función render_mesh_with_depth toma una malla 3D, representada por sus vértices, colores de vértices y triángulos, y la renderiza en un marco de profundidad 2D. La función comienza inicializando búferes de profundidad y color para contener la salida renderizada. Luego proyecta los vértices de la malla 3D sobre el marco 2D utilizando parámetros intrínsecos de la cámara. La función utiliza renderizado de barrido de líneas para recorrer cada triángulo de la malla, rasterizándolo en píxeles en el marco 2D. Durante este proceso, la función calcula coordenadas baricéntricas para cada píxel para interpolar valores de profundidad y color. Estos valores interpolados se utilizan luego para actualizar los búferes de profundidad y color, pero solo si la profundidad interpolada del píxel está más cerca de la cámara que el valor existente en el búfer de profundidad. Finalmente, la función devuelve los búferes de color y profundidad como la salida renderizada, con el búfer de color convertido a un formato uint8 adecuado para la visualización de imágenes.

def render_mesh_with_depth(mesh_vertices, vertex_colors, triangles, depth_frame, intrinsic):    vertex_colors = np.asarray(vertex_colors)        # Inicializar los búferes de profundidad y color    buffer_width, buffer_height = depth_frame.shape[1], depth_frame.shape[0]    mesh_depth_buffer = np.ones((buffer_height, buffer_width)) * np.inf        # Proyectar los vértices 3D en coordenadas de imagen 2D    vertices_homogeneos = np.hstack((mesh_vertices, np.ones((mesh_vertices.shape[0], 1))))    coordenadas_camara = vertices_homogeneos.T[:-1,:]    vertices_proyectados = intrinsic @ coordenadas_camara    vertices_proyectados /= vertices_proyectados[2, :]    vertices_proyectados = vertices_proyectados[:2, :].T.astype(int)    profundidades = coordenadas_camara[2, :]    mesh_color_buffer = np.zeros((buffer_height, buffer_width, 3), dtype=np.float32)        # Recorrer cada triángulo para renderizarlo    for triángulo in triangles:        # Obtener puntos 2D y profundidades para los vértices del triángulo        puntos_2d = np.array([vertices_proyectados[v] for v in triángulo])        profundidades_triángulo = [profundidades[v] for v in triángulo]        colores = np.array([vertex_colors[v] for v in triángulo])                # Ordenar los vértices según sus coordenadas y para el renderizado de barrido de líneas        orden = np.argsort(puntos_2d[:, 1])        puntos_2d = puntos_2d[orden]        profundidades_triángulo = np.array(profundidades_triángulo)[orden]        colores = colores[orden]        y_medio = puntos_2d[1, 1]        for y in range(puntos_2d[0, 1], puntos_2d[2, 1] + 1):            if y < 0 or y >= buffer_height:                continue                        # Determinar las coordenadas de inicio y fin de x para la línea de escaneo actual            if y < y_medio:                x_inicio = interpolate_values(y, puntos_2d[0, 1], puntos_2d[1, 1], puntos_2d[0, 0], puntos_2d[1, 0])                x_fin = interpolate_values(y, puntos_2d[0, 1], puntos_2d[2, 1], puntos_2d[0, 0], puntos_2d[2, 0])            else:                x_inicio = interpolate_values(y, puntos_2d[1, 1], puntos_2d[2, 1], puntos_2d[1, 0], puntos_2d[2, 0])                x_fin = interpolate_values(y, puntos_2d[0, 1], puntos_2d[2, 1], puntos_2d[0, 0], puntos_2d[2, 0])                        x_inicio, x_fin = int(x_inicio), int(x_fin)            # Recorrer cada píxel en la línea de escaneo            for x in range(x_inicio, x_fin + 1):                if x < 0 or x >= buffer_width:                    continue                                # Calcular las coordenadas baricéntricas para el píxel                s, t, u = compute_barycentric_coords(puntos_2d, x, y)                                # Verificar si el píxel se encuentra dentro del triángulo                if s >= 0 and t >= 0 and u >= 0:                    # Interpolar profundidad y color para el píxel                    profundidad_interp = s * profundidades_triángulo[0] + t * profundidades_triángulo[1] + u * profundidades_triángulo[2]                    color_interp = s * colores[0] + t * colores[1] + u * colores[2]                                        # Actualizar el píxel si está más cerca de la cámara                    if profundidad_interp < mesh_depth_buffer[y, x]:                        mesh_depth_buffer[y, x] = profundidad_interp                        mesh_color_buffer[y, x] = color_interp    # Convertir colores flotantes a uint8    mesh_color_buffer = (mesh_color_buffer * 255).astype(np.uint8)        return mesh_color_buffer, mesh_depth_buffer

Los buffers de color y profundidad de la malla transformada se alimentan en la función combine_frames() junto con la imagen RGB original y su mapa de profundidad estimado. Esta función está diseñada para combinar dos conjuntos de imágenes y marcos de profundidad. Utiliza la información de profundidad para decidir qué píxeles en el marco original deben ser reemplazados por los píxeles correspondientes en el marco de la malla renderizada. Específicamente, para cada píxel, la función verifica si el valor de profundidad de la malla renderizada es menor que el valor de profundidad de la escena original. Si es así, ese píxel se considera “más cercano” a la cámara en el marco de la malla renderizada, y los valores de píxel tanto en los marcos de color como de profundidad se reemplazan en consecuencia. La función devuelve los marcos de color y profundidad combinados, superponiendo efectivamente la malla renderizada sobre la escena original basada en la información de profundidad.

# Combinar los marcos originales y renderizados de la malla basándose en la información de profundidaddef combine_frames(original_frame, rendered_mesh_img, original_depth_frame, mesh_depth_buffer):    # Crear una máscara donde la malla está más cerca que la profundidad original    mesh_mask = mesh_depth_buffer < original_depth_frame        # Inicializar los marcos combinados    combined_frame = original_frame.copy()    combined_depth = original_depth_frame.copy()        # Actualizar los marcos combinados con la información de la malla donde la máscara es Verdadera    combined_frame[mesh_mask] = rendered_mesh_img[mesh_mask]    combined_depth[mesh_mask] = mesh_depth_buffer[mesh_mask]        return combined_frame, combined_depth

Así es como se ve el mesh_color_buffer, mesh_depth_buffer y el combined_frame del primer objeto, un elefante. Dado que el objeto de elefante no está oculto por ningún otro elemento dentro del marco, permanece completamente visible. En diferentes ubicaciones, ocurriría la occlusión.

(Izquierda) Buffer de color calculado de la malla de elefante | (Derecha) Buffer de profundidad calculado de la malla de elefante | (Abajo) Marco combinado

En consecuencia, coloqué la segunda malla, el automóvil, en el lado de la acera de la carretera. También ajusté su orientación inicial de manera que parezca que ha sido estacionado allí. Las siguientes imágenes muestran el mesh_color_buffer, mesh_depth_buffer y el combined_frame correspondientes a esta malla.

(Izquierda) Buffer de color calculado de la malla de automóvil | (Derecha) Buffer de profundidad calculado de la malla de automóvil | (Abajo) Marco combinado

La visualización de la nube de puntos con ambos objetos insertados se muestra a continuación. Se introducen más espacios en blanco debido a las nuevas áreas de occlusión que surgieron con los nuevos objetos.

Nube de puntos generada del primer marco con objetos insertados

Después de calcular las imágenes superpuestas para cada uno de los cuadros de vídeo, ahora estamos listos para renderizar nuestro vídeo.

Paso 4: Renderizar vídeo a partir de los cuadros procesados

En la última sección de depth_aware_object_insertion.py, simplemente renderizamos un vídeo a partir de los cuadros con objetos insertados utilizando la función render_video_from_frames. También puedes ajustar los fps del vídeo de salida en este paso. El código se muestra a continuación:

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 # renderizando un vídeo de los cuadros superpuestosrender_video_from_frames(frame_directory, image_extension, save_directory, video_name, fps)

Aquí está mi video de demostración:

(Izquierda) Material de archivo proporcionado por Videvo, descargado de www.videvo.net | (Derecha) Material de archivo con dos objetos insertados

Se ha subido una versión de mayor resolución de esta animación a YouTube.

En general, la integridad de los objetos parece estar bien mantenida; por ejemplo, el objeto del coche está convincentemente oculto por el poste de la farola en la escena. Aunque hay una ligera vibración perceptible en la posición del coche a lo largo del video, probablemente debido a imperfecciones en la estimación de la posición de la cámara, el mecanismo de bloqueo del mundo generalmente funciona según lo esperado en el video de demostración.

Aunque el concepto de inserción de objetos en videos no es nuevo, con herramientas establecidas como After Effects que ofrecen métodos basados en el seguimiento de características, estos enfoques tradicionales a menudo pueden ser muy desafiantes y costosos para aquellos que no están familiarizados con las herramientas de edición de video. Aquí es donde entra en juego la promesa de algoritmos basados en Python. Aprovechando el aprendizaje automático y las estructuras de programación básicas, estos algoritmos tienen el potencial de democratizar tareas avanzadas de edición de video, haciéndolas accesibles incluso para personas con experiencia limitada en el campo. Por lo tanto, a medida que la tecnología continúa evolucionando, anticipo que los enfoques basados en software servirán como poderosos facilitadores, nivelando el campo de juego y abriendo nuevas oportunidades para la expresión creativa en la edición de video.

¡Que tengas un excelente día!

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Los hackers exploran formas de abusar de la IA en una importante prueba de seguridad

Casi 2,500 hackers en la aldea de IA de la conferencia DEFCON pasaron este fin de semana investigando y probando algu...

Inteligencia Artificial

Mejorando la Sumarización de GPT-4 a través de una Cadena de Indicaciones de Densidad

Los Modelos de Lenguaje Grandes han ganado mucha atención en los últimos tiempos debido a sus excelentes capacidades....

Ciencia de Datos

Manteniendo la Calidad de Datos en Sistemas de Aprendizaje Automático

En el deslumbrante mundo del aprendizaje automático (ML), es bastante fácil quedar absorto en la emoción de idear alg...

Inteligencia Artificial

Aplicación de juegos bilingües tiene como objetivo combatir la demencia

Una aplicación multilingüe desarrollada por investigadores de la Universidad de Tecnología y Diseño de Singapur tiene...

Inteligencia Artificial

Comprendiendo el concepto de GPT-4V(ision) La nueva tendencia de la inteligencia artificial

OpenAI ha estado a la vanguardia de los últimos avances en IA, con modelos altamente competentes como GPT y DALLE. Cu...

Inteligencia Artificial

Oracle Cloud Infrastructure ofrece nuevas instancias de cómputo aceleradas por GPU NVIDIA

Con la inteligencia artificial generativa y los grandes modelos de lenguaje (LLMs) impulsando innovaciones revolucion...