Procesamiento de video

Objetivo: Aprender a abrir, procesar y almacenar videos con python. Hacer un seguimiento de un objeto filtrándolo por color e incrustar una imagen en su centro de masa.

Conocimientos previos: Conceptos básicos de imágenes digitales color y escala de grises. Programación en Python con la librería Numpy. Procesamiento básico de imágenes. Conversión y filtrado con modelo de color HSV. Incrustación de una imagen dentro de otra.

Importante: Para que este cuaderno funcione correctamente, la imagen fuego.jpg y el video gesto.mp4 deben estar en el mismo directorio que este cuaderno.

En este tutorial veremos cómo abrir un video y procesar sus diferentes frames. Haremos un seguimiento del objeto de color, localizándolo como vimos en el tutorial anterior. Una vez reconocido el objeto (la mano con el guante), incrustaremos otra imagen dentro del video, decorando la figura original.

En primer lugar, entonces, necesitamos abrir e interpretar el video que queremos procesar. Un video puede entenderse simplemente como una secuencia de imágenes RGB (o frames). En python existen distintas librerías para abrir un video. Generalmente tenemos dos enfoques: podemos leer el video completamente y cargarlo en memoria RAM (esto puede saturar la memoria si el video es muy grande); o podemos leer frame a frame procesando cada imagen individualmente hasta terminar el video.

Abrir un video

Para abrir un video, usaremos la librería imageio que trae nativamente Anaconda. Probablemente sea necesario instalar en el sistema ffmpeg para que las librerías de video funcionen:

conda install -c menpo ffmpeg

Para abrir el video necesitamos crear un objeto Reader que nos permitirá leer frame a frame. Para este tutorial, convertiremos el objeto reader en un array de Numpy.

In [ ]:
import numpy as np  # procesamiento matricial

import matplotlib.pyplot as plt  # para mostrar imagenes
plt.rcParams['image.cmap'] = 'gray'
%matplotlib inline

# para leer/guardar videos
import imageio

# Crear un objeto lector de videos
file_name= "gesto.mp4"
print("Abriendo video...")
vid_reader = imageio.get_reader(file_name)

# ver los metadatos del video
mdata = vid_reader.get_meta_data()
print(mdata)

Lectura del video y conversión a arreglo de Numpy

Ahora vamos a leer frame a frame del video y convertirlo a un arreglo Numpy de 4 dimensiones (cantFrames, filas, columnas, canales).

In [ ]:
# convertir el video a un arreglo numpy
print("Convirtiendo video a Numpy array...")

# dimensiones
cant_frames= vid_reader.get_length()
dimensiones = (cant_frames, mdata['source_size'][1], mdata['source_size'][0], 3)

# se crea un arreglo numpy de 4 dimensiones (nframes, filas, columnas, 3)
video_np= np.zeros(dimensiones)

# lista con los frames del video. Cada frame es una imagen
lista_video= list(vid_reader)

# iterar por todas las imagenes del video
for i in range(cant_frames):
    video_np[i,:,:,:]=lista_video[i]

# convertir a rango 0-1
video_np = video_np/255

# cerrar lector de video
vid_reader.close()

# mostrar una imagen para corroborar el video
plt.imshow(video_np[30,:,:,:])

print("...Listo")

Visualización del video

Visualizamos algunos frames del video para verificar que se cargó correctamente.

In [ ]:
# Definimos una función para visualizar un video en una figura
def ver_video(video, cant_frames):
    # para que Jupyter haga el plot en una nueva figura
    %matplotlib qt5 
    plt.figure(figsize=(12,9)) # cuidado: dimensiones en PULGADAS
    for i in range(cant_frames):
        plt.imshow(video[i])
        plt.show()
        plt.pause(0.005)
    # retorna el control de las figuras "inline"    
    %matplotlib inline   

# llamamos a la función con el arreglo Numpy como parámetro para ver solo 25 frames
ver_video(video_np, 25)

Procesamiento del video de forma matricial

