En este artículo voy a explicar cómo podemos hacer la detección de movimiento con OpenCV y Python. Antes de comenzar debes tener claro que ventajas nos aporta la visión artificial y cómo podemos empezar a programar y desarrollar nuestras propias aplicaciones de visión artificial. El primer paso es preparar el sistema. Yo utilizo una plataforma donde tienes todo disponible en el mismo instalador, Anaconda. También hay que instalar la librería OpenCV para Python. Entiendo que a partir de ahora ya tienes todo dispuesto para empezar a programar así que comencemos.
Indice de contenidos
Detección de movimiento con OpenCV y Python
En muchas aplicaciones basadas en la visión artificial, se utiliza la detección de movimiento. Por ejemplo, cuando queremos contar las personas que pasan por un determinado lugar o cuántos coches han pasado por un peaje. En todos estos casos, lo primero que tenemos que hacer es extraer las personas o vehículos que hay en la escena.
Existen diferentes técnicas, métodos o algoritmos que posibilitan la detección de movimiento. Al igual que en otras materias, en la visión artificial no hay casos genéricos. Dependerá de cada situación usar uno u otro. En este artículo te voy a mostrar 4 métodos para hacer la detección de movimiento con OpenCV y Python.
Los métodos que te voy a explicar a continuación están basados en la substracción del fondo, una técnica muy común en la detección de movimiento.
El objeto de este artículo no es entrar en detalle en la base matemática y científica que hay detrás de esta técnica. En el mundo en el que vivimos, el tiempo es un bien preciado. Voy a intentar ser práctico y te voy a dar los fundamentos de la sustracción de fondo y como utilizar esta técnica para la detección de movimiento con OpenCV y Python.
Substracción de fondo
La substracción de fondo consiste en tomar una imagen de la escena sin movimiento y restar los sucesivos fotogramas que vamos obteniendo de un vídeo. A la imagen sin movimiento se le llama fondo o segundo plano (background en inglés). El fotograma que vamos a analizar sería el primer plano (foreground en inglés). Por lo tanto, tenemos un fondo al que vamos restando los diferentes fotogramas.
El resultado, como ves en la imagen, es una escena con fondo negro. Donde se detecta movimiento, el color es diferente. Es una técnica muy sencilla. No requiere que el sujeto u objeto que se está intentando detectar deba tener algo que lo identifique como un sensor, baliza o traje especial. Por el contrario, la substracción de fondo es muy sensible a los cambios de iluminación como las sombras o los cambios producidos por la luz natural. Otra desventaja es que si el sujeto u objeto tiene un color parecido al del fondo, o no se detecta el movimiento o se detecta mal.
Dentro de la técnica de la substracción de fondo, existen dos modalidades. Dependerá de cómo se obtiene el fondo o segundo plano, con imagen de referencia o con fotogramas anteriores.
Substracción con imagen de referencia
Esta modalidad consiste en tener una imagen de referencia donde no haya ningún objeto en movimiento. A partir de esta imagen se obtienen los elementos en movimiento restando cada fotograma con la imagen de referencia. Normalmente se coge el primer fotograma de una secuencia de vídeo.
Es muy sensible a los cambios de luz. Imagínate que tomas la imagen de referencia en una habitación con luz natural. A las 10:00 de la mañana habrá unas condiciones de luz. Pero a las 18:00 de la tarde habrá otras. También es muy sensible a los movimientos de la cámara. Un movimiento muy pequeño puede hacer que se detecten falsos positivos en la escena. Por el contrario, este método funciona muy bien en entornos con iluminación controlada y detecta perfectamente la silueta de los objetos en movimiento.
Substracción con fotogramas anteriores
En esta modalidad, el fondo o segundo plano se obtiene de los fotogramas anteriores. La técnica consiste en tomar una imagen de referencia, dejar pasar un tiempo aplicando un retardo y empezar a comparar con los fotogramas que vamos obteniendo. Este retardo dependerá de factores como la velocidad de los objetos.
Una de las mayores desventajas es que si el objeto o la persona en movimiento se quedan quietos, no se detecta. No es capaz de detectar siluetas. Sin embargo, es un método bastante robusto a los cambios de iluminación y a los movimientos de cámara. Consigue estabilizarse pasado un tiempo.
Fases del proceso de detección de movimiento
Ahora veremos cuales son las fases que debemos seguir para crear un algoritmo que nos permita la detección de movimiento con OpenCV. El proceso realizará varias tareas.
- Conversión a escala de grises y eliminación de ruido.
- Operación de substracción entre el segundo plano y el primer plano.
- Aplicar un umbral a la imagen resultado de la resta.
- Detección de contornos o blobs.
Algo muy común en las ciencias de la computación, en particular en la visión artificial, son los parámetros. Cada parámetro puede tener un rango de valores. El valor correcto dependerá de muchos factores. Está en nuestras manos adaptar cada valor a una situación concreta.
Existen diferentes técnicas que nos permiten estimar cual es el conjunto de valores que nos da mejor resultados. Una de ellas es Simulated Annealing. No te voy a hablar de esta técnica ya que es muy compleja y no es el objeto de este artículo, pero debes tenerlo en cuenta cuando tu objetivo es hacer una aplicación profesional. Es la única manera de ajustar los parámetros para obtener unos resultados aceptables.
Conversión a escala de grises y eliminación de ruido
Antes de realizar ninguna operación con las imágenes, es conveniente convertir a escala de grises. Resulta menos complejo y más óptimo trabajar con este tipo de imágenes. Por otro lado hay que minimizar el ruido provocado por la propia cámara y por la iluminación. Esto se hace a través de promediar cada píxel con sus vecinos. Se conoce comúnmente como suavizado.
OpenCV proporciona los métodos que nos permiten convertir a escala de grises y suavizar la imagen.
1 2 |
# Convertir la imagen a escala de grises gris = cv2.cvtColor(fotograma, cv2.COLOR_BGR2GRAY) |
1 2 |
# Suavizado de la imagen gris = cv2.GaussianBlur(gris, (21, 21), 0) |
Substracción entre el segundo plano y el primer plano
A la hora de restar una imagen con otra, debemos tener en cuenta que podemos obtener valores negativos. Para evitar esto lo que haremos será restar y obtener los valores absolutos de cada píxel. OpenCV nos proporciona un método para hacer esto.
1 2 |
# Resta absoluta resta = cv2.absdiff(fondo, gris) |
Aplicación de umbral
En esta parte del proceso lo que hacemos es quedarnos con aquellos píxeles que superen un umbral. El objetivo es binarizar la imagen es decir, tener dos posibles valores. Todos aquellos que superen el umbral serán píxeles blancos y los que no lo superen serán píxeles negros. Esto nos servirá para seleccionar el objeto en movimiento.
En OpenCV existe un método para aplicar un umbral.
1 2 |
# Aplicamos el umbral a la imagen umbral = cv2.threshold(resta, 25, 255, cv2.THRESH_BINARY) |
Detección de contornos o blobs
Una vez que tenemos la imagen con píxeles blancos o negros, tenemos que detectar los contornos o blobs. Un blob es un conjunto de píxeles que están conectados entre si es decir, tiene vecinos con el mismo valor. Cuando hablamos de vecinos es que estén al lado. En la siguiente imagen te muestro un ejemplo.
Se han detectado 3 blobs o contornos. Todos ellos tienen píxeles que están conectados entre si de alguna manera.
En OpenCV hay un método que busca los contornos de una imagen.
1 2 |
# Buscamos contorno en la imagen im, contornos, hierarchy = cv2.findContours(umbral,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) |
Por último y dentro de esta fase, tenemos que descartar los contornos más pequeños. Son aquellos que han pasado del suavizado y de la aplicación del umbral. Eso lo hacemos determinando un área mínima por el cual decidimos que son susceptibles de ser analizados.
Código
Ahora toca programar. Con todo lo que hemos visto podemos llegar a crear una aplicación para la detección de movimiento con OpenCV y Python. En este caso vamos a empezar con uno muy ligero, básico. Esto nos permitirá utilizarlo en aplicaciones donde los requerimientos son mínimos. Una posible aplicación sería con una Raspberry Pi que nos permita detectar la presencia de personas. Esto será otro artículo del blog.
Algoritmo básico de la detección de movimiento con OpenCV y Python
A continuación te dejo el código de este algoritmo basado en todo lo anteriormente explicado. Nos servirá para la detección de movimiento con OpenCV y Python básico.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# Importamos las librerías necesarias import numpy as np import cv2 import time # Cargamos el vídeo camara = cv2.VideoCapture("detector-movimiento-opencv.mp4") # Inicializamos el primer frame a vacío. # Nos servirá para obtener el fondo fondo = None # Recorremos todos los frames while True: # Obtenemos el frame (grabbed, frame) = camara.read() # Si hemos llegado al final del vídeo salimos if not grabbed: break # Convertimos a escala de grises gris = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # Aplicamos suavizado para eliminar ruido gris = cv2.GaussianBlur(gris, (21, 21), 0) # Si todavía no hemos obtenido el fondo, lo obtenemos # Será el primer frame que obtengamos if fondo is None: fondo = gris continue # Calculo de la diferencia entre el fondo y el frame actual resta = cv2.absdiff(fondo, gris) # Aplicamos un umbral umbral = cv2.threshold(resta, 25, 255, cv2.THRESH_BINARY)[1] # Dilatamos el umbral para tapar agujeros umbral = cv2.dilate(umbral, None, iterations=2) # Copiamos el umbral para detectar los contornos contornosimg = umbral.copy() # Buscamos contorno en la imagen im, contornos, hierarchy = cv2.findContours(contornosimg,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) # Recorremos todos los contornos encontrados for c in contornos: # Eliminamos los contornos más pequeños if cv2.contourArea(c) < 500: continue # Obtenemos el bounds del contorno, el rectángulo mayor que engloba al contorno (x, y, w, h) = cv2.boundingRect(c) # Dibujamos el rectángulo del bounds cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # Mostramos las imágenes de la cámara, el umbral y la resta cv2.imshow("Camara", frame) cv2.imshow("Umbral", umbral) cv2.imshow("Resta", resta) cv2.imshow("Contorno", contornosimg) # Capturamos una tecla para salir key = cv2.waitKey(1) & 0xFF # Tiempo de espera para que se vea bien time.sleep(0.015) # Si ha pulsado la letra s, salimos if key == ord("s"): break # Liberamos la cámara y cerramos todas las ventanas camara.release() cv2.destroyAllWindows() |
Puedes ver el resultado a continuación.
Métodos para la detección de movimiento con OpenCV y Python
Y como todo en la vida, existe un camino corto y en principio, «más sencillo». Este camino consiste en utilizar los métodos que nos proporciona OpenCV para la detección de movimiento. Están basados en trabajos realizados por investigadores y que se han incorporado al core de OpenCV. Dentro de la detección de movimiento con OpenCV hay varios algoritmos. Los tres que veremos aquí son:
- BackgroundSubtractorMOG.
- BackgroundSubtractorMOG2.
- BackgroundSubtractorKNN
BackgroundSubtractorMOG
Detectar la detección de movimiento con OpenCV y Python utilizando este método, puede ser sencillo con respecto a la programación. Lo realmente complejo es dar con los parámetros idóneos para tu proyecto. Aquí no queda otra que leer el paper o documento científico en el que se basó.
1 |
cv2.bgsegm.createBackgroundSubtractorMOG(history=200, nmixtures=5, backgroundRatio=0.7, noiseSigma=0) |
Donde:
- history: tamaño del histórico.
- nmistures: número de mezclas Gaussianas.
- backgroundRatio: relación de fondo.
- noiseSigma: potencia del ruido (desviación estándar de la luminosidad o de cada canal de color). 0 significa que obtiene un valor automático.
A continuación te dejo el código de ejemplo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# Importación de librerías import numpy as np import cv2 # Capturamos el vídeo cap = cv2.VideoCapture('detector-movimiento-opencv.mp4') # Llamada al método fgbg = cv2.bgsegm.createBackgroundSubtractorMOG(history=200, nmixtures=5, backgroundRatio=0.7, noiseSigma=0) # Deshabilitamos OpenCL, si no hacemos esto no funciona cv2.ocl.setUseOpenCL(False) while(1): # Leemos el siguiente frame ret, frame = cap.read() # Si hemos llegado al final del vídeo salimos if not ret: break # Aplicamos el algoritmo fgmask = fgbg.apply(frame) # Copiamos el umbral para detectar los contornos contornosimg = fgmask.copy() # Buscamos contorno en la imagen im, contornos, hierarchy = cv2.findContours(contornosimg,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) # Recorremos todos los contornos encontrados for c in contornos: # Eliminamos los contornos más pequeños if cv2.contourArea(c) < 500: continue # Obtenemos el bounds del contorno, el rectángulo mayor que engloba al contorno (x, y, w, h) = cv2.boundingRect(c) # Dibujamos el rectángulo del bounds cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # Mostramos las capturas cv2.imshow('Camara',frame) cv2.imshow('Umbral',fgmask) cv2.imshow('Contornos',contornosimg) # Sentencias para salir, pulsa 's' y sale k = cv2.waitKey(30) & 0xff if k == ord("s"): break # Liberamos la cámara y cerramos todas las ventanas cap.release() cv2.destroyAllWindows() |
Puedes ver el resultado a continuación.
BackgroundSubtractorMOG2
Es una variante del método anterior y está basado en la misma idea. Ya te he comentado que en la detección de movimiento con OpenCV es muy importante ajustar los parámetros. En el método que vamos a ver ahora no puede ser de otra manera. Para profundizar más en cómo funciona este método, puedes leer la documentación científica en la que se basó. Documento 1 y documento 2.
1 |
cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=50, detectShadows=False) |
Donde:
- history: tamaño del histórico.
- varThreshold: umbral de la distancia de Mahalanobis al cuadrado entre el píxel y el modelo para decidir si un píxel está bien descrito por el fondo.
- detectShadows: con un valor verdadero (True) detecta las sombras. Esto reduce la velocidad así que si no se utiliza ponerlo a falso.
A continuación te dejo el código de ejemplo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# Importación de librerías import numpy as np import cv2 # Capturamos el vídeo cap = cv2.VideoCapture('detector-movimiento-opencv.mp4') # Llamada al método fgbg = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=50, detectShadows=False) # Deshabilitamos OpenCL, si no hacemos esto no funciona cv2.ocl.setUseOpenCL(False) while(1): # Leemos el siguiente frame ret, frame = cap.read() # Si hemos llegado al final del vídeo salimos if not ret: break # Aplicamos el algoritmo fgmask = fgbg.apply(frame) # Copiamos el umbral para detectar los contornos contornosimg = fgmask.copy() # Buscamos contorno en la imagen im, contornos, hierarchy = cv2.findContours(contornosimg,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) # Recorremos todos los contornos encontrados for c in contornos: # Eliminamos los contornos más pequeños if cv2.contourArea(c) < 500: continue # Obtenemos el bounds del contorno, el rectángulo mayor que engloba al contorno (x, y, w, h) = cv2.boundingRect(c) # Dibujamos el rectángulo del bounds cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # Mostramos las capturas cv2.imshow('Camara',frame) cv2.imshow('Umbral',fgmask) cv2.imshow('Contornos',contornosimg) # Sentencias para salir, pulsa 's' y sale k = cv2.waitKey(30) & 0xff if k == ord("s"): break # Liberamos la cámara y cerramos todas las ventanas cap.release() cv2.destroyAllWindows() |
Puedes ver el resultado a continuación.
BackgroundSubtractorKNN
Este método se basa en el algoritmo K-nearest neigbours. Igual que los otros dos, tenemos que ser capaces de buscar los parámetros más adecuados para nuestro proyecto. Esto nos permitirá la detección de movimiento con OpenCV de una manera óptima.
1 |
cv2.createBackgroundSubtractorKNN(history=500, dist2Threshold=400, detectShadows=False) |
Donde:
- history: tamaño del histórico.
- dist2Threshold: umbral de la distancia al cuadrado entre el píxel y la muestra para decidir si un píxel está cerca de esa muestra.
- detectShadows: con un valor verdadero (True) detecta las sombras. Esto reduce la velocidad así que si no se utiliza ponerlo a falso.
A continuación te dejo el código de ejemplo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# Importación de librerías import numpy as np import cv2 # Capturamos el vídeo cap = cv2.VideoCapture('detector-movimiento-opencv.mp4') # Llamada al método fgbg = cv2.createBackgroundSubtractorKNN(history=500, dist2Threshold=400, detectShadows=False) # Deshabilitamos OpenCL, si no hacemos esto no funciona cv2.ocl.setUseOpenCL(False) while(1): # Leemos el siguiente frame ret, frame = cap.read() # Si hemos llegado al final del vídeo salimos if not ret: break # Aplicamos el algoritmo fgmask = fgbg.apply(frame) # Copiamos el umbral para detectar los contornos contornosimg = fgmask.copy() # Buscamos contorno en la imagen im, contornos, hierarchy = cv2.findContours(contornosimg,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) # Recorremos todos los contornos encontrados for c in contornos: # Eliminamos los contornos más pequeños if cv2.contourArea(c) < 500: continue # Obtenemos el bounds del contorno, el rectángulo mayor que engloba al contorno (x, y, w, h) = cv2.boundingRect(c) # Dibujamos el rectángulo del bounds cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # Mostramos las capturas cv2.imshow('Camara',frame) cv2.imshow('Umbral',fgmask) cv2.imshow('Contornos',contornosimg) # Sentencias para salir, pulsa 's' y sale k = cv2.waitKey(30) & 0xff if k == ord("s"): break # Liberamos la cámara y cerramos todas las ventanas cap.release() cv2.destroyAllWindows() |
Puedes ver el resultado a continuación.
Conclusión
La detección de movimiento con OpenCV es una tarea que se puede complicar todo lo que queramos. Dependerá del objetivo de nuestro proyecto. Mi consejo es que si estás interesado te suscribas al curso de Introducción a la Visión Artificial con OpenCV y Python. La única manera de hacer bien las cosas en entenderlas.