5.3. Formatos de pixel

Um algoritmo que detecta bordas espera que cada pixel contenha um valor de brilho. Um algoritmo que rastreia um objeto colorido espera que cada pixel carregue cor. Um algoritmo que executa fechamento morfológico espera que cada pixel esteja ligado ou desligado. O formato de pixel que uma Image carrega – um dos enumerados no catálogo de Vision Sensors – é o que torna essas expectativas verificáveis de antemão: o formato diz, com antecedência, em que forma estão os pixels e, portanto, quais algoritmos podem ser executados sobre eles sem uma etapa de conversão.

Esta página trata de como essa restrição se manifesta na prática. Qual formato é a escolha certa depende do que o pipeline vai fazer, e os métodos de conversão entre formatos são a maneira como um pipeline que precisa de mais de um deles encadeia as etapas.

Uma pilha vertical de cinco faixas rotuladas de layout de bytes. BINARY mostra um byte dividido em oito células de bit único, marcado como "8 pixels por byte". GRAYSCALE mostra três células rotuladas de um byte cada, marcadas como "1 pixel". RGB565 mostra dois bytes adjacentes com os campos de bits RRRRR GGGGGG BBBBB rotulados como "1 pixel". YUV422 mostra quatro células de byte rotuladas Y0, U, Y1, V marcadas como "2 pixels". BAYER mostra duas linhas de quatro células de byte rotuladas: R G R G na linha superior, G B G B na linha inferior.

Os cinco formatos de pixel não comprimidos e como seus bytes são empacotados. JPEG e PNG não são desenhados aqui porque são fluxos comprimidos de comprimento variável, e não grades de pixels de tamanho fixo.

5.3.1. O cavalo de batalha da escala de cinza

A maior parte da visão de máquina clássica se resume a trabalhar com valores de brilho. Detecção de bordas, correspondência de modelos, decodificação de AprilTag, estimativa de fluxo óptico, os operadores morfológicos, análise de blob – todos eles, no nível em que os algoritmos operam, observam quão brilhante é cada pixel e como o brilho se compara ao brilho dos pixels vizinhos. A cor da cena é frequentemente útil para a aplicação que os chama, mas os próprios algoritmos não precisam dela.

O formato de escala de cinza entrega aos algoritmos exatamente isso, sem sobrecarga. Um byte por pixel contém um valor de brilho de 0 (preto) a 255 (branco). O formato tem metade do tamanho de RGB565 e YUV422 e um terço do tamanho de RGB888, de modo que cada operação processa menos dados – tanto mais rápido quanto com menos pressão sobre o buffer. Nas câmeras menores, onde o frame buffer disputa com o resto do script pela RAM, essa diferença de tamanho pode ser o que decide se um pipeline cabe ou não. Se a cor não é a pista de que o algoritmo precisa, a escala de cinza é a resposta certa.

5.3.2. Cor por meio de RGB565

Quando a cor é a pista – rastrear um marcador colorido, distinguir maçãs vermelhas de verdes, identificar um elemento de UI por sua matiz – dois bytes por pixel compram cor suficiente para os tipos de classificação que os algoritmos realizam. RGB565 é o formato de cor padrão na câmera, e o que os métodos cientes de cor da interface esperam.

Renderizar um quadro anotado – desenhar caixas de detecção, escrever texto de diagnóstico, colocar o quadro em uma tela ou enviá-lo a um visualizador remoto – também naturalmente exige RGB565. A pré-visualização da IDE, os controladores de display embarcados e a maioria dos destinos de rede ou consomem o formato diretamente ou convertem a partir dele de forma barata.

5.3.3. Bayer como formato de armazenamento

Uma imagem Bayer é a saída bruta do sensor, antes que o ISP a debayerasse em uma representação de cor finalizada. Cada pixel é um byte contendo um único canal de cor – aquele que o filtro de cor naquela posição do mosaico deixou passar. Isso faz com que uma imagem Bayer tenha o mesmo tamanho de uma imagem em escala de cinza e um terço do tamanho de RGB888, o que se alinha com aquilo para que Bayer é de fato útil: armazenar muitos quadros de uma vez quando a RAM é a restrição limitante.

