5.6. Desenhando formas e texto

Um algoritmo que decide algo sobre uma imagem frequentemente precisa que essa decisão seja visível. Um detector de blobs encontra uma região de interesse da aplicação; a aplicação quer a região desenhada no quadro para que um operador – ou o desenvolvedor que executa o script – possa ver o que foi encontrado. Uma transformação de coordenadas mapeia uma posição de entrada para uma de saída; depurá-la geralmente significa marcar as duas posições na mesma imagem. A pré-visualização do IDE lê o que estiver no frame buffer no momento em que faz a consulta, então a maneira mais simples de tornar a saída do algoritmo visível é escrever anotações no próprio frame buffer. A família de métodos de desenho da classe Image é o conjunto de ferramentas exatamente para esse trabalho.

5.6.1. As primitivas

Cada método de desenho coloca um tipo específico de marca na imagem. O catálogo é pequeno e se mantém próximo das primitivas geométricas que uma anotação realmente precisa:

  • draw_line() – um segmento de linha reta entre dois pontos extremos.

  • draw_rectangle() – um retângulo alinhado aos eixos, vazado ou preenchido.

  • draw_circle() – um círculo em torno de um centro, vazado ou preenchido.

  • draw_ellipse() – uma elipse com rotação arbitrária.

  • draw_cross() – um sinal de mais em um ponto, a marca usual para um centroide ou um alvo de clique.

  • draw_arrow() – uma seta de um ponto inicial a um ponto final.

  • draw_edges() – os quatro lados de um quadrilátero arbitrário, dados os quatro pontos dos cantos; a maneira natural de contornar uma tag detectada ou uma região distorcida em perspectiva.

  • draw_string() – texto a partir de uma fonte bitmap embutida.

Cada um desses métodos modifica a imagem de origem in-place e retorna a mesma imagem para encadeamento, seguindo a convenção dos métodos operadores estabelecida anteriormente.

Uma grade de pequenos painéis mostrando cada uma das oito primitivas de desenho aplicada uma vez. Cada painel contém uma linha, um retângulo, um círculo, uma elipse, uma cruz, uma seta, um quadrilátero ou uma string de texto curta, com o nome do método que a produziu rotulado embaixo.

As oito primitivas de desenho, uma por painel. Cada método faz um tipo de marca.

5.6.2. Cor

Cada método de desenho recebe um argumento color que decide qual valor escrever em cada pixel pintado. A forma desse argumento depende do formato da imagem. Para uma imagem RGB565, a forma natural é uma tupla (r, g, b) com cada canal de 0 a 255; o módulo compacta isso na palavra RGB565 de 16 bits antes de escrevê-la. Para uma imagem em escala de cinza, a forma natural é um único inteiro de brilho, de 0 (preto) a 255 (branco). Os métodos também aceitam o valor bruto armazenado do formato – uma palavra empacotada de 16 bits para RGB565, um inteiro de 8 bits para escala de cinza – que é a forma eficiente quando a cor foi calculada em outro lugar e já está na forma armazenada.

Omitir o argumento color pinta de branco. Esse padrão é conveniente para trabalho em escala de cinza, onde o branco é o valor máximo e se destaca claramente contra a maioria dos fundos. Para sobreposições de depuração em RGB565, ele quase sempre está errado: verde ou vermelho geralmente se destacam melhor contra o tipo de cena que uma câmera realmente vê, e uma cor explícita comunica a intenção.

5.6.3. Espessura e preenchimento

A maioria dos métodos geométricos recebe duas flags que decidem como a marca é desenhada:

  • thickness=N define a largura da linha em pixels. O padrão é 1, que serve para a maioria das sobreposições; um valor maior é útil quando uma anotação precisa permanecer visível contra uma cena carregada ou após uma etapa posterior do pipeline modificar ainda mais a imagem.

  • fill=True muda a marca de um contorno para uma forma sólida, pintando cada pixel interno com a cor escolhida. O padrão é False.

Essas flags não se aplicam às primitivas que não têm interior a preencher – a linha, a cruz, a seta, o quadrilátero – onde apenas thickness tem significado.

5.6.4. Desenhando texto

draw_string() escreve caracteres usando uma fonte bitmap embutida de 8 por 10 pixels. x e y são o canto superior esquerdo da célula do primeiro caractere, text é a string a desenhar, e color segue a mesma convenção dos métodos geométricos. A fonte abrange toda a faixa ASCII imprimível e não tem kerning – cada caractere ocupa a mesma célula de 8 pixels de largura – o que torna a saída fácil de posicionar.

img.draw_string(10, 10, "blobs: 3", color=(0, 255, 0))

A string pode incluir quebras de linha (\n); cada quebra de linha move o próximo caractere para o início de uma nova linha dez pixels abaixo da anterior. O argumento scale desenha cada caractere em um tamanho maior por um fator de ponto flutuante, e x_spacing e y_spacing adicionam espaçamento em torno de cada caractere. Um pequeno conjunto de flags de rotacionar / espelhar / inverter se aplica tanto à string inteira quanto a cada caractere individualmente – controle suficiente para dispor o texto ao longo de um ângulo ou contra uma borda não horizontal quando o layout exigir.

5.6.5. Limpando a tela

Um método da família não desenha nenhuma marca específica. Ele apenas redefine cada pixel da imagem para zero:

  • clear() – zera cada pixel, opcionalmente restrito a uma ROI ou delimitado por uma máscara.

clear() é a resposta certa quando uma aplicação está compondo uma anotação do zero a cada quadro – começar com uma tela preta, desenhar as novas anotações, entregar o resultado ao display – em vez de sobrepor sobre o quadro capturado. É também a maneira mais barata de preparar uma imagem temporária para uso como buffer de máscara.

Uma imagem recém-alocada já vem zerada do construtor, então clear() importa especificamente para buffers reutilizados entre quadros.