5.4. Lendo e escrevendo pixels¶
A maioria das operações em uma imagem oculta seu trabalho por pixel dentro de uma única chamada de método, onde os laços que tocam cada pixel acontecem em velocidade nativa. Há casos, porém, em que o código da aplicação quer tocar diretamente em um pixel específico: para ler o que está em uma posição particular, para escrever um novo valor em um deles, para amostrar um único ponto em uma etapa de calibração ou para depurar um valor em uma localização conhecida. O módulo image expõe esse nível de acesso por meio de duas formas de endereçamento, cada uma adequada a uma maneira diferente de pensar sobre onde um pixel reside.
5.4.1. Endereçamento por coordenada¶
A forma mais natural é aquela para a qual Coordenadas já desenvolveu o vocabulário: nomear um pixel por suas coordenadas cartesianas (x, y). get_pixel() recebe (x, y) e retorna o valor naquela posição; set_pixel() recebe o mesmo (x, y) junto com um valor e o escreve.
O que essas chamadas retornam ou aceitam depende do formato da imagem. Imagens em escala de cinza, binárias e Bayer carregam um único valor por pixel – um brilho para escala de cinza, um 0 ou 1 para binárias, uma única amostra de canal de cor para Bayer – de modo que get_pixel() retorna um único inteiro. RGB565 carrega três canais de cor empacotados em 16 bits, e get_pixel os desempacota em uma tupla (r, g, b) por padrão, com cada canal mapeado para a faixa 0 – 255.
O comportamento padrão pode ser invertido em qualquer das pontas. Passar rgbtuple=False para get_pixel em uma imagem RGB565 recorre à palavra empacotada bruta de 16 bits – a mesma forma que o índice linear retorna, e a forma eficiente quando a aplicação vai escrever o mesmo valor empacotado de volta diretamente. Passar rgbtuple=True em uma imagem de canal único faz o oposto: o valor armazenado é convertido em uma tupla RGB888 antes de retornar, com as imagens Bayer passando por uma etapa de debayer na hora. O argumento existe para que o código chamador possa pedir pixels em um espaço de cor uniforme, independentemente de como a imagem subjacente os armazena.
Imagens comprimidas – JPEG e PNG – não são suportadas por get_pixel ou set_pixel. Seus bytes não representam pixels em posições conhecidas, e os métodos levantam um erro em vez de retornar um valor que não significaria nada.
Na prática, os padrões se parecem com:
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 retorna None e set_pixel não faz nada. Isso é tolerante por design: muitos algoritmos percorrem perto das bordas de uma imagem e indexam brevemente posições fora da faixa, e uma operação nula silenciosa é menos disruptiva do que uma exceção toda vez que isso acontece.
5.4.2. Endereçamento por índice linear¶
A outra forma é endereçar pixels por sua posição no buffer subjacente. Lembre-se do layout do buffer: os pixels são armazenados linha por linha, primeiro todos os pixels da linha superior, depois todos os da linha seguinte, e assim por diante até a inferior. Esse arranjo 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 por sua vez. O pixel na coordenada (x, y) tem índice linear y * width + x.
Os pixels são endereçados tanto por coordenadas cartesianas (x, y) quanto por um índice linear que percorre o buffer linha por linha, da esquerda para a direita.¶
O módulo image expõe esse índice por meio da notação de subscrito comum do Python: img[i] lê o pixel no índice linear i, img[i] = value escreve um. O que a forma de índice retorna é o valor bruto armazenado para o formato, não a tupla desempacotada que get_pixel() retorna por padrão. Essa distinção importa porque o formato escolhido anteriormente decide como o valor bruto se parece:
Pixels em escala de cinza e Bayer retornam como inteiros de 8 bits.
Pixels RGB565 e YUV422 retornam como inteiros de 16 bits – a palavra empacotada.
Pixels binários retornam como
0ou1.Pixels JPEG e PNG retornam como inteiros de 8 bits, um byte por vez do fluxo comprimido. Esses valores são opacos – são pedaços de uma codificação comprimida, em vez de pixels em qualquer sentido comum.
A forma de índice serve a código que já está pensando em termos de deslocamentos de buffer: um laço que percorre cada pixel uma vez, um algoritmo que precisa saltar uma linha por vez ou um trecho de código que traduz entre layouts de buffer. Código que está pensando em termos de coordenadas x e y é melhor servido por get_pixel e set_pixel; as duas formas endereçam os mesmos pixels por meio de modelos mentais diferentes.
A Image também é iterável. for v in img: percorre o buffer na mesma ordem linha-maior, produzindo os valores brutos um pixel por vez, e len(img) é a contagem de pixels para formatos não comprimidos ou a contagem de bytes para fluxos comprimidos.
5.4.3. Por que o Python por pixel é o caminho lento¶
Uma nota prática que vale a pena ser honesto a respeito. Percorrer uma imagem um pixel por vez a partir do Python é lento. Uma imagem em escala de cinza de 320 × 240 contém 76.800 pixels; chamar get_pixel() em cada um deles em um laço for executa milhões de instruções de bytecode do MicroPython para realizar um trabalho que um método nativo equivalente poderia concluir em algumas centenas de microssegundos. Isso não é um fator pequeno. É a diferença entre um script que processa quadros em tempo real e um que se arrasta bem abaixo da taxa de quadros da câmera.
Quase todo método na superfície de Image existe porque há uma versão nativa mais rápida de um padrão comum por pixel. Um laço que soma duas imagens torna-se uma única chamada nativa. Um laço que suaviza cada pixel calculando sua média com os vizinhos torna-se outra. Um laço que classifica cada pixel contra um limiar torna-se uma terceira. O trabalho da aplicação, na maior parte do tempo, é reconhecer qual método de imagem inteira corresponde ao trabalho que o laço teria feito, e recorrer a ele em vez de escrever o laço à mão.
A leitura e a escrita em nível de pixel ainda são a ferramenta certa quando nada mais se encaixa – aplicando uma medição específica de volta no buffer, amostrando uma posição para uma etapa de calibração, depurando um valor em uma localização conhecida. O ponto é que elas são o caminho lento, usado quando os métodos de imagem inteira não têm a forma de que a aplicação precisa, não como a maneira padrão de operar em pixels.