5.32. Salvamento e compressão

Toda página até aqui trabalhou com imagens na câmera: capturadas no frame buffer ou alocadas no heap do MicroPython, manipuladas através dos métodos do módulo image e, então, exibidas no preview da IDE ou alimentadas em um estágio posterior dentro do mesmo script. A maioria das aplicações precisa, em algum momento, fazer o oposto: pegar uma imagem que está atualmente na RAM e colocá-la em algum lugar persistente – no cartão SD, em um host USB, através de uma rede – onde algo além da câmera possa lê-la.

O módulo image expõe dois caminhos para esse trabalho. O caminho de salvamento grava a imagem em um arquivo no sistema de arquivos, com o formato do arquivo escolhido pela extensão e os detalhes de codificação tratados pelo método. O caminho de conversão de formato retorna um objeto Image contendo o fluxo de bytes codificado, adequado para ser entregue a uma chamada de streaming ou de rede sem nunca tocar no sistema de arquivos. Cada um se encaixa em uma aplicação diferente; ambos se baseiam no mesmo mecanismo de compressão por baixo.

5.32.1. Salvando em um arquivo

save() grava a imagem no sistema de arquivos em um caminho:

img.save("/sdcard/capture.jpg")
img.save("/sdcard/capture.bmp")
img.save("/sdcard/region.jpg", roi=(40, 60, 200, 150), quality=85)

O formato é escolhido a partir da extensão do arquivo. Cinco extensões são reconhecidas: .bmp grava um Windows bitmap (sem perdas, sem compressão, os pixels capturados byte por byte); .pgm grava um portable graymap (sem perdas, somente escala de cinza); .ppm grava um portable pixmap (sem perdas, RGB); .jpg e .jpeg ambos gravam um JPEG (com perdas, comprimido). A imagem receptora já deve estar no formato de cor correto para o contêiner escolhido – uma imagem colorida salva como .pgm é um erro.

roi restringe o salvamento a um sub-retângulo da imagem, da mesma forma que faz a palavra-chave roi de todos os outros métodos do módulo image. A imagem inteira é o padrão. A palavra-chave é ignorada ao salvar uma imagem comprimida em JPEG, porque a forma em disco já cobre o quadro inteiro e recodificar através de um recorte derrotaria o propósito de salvar os bytes comprimidos existentes.

quality é a qualidade de compressão JPEG de 0 a 100 e só é significativa quando a saída é JPEG (a palavra-chave é ignorada para os formatos sem perdas). O padrão de 50 é o equilíbrio certo para a maioria das aplicações; 70 a 85 é a faixa para maior qualidade visual, 30 a 50 é o intervalo certo para miniaturas pequenas e transmissão com largura de banda limitada, e 90 para cima é reservado para casos em que a imagem será inspecionada manualmente ou processada por um algoritmo posterior sensível a artefatos de compressão.

A imagem receptora é retornada para que a chamada possa ser encadeada: img.save("/sdcard/x.jpg").draw_string(0, 0, "saved"). O objeto retornado é a mesma imagem em memória; o salvamento é um efeito colateral.

Um uso típico é o padrão captura-e-registro. Um gatilho dispara (um blob é detectado, um botão é pressionado, um timer expira); o script captura um quadro; ele acrescenta um timestamp ao nome do arquivo; e chama save() para enviar a imagem ao cartão SD. O preview da IDE continua rodando, o próximo gatilho dispara, e os arquivos salvos se acumulam.

5.32.2. Codificando para a memória

Quando o destino não é o sistema de arquivos, mas uma conexão de rede, uma porta serial ou a entrada de outro módulo, a aplicação precisa do fluxo de bytes codificado na memória em vez de em disco. to_jpeg() e to_png() produzem exatamente isso:

encoded = img.to_jpeg(quality=80, copy=True)
bytes_to_send = encoded.bytearray()
sock.send(bytes_to_send)

O comportamento padrão é a conversão no local: a imagem receptora é convertida em uma imagem JPEG (ou PNG) e o mesmo objeto é retornado. Com copy=True a conversão grava em um objeto recém-alocado no heap; com copy_to_fb=True a saída vai para o frame buffer. A escolha é a mesma que qualquer outro método de conversão oferece – no local por padrão, cópia quando o original ainda for necessário depois.

