5.33. Streams ImageIO¶
save() e to_jpeg() cobrem o caso de I/O de fotograma único: uma aplicação captura um fotograma, codifica-o e envia-o para algum destino. Uma classe diferente de aplicação necessita do caso de sequência: gravar muitos fotogramas consecutivos à taxa de captura natural, armazená-los num local de onde possam ser recuperados mais tarde e reproduzi-los à velocidade correta. Um script de recolha de dados de treino captura algumas centenas de fotogramas de exemplo para uma pipeline de aprendizagem automática; um registo de estação de inspeção regista cada peça capturada para rastreabilidade; um script de desenvolvimento reproduz uma sequência armazenada para testar um novo algoritmo com dados que foram previamente capturados ao vivo.
A classe ImageIO é o gravador/reprodutor do módulo de imagem. Um único stream contém uma sequência de fotogramas Image – possivelmente de diferentes tamanhos e formatos de pixel – juntamente com o intervalo entre fotogramas de cada um, para que a reprodução possa recriar a taxa de fotogramas original. Estão disponíveis dois armazenamentos de suporte: um ficheiro no sistema de ficheiros ou um buffer de tamanho fixo em RAM.
5.33.1. Os dois armazenamentos de suporte¶
Um stream de ficheiro persiste a gravação entre ciclos de energia e tem como tamanho apenas o do armazenamento que o suporta. Começa com um cabeçalho mágico de 16 bytes OMV IMG STR Vx.y seguido de um bloco por fotograma; o escritor atual emite V2.0 e o leitor ainda aceita ficheiros V1.0 e V1.1 para compatibilidade retroativa. O caminho do ficheiro é o argumento do construtor; o modo é o modo de abertura do ficheiro ('r' para ler um stream existente, 'w' para truncar e escrever de novo).
# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
img = csi0.snapshot()
stream.write(img)
stream.close()
Um stream de memória reside num buffer de RAM alocado na construção. O construtor recebe um 3-tuple (w, h, pixformat) em vez de um caminho, e o argumento mode torna-se o número pré-alocado de espaços para fotogramas. O buffer é dimensionado exatamente para esse número de fotogramas nas dimensões fornecidas e não pode crescer após a alocação – escrever para além do último espaço levanta EOFError, e escrever um fotograma maior do que o buffer de espaço levanta ValueError. Os streams de memória são a ferramenta adequada quando a aplicação precisa de entregar uma gravação a uma fase seguinte sem passar pelo sistema de ficheiros (por exemplo, um buffer de anel curto de fotogramas recentes para um padrão de disparo e reprodução).
# Pre-allocate space for 32 QVGA RGB565 frames in RAM
stream = image.ImageIO((320, 240, image.RGB565), 32)
for _ in range(32):
stream.write(csi0.snapshot())
Para os formatos de pixel comprimidos (image.JPEG, image.PNG) o tamanho por espaço é estimado em 2 bits por pixel; um fotograma codificado maior do que a estimativa levanta ValueError no momento da escrita, pelo que uma aplicação que espera armazenar JPEGs de alta qualidade terá de alocar em excesso a contagem de espaços ou codificar primeiro com qualidade inferior.
type() devolve image.ImageIO.FILE_STREAM ou image.ImageIO.MEMORY_STREAM para que o código seguinte possa adaptar-se ao armazenamento de suporte que lhe for dado.
5.33.2. Gravação¶
write() acrescenta um Image capturado a um stream de ficheiro (ou armazena-o no espaço atual de um stream de memória) e avança o deslocamento em um. A mesma chamada regista o intervalo entre fotogramas desde a última escrita, para que a metade de reprodução possa pausar pelo tempo correto entre fotogramas e a taxa de fotogramas natural da gravação seja preservada.
Fotogramas heterogéneos são permitidos num único stream de ficheiro: uma gravação pode misturar capturas RGB565, recortes em escala de cinzentos e miniaturas codificadas em JPEG livremente, e o leitor descodificará cada um no seu tamanho e formato originais. Os streams de memória são homogéneos (todos os espaços partilham o (w, h, pixformat) fornecido ao construtor), pelo que uma gravação em memória está restrita a uma configuração de fotograma.
write() devolve o objeto stream para que as chamadas possam encadear-se. Escrever num deslocamento que não seja o fim de um stream de ficheiro trunca o resto do ficheiro – útil para editar uma sequência armazenada, arriscado se a posição de próxima escrita foi movida involuntariamente por um seek() anterior.
sync() descarrega as escritas pendentes para disco em streams de ficheiro (é uma operação sem efeito em streams de memória) e deve ser chamado periodicamente em gravações longas, para evitar a perda do final da gravação caso a câmara reinicie antes de o ficheiro ser fechado. O destrutor fecha o stream automaticamente quando o ImageIO sai do âmbito, mas o uso explícito de close() é a prática recomendada.
5.33.3. Reprodução¶
read() lê o fotograma no deslocamento atual, avança o deslocamento e devolve o novo Image. O fotograma permanece no buffer de fotograma quando copy_to_fb=True (o padrão), pelo que a imagem devolvida pode ser visualizada através da pré-visualização do IDE; com copy_to_fb=False o fotograma fica na heap do MicroPython.
# Loop a recorded stream at its natural frame rate
stream = image.ImageIO("/sdcard/run.bin", "r")
while True:
img = stream.read()
# img is now in the frame buffer; the IDE shows it
# and the script can run any analysis it likes
Duas palavras-chave controlam o comportamento de reprodução. loop=True (o padrão para streams de ficheiro) envolve o ponteiro de leitura de volta ao início quando o fim da gravação é atingido, pelo que a chamada nunca devolve None; loop=False devolve None quando a gravação se esgota e o ciclo do chamador termina. pause=True (o padrão) bloqueia a chamada até que o intervalo entre fotogramas registado no momento da escrita tenha decorrido, para que a taxa de fotogramas de reprodução corresponda à taxa de captura original; pause=False devolve imediatamente, útil para pipelines de análise que pretendem processar a gravação o mais rapidamente possível sem respeitar o tempo original.
O mesmo padrão de ciclo funciona para streams de memória, exceto que loop é ignorado – ler para além do fim de um stream de memória levanta EOFError. O padrão esperado para um anel de memória é utilizar seek() de volta a zero explicitamente quando se pretende o envolvimento.
5.33.5. Gravações reproduzíveis no anfitrião¶
Os streams ImageIO são a ferramenta adequada quando a gravação vai ser reproduzida na câmara – preservam todos os fotogramas capturados no seu formato de pixel nativo, o intervalo entre fotogramas é registado com exatidão, e um script seguinte pode percorrê-los, procurar e re-analisar sem perdas. No entanto, não são a ferramenta adequada quando a gravação tem de ser reproduzível num anfitrião – uma estação de trabalho, um telefone, um leitor web. Um anfitrião espera um contentor de vídeo padrão, não o formato com cabeçalho mágico em disco do OpenMV.
Dois módulos separados cobrem o caso reproduzível no anfitrião. O módulo mjpeg grava Motion JPEG: uma sequência de fotogramas comprimidos em JPEG empacotados num único contentor de estilo AVI que VLC, QuickTime, ffmpeg e a tag de vídeo web padrão reproduzem diretamente. O módulo gif grava um GIF animado: uma sequência de fotogramas não comprimidos (ou comprimidos com paleta) com atrasos explícitos por fotograma, reproduzível em qualquer navegador web ou visualizador de imagens que suporte GIFs animados.
O módulo mjpeg é a escolha natural para gravações longas. A compressão JPEG mantém o tamanho do ficheiro gerível – comparável a to_jpeg() na qualidade configurada, fotograma após fotograma – pelo que uma sessão de captura prolongada permanece dentro do orçamento do cartão SD. A utilização espelha de perto a gravação com ImageIO:
import mjpeg
m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
m.add_frame(csi0.snapshot(), quality=85)
m.close()
mjpeg.Mjpeg aceita as mesmas palavras-chave posicionais e de escala de estilo desenho que outros métodos de imagem aceitam, pelo que uma gravação pode ser redimensionada, recortada ou mapeada por paleta por fotograma na entrada. Os argumentos width e height do construtor têm como padrão as dimensões do buffer de fotograma principal e fixam a resolução de saída; cada fotograma acrescentado é redimensionado (preservando a proporção) para caber. sync() descarrega o ficheiro para disco durante uma gravação longa, e close() finaliza o contentor – um ficheiro Motion JPEG que não foi fechado corretamente não é reproduzível, pelo que a disciplina é importante.
O módulo gif é a escolha natural para gravações curtas partilhadas tal como estão com um espetador não técnico – alguns segundos de ação capturados para uma demonstração, uma ilustração animada para documentação, um clip de evento incorporado numa mensagem de chat. Os fotogramas GIF são armazenados sem compressão (ou com compressão de paleta a 7 bits de profundidade de cor), o que torna os ficheiros muito maiores por segundo do que Motion JPEG e exclui o formato para gravações com mais de alguns segundos, mas o resultado integra-se diretamente em qualquer navegador:
import gif
g = gif.Gif("/sdcard/clip.gif")
while running:
g.add_frame(csi0.snapshot(), delay=10)
g.close()
O argumento delay em add_frame() é o tempo de apresentação por fotograma em centissegundos (10 é 100 ms por fotograma, ou 10 fps), que é o controlo de reprodução GIF padrão. A palavra-chave loop do construtor define se o clip resultante se repete automaticamente nos visualizadores (o padrão é True, o que corresponde à expectativa convencional de «GIF animado»).
Os três caminhos de gravação cobrem os casos comuns entre eles: ImageIO para reprocessamento na câmara, Motion JPEG para gravações longas reproduzíveis no anfitrião, GIF animado para clips curtos reproduzíveis no anfitrião. A escolha entre eles depende de quem reproduz a gravação. Uma fase seguinte a correr na câmara lê ImageIO; uma estação de trabalho anfitriã ou visualizador web lê MJPEG ou GIF.
5.33.6. Um padrão de disparo e reprodução¶
Um padrão útil combina um stream de memória com uma condição de disparo. A câmara grava continuamente num buffer de anel de memória com count espaços, sobrescrevendo o espaço mais antigo em cada volta. Quando uma condição de disparo é acionada (uma mancha entra no fotograma, um evento de movimento excede o limiar, um botão é premido), a aplicação captura o conteúdo do anel – os count fotogramas mais recentes – e escreve-os num stream de ficheiro no cartão SD. O resultado é uma gravação pré-disparo que captura os segundos antes do evento que a câmara efetivamente detetou, não apenas os segundos depois, o que é a limitação clássica de um gravador ingénuo de «capturar quando acionado».
A implementação é simples quando as classes de stream estão disponíveis: um stream de memória de tamanho fixo serve como anel (com seek() explícito para zero quando o deslocamento atinge a contagem de espaços), o ciclo principal captura para ele em cada iteração, e o manipulador de disparo lê o stream de memória fotograma a fotograma e escreve cada um num stream de ficheiro com o nome do timestamp do disparo.