En el anterior post de openCV vimos como podemos empezar a hacer tratamientos de imagen muy básicos. En esta entrada os voy a explicar como eliminar el fondo de los vídeos, partiendo de una imagen estática de referencia. Va a ser un post muy corto, pero que a mi me hubiese venido muy bien cuando lo necesitaba.
El problema
El escenario que tenemos es este: Tenemos una cámara de vídeo colocada en un sitio fijo y sabemos que el fondo no se va a mover. Además, también sabemos que lo queremos segmentar u obtener del vídeo es las “cosas extrañas” que aparezca, es decir, todo lo que no es fondo.
La solución más sencilla que se me ha ocurrido ha sido obtener una imagen de referencia de fondo y restarla de cada frame del vídeo para así poder segmentar más fácil las cosas nuevas que han aparecido.
A lo largo del post vamos a usar como referencia este pequeño video que he encontrado en internet y que nos viene al pelo. Podéis descargarlo desde: https://pixabay.com/videos/walking-people-city-bucharest-6099/

Video original
Obteniendo los datos de la camara
Lo primero que tenemos que hacer es ir preparando el script para simplemente leer la webcam, ajustar algún parámetro y capturar el último fotograma. Es importante, al usar este método, que desactivemos la autoexposición de nuestra cámara ya que no nos interesa que al aparecer más cosas en la imagen, se nos recalcule la exposición. Ya que si esto pasa, el fondo ya no sería exactamente igual y el resto del programa no nos funcionaría.
En la siguiente porción de código podéis ver como hacer la primera parte.
import cv2 # Adquirimos la webcam webcam = cv2.VideoCapture(0) webcam.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0) while True: check , frame = webcam.read() if not check: break # Mostramos el frame capturado cv2.imshow("Frame", frame) # Si pulsamos 'q' salimos key = cv2.waitKey(1) if key == ord('q'): webcam.release() cv2.destroyAllWindows() break
Además, para hacer el código más sencillo de seguir e implementar, vamos a separar el módulo que se encargará de realizar todo el procesamiento, en un paquete a parte.
El procesador de imagen
Vamos a definir la clase que se va a encargar de procesar la imagen que recibimos de la cámara. Esta nueva clase se tiene que encargar de:
- Recibir los datos de la cámara
- Guardar un fotograma como fotograma de referencia para calcular luego la diferencia. Este fotograma será el fondo cuando no hay nada en él.
- Procesar cada frame y devolver el frame modificado.
Así que vamos a ir creando la estructura de la clase y algunas funciones sencillas. Básicamente lo que hacemos de momento es, cuando se recibe la pulsación de la tecla ‘b’ guardamos el frame actual como frame de referencia. Nada más y nada menos. Así que también necesitamos modificar el main.py para empezar a usar nuestra nueva clase.
class Procesador: # Variable interna para guardar el fotograma del fondo background = None def __init__(self): self.background = None # Guardamos la variable de fondo def setBackground(self, image): self.background = image # Funcion para manejar las entradas de teclado def actuate(self, key, frame): if key == ord('b'): self.setBackground(frame)
import cv2 from procesador import * # Adquirimos la webcam webcam = cv2.VideoCapture(0) webcam.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0) # Creamos nuestro procesador proc = Procesador() while True: check , frame = webcam.read() if not check: break # Mostramos el frame capturado cv2.imshow("Frame", frame) # Si pulsamos 'q' salimos key = cv2.waitKey(1) if key == ord('q'): webcam.release() cv2.destroyAllWindows() break else: proc.actuate(key, frame)
Obteniendo la diferencia
Como queremos quitar el fondo de nuestra imagen, de forma que obtengamos una máscara que aplicar a nuestra imagen, que nos separe lo nuevo del fondo. Para esto vamos a necesitar dos funciones de openCV:
- absdiff: Hace el valor absoluto de la diferencia entre dos array. En nuestro caso vamos a hacer la diferencia entre la imagen de fondo y cada uno de los frames que vayamos procesando. (Doc oficial)
- threshold: Esta función, tiene varias formas de funcionar, nosotros vamos a usar la que lleva los valores a los extremos a partir de un nivel. Es decir, si el valor de un pixel es mayor de X lo ponemos a 255 (blanco) y sino a 0 (negro). Para ver el resto de funcionalidades que tiene, echa un vistazo aquí.
Por lo tanto nuestra función de python que hará esto será tan sencilla como: coger la imagen actual y el fondo y convertirlos en blanco y negro, y aplicarles un threshold. De esta manera tendremos una imagen en negro donde los valores sean iguales, o parecidos, entre la imagen actual y el fotograma de referencia. Y zonas blancas donde la diferencia supere un umbral.
class procesador: #... def procesar(self, image): #Esperamos a tener el background if np.shape(self.background) == (): return image #Pasamos las imágenes a blanco y negro image_blur = cv2.GaussianBlur(image, (51,51), cv2.BORDER_DEFAULT) image_bw = cv2.cvtColor(image_blur, cv2.COLOR_BGR2GRAY) backblur = cv2.GaussianBlur(self.background, (51,51), cv2.BORDER_DEFAULT) back_bw = cv2.cvtColor(backblur, cv2.COLOR_BGR2GRAY) # Hacemos la diferencia y le aplicamos el threshold diff = cv2.absdiff(image_bw ,back_bw ) ret, mascara = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) cv2.imshow("Mascara", mascara) #...
El problema ahora es que la diferencia no siempre va a ser tan clara como nos imaginamos. Puede ser que parte de lo nuevo se parezca al fondo, aunque sea en parte, y al hacer la resta nos dé que esa parte es fondo… Además, el ruido puede hacer que aparezcan pixeles sueltos que nos salgan como zonas “nuevas”.
Nuestro ejemplo queda tal que así, aquí no tenemos tanto ruido ni pixeles sueltos, aun así os recomiendo seguir los pasos anteriores si vuestro video tiene menor calidad o más ruido.