O problema é que os algoritmos do módulo image não operam diretamente sobre imagens Bayer. Sem o debayering, nenhum pixel carrega informação suficiente para fazer um julgamento de cor por conta própria, e os padrões que os algoritmos procuram – bordas, cantos, blobs – seriam distorcidos pelo mosaico. As únicas maneiras de ler ou modificar uma imagem Bayer são get_pixel() e set_pixel(); todo o resto espera uma representação finalizada.

O padrão que emerge é armazenar os quadros como Bayer pelo tempo que eles precisem ficar em uma fila e converter cada um para escala de cinza ou RGB565 no momento em que seu processamento de fato começa. A conversão custa ciclos de CPU, mas economiza a RAM que de outra forma ficaria presa retendo quadros finalizados durante todo o tempo de vida da aplicação.

Nota

As únicas operações do módulo image diretamente sobre pixels Bayer são get_pixel(), set_pixel() e o caminho de codificação JPEG que alimenta a pré-visualização da IDE ou um visualizador remoto. Desenho, análise e filtragem exigem, todos, conversão primeiro para escala de cinza, RGB565 ou binário.

5.3.4. YUV422 para pipelines que querem ambos

YUV422 separa a informação de cada pixel em um canal de luminância (Y) e dois canais de crominância (U e V), e subamostra a crominância de modo que pares de pixels adjacentes compartilhem um único U e um único V. Os bytes por pixel resultam em uma média de dois – o mesmo que RGB565 – mas são dispostos de forma que o canal Y já seja uma imagem contínua em escala de cinza de 8 bits situada em deslocamentos conhecidos no buffer.

Esse layout é exatamente o que um pipeline quer quando algumas de suas etapas são trabalho em escala de cinza e outras precisam de cor. Ler os valores de Y diretamente para as etapas em escala de cinza evita o custo de uma conversão explícita; os canais U e V estão lá para quando uma etapa posterior realmente precisar de cor. Fora desse padrão específico, RGB565 costuma ser a escolha mais simples para cor e a escala de cinza a escolha mais simples para trabalho apenas com brilho – o valor de YUV422 vem de ser bom em ambos ao mesmo tempo.

Nota

O módulo image opera sobre YUV422 de uma forma mais limitada do que sobre escala de cinza, RGB565 ou binário – leituras diretas do canal Y para trabalho em escala de cinza e o caminho de codificação JPEG que alimenta a pré-visualização da IDE ou um visualizador remoto. Os métodos cientes de cor esperam RGB565; quadros YUV422 precisam de uma conversão explícita antes da análise de cor ou do desenho.

5.3.5. Binário, máscaras e saída limiarizada

Uma imagem binária é um bit por pixel: cada pixel é 0 ou 1. O formato raramente aparece como uma captura do sensor; em vez disso, ele surge como a saída natural da limiarização (onde um teste de cor ou brilho classifica cada pixel em “sim, corresponde” ou “não, não corresponde”) e como a entrada natural para operações morfológicas e para o argumento mask que muitos métodos aceitam.

A vantagem prática do formato é o seu tamanho. Uma imagem binária ocupa um oitavo do tamanho de uma imagem em escala de cinza, de modo que carregar uma máscara grande – uma escolha por pixel de quais posições alguma operação posterior deve tocar – é barato. O fato de muitas operações aceitarem uma imagem binária como argumento de palavra-chave mask= é o outro lado da mesma questão: o formato é pequeno, e encadear a saída binária de uma etapa na entrada de máscara de outra é um padrão de pipeline comum.

5.3.6. JPEG e PNG na fronteira