quality e subsampling são os mesmos ajustes de JPEG que o caminho de salvamento expõe. subsampling escolhe o esquema de subamostragem de croma: image.JPEG_SUBSAMPLING_AUTO escolhe o melhor para a qualidade selecionada, image.JPEG_SUBSAMPLING_444 mantém o croma em resolução total (maior arquivo, melhor precisão de cor), image.JPEG_SUBSAMPLING_422 e image.JPEG_SUBSAMPLING_420 reduzem pela metade a resolução do croma em um ou em ambos os eixos (arquivos menores, leve suavização de cor que é invisível em distâncias de visualização típicas). O padrão AUTO é a escolha certa, a menos que a aplicação tenha uma necessidade específica.

PNG via to_png() é sem perdas, mas mais lento de codificar e produz arquivos maiores que o JPEG para conteúdo fotográfico (conteúdo fotográfico comprime mal sob o esquema de predição do PNG). Use PNG quando a imagem for line art, uma captura de tela, ou contiver gráficos de bordas nítidas desenhados sobre um quadro capturado – a codificação sem perdas preserva as bordas afiadas que o JPEG suavizaria. Caso contrário, JPEG é o padrão certo.

Tanto to_jpeg() quanto to_png() aceitam as mesmas palavras-chave posicionais e de escala no estilo de desenho que outros métodos de conversão recebem – x_scale, y_scale, roi, rgb_channel, alpha, color_palette, alpha_palette, hint – de modo que a mesma chamada pode codificar uma versão escalada, recortada ou mapeada por paleta da origem em uma única etapa. compress() é a grafia legada de to_jpeg(); as duas recebem os mesmos argumentos e produzem o mesmo resultado.

5.32.3. O que a compressão proporciona

Os números por trás do compromisso JPEG-versus-bruto valem a pena serem analisados uma vez.

Um quadro RGB565 de 320 por 240 tem 153.600 bytes (um quadro capturado em QVGA). Um quadro de 640 por 480 tem 614.400 bytes; um quadro de 1280 por 960 tem 2.457.600 bytes. Nenhum deles é grande em comparação com um monitor de desktop ou de celular, mas são substanciais no contexto de uma câmera que tem alguns MB de RAM no total, um cartão SD com largura de banda de gravação finita e um link de host que normalmente funciona sobre USB CDC, uma UART ou um módulo sem fio a velocidades modestas.

JPEG com quality=50 normalmente comprime um quadro fotográfico capturado em 10x a 20x: aquele quadro de 640 por 480 de 614 KB torna-se um fluxo de bytes codificado de 30 a 60 KB. Com quality=85 a compressão cai para 5x a 10x (60 a 120 KB para o mesmo quadro). Com quality=10 – cheio de artefatos, mas ainda reconhecível – a compressão alcança 30x a 50x (12 a 20 KB).

Esses números determinam o que é prático fazer com os quadros salvos. Um caminho de cartão SD sustentando 10 MB/s lida com 30 quadros por segundo de conteúdo VGA codificado em JPEG com quality=50 com folga (cerca de 1 a 2 MB/s); salvar o mesmo conteúdo sem compressão exige mais de 18 MB/s, além do que o caminho do sistema de arquivos da câmera sustenta para o cartão. Um host USB puxando quadros codificados em JPEG sobre CDC a 1 MB/s recebe quadros de 30 a 60 KB a aproximadamente 15 a 30 quadros por segundo; puxando quadros brutos na mesma taxa, ele obtém um ou dois quadros por segundo.

Em resumo: os métodos de compressão não são apenas uma conveniência para salvar. Eles são o que torna o quadro capturado utilizável fora da câmera nas taxas de quadros com as quais a aplicação se importa. Escolher a compressão certa – JPEG quality 50 para registro geral, 80 para trabalho de qualidade, PNG para captura de line art – faz parte do trabalho rotineiro de qualquer aplicação de câmera não trivial.