Ahora que tenemos el video cargado como un arreglo Numpy, sólo debemos procesarlo avanzando frame por frame. Entonces, lo que haremos será recorrer cada frame, filtrando las imágenes como lo hicimos en el tutorial anterior. Para mayor modularización y legibilidad del código, usaremos funciones de aquí en adelante.

En primer lugar, definamos las funciones para filtrar por color, calcular centro de masa, e incrustar un objeto.

Filtrado de color

Comenzamos una pequeña variante del filtrado por color que ya vimos: ahora filtrando de forma matricial.

In [ ]:
import skimage.morphology  # para erosionar
import matplotlib.colors   # para convertir a HSV

# esta función recibe una imagen RGB, 
#segmenta el objeto con rango de colores limites_HSV y
# retorna una imagen binaria con el resultado
def filtrar_imagen(img_rgb, limites_HSV):
    
    h_min,h_max= limites_HSV[0]
    s_min,s_max= limites_HSV[1]
    v_min,v_max= limites_HSV[2]
    
    # inits
    h,w,c= img_rgb.shape
    segmentation_mask= np.zeros((h,w))
    
    # transformar espacio de color
    img_hsv = matplotlib.colors.rgb_to_hsv(img_rgb)

    # segmentar cada canal (se realiza de forma matricial)
    segmentation_mask_h= np.logical_and(img_hsv[:,:,0]> h_min,  img_hsv[:,:,0]< h_max)
    segmentation_mask_s= np.logical_and(img_hsv[:,:,1]> s_min,  img_hsv[:,:,1]< s_max)
    segmentation_mask_v= np.logical_and(img_hsv[:,:,2]> v_min,  img_hsv[:,:,2]< v_max)
    
    # unir las 3 máscaras
    segmentation_mask= np.logical_and(segmentation_mask_h, segmentation_mask_s, segmentation_mask_v)
    
    # erosionar con skimage
    segmentation_mask= skimage.morphology.binary_erosion(segmentation_mask)
    
    return segmentation_mask

Cálculo de centro de masa

También lo hacemos de forma matricial.

In [ ]:
# calcular centro de masa de la imagen pasada como argumento
# retorna un vector numpy de dos elementos
def calcular_centro_de_masa(mask_img):
    
    r,c=np.where(mask_img>0) #calculo las posiciones de los pixeles con valor 1
    # El elemento `i` de r tiene la fila del pixel i con valor 1. Lo mismo para c con la columna
    # r y c tienen tamaño = pixeles en blanco x 1
    
    coordinates=np.vstack((r,c))# creo una matriz de pixeles x 2, juntando el vector de filas con el de columnas
    #coordinates tiene tamaño = pixeles en blanco x 2
    
    masa_center_position=np.mean(coordinates,axis=1) #calculo la coordenada promedio entre las seleccionadas
    masa_center_position=masa_center_position.round().astype(int) # redondeo y convierto a entero el resultado
    # para que sea una coordenada

    return masa_center_position

Incrustación de una imagen dentro de otra.

De nuevo, proveemos una versión matricial del mismo algoritmo.

In [ ]:
                    
