5.33. Flujos ImageIO¶
save() y to_jpeg() cubren el caso de E/S de un solo fotograma: una aplicación captura un fotograma, lo codifica y lo envía a algún destino. Otra clase de aplicación necesita el caso de secuencia: grabar muchos fotogramas seguidos a la velocidad natural de captura, almacenarlos en algún lugar del que puedan recuperarse más tarde y reproducirlos a la velocidad correcta. Un script de recopilación de datos de entrenamiento captura unos cientos de fotogramas de ejemplo para una canalización de aprendizaje automático; un registro de una estación de inspección guarda cada pieza capturada para garantizar la trazabilidad; un script de desarrollo reproduce una secuencia almacenada para probar un nuevo algoritmo contra datos que se capturaron previamente en directo.
La clase ImageIO es la grabadora / reproductora del módulo image. Un único flujo contiene una secuencia de fotogramas Image – posiblemente de distintos tamaños y formatos de píxel – junto con el intervalo entre fotogramas de cada uno, de modo que la reproducción pueda recrear la velocidad de fotogramas original. Hay dos almacenes de respaldo disponibles: un archivo en el sistema de archivos o un búfer de tamaño fijo en la RAM.
5.33.1. Los dos almacenes de respaldo¶
Un flujo de archivo conserva la grabación a lo largo de los ciclos de encendido y su tamaño solo está limitado por el almacenamiento que lo respalda. Comienza con una cabecera mágica de 16 bytes OMV IMG STR Vx.y seguida de un fragmento por fotograma; el escritor actual emite V2.0 y el lector todavía acepta archivos V1.0 y V1.1 por compatibilidad con versiones anteriores. La ruta del archivo es el argumento del constructor; el modo es el modo de apertura del archivo ('r' para leer un flujo existente, 'w' para truncar y escribir desde cero).
# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
img = csi0.snapshot()
stream.write(img)
stream.close()
Un flujo de memoria reside en un búfer de RAM asignado en la construcción. El constructor toma una tupla de 3 elementos (w, h, pixformat) en lugar de una ruta, y el argumento mode se convierte en el número de ranuras de fotogramas preasignadas. El búfer se dimensiona exactamente para esa cantidad de fotogramas con las dimensiones suministradas y no se le permite crecer una vez asignado – escribir más allá de la última ranura genera EOFError, y escribir un fotograma más grande que el búfer por ranura genera ValueError. Los flujos de memoria son la herramienta adecuada cuando la aplicación necesita entregar una grabación a una etapa posterior sin pasar por el sistema de archivos (por ejemplo, un búfer circular corto de fotogramas recientes para un patrón de activación y reproducción).
# Pre-allocate space for 32 QVGA RGB565 frames in RAM
stream = image.ImageIO((320, 240, image.RGB565), 32)
for _ in range(32):
stream.write(csi0.snapshot())
Para los formatos de píxel comprimidos (image.JPEG, image.PNG) el tamaño por ranura se estima en 2 bits por píxel; un fotograma codificado más grande que la estimación genera ValueError al escribir, por lo que una aplicación que espera almacenar JPEG de alta calidad tiene que sobreasignar el número de ranuras o bien codificar primero con una calidad menor.
type() devuelve image.ImageIO.FILE_STREAM o image.ImageIO.MEMORY_STREAM para que el código posterior pueda adaptarse al almacén de respaldo que se le proporcione.
5.33.2. Grabación¶
write() añade una Image capturada a un flujo de archivo (o la almacena en la ranura actual de un flujo de memoria) y avanza el desplazamiento en uno. La misma llamada registra el intervalo entre fotogramas transcurrido desde la última escritura, de modo que la mitad de reproducción pueda hacer una pausa durante el tiempo correcto entre fotogramas y se conserve la velocidad de fotogramas natural de la grabación.
Se permiten fotogramas heterogéneos dentro de un único flujo de archivo: una grabación puede mezclar libremente capturas RGB565, recortes en escala de grises y miniaturas codificadas en JPEG, y el lector decodificará cada una en su tamaño y formato originales. Los flujos de memoria son homogéneos (todas las ranuras comparten el (w, h, pixformat) suministrado al constructor), por lo que una grabación en memoria está restringida a una única configuración de fotograma.
write() devuelve el objeto de flujo para que las llamadas se puedan encadenar. Escribir en un desplazamiento que no sea el final de un flujo de archivo trunca el resto del archivo – útil para editar una secuencia almacenada, arriesgado si la posición de la siguiente escritura se movió involuntariamente por un seek() anterior.
sync() vuelca las escrituras pendientes al disco para los flujos de archivo (no hace nada en los flujos de memoria) y debe llamarse periódicamente cuando la grabación es de larga duración, para evitar perder el final de la grabación si la cámara se reinicia antes de cerrar el archivo. El destructor cierra el flujo automáticamente cuando el ImageIO sale del ámbito, pero llamar explícitamente a close() es la disciplina correcta.
5.33.3. Reproducción¶
read() lee el fotograma en el desplazamiento actual, avanza el desplazamiento y devuelve la nueva Image. El receptor permanece en el búfer de fotogramas (frame buffer) cuando copy_to_fb=True (el valor predeterminado), de modo que la imagen devuelta se puede dibujar a través de la vista previa del IDE; con copy_to_fb=False el fotograma queda en el montón de MicroPython.
# Loop a recorded stream at its natural frame rate
stream = image.ImageIO("/sdcard/run.bin", "r")
while True:
img = stream.read()
# img is now in the frame buffer; the IDE shows it
# and the script can run any analysis it likes
Dos palabras clave controlan el comportamiento de la reproducción. loop=True (el valor predeterminado para los flujos de archivo) hace que el puntero de lectura vuelva al inicio cuando se alcanza el final de la grabación, de modo que la llamada nunca devuelve None; loop=False devuelve None una vez agotada la grabación y el bucle del llamante termina. pause=True (el valor predeterminado) bloquea la llamada hasta que ha transcurrido el intervalo entre fotogramas registrado al escribir, de modo que la velocidad de fotogramas de reproducción coincide con la velocidad de fotogramas de captura original; pause=False regresa de inmediato, lo que resulta útil para canalizaciones de análisis que quieren procesar la grabación lo más rápido posible sin respetar la temporización original.
El mismo patrón de bucle funciona para los flujos de memoria, salvo que loop se ignora – leer más allá del final de un flujo de memoria genera EOFError. El patrón esperado para un anillo de memoria es hacer un seek() de vuelta a cero explícitamente cuando se desea que vuelva a empezar.
5.33.5. Grabaciones reproducibles en un host¶
Los flujos ImageIO son la herramienta adecuada cuando la grabación se va a reproducir en la cámara – conservan cada fotograma capturado en su formato de píxel nativo, el intervalo entre fotogramas se registra con exactitud y un script posterior puede recorrerlos, hacer seek y reanalizar sin pérdida alguna. Sin embargo, no son la herramienta adecuada cuando la grabación tiene que poder reproducirse en un host – una estación de trabajo, un teléfono, un reproductor web. Un host espera un contenedor de vídeo estándar, no el formato de cabecera mágica en disco de OpenMV.
Dos módulos distintos cubren el caso de reproducción en un host. El módulo mjpeg graba Motion JPEG: una secuencia de fotogramas comprimidos en JPEG empaquetados en un único contenedor estilo AVI que VLC, QuickTime, ffmpeg y la etiqueta de vídeo web estándar reproducen directamente. El módulo gif graba un GIF animado: una secuencia de fotogramas sin comprimir (o comprimidos con paleta) con retardos explícitos por fotograma, reproducibles en cualquier navegador web o visor de imágenes que admita GIF animados.
El módulo mjpeg es la opción natural para grabaciones largas. La compresión JPEG mantiene el tamaño del archivo manejable – comparable a to_jpeg() con la calidad configurada, fotograma tras fotograma – de modo que una sesión de captura prolongada se mantiene dentro del presupuesto de la tarjeta SD. El uso refleja de cerca la grabación con ImageIO:
import mjpeg
m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
m.add_frame(csi0.snapshot(), quality=85)
m.close()
mjpeg.Mjpeg acepta las mismas palabras clave posicionales y de escala de estilo de dibujo que toman otros métodos de imagen, de modo que una grabación se puede escalar, recortar o mapear con paleta por fotograma a medida que entra. Los argumentos width y height del constructor toman por defecto las dimensiones del búfer de fotogramas (frame buffer) principal y fijan la resolución de salida; cada fotograma añadido se escala (conservando la relación de aspecto) para que encaje. sync() vuelca el archivo al disco durante una grabación larga, y close() finaliza el contenedor – un archivo Motion JPEG que no se ha cerrado limpiamente no es reproducible, así que la disciplina importa.
El módulo gif es la opción natural para grabaciones cortas compartidas tal cual con un espectador no técnico – unos segundos de acción capturados para una demostración, una ilustración animada para documentación, un clip de un evento incrustado en un mensaje de chat. Los fotogramas GIF se almacenan sin comprimir (o comprimidos con paleta a una profundidad de color de 7 bits), lo que hace que los archivos sean mucho más grandes por segundo que Motion JPEG y descarta el formato para grabaciones de más de unos pocos segundos, pero el resultado se puede incorporar directamente a cualquier navegador:
import gif
g = gif.Gif("/sdcard/clip.gif")
while running:
g.add_frame(csi0.snapshot(), delay=10)
g.close()
El argumento delay de add_frame() es el tiempo de visualización por fotograma en centisegundos (10 son 100 ms por fotograma, o 10 fps), que es el control estándar de reproducción de GIF. La palabra clave loop del constructor establece si el clip resultante se reproduce automáticamente en bucle en los visores (el valor predeterminado es True, que coincide con la expectativa convencional de un «GIF animado»).
Las tres vías de grabación cubren entre todas los casos comunes: ImageIO para reprocesamiento en la cámara, Motion JPEG para grabaciones largas reproducibles en un host, GIF animado para clips cortos reproducibles en un host. La elección entre ellas se reduce a quién reproduce la grabación. Una etapa posterior que se ejecuta en la propia cámara lee ImageIO; una estación de trabajo host o un visor web lee MJPEG o GIF.
5.33.6. Un patrón de activación y reproducción¶
Un patrón útil combina un flujo de memoria con una condición de activación. La cámara graba de forma continua en un búfer circular de memoria de count ranuras, sobrescribiendo cada vez la ranura más antigua. Cuando se dispara una condición de activación (una mancha (blob) entra en el fotograma, un evento de movimiento supera un umbral, se pulsa un botón) la aplicación toma una instantánea del contenido del anillo – los count fotogramas más recientes – y los escribe en un flujo de archivo en la tarjeta SD. El resultado es una grabación previa al disparo que captura los segundos anteriores al evento que la cámara realmente detectó, no solo los segundos posteriores, que es la limitación clásica de una grabadora ingenua de «capturar al activarse».
La implementación es sencilla una vez que se dominan las clases de flujo: un flujo de memoria de tamaño fijo sirve como anillo (con un seek() explícito a cero cuando el desplazamiento alcanza el número de ranuras), el bucle principal captura en él en cada iteración, y el manejador de activación lee el flujo de memoria fotograma a fotograma y escribe cada uno en un flujo de archivo cuyo nombre corresponde a la marca de tiempo del disparo.