5.3. Formati dei pixel¶
Un algoritmo che rileva i bordi si aspetta che ogni pixel contenga un valore di luminosità. Un algoritmo che traccia un oggetto colorato si aspetta che ogni pixel porti con sé un colore. Un algoritmo che esegue la chiusura morfologica si aspetta che ogni pixel sia acceso o spento. Il formato dei pixel che un’immagine Image porta con sé – uno tra quelli enumerati nel catalogo dei Vision Sensors – è ciò che rende verificabili in anticipo queste aspettative: il formato dice, in anticipo, in che forma sono i pixel e quali algoritmi possono quindi essere eseguiti su di essi senza un passaggio di conversione.
Questa pagina riguarda come quel vincolo si traduce nella pratica. Quale formato sia la scelta giusta dipende da ciò che la pipeline andrà a fare, e i metodi di conversione tra i formati sono il modo in cui una pipeline che ne richiede più di uno concatena le varie fasi.
I cinque formati di pixel non compressi e come si impacchettano i loro byte. JPEG e PNG non sono rappresentati qui perché sono flussi compressi a lunghezza variabile anziché griglie di pixel di dimensione fissa.¶
5.3.1. Il cavallo da lavoro: scala di grigi¶
Gran parte della visione artificiale classica si riduce a lavorare con valori di luminosità. Il rilevamento dei bordi, il template matching, la decodifica degli AprilTag, la stima del flusso ottico, gli operatori morfologici, l’analisi dei blob – tutti quanti, al livello a cui operano gli algoritmi, guardano a quanto è luminoso ogni pixel e a come la luminosità si confronta con quella dei pixel vicini. Il colore della scena è spesso utile per l’applicazione che li richiama, ma gli algoritmi stessi non ne hanno bisogno.
Il formato in scala di grigi consegna agli algoritmi esattamente questo, senza alcun overhead. Un byte per pixel contiene un valore di luminosità da 0 (nero) fino a 255 (bianco). Il formato ha la metà della dimensione di RGB565 e YUV422 e un terzo di quella di RGB888, quindi ogni operazione elabora meno dati – è sia più veloce sia meno gravosa per il buffer. Sulle cam più piccole, dove il frame buffer compete con il resto dello script per la RAM, quella differenza di ingombro può essere ciò che decide se una pipeline ci sta del tutto. Se il colore non è l’indizio di cui l’algoritmo ha bisogno, la scala di grigi è la risposta giusta.
5.3.2. Il colore tramite RGB565¶
Quando il colore è l’indizio – tracciare un marcatore colorato, distinguere le mele rosse da quelle verdi, individuare un elemento dell’interfaccia in base alla sua tonalità – due byte per pixel comprano colore a sufficienza per i tipi di classificazione che gli algoritmi eseguono. RGB565 è il formato colore predefinito sulla cam, e quello che i metodi color-aware in superficie si aspettano.
Anche il rendering di un frame annotato – disegnare i riquadri di rilevamento, scrivere testo diagnostico, portare il frame su uno schermo o inviarlo a un visualizzatore remoto – richiede naturalmente RGB565. L’anteprima dell’IDE, i controller del display a bordo e la maggior parte delle destinazioni di rete o consumano direttamente il formato oppure lo convertono da esso a basso costo.
5.3.3. Bayer come formato di archiviazione¶
Un’immagine Bayer è l’output grezzo del sensore, prima che l’ISP la debayerizzi in una rappresentazione di colore finita. Ogni pixel è un byte che contiene un singolo canale di colore – quello che il filtro colore in quella posizione del mosaico ha lasciato passare. Questo rende un’immagine Bayer della stessa dimensione di un’immagine in scala di grigi e un terzo di quella di RGB888, il che si allinea con ciò per cui Bayer è effettivamente utile: archiviare molti frame contemporaneamente quando la RAM è il vincolo determinante.
Il rovescio della medaglia è che gli algoritmi del modulo image non operano direttamente sulle immagini Bayer. Senza debayerizzazione, nessun pixel porta con sé informazioni sufficienti per esprimere un giudizio sul colore da solo, e i pattern che gli algoritmi cercano – bordi, angoli, blob – verrebbero distorti dal mosaico. Gli unici modi per leggere o modificare un’immagine Bayer sono get_pixel() e set_pixel(); tutto il resto si aspetta una rappresentazione finita.
Il pattern che ne deriva è archiviare i frame come Bayer per tutto il tempo in cui devono rimanere in una coda e convertire ciascuno in scala di grigi o in RGB565 nel momento in cui la sua elaborazione inizia effettivamente. La conversione costa cicli di CPU ma risparmia la RAM che altrimenti rimarrebbe occupata a mantenere i frame finiti per tutta la durata di vita dell’applicazione.
Nota
Le uniche operazioni del modulo image direttamente sui pixel Bayer sono get_pixel(), set_pixel() e il percorso di codifica JPEG che alimenta l’anteprima dell’IDE o un visualizzatore remoto. Il disegno, l’analisi e il filtraggio richiedono tutti prima la conversione in scala di grigi, RGB565 o binario.
5.3.4. YUV422 per le pipeline che vogliono entrambi¶
YUV422 separa le informazioni di ogni pixel in un canale di luminanza (Y) e due canali di crominanza (U e V), e sottocampiona la crominanza in modo che coppie di pixel adiacenti condividano un singolo U e un singolo V. I byte per pixel mediano a due – gli stessi di RGB565 – ma sono disposti in modo che il canale Y sia già un’immagine in scala di grigi continua a 8 bit posizionata a offset noti nel buffer.
Quella disposizione è esattamente ciò che una pipeline desidera quando alcune delle sue fasi sono lavoro in scala di grigi e altre richiedono il colore. Leggere direttamente i valori Y per le fasi in scala di grigi salta il costo di una conversione esplicita; i canali U e V sono lì quando una fase successiva ha effettivamente bisogno del colore. Al di fuori di quello specifico pattern, RGB565 è di solito la scelta più semplice per il colore e la scala di grigi è la scelta più semplice per il lavoro basato solo sulla luminosità – il valore di YUV422 deriva dall’essere buono in entrambi allo stesso tempo.
Nota
Il modulo image opera su YUV422 in un modo più limitato rispetto alla scala di grigi, RGB565 o binario – letture dirette del canale Y per il lavoro in scala di grigi e il percorso di codifica JPEG che alimenta l’anteprima dell’IDE o un visualizzatore remoto. I metodi color-aware si aspettano RGB565; i frame YUV422 necessitano di una conversione esplicita prima dell’analisi del colore o del disegno.
5.3.5. Binario, maschere e output con soglia applicata¶
Un’immagine binaria è un bit per pixel: ogni pixel è 0 oppure 1. Il formato raramente compare come acquisizione del sensore; appare invece come output naturale della soglia (dove un test sul colore o sulla luminosità classifica ogni pixel in «sì, corrisponde» o «no, non corrisponde») e come input naturale delle operazioni morfologiche e dell’argomento mask che molti metodi accettano.
Il vantaggio pratico del formato è la sua dimensione. Un’immagine binaria occupa un ottavo dell’ingombro di un’immagine in scala di grigi, quindi portarsi dietro una grande maschera – una scelta per pixel di quali posizioni una qualche operazione a valle dovrebbe toccare – costa poco. Il fatto che molte operazioni accettino un’immagine binaria come argomento keyword mask= è l’altra faccia dello stesso punto: il formato è piccolo, e concatenare l’output binario di una fase nell’input maschera di un’altra è un pattern di pipeline comune.
5.3.6. JPEG e PNG al confine¶
Gli oggetti Image JPEG e PNG sono diversi dagli altri nel catalogo. Non sono griglie di pixel; sono flussi di byte compressi che codificano i dati dei pixel in una forma che le operazioni a livello di pixel non possono leggere. Chiamare get_pixel() su un JPEG non restituisce il pixel in una posizione; il pixel non è scompattato da nessuna parte nel buffer perché il metodo lo recuperi.
JPEG e PNG compaiono al confine dell’elaborazione delle immagini, dove i dati dei pixel stanno lasciando o entrando nella cam in forma compressa. Salvare un frame su disco come JPEG mantiene il file piccolo; inviare un frame su una rete come JPEG mantiene la trasmissione economica; caricare un frame di riferimento da un file JPEG gli permette di stare su disco in una forma molto più piccola di quanto sarebbero i pixel grezzi. Per uno qualsiasi di questi casi d’uso la rappresentazione compressa è la risposta giusta. Per fare però una qualsiasi elaborazione effettiva su un JPEG, l’applicazione lo converte prima in un formato lavorabile – e quella conversione è dove i byte compressi vengono espansi in pixel e dove avviene effettivamente il gonfiarsi del buffer (un JPEG da 30 KB può diventare 600 KB di RGB565).
5.3.7. Conversione tra formati¶
Il percorso di conversione è ciò che cuce insieme formati diversi in un’unica pipeline. Cinque metodi della classe Image prendono un’immagine esistente e ne restituiscono una nuova in un formato diverso:
to_grayscale()produce un’immagine a un singolo byte per pixel, il formato che vogliono gli algoritmi classici.to_rgb565()produce il formato colore a due byte per pixel che parlano sia i metodi color-aware sia l’anteprima dell’IDE.to_bitmap()produce un’immagine binaria a un bit, il formato che accettano la morfologia e gli argomentimask.to_jpeg()produce un’immagine compressa in JPEG adatta al salvataggio o alla trasmissione.to_png()produce un’immagine compressa in PNG quando si preferisce una codifica senza perdita rispetto ai file più piccoli del JPEG.
Ogni conversione viene eseguita in place per impostazione predefinita: il buffer dell’immagine sorgente viene sovrascritto con il risultato convertito, e i pixel originali della sorgente sono persi dopo che la chiamata ritorna. Questa è l’opzione più economica sia per la CPU sia per la memoria, ed è la risposta giusta quando il frame sorgente non servirà per nient’altro.
Quando la sorgente serve ancora – quando una fase successiva della pipeline deve vedere il frame originale – due argomenti keyword scavalcano il comportamento in place predefinito. copy=True alloca un buffer separato per l’immagine convertita sull’heap di Python e lascia intatta la sorgente. copy_to_fb=True esegue la stessa allocazione ma la colloca nel frame buffer anziché nell’heap – che è ciò a cui un’applicazione ricorre quando l’immagine convertita deve finire nell’anteprima dell’IDE, dato che l’IDE legge dal frame buffer.
Altri due metodi producono immagini RGB565 colorate tramite una palette anziché con una conversione diretta. to_rainbow() mappa ogni valore di input a canale singolo su un colore lungo un gradiente uniforme che attraversa lo spettro visibile. to_ironbow() mappa ogni valore di input sulla palette non lineare delle termocamere che va dal nero attraverso i rossi scuri e gli arancioni fino al bianco. Entrambi sono strumenti di visualizzazione anziché di misura; l’obiettivo è rendere leggibile a colpo d’occhio un’immagine a canale singolo i cui valori grezzi sarebbero altrimenti invisibili all’occhio.
5.3.8. Dimensione del buffer¶
Un ultimo dettaglio sui formati che vale la pena rendere esplicito. size() riporta sempre la dimensione del buffer di byte, non il numero di pixel. Per i formati non compressi questa deriva direttamente dalle dimensioni e dai byte per pixel: width * height * bytes_per_pixel. Per JPEG e PNG è la dimensione del flusso compresso, che varia da frame a frame a seconda di ciò che la scena contiene. Il codice che alloca i buffer a partire da budget di byte usa size() per il primo caso; il codice che fa lo streaming di frame compressi fuori dalla cam lo legge dopo ogni compressione per sapere quanti byte contiene effettivamente il flusso.