def dibujar_objeto_en_imagen(img, objeto, posicion):
    h_obj, w_obj, c= objeto.shape
    h_img, w_img, c= img.shape
    dim_obj=np.array([h_obj, w_obj]) #dimension 
    # comienzo tiene las coordenadas de la esquina superior izquierda
    comienzo= np.array(posicion) - dim_obj//2
    # fin tiene las coordenadas de la esquina inferior derecha
    fin=comienzo+dim_obj
    
    # Ajustamos comienzo y fin por si están fuera de la imagen
    # al mismo tiempo achicamos la imagen del objeto si es necesario
    # para remover las partes que no van dentro de la pantalla
    if (comienzo[0]<0):
        extra=-comienzo[0]
        objeto=objeto[extra:,:,:]
        comienzo[0]=0
        
    if (comienzo[1]<0):
        extra=-comienzo[1]
        objeto=objeto[:,extra:,:]
        comienzo[1]=0
        
    if (fin[0]>=h_img):
        extra=fin[0]-h_img
        objeto=objeto[:-extra,:,:]
        fin[0]=h_img
        
    if (fin[1]>=w_img):
        extra=fin[1]-w_img
        objeto=objeto[:,:-extra,:]
        fin[1]=w_img
    # una vez que tenemos estas coordenadas, creamos una vista a la submatriz
    # de la imagen original donde vamos a pegar el objeto
    vista_submatriz_objetivo=img[comienzo[0]:fin[0],comienzo[1]:fin[1],:]
    # calculamos la intensidad de cada pixel de la imagen del objeto
    intensidad_objeto=np.mean(objeto,axis=2)
    # tanto la vista, como el objeto, como la matriz de intensidad tienen las mismas dimensiones
    # espaciales (ancho y alto)
    
    #modificamos la vista, solo en las posiciones en donde la intensidad del objeto es mayor a 0.1,
    # es decir, donde el objeto no es negro o muy oscuro.
    vista_submatriz_objetivo[intensidad_objeto>0.1,:]=objeto[intensidad_objeto>0.1,:]
       
    

Función para procesar un frame

Al igual que antes, unimos las tres funciones anteriores para procesar un frame.

In [ ]:
       
#Recibe:
#frame: la imagen a procesar (por referencia)
#limites_HSV: los rangos de color para encontrar el guante
#img_objeto: la imagen del objeto que se va a superponer en la posición del guante
def procesar_frame(frame,limites_HSV, img_objeto):
    #IMPLEMENTAR - COMIENZO
    
    #1) Calcular máscara de segmentación 
    
    #2) calcular centro de masa
    
    #3) colocar objeto en la imagen. Recordar que los parámetros en Python pasan por referencia
    
    #IMPLEMENTAR - FIN

        

Procesamiento del video

Ahora que tenés todas las funciones necesarias definidas, procesá el video frame a frame.

In [ ]:
import skimage.io # para abrir imagenes

# procesar el video frame a frame segmentando el guante de color rosa
def procesar_video(video_np, limites_HSV, img_objeto):
   
    # copia del video original
    video_procesado= np.copy(video_np)
    
    # procesar todos los frames
    #IMPLEMENTAR - COMIENZO
    
    #IMPLEMENTAR - FIN
            
    return video_procesado


# segmentos HSV para guante rosa
LIMITES_H = (230/255,250/255)
LIMITES_S = (170/255,240/255) 
LIMITES_V = (40/255,170/255)
LIMITES_HSV = (LIMITES_H, LIMITES_S, LIMITES_V)

# objeto a incrustar
img_objeto= skimage.io.imread("fuego.jpg") /255

# procesamos el video
print("Procesando video:")
video_procesado= procesar_video(video_np, LIMITES_HSV, img_objeto)
print("...Listo!")
In [ ]:
# visualizar resultado
ver_video(video_procesado, 50)

Guardar video

Por último, podemos guardar el video generado para tenerlo como archivo mp4.

In [ ]:
def save_video(video_np, file_name):
    # abrir un writer video
    vid_writer = imageio.get_writer(file_name)
    # iterar sobre todos los frames
    for i in range(video_np.shape[0]):
        vid_writer.append_data(video_np[i]*255) # reconvertimos a escala 0-255
    # cerrar writer
    vid_writer.close()
    
# guardar video
    print("Guardando video...")
save_video(video_procesado, "nuevo_video.mp4")
print("...Listo!")

Otras cosas a probar

Hemos logrado detectar un objeto simple (guante con color uniforme) en un video, y reemplazarlo por otra imagen!

En base a esto, puedes ahora probar otras cosas:

  • Detectar el guante verde y superponerle otra imagen encima
  • Detectar el guante verde, calcular el punto medio entre ambos guantes, y ubicar ahí la imagen de fuego
  • Cuando las manos se tocan aparece el objeto, y cuando se vuelven a tocar desaparece
  • Utilizar otros videos (la carpeta videos tiene algunos que sugerimos)