5.4. Lectura y escritura de píxeles¶
La mayoría de las operaciones sobre una imagen ocultan su trabajo por píxel dentro de una única llamada a un método, donde los bucles que recorren cada píxel se ejecutan a velocidad nativa. Sin embargo, hay casos en los que el código de la aplicación necesita acceder directamente a un píxel específico: para leer lo que hay en una posición concreta, para escribir un nuevo valor en uno, para muestrear un único punto en un paso de calibración o para depurar un valor en una ubicación conocida. El módulo image expone ese nivel de acceso a través de dos formas de direccionamiento, cada una de las cuales se adapta a una manera distinta de pensar sobre dónde reside un píxel.
5.4.1. Direccionamiento por coordenada¶
La forma más natural es la que el vocabulario de Coordinates ya desarrolló: nombrar un píxel por su coordenada cartesiana (x, y). get_pixel() toma (x, y) y devuelve el valor en esa posición; set_pixel() toma esa misma (x, y) junto con un valor y lo escribe.
Lo que esas llamadas devuelven o aceptan depende del formato de la imagen. Las imágenes en escala de grises, binarias y Bayer llevan un único valor por píxel – un brillo para la escala de grises, un 0 o un 1 para las binarias, una única muestra de canal de color para Bayer – por lo que get_pixel() devuelve un único entero. RGB565 lleva tres canales de color empaquetados en 16 bits, y get_pixel los desempaqueta de forma predeterminada en una tupla (r, g, b), con cada canal mapeado al rango 0 – 255.
El comportamiento predeterminado puede invertirse en cualquiera de los dos extremos. Pasar rgbtuple=False a get_pixel en una imagen RGB565 recurre a la palabra empaquetada de 16 bits sin procesar – la misma forma que devuelve el índice lineal, y la forma eficiente cuando la aplicación va a volver a escribir el mismo valor empaquetado directamente. Pasar rgbtuple=True en una imagen de un solo canal hace lo contrario: el valor almacenado se convierte en una tupla RGB888 antes de devolverlo, y las imágenes Bayer pasan por un paso de debayer sobre la marcha. El argumento existe para que el código que llama pueda solicitar los píxeles en un espacio de color uniforme, independientemente de cómo los almacene internamente la imagen subyacente.
Las imágenes comprimidas – JPEG y PNG – no son compatibles con get_pixel ni set_pixel. Sus bytes no representan píxeles en posiciones conocidas, y los métodos generan un error en lugar de devolver un valor que no significaría nada.
En la práctica, los patrones tienen este aspecto:
v = img.get_pixel(40, 30) # grayscale: int 0..255
img.set_pixel(40, 30, 255) # write white
r, g, b = img.get_pixel(40, 30) # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0)) # write red
Si la (x, y) solicitada está fuera de la imagen, get_pixel devuelve None y set_pixel no hace nada. Esto es indulgente por diseño: muchos algoritmos recorren las zonas cercanas a los bordes de una imagen e indexan brevemente posiciones fuera de rango, y una operación nula silenciosa es menos disruptiva que una excepción cada vez que esto ocurre.
5.4.2. Direccionamiento por índice lineal¶
La otra forma consiste en direccionar los píxeles por su posición en el búfer subyacente. Recuerda la disposición del búfer: los píxeles se almacenan fila por fila, primero todos los píxeles de la fila superior, luego todos los de la siguiente, y así sucesivamente hasta la inferior. Esa disposición significa que cada píxel tiene un único índice entero que empieza en 0 en la esquina superior izquierda y va aumentando a lo largo de cada fila por turnos. El píxel en la coordenada (x, y) tiene el índice lineal y * width + x.
Los píxeles se direccionan tanto por la coordenada cartesiana (x, y) como por un índice lineal que recorre el búfer fila por fila, de izquierda a derecha.¶
El módulo image expone ese índice mediante la notación de subíndice habitual de Python: img[i] lee el píxel en el índice lineal i, img[i] = value escribe uno. Lo que devuelve la forma de índice es el valor almacenado sin procesar para el formato, no la tupla desempaquetada que get_pixel() devuelve de forma predeterminada. Esa distinción importa porque el formato elegido anteriormente decide qué aspecto tiene el valor sin procesar:
Los píxeles en escala de grises y Bayer se devuelven como enteros de 8 bits.
Los píxeles RGB565 y YUV422 se devuelven como enteros de 16 bits – la palabra empaquetada.
Los píxeles binarios se devuelven como
0o1.Los píxeles JPEG y PNG se devuelven como enteros de 8 bits, un byte cada vez del flujo comprimido. Esos valores son opacos – son fragmentos de una codificación comprimida en lugar de píxeles en cualquier sentido habitual.
La forma de índice se adapta al código que ya está pensando en términos de desplazamientos del búfer: un bucle que recorre cada píxel una vez, un algoritmo que necesita saltar una fila a la vez, o un fragmento de código que traduce entre disposiciones de búfer. El código que está pensando en términos de coordenadas x e y se beneficia más de get_pixel y set_pixel; ambas formas direccionan los mismos píxeles a través de modelos mentales diferentes.
La Image también es iterable. for v in img: recorre el búfer en el mismo orden de fila principal, produciendo los valores sin procesar de un píxel a la vez, y len(img) es el recuento de píxeles para los formatos sin comprimir o el recuento de bytes para los flujos comprimidos.
5.4.3. Por qué el procesamiento por píxel en Python es la vía lenta¶
Una nota práctica que merece la pena reconocer con honestidad. Recorrer una imagen píxel a píxel desde Python es lento. Una imagen en escala de grises de 320 × 240 contiene 76.800 píxeles; llamar a get_pixel() en cada uno de ellos dentro de un bucle for ejecuta millones de instrucciones de bytecode de MicroPython para hacer un trabajo que un método nativo equivalente podría terminar en unos pocos cientos de microsegundos. No es un factor pequeño. Es la diferencia entre un script que procesa fotogramas en tiempo real y otro que avanza a duras penas, muy por debajo de la frecuencia de fotogramas de la cámara.
Casi todos los métodos de la superficie de Image existen porque hay una versión nativa más rápida de un patrón común por píxel. Un bucle que suma dos imágenes se convierte en una única llamada nativa. Un bucle que suaviza cada píxel promediándolo con sus vecinos se convierte en otra. Un bucle que clasifica cada píxel frente a un umbral se convierte en una tercera. El trabajo de la aplicación, la mayor parte del tiempo, consiste en reconocer qué método de imagen completa coincide con el trabajo que habría hecho el bucle, y recurrir a ese en lugar de escribir el bucle a mano.
La lectura y escritura a nivel de píxel siguen siendo la herramienta adecuada cuando nada más encaja – para volver a insertar una medición específica en el búfer, muestrear una posición para un paso de calibración o depurar un valor en una ubicación conocida. La cuestión es que son la vía lenta, que se utiliza cuando los métodos de imagen completa no tienen la forma que la aplicación necesita, no como la manera predeterminada de operar sobre los píxeles.