Objetos Image JPEG e PNG são diferentes dos demais no catálogo. Eles não são grades de pixels; são fluxos de bytes comprimidos que codificam dados de pixel em uma forma que operações no nível de pixel não conseguem ler. Chamar get_pixel() em um JPEG não retorna o pixel em uma posição; o pixel não está descompactado em lugar algum do buffer para o método buscar.

JPEG e PNG aparecem na fronteira do processamento de imagens, onde os dados de pixel estão saindo da câmera ou entrando nela em forma comprimida. Salvar um quadro no disco como JPEG mantém o arquivo pequeno; enviar um quadro por uma rede como JPEG mantém a transmissão barata; carregar um quadro de referência a partir de um arquivo JPEG permite que ele fique no disco em uma forma muito menor do que os pixels brutos ocupariam. Para qualquer um desses casos de uso, a representação comprimida é a resposta certa. Para fazer qualquer processamento real em um JPEG, porém, a aplicação primeiro o converte para um formato trabalhável – e essa conversão é onde os bytes comprimidos são expandidos em pixels e onde o inchaço do buffer (um JPEG de 30 KB pode se tornar 600 KB de RGB565) de fato acontece.

5.3.7. Convertendo entre formatos

O caminho de conversão é o que costura formatos diferentes em um único pipeline. Cinco métodos da classe Image recebem uma imagem existente e retornam uma nova em um formato diferente:

  • to_grayscale() produz uma imagem de um byte por pixel, o formato que os algoritmos clássicos querem.

  • to_rgb565() produz o formato de cor de dois bytes por pixel que tanto os métodos cientes de cor quanto a pré-visualização da IDE falam.

  • to_bitmap() produz uma imagem binária de um bit, o formato que a morfologia e os argumentos mask aceitam.

  • to_jpeg() produz uma imagem comprimida em JPEG adequada para salvar ou transmitir.

  • to_png() produz uma imagem comprimida em PNG quando a codificação sem perdas é preferível aos arquivos menores do JPEG.

Cada conversão ocorre no local por padrão: o buffer da imagem de origem é sobrescrito com o resultado convertido, e os pixels originais da origem desaparecem após o retorno da chamada. Essa é a opção mais barata tanto em CPU quanto em memória, e é a resposta certa quando o quadro de origem não será necessário para mais nada.

Quando a origem ainda é necessária – quando uma etapa posterior do pipeline precisa ver o quadro original – dois argumentos de palavra-chave sobrescrevem o padrão de execução no local. copy=True aloca um buffer separado para a imagem convertida no heap do Python e deixa a origem intacta. copy_to_fb=True faz a mesma alocação, mas a coloca no frame buffer em vez de no heap – que é a opção a que uma aplicação recorre quando a imagem convertida precisa aparecer na pré-visualização da IDE, já que a IDE lê a partir do frame buffer.

Dois métodos adicionais produzem imagens RGB565 coloridas por meio de uma paleta em vez de por uma conversão direta. to_rainbow() mapeia cada valor de entrada de canal único para uma cor ao longo de um gradiente suave que percorre o espectro visível. to_ironbow() mapeia cada valor de entrada para a paleta não linear de termovisor que vai do preto, passando por vermelhos e laranjas escuros, até o branco. Ambos são ferramentas de visualização em vez de medição; o objetivo é tornar legível à primeira vista uma imagem de canal único cujos valores brutos de outra forma seriam invisíveis ao olho.

5.3.8. Tamanho do buffer

Um último detalhe sobre formatos que vale a pena deixar explícito. size() sempre informa o tamanho do buffer de bytes, não a contagem de pixels. Para formatos não comprimidos, isso decorre diretamente das dimensões e dos bytes por pixel: width * height * bytes_per_pixel. Para JPEG e PNG, é o tamanho do fluxo comprimido, que varia de quadro para quadro dependendo do que a cena contém. Código que aloca buffers a partir de orçamentos de bytes usa size() para o primeiro caso; código que transmite quadros comprimidos para fora da câmera o lê após cada compressão para saber quantos bytes o fluxo de fato contém.