5.11. Diferenciación de fotogramas¶
La diferenciación de fotogramas compara cada nuevo fotograma con un fotograma de referencia almacenado para localizar las partes de la escena que han cambiado. Es la herramienta básica de las aplicaciones de cámara que vigilan que algo ocurra – captura activada por movimiento, alertas de intrusión, «guardar un vídeo cuando algo se mueve» – y se construye por completo a partir de las operaciones por píxel tratadas anteriormente: una diferencia absoluta, un umbral y una búsqueda de regiones, ejecutadas en cada fotograma.
5.11.1. El proceso básico¶
La primera etapa consiste en adquirir una referencia. En algún momento cercano al arranque – idealmente cuando la escena se encuentra en el estado que significa «sin cambios» – la aplicación captura un fotograma y lo conserva. Ese fotograma se convierte en la línea base con la que se comparará cada captura posterior.
reference = csi0.snapshot().copy()
El .copy() es importante. csi0.snapshot() por sí solo devuelve una Image cuyo búfer reside en el búfer de fotogramas (frame buffer), donde la siguiente llamada a snapshot lo sobrescribirá. .copy() reserva un búfer aparte para la referencia y permite que los píxeles de este fotograma sobrevivan más allá de la siguiente captura.
La segunda etapa se ejecuta en cada fotograma: capturar una imagen nueva y luego calcular la diferencia absoluta entre ella y la referencia. Eso es exactamente lo que hace difference():
current = csi0.snapshot()
current.difference(reference)
Tras esta llamada, current contiene una imagen cuyos píxeles distintos de cero marcan cada posición donde la escena cambió desde que se tomó la referencia, con la magnitud de cada píxel proporcional a cuánto cambió en esa posición.
La tercera etapa umbraliza la imagen de diferencia. La diferencia en bruto siempre contiene algo de ruido: pequeñas variaciones de brillo por el ruido de disparo del sensor, cambios de gradiente por deriva de iluminación, fluctuaciones subpíxel por leves movimientos de la cámara. Una pasada de umbral – binary() con un umbral fijado por encima de ese nivel de ruido – conserva únicamente los cambios lo bastante grandes como para considerarse movimiento real y descarta el resto, produciendo una imagen binaria cuyos píxeles distintos de cero son las posiciones que realmente cambiaron.
La cuarta etapa extrae las regiones conectadas de esa máscara binaria – grupos de píxeles adyacentes distintos de cero que forman parches contiguos. find_blobs() lo hace en una sola llamada, devolviendo una lista de regiones de movimiento, cada una con un cuadro delimitador y un recuento de píxeles, sobre la que el resto de la aplicación puede actuar.
El proceso de diferenciación de fotogramas: un fotograma de referencia más un fotograma actual se convierten en una imagen de diferencia; la umbralización transforma la diferencia en una máscara binaria de las posiciones que cambiaron; un paso de regiones conectadas convierte la máscara en una lista de regiones de movimiento.¶
5.11.2. Referencias en memoria y en disco¶
El proceso básico mantiene el fotograma de referencia en la RAM. Esa es la respuesta correcta cuando la referencia se captura en esta ejecución del script y solo tiene que sobrevivir mientras el script siga ejecutándose.
Para una aplicación de larga duración – una cámara que debería reanudar la detección de cambios tras un ciclo de apagado, un script intermitente que necesita detectar cualquier cambio desde algún momento anterior – el fotograma de referencia tiene que sobrevivir al script en ejecución. El patrón consiste en guardar la referencia en disco:
csi0.snapshot().save("/sdcard/reference.bmp")
y volver a cargarla al inicio de cada ejecución:
reference = image.Image("/sdcard/reference.bmp")
La lógica de diferenciación no cambia; solo cambia dónde reside la referencia entre capturas. Algunas mejoras extienden de forma natural esta variante en disco – recaptura automática de la referencia mediante un temporizador, promedios móviles opcionales para seguir la deriva lenta de iluminación – pero la sustitución en el centro es la misma.
5.11.3. Aislamiento de fuentes de luz¶
El mismo patrón de resta aparece en un escenario ligeramente distinto: aislar una fuente de luz del resto de la escena. El truco consiste en capturar una referencia con las «luces apagadas» – un fotograma tomado cuando aquello que se va a detectar (una baliza IR, un píxel de pantalla, un indicador de estado) no está iluminado – y restar esa referencia de cada fotograma posterior. El resultado tiene brillo cero en todos los lugares donde la escena fue igual en ambas capturas, y brillo distinto de cero solo donde la fuente de luz realmente se encendió.
5.11.4. Elegir difference o sub¶
Una nota práctica sobre qué operación aritmética elegir. difference() devuelve el valor absoluto del cambio – sin signo – lo que la hace sensible a cambios en cualquier dirección (aclaramiento u oscurecimiento) a costa de no indicarle a la aplicación en qué dirección se produjo el cambio. Para la pura detección de movimiento esa es la respuesta correcta: cualquier cosa que se haya movido es interesante, independientemente de hacia dónde se haya desplazado el brillo.
Para la detección de fuentes de luz, el píxel iluminado siempre es más brillante que la referencia con las luces apagadas, por lo que sub() (con su recorte en cero) es la opción más fiel. En cualquier lugar donde el fotograma actual sea más oscuro que la referencia (lo que correspondería a ruido del sensor alrededor del valor sin iluminar) se recorta a cero en lugar de informar de una señal espuria de «la luz estaba encendida».