5.32. Salvataggio e compressione

Ogni pagina fino ad ora ha lavorato con immagini sulla cam: catturate nel frame buffer o allocate sull’heap di MicroPython, manipolate tramite i metodi del modulo image e poi visualizzate nell’anteprima dell’IDE oppure inviate a una fase successiva all’interno dello stesso script. La maggior parte delle applicazioni a un certo punto deve fare l’opposto: prendere un’immagine che si trova attualmente in RAM e metterla da qualche parte in modo persistente – sulla scheda SD, su un host USB, attraverso una rete – dove qualcosa di diverso dalla camera possa leggerla.

Il modulo image espone due percorsi per questo lavoro. Il percorso save scrive l’immagine in un file sul filesystem, con il formato del file scelto dall’estensione e i dettagli di codifica gestiti dal metodo. Il percorso to-format restituisce un oggetto Image contenente il flusso di byte codificato, adatto a essere passato a una chiamata di streaming o di rete senza mai toccare il filesystem. Ciascuno si adatta a un’applicazione diversa; entrambi si basano sullo stesso motore di compressione sottostante.

5.32.1. Salvataggio in un file

save() scrive l’immagine sul filesystem in un percorso:

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

Il formato viene scelto dall’estensione del file. Sono riconosciute cinque estensioni: .bmp scrive una Windows bitmap (senza perdita, senza compressione, i pixel catturati byte per byte); .pgm scrive una portable graymap (senza perdita, solo scala di grigi); .ppm scrive una portable pixmap (senza perdita, RGB); .jpg e .jpeg scrivono entrambi un JPEG (con perdita, compresso). L’immagine ricevente deve già essere nel formato di colore corretto per il contenitore scelto – un’immagine a colori salvata come .pgm è un errore.

roi limita il salvataggio a un sotto-rettangolo dell’immagine, nello stesso modo in cui fa la parola chiave roi di ogni altro metodo del modulo image. L’immagine completa è il valore predefinito. La parola chiave viene ignorata quando si salva un’immagine compressa in JPEG, perché la forma su disco copre già il frame completo e ricodificare attraverso un ritaglio vanificherebbe lo scopo di salvare i byte già compressi esistenti.

quality è la qualità di compressione JPEG da 0 a 100 e ha significato solo quando l’output è JPEG (la parola chiave viene ignorata per i formati senza perdita). Il valore predefinito di 50 è il giusto compromesso per la maggior parte delle applicazioni; da 70 a 85 è la fascia per una qualità visiva più elevata, da 30 a 50 è l’intervallo giusto per piccole miniature e trasmissioni con larghezza di banda limitata, mentre da 90 in su è riservato ai casi in cui l’immagine verrà ispezionata manualmente o elaborata da un algoritmo a valle sensibile agli artefatti di compressione.

L’immagine ricevente viene restituita in modo che la chiamata sia concatenabile: img.save("/sdcard/x.jpg").draw_string(0, 0, "saved"). L’oggetto restituito è la stessa immagine in memoria; il salvataggio è un effetto collaterale.

Un uso tipico è il pattern capture-and-log. Si verifica un trigger (viene rilevato un blob, viene premuto un pulsante, scade un timer); lo script cattura un frame; aggiunge un timestamp al nome del file; e chiama save() per inviare l’immagine sulla scheda SD. L’anteprima dell’IDE continua a funzionare, si verifica il trigger successivo e i file salvati si accumulano.

5.32.2. Codifica in memoria

Quando la destinazione non è il filesystem ma una connessione di rete, una porta seriale o l’input di un altro modulo, l’applicazione ha bisogno del flusso di byte codificato in memoria anziché su disco. to_jpeg() e to_png() producono esattamente questo:

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

Il comportamento predefinito è la conversione in-place: il ricevente viene convertito in un’immagine JPEG (o PNG) e viene restituito lo stesso oggetto. Con copy=True la conversione scrive in un oggetto appena allocato sull’heap; con copy_to_fb=True l’output finisce nel frame buffer. La scelta è la stessa offerta da qualsiasi altro metodo di conversione – in-place per impostazione predefinita, copia quando l’originale serve in seguito.

quality e subsampling sono le stesse manopole di regolazione JPEG esposte dal percorso di salvataggio. subsampling sceglie lo schema di sottocampionamento della crominanza: image.JPEG_SUBSAMPLING_AUTO sceglie il migliore per la qualità selezionata, image.JPEG_SUBSAMPLING_444 mantiene la crominanza a piena risoluzione (file più grande, migliore accuratezza del colore), image.JPEG_SUBSAMPLING_422 e image.JPEG_SUBSAMPLING_420 dimezzano la risoluzione della crominanza lungo uno o entrambi gli assi (file più piccoli, leggera attenuazione del colore invisibile alle tipiche distanze di visualizzazione). Il valore predefinito di AUTO è la scelta giusta a meno che l’applicazione non abbia un’esigenza specifica.

PNG tramite to_png() è senza perdita ma più lento da codificare e produce file più grandi di JPEG per i contenuti fotografici (i contenuti fotografici si comprimono male sotto lo schema di predizione del PNG). Usa PNG quando l’immagine è line art, uno screenshot o contiene grafica dai bordi netti disegnata sopra un frame catturato – la codifica senza perdita preserva i bordi nitidi che JPEG attenuerebbe. Altrimenti JPEG è il valore predefinito corretto.

Sia to_jpeg() che to_png() accettano le stesse parole chiave posizionali in stile disegno e di scala degli altri metodi di conversione – x_scale, y_scale, roi, rgb_channel, alpha, color_palette, alpha_palette, hint – in modo che la stessa chiamata possa codificare una versione scalata, ritagliata o mappata su palette della sorgente in un solo passaggio. compress() è la grafia legacy di to_jpeg(); i due accettano gli stessi argomenti e producono lo stesso risultato.

5.32.3. Cosa offre la compressione

Vale la pena ragionare una volta sui numeri dietro il compromesso tra JPEG e raw.

Un frame RGB565 di 320 per 240 è di 153.600 byte (un frame catturato a QVGA). Un frame di 640 per 480 è di 614.400 byte; un frame di 1280 per 960 è di 2.457.600 byte. Nessuno di questi è grande rispetto a un display desktop o di un telefono, ma sono sostanziali nel contesto di una cam che ha in totale pochi MB di RAM, una scheda SD con una larghezza di banda di scrittura finita e un collegamento all’host che tipicamente funziona su USB CDC, su una UART o su un modulo wireless a velocità modeste.

JPEG a quality=50 tipicamente comprime un frame fotografico catturato di 10x-20x: quel frame di 640 per 480 da 614 KB diventa un flusso di byte codificato da 30 a 60 KB. A quality=85 la compressione scende a 5x-10x (da 60 a 120 KB per lo stesso frame). A quality=10 – pieno di artefatti ma ancora riconoscibile – la compressione raggiunge 30x-50x (da 12 a 20 KB).

Questi numeri determinano cosa è pratico fare con i frame salvati. Un percorso verso scheda SD che sostiene 10 MB/s gestisce 30 frame al secondo di contenuto VGA codificato in JPEG a quality=50 con ampio margine (circa 1-2 MB/s); salvare lo stesso contenuto non compresso richiede oltre 18 MB/s, oltre ciò che il percorso del filesystem della cam riesce a sostenere verso la scheda. Un host USB che preleva frame codificati in JPEG su CDC a 1 MB/s riceve frame da 30 a 60 KB a circa 15-30 frame al secondo; prelevando frame raw alla stessa velocità ottiene uno o due frame al secondo.

In breve: i metodi di compressione non sono solo una comodità per il salvataggio. Sono ciò che rende il frame catturato utilizzabile al di fuori della cam ai frame rate che interessano all’applicazione. Scegliere la compressione giusta – JPEG qualità 50 per il logging generale, 80 per il lavoro di qualità, PNG per la cattura di line art – fa parte del lavoro di routine di qualsiasi applicazione cam non banale.