Máscara sin filtrar
Filtrando la máscara
Para ello tenemos que aplicar algunos filtros, el primero para eliminar el ruido, y el segundo para rellenar huecos. Para eliminar ruido, vamos a erosionar la mascara y posteriormente la dilataremos. Por el contrario para rellenar huecos vamos a dilatar la máscara y luego la erosionamos. Aunque para hacerlo todo un poco más fácil vamos a aplicar un filtro Gausiano a nuestra imagen para suavizarla, así los cambios menores se filtrarán más rápido.
class procesador: #... def procesar(self, image): #Esperamos a tener la imagen de fondo if np.shape(self.background) == (): return image #Pasamos las imágenes a blanco y negro image_blur = cv2.GaussianBlur(image, (51,51), cv2.BORDER_DEFAULT) image_bw = cv2.cvtColor(image_blur, cv2.COLOR_BGR2GRAY) backblur = cv2.GaussianBlur(self.background, (51,51), cv2.BORDER_DEFAULT) back_bw = cv2.cvtColor(backblur, cv2.COLOR_BGR2GRAY) # Hacemos la diferencia y le aplicamos el threshold diff = cv2.absdiff(image_bw ,back_bw ) ret, mascara = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) cv2.imshow("Mascara", mascara) # Eliminamos el posible ruido kernel_1 = np.ones((4,4), np.uint8) mascara = cv2.erode( mascara, kernel, iterations = 5) mascara = cv2.dilate(mascara, kernel, iterations = 2) kernel_2 = np.ones((10,10), np.uint8) mascara = cv2.dilate(mascara, kernel, iterations = 20) mascara = cv2.erose( mascara, kernel, iterations = 10) cv2.imshow("MascaraFiltrada", mascara) #...

Máscara filtrada

Video original
Rellenando huecos
En función de la aplicación que queramos hacer, puede ser que necesitemos rellenar huecos. En mi caso quiero saber las áreas donde hay algo diferente para dentro de ellas hacer una segmentación. Por lo tanto, me interesa que cualquier hueco que haya dentro de las áreas grandes, sea cerrado.
Lo que podemos hacer para eso es sacar el contorno de las áreas y luego dibujarlas. Para ello existe la función drawContours que nos permite, además de dibujar el contorno encontrado, rellenarlo del color que queramos.
def procesar(self, image): #Esperamos a tener la imagen de fondo if np.shape(self.background) == (): return image #Pasamos las imágenes a blanco y negro image_blur = cv2.GaussianBlur(image, (51,51), cv2.BORDER_DEFAULT) image_bw = cv2.cvtColor(image_blur, cv2.COLOR_BGR2GRAY) backblur = cv2.GaussianBlur(self.background, (51,51), cv2.BORDER_DEFAULT) back_bw = cv2.cvtColor(backblur, cv2.COLOR_BGR2GRAY) # Hacemos la diferencia y le aplicamos el threshold diff = cv2.absdiff(image_bw ,back_bw ) ret, mascara = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) cv2.imshow("Mascara", mascara) # Eliminamos el posible ruido kernel_1 = np.ones((4,4), np.uint8) mascara = cv2.erode( mascara, kernel_1, iterations = 5) mascara = cv2.dilate(mascara, kernel_1, iterations = 2) kernel_2 = np.ones((10,10), np.uint8) mascara = cv2.dilate(mascara, kernel_2, iterations = 20) mascara = cv2.erose( mascara, kernel_2, iterations = 10) cv2.imshow("MascaraFiltrada", mascara) # Buscamos los contornos exteriores cnts = cv2.findContours(mascara, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] # Rellenamos los contornos cv2.drawContours(mascara, cnts, -1, 255, -1) cv2.imshow("MascaraRellena", mascara) # Aplicamos la máscara y devolvemos img_masked = cv2.bitwise_and(image, image, mask=mascara) return img_masked
Lo único que falta es llamar a la nueva función desde el bucle principal y mostrar el resultado. Al final, el código completo de los dos ficheros quedaría tal que:
import cv2 from procesador import * # Adquirimos la webcam webcam = cv2.VideoCapture(0) webcam.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0) # Creamos nuestro procesador proc = Procesador() while True: check , frame = video.read() if not check: break # Mostramos el frame capturado cv2.imshow("Frame", frame) # Procesamos el frame res = proc.procesar(frame) #Mostramos el resultado cv2.imshow("Resultado", frame) # Si pulsamos 'q' salimos key = cv2.waitKey(1) if key == ord('q'): webcam.release() cv2.destroyAllWindows() break else: proc.actuate(key, frame)
class Procesador: # Variable interna para guardar el fotograma del fondo background = None def __init__(self): self.background = None # Guardamos la variable de fondo def setBackground(self, image): self.background = image # Funcion para manejar las entradas de teclado def actuate(self, key, frame): if key == ord('b'): self.setBackground(frame) def procesar(self, image): #Esperamos a tener la imagen de fondo if np.shape(self.background) == (): return image #Pasamos las imágenes a blanco y negro image_blur = cv2.GaussianBlur(image, (51,51), cv2.BORDER_DEFAULT) image_bw = cv2.cvtColor(image_blur, cv2.COLOR_BGR2GRAY) backblur = cv2.GaussianBlur(self.background, (51,51), cv2.BORDER_DEFAULT) back_bw = cv2.cvtColor(backblur, cv2.COLOR_BGR2GRAY) # Hacemos la diferencia y le aplicamos el threshold diff = cv2.absdiff(image_bw ,back_bw ) ret, mascara = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) cv2.imshow("Mascara", mascara) # Eliminamos el posible ruido kernel_1 = np.ones((4,4), np.uint8) mascara = cv2.erode( mascara, kernel_1, iterations = 5) mascara = cv2.dilate(mascara, kernel_1, iterations = 2) kernel_2 = np.ones((10,10), np.uint8) mascara = cv2.dilate(mascara, kernel_2, iterations = 20) mascara = cv2.erose( mascara, kernel_2, iterations = 10) cv2.imshow("MascaraFiltrada", mascara) # Buscamos los contornos exteriores cnts = cv2.findContours(mascara, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] # Rellenamos los contornos cv2.drawContours(mascara, cnts, -1, 255, -1) cv2.imshow("MascaraRellena", mascara) # Aplicamos la máscara y devolvemos img_masked = cv2.bitwise_and(image, image, mask=mascara) return img_masked
Como podéis ver, tenemos solo las áreas del vídeo que tienen algo de diferencia con el fondo. De esta manera, podemos aplicar luego máscaras solo dentro de la parte que nos interesa, evitando el posible ruido que nos generaría tener el fondo. Además, en la variable cnts del código tenemos todos los contornos separados, en el caso de que nos hiciera falta tratar cada uno de una manera diferente.
Y en el resultado podemos ver como tenemos a nuestro objeto “extraño” separado del resto de la imagen para trabajar con él:

Video original
Espero que os haya servido este pequeño tutorial y no dudéis en preguntar cualquier duda que tengáis.