5.4. Leitura e escrita de pixels¶
A maioria das operações sobre uma imagem oculta o trabalho pixel a pixel dentro de uma única chamada de método, onde os ciclos que percorrem cada pixel são executados à velocidade nativa. No entanto, há casos em que o código de aplicação precisa de aceder diretamente a um pixel específico: ler o que existe numa determinada posição, escrever um novo valor, amostrar um único ponto num passo de calibração, ou depurar um valor numa localização conhecida. O módulo image expõe esse nível de acesso através de duas formas de endereçamento, cada uma adequada a uma forma diferente de pensar sobre onde está um pixel.
5.4.1. Endereçamento por coordenada¶
A forma mais natural é aquela para a qual as Coordenadas já desenvolveram o vocabulário: identificar um pixel pelas suas coordenadas cartesianas (x, y). get_pixel() recebe (x, y) e devolve o valor nessa posição; set_pixel() recebe os mesmos (x, y) juntamente com um valor e escreve-o.
O que essas chamadas devolvem ou aceitam depende do formato da imagem. Imagens em escala de cinzentos, binárias e Bayer armazenam um único valor por pixel – um brilho para escala de cinzentos, 0 ou 1 para binário, uma amostra de um único canal de cor para Bayer – pelo que get_pixel() devolve um inteiro simples. O formato RGB565 armazena três canais de cor compactados em 16 bits, e get_pixel descompacta-os para um tuplo (r, g, b) por omissão, com cada canal mapeado no intervalo 0 – 255.
O comportamento predefinido pode ser alterado em qualquer extremo. Passar rgbtuple=False a get_pixel numa imagem RGB565 devolve a palavra compactada de 16 bits em bruto – a mesma forma que o índice linear devolve, e a forma eficiente quando a aplicação vai escrever de volta esse mesmo valor compactado. Passar rgbtuple=True numa imagem de canal único faz o oposto: o valor armazenado é convertido num tuplo RGB888 antes de ser devolvido, com as imagens Bayer passando por um passo de debayer no momento. O argumento existe para que o código chamador possa pedir pixels num espaço de cor uniforme, independentemente de como a imagem subjacente os armazena.
As imagens comprimidas – JPEG e PNG – não são suportadas por get_pixel nem por set_pixel. Os seus bytes não representam pixels em posições conhecidas, e os métodos levantam um erro em vez de devolver um valor que não teria qualquer significado.
Na prática, os padrões são assim:
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
Se o (x, y) solicitado estiver fora da imagem, get_pixel devolve None e set_pixel não faz nada. Isto é permissivo por design: muitos algoritmos percorrem as bordas de uma imagem e acedem brevemente a posições fora do intervalo, e uma operação silenciosa sem efeito é menos perturbadora do que uma exceção cada vez que isso acontece.
5.4.2. Endereçamento por índice linear¶
A outra forma consiste em endereçar os pixels pela sua posição no buffer subjacente. Recorde o esquema do buffer: os pixels são armazenados linha a linha, primeiro todos os pixels da linha superior, depois os da linha seguinte, e assim sucessivamente até ao fundo. Essa organização significa que cada pixel tem um único índice inteiro contando a partir de 0 no canto superior esquerdo e incrementando ao longo de cada linha. O pixel na coordenada (x, y) tem o índice linear y * width + x.
Os pixels são endereçados tanto por coordenadas cartesianas (x, y) como por um índice linear que percorre o buffer linha a linha, da esquerda para a direita.¶
O módulo image expõe esse índice através da notação de subscrito Python normal: img[i] lê o pixel no índice linear i, img[i] = value escreve-o. O que a forma de índice devolve é o valor armazenado em bruto para o formato, não o tuplo descompactado que get_pixel() devolve por omissão. Essa distinção importa porque o formato escolhido anteriormente determina o aspeto do valor em bruto:
Os pixels Grayscale e Bayer são devolvidos como inteiros de 8 bits.
Os pixels RGB565 e YUV422 são devolvidos como inteiros de 16 bits – a palavra compactada.
Os pixels Binary são devolvidos como
0ou1.Os pixels JPEG e PNG são devolvidos como inteiros de 8 bits, um byte de cada vez do fluxo comprimido. Esses valores são opacos – são partes de uma codificação comprimida e não pixels em nenhum sentido convencional.
A forma de índice adapta-se bem ao código que já pensa em termos de deslocamentos no buffer: um ciclo que percorre cada pixel uma vez, um algoritmo que precisa de saltar uma linha de cada vez, ou um trecho de código que converte entre esquemas de buffer. O código que pensa em termos de coordenadas x e y é melhor servido por get_pixel e set_pixel; as duas formas endereçam os mesmos pixels através de modelos mentais diferentes.
A Image também é iterável. for v in img: percorre o buffer na mesma ordem linha a linha, produzindo os valores em bruto um pixel de cada vez, e len(img) corresponde ao número de pixels para formatos não comprimidos ou ao número de bytes para fluxos comprimidos.
5.4.3. Por que razão o Python pixel a pixel é o caminho lento¶
Uma nota prática que vale a pena ser honesto sobre. Percorrer uma imagem pixel a pixel a partir do Python é lento. Uma imagem em escala de cinzentos de 320 × 240 contém 76 800 pixels; chamar get_pixel() em cada um deles num ciclo for executa milhões de instruções bytecode do MicroPython para fazer um trabalho que um método nativo equivalente poderia terminar em algumas centenas de microssegundos. Não é um fator pequeno. É a diferença entre um script que processa fotogramas em tempo real e um que avança lentamente muito abaixo da taxa de fotogramas da câmara.
Quase todos os métodos na superfície da Image existem porque há uma versão nativa mais rápida de um padrão pixel a pixel comum. Um ciclo que soma duas imagens torna-se uma única chamada nativa. Um ciclo que suaviza cada pixel calculando a média com os vizinhos torna-se outra. Um ciclo que classifica cada pixel face a um limiar torna-se uma terceira. O trabalho da aplicação, na maior parte das vezes, é reconhecer qual o método de imagem completa que corresponde ao trabalho que o ciclo teria feito, e utilizá-lo em vez de escrever o ciclo manualmente.
A leitura e escrita ao nível do pixel continuam a ser a ferramenta certa quando nada mais se adequa – corrigir uma medição específica de volta no buffer, amostrar uma posição para um passo de calibração, depurar um valor numa localização conhecida. O ponto é que se trata do caminho lento, utilizado quando os métodos de imagem completa não têm a forma de que a aplicação necessita, e não como a forma predefinida de operar sobre pixels.