5.3. Formatos de píxel

Un algoritmo que detecta bordes espera que cada píxel contenga un valor de brillo. Un algoritmo que rastrea un objeto coloreado espera que cada píxel lleve color. Un algoritmo que ejecuta cierre morfológico espera que cada píxel esté encendido o apagado. El formato de píxel que lleva una Image – uno de los que enumeran los sensores de visión del catálogo – es lo que hace que esas expectativas se puedan comprobar de antemano: el formato indica, por adelantado, qué forma tienen los píxeles y, por tanto, qué algoritmos pueden ejecutarse sobre ellos sin un paso de conversión.

Esta página trata de cómo se manifiesta esa restricción en la práctica. Qué formato es la elección correcta depende de lo que vaya a hacer la canalización, y los métodos de conversión entre formatos son la manera en que una canalización que necesita más de uno de ellos encadena las etapas.

Una pila vertical de cinco tiras etiquetadas de disposición de bytes. BINARY muestra un byte dividido en ocho celdas de un solo bit, marcado como "8 píxeles por byte". GRAYSCALE muestra tres celdas etiquetadas de un solo byte, cada una marcada como "1 píxel". RGB565 muestra dos bytes adyacentes con los campos de bits RRRRR GGGGGG BBBBB etiquetados como "1 píxel". YUV422 muestra cuatro celdas de byte etiquetadas Y0, U, Y1, V marcadas como "2 píxeles". BAYER muestra dos filas de cuatro celdas de byte etiquetadas: R G R G en la fila superior, G B G B en la fila inferior.

Los cinco formatos de píxel sin comprimir y cómo se empaquetan sus bytes. JPEG y PNG no se dibujan aquí porque son flujos comprimidos de longitud variable en lugar de rejillas de píxeles de tamaño fijo.

5.3.1. El caballo de batalla de la escala de grises

La mayor parte de la visión artificial clásica se reduce a trabajar con valores de brillo. La detección de bordes, la coincidencia de plantillas, la decodificación de AprilTag, la estimación de flujo óptico, los operadores morfológicos, el análisis de manchas (blob) – todos ellos, al nivel en que operan los algoritmos, observan cuán brillante es cada píxel y cómo se compara ese brillo con el de los píxeles cercanos. El color de la escena suele ser útil para la aplicación que los invoca, pero los algoritmos en sí no lo necesitan.

El formato de escala de grises entrega a los algoritmos exactamente eso, sin sobrecarga. Un byte por píxel contiene un valor de brillo desde 0 (negro) hasta 255 (blanco). El formato tiene la mitad del tamaño de RGB565 y YUV422 y un tercio del tamaño de RGB888, así que cada operación recorre menos datos – más rápido y con menos presión sobre el búfer. En las cámaras más pequeñas, donde el búfer de fotogramas (frame buffer) compite con el resto del script por la RAM, esa diferencia de huella puede ser lo que decida si una canalización cabe siquiera. Si el color no es la señal que el algoritmo necesita, la escala de grises es la respuesta correcta.

5.3.2. Color mediante RGB565

Cuando el color es la señal – rastrear un marcador coloreado, distinguir manzanas rojas de verdes, identificar un elemento de la interfaz por su tono – dos bytes por píxel proporcionan suficiente color para los tipos de clasificación que realizan los algoritmos. RGB565 es el formato de color predeterminado en la cámara y el que esperan los métodos sensibles al color de la superficie.

Renderizar un fotograma anotado – dibujar cuadros de detección, escribir texto de diagnóstico, llevar el fotograma a una pantalla o enviarlo a un visor remoto – también requiere de forma natural RGB565. La vista previa del IDE, los controladores de pantalla integrados y la mayoría de los destinos de red consumen el formato directamente o convierten desde él de forma económica.

5.3.3. Bayer como formato de almacenamiento

Una imagen Bayer es la salida en bruto del sensor, antes de que el ISP la haya convertido (debayer) en una representación de color terminada. Cada píxel es un byte que contiene un único canal de color – el que dejó pasar el filtro de color en esa posición del mosaico. Eso hace que una imagen Bayer tenga el mismo tamaño que una imagen en escala de grises y un tercio del tamaño de RGB888, lo cual concuerda con aquello para lo que Bayer es realmente útil: almacenar muchos fotogramas a la vez cuando la RAM es la restricción limitante.

El inconveniente es que los algoritmos del módulo image no operan directamente sobre imágenes Bayer. Sin debayering, ningún píxel lleva suficiente información para emitir un juicio de color por sí solo, y los patrones que buscan los algoritmos – bordes, esquinas, manchas (blob) – se verían distorsionados por el mosaico. Las únicas formas de leer o modificar una imagen Bayer son get_pixel() y set_pixel(); todo lo demás espera una representación terminada.

El patrón que se deriva es almacenar los fotogramas como Bayer durante todo el tiempo que necesiten permanecer en una cola y convertir cada uno a escala de grises o a RGB565 en el momento en que realmente comienza su procesamiento. La conversión cuesta ciclos de CPU pero ahorra la RAM que, de otro modo, quedaría ocupada conteniendo fotogramas terminados durante toda la vida de la aplicación.

Nota

Las únicas operaciones del módulo image directamente sobre píxeles Bayer son get_pixel(), set_pixel() y la ruta de codificación JPEG que alimenta la vista previa del IDE o un visor remoto. El dibujo, el análisis y el filtrado requieren todos convertir antes a escala de grises, RGB565 o binario.

5.3.4. YUV422 para canalizaciones que quieren ambos

YUV422 separa la información de cada píxel en un canal de luminancia (Y) y dos canales de crominancia (U y V), y submuestrea la crominancia de modo que los pares de píxeles adyacentes comparten una sola U y una sola V. Los bytes por píxel promedian dos – igual que RGB565 – pero están dispuestos de manera que el canal Y ya es una imagen en escala de grises continua de 8 bits situada en desplazamientos conocidos dentro del búfer.

Esa disposición es exactamente lo que quiere una canalización cuando algunas de sus etapas son trabajo en escala de grises y otras necesitan color. Leer los valores Y directamente para las etapas en escala de grises evita el coste de una conversión explícita; los canales U y V están ahí cuando una etapa posterior realmente necesita color. Fuera de ese patrón específico, RGB565 suele ser la elección más sencilla para el color y la escala de grises la más sencilla para el trabajo basado solo en brillo – el valor de YUV422 proviene de ser bueno en ambos al mismo tiempo.

Nota

El módulo image opera sobre YUV422 de una manera más limitada que sobre la escala de grises, RGB565 o binario – lecturas directas del canal Y para el trabajo en escala de grises y la ruta de codificación JPEG que alimenta la vista previa del IDE o un visor remoto. Los métodos sensibles al color esperan RGB565; los fotogramas YUV422 necesitan una conversión explícita antes del análisis de color o del dibujo.

5.3.5. Binario, máscaras y salida con umbral

Una imagen binaria es un bit por píxel: cada píxel es 0 o 1. El formato rara vez aparece como captura del sensor; en cambio, aparece como la salida natural del umbralizado (donde una prueba de color o de brillo clasifica cada píxel en «sí, coincide» o «no, no coincide») y como la entrada natural a las operaciones morfológicas y al argumento mask que aceptan muchos métodos.

La ventaja práctica del formato es su tamaño. Una imagen binaria ocupa una octava parte de la huella de una imagen en escala de grises, así que llevar una máscara grande de un lado a otro – una elección por píxel de qué posiciones debe tocar alguna operación posterior – es económico. El hecho de que muchas operaciones acepten una imagen binaria como argumento de palabra clave mask= es la otra cara de la misma cuestión: el formato es pequeño, y encadenar la salida binaria de una etapa con la entrada de máscara de otra es un patrón de canalización habitual.

5.3.6. JPEG y PNG en la frontera

Los objetos Image JPEG y PNG son distintos de los demás del catálogo. No son rejillas de píxeles; son flujos de bytes comprimidos que codifican datos de píxel en una forma que las operaciones a nivel de píxel no pueden leer. Llamar a get_pixel() sobre un JPEG no devuelve el píxel en una posición; el píxel no está sin comprimir en ningún lugar del búfer para que el método lo recupere.

JPEG y PNG aparecen en la frontera del procesamiento de imágenes, donde los datos de píxel salen o entran en la cámara en forma comprimida. Guardar un fotograma en disco como JPEG mantiene el archivo pequeño; enviar un fotograma por una red como JPEG mantiene la transmisión económica; cargar un fotograma de referencia desde un archivo JPEG permite que resida en disco en una forma mucho más pequeña que la que ocuparían los píxeles en bruto. Para cualquiera de esos casos de uso la representación comprimida es la respuesta correcta. Sin embargo, para hacer cualquier procesamiento real sobre un JPEG, la aplicación lo convierte primero a un formato manejable – y esa conversión es donde los bytes comprimidos se expanden en píxeles y donde ocurre realmente la inflación del búfer (un JPEG de 30 KB puede convertirse en 600 KB de RGB565).

5.3.7. Conversión entre formatos

La ruta de conversión es lo que cose distintos formatos en una sola canalización. Cinco métodos de la clase Image toman una imagen existente y devuelven una nueva en un formato diferente:

  • to_grayscale() produce una imagen de un byte por píxel, el formato que quieren los algoritmos clásicos.

  • to_rgb565() produce el formato de color de dos bytes por píxel que hablan tanto los métodos sensibles al color como la vista previa del IDE.

  • to_bitmap() produce una imagen binaria de un bit, el formato que aceptan la morfología y los argumentos mask.

  • to_jpeg() produce una imagen comprimida en JPEG adecuada para guardar o transmitir.

  • to_png() produce una imagen comprimida en PNG cuando se prefiere la codificación sin pérdidas frente a los archivos más pequeños de JPEG.

Cada conversión se ejecuta en el lugar (in place) de forma predeterminada: el búfer de la imagen de origen se sobrescribe con el resultado convertido, y los píxeles originales del origen desaparecen una vez que la llamada retorna. Esa es la opción más económica tanto para la CPU como para la memoria, y es la respuesta correcta cuando el fotograma de origen no se va a necesitar para nada más.

Cuando el origen se sigue necesitando – cuando una etapa posterior de la canalización tiene que ver el fotograma original – dos argumentos de palabra clave anulan el comportamiento predeterminado en el lugar. copy=True asigna un búfer separado para la imagen convertida en el montículo (heap) de Python y deja intacto el origen. copy_to_fb=True hace la misma asignación pero la coloca en el búfer de fotogramas (frame buffer) en lugar del montículo – que es a lo que recurre una aplicación cuando la imagen convertida debe acabar en la vista previa del IDE, ya que el IDE lee desde el búfer de fotogramas.

Otros dos métodos producen imágenes RGB565 coloreadas mediante una paleta en lugar de por una conversión directa. to_rainbow() asigna cada valor de entrada de un solo canal a un color a lo largo de un gradiente suave que recorre el espectro visible. to_ironbow() asigna cada valor de entrada a la paleta no lineal de cámara térmica que va del negro a través de rojos y naranjas oscuros hasta el blanco. Ambos son herramientas de visualización más que de medición; la idea es hacer legible de un vistazo una imagen de un solo canal cuyos valores en bruto serían, de otro modo, invisibles al ojo.

5.3.8. Tamaño del búfer

Un último detalle sobre los formatos que vale la pena explicitar. size() siempre informa del tamaño del búfer de bytes, no del número de píxeles. Para los formatos sin comprimir esto se deriva directamente de las dimensiones y de los bytes por píxel: width * height * bytes_per_pixel. Para JPEG y PNG es el tamaño del flujo comprimido, que varía de fotograma a fotograma según lo que contenga la escena. El código que asigna búferes a partir de presupuestos de bytes usa size() para el primer caso; el código que transmite fotogramas comprimidos desde la cámara lo lee después de cada compresión para saber cuántos bytes contiene realmente el flujo.