5.4. Lettura e scrittura dei pixel¶
La maggior parte delle operazioni su un’immagine nasconde il lavoro per singolo pixel all’interno di una singola chiamata di metodo, dove i cicli che attraversano ogni pixel avvengono a velocità nativa. Esistono però casi in cui il codice dell’applicazione vuole accedere direttamente a uno specifico pixel: per leggere ciò che si trova in una particolare posizione, per scrivervi un nuovo valore, per campionare un singolo punto per una fase di calibrazione, o per fare il debug di un valore in una posizione nota. Il modulo image espone questo livello di accesso attraverso due forme di indirizzamento, ciascuna adatta a un diverso modo di pensare a dove risiede un pixel.
5.4.1. Indirizzamento per coordinate¶
La forma più naturale è quella per cui Coordinates ha già sviluppato il vocabolario: identificare un pixel tramite le sue coordinate cartesiane (x, y). get_pixel() accetta (x, y) e restituisce il valore in quella posizione; set_pixel() accetta le stesse (x, y) insieme a un valore e lo scrive.
Ciò che queste chiamate restituiscono o accettano dipende dal formato dell’immagine. Le immagini in scala di grigi, binarie e Bayer trasportano un singolo valore per pixel – una luminosità per la scala di grigi, uno 0 o 1 per le binarie, un singolo campione di canale di colore per Bayer – quindi get_pixel() restituisce un singolo intero. RGB565 trasporta tre canali di colore impacchettati in 16 bit, e get_pixel per impostazione predefinita li scompatta in una tupla (r, g, b), con ogni canale mappato nell’intervallo 0 – 255.
Il comportamento predefinito può essere invertito da entrambe le parti. Passare rgbtuple=False a get_pixel su un’immagine RGB565 ripiega sulla parola impacchettata grezza a 16 bit – la stessa forma restituita dall’indice lineare, e la forma efficiente quando l’applicazione sta per riscrivere direttamente lo stesso valore impacchettato. Passare rgbtuple=True su un’immagine a canale singolo fa l’opposto: il valore memorizzato viene convertito in una tupla RGB888 prima di essere restituito, con le immagini Bayer che passano attraverso una fase di debayer effettuata al momento. L’argomento esiste affinché il codice chiamante possa richiedere i pixel in uno spazio di colore uniforme indipendentemente da come l’immagine sottostante li memorizza.
Le immagini compresse – JPEG e PNG – non sono supportate da get_pixel o set_pixel. I loro byte non rappresentano pixel in posizioni note, e i metodi sollevano un errore invece di restituire un valore che non avrebbe alcun significato.
In pratica i pattern hanno questo aspetto:
v = img.get_pixel(40, 30) # grayscale: int 0..255
img.set_pixel(40, 30, 255) # write white
r, g, b = img.get_pixel(40, 30) # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0)) # write red
Se le (x, y) richieste sono al di fuori dell’immagine, get_pixel restituisce None e set_pixel non fa nulla. Questo è tollerante per scelta progettuale: molti algoritmi camminano vicino ai bordi di un’immagine e indicizzano brevemente posizioni fuori dall’intervallo, e un silenzioso no-op è meno dirompente di un’eccezione ogni volta che ciò accade.
5.4.2. Indirizzamento per indice lineare¶
L’altra forma consiste nell’indirizzare i pixel tramite la loro posizione nel buffer sottostante. Si ricordi la disposizione del buffer: i pixel sono memorizzati riga per riga, prima tutti i pixel della riga superiore, poi tutti quelli della riga successiva, e così via fino in fondo. Tale disposizione significa che ogni pixel ha un singolo indice intero che conta a partire da 0 in alto a sinistra e si incrementa lungo ciascuna riga a turno. Il pixel alla coordinata (x, y) ha indice lineare y * width + x.
I pixel sono indirizzati sia tramite coordinate cartesiane (x, y) sia tramite un indice lineare che percorre il buffer riga per riga, da sinistra a destra.¶
Il modulo image espone questo indice attraverso la consueta notazione di subscript di Python: img[i] legge il pixel all’indice lineare i, img[i] = value ne scrive uno. Ciò che la forma a indice restituisce è il valore grezzo memorizzato per il formato, non la tupla scompattata che get_pixel() restituisce per impostazione predefinita. Questa distinzione è importante perché il formato scelto in precedenza determina l’aspetto del valore grezzo:
I pixel in scala di grigi e Bayer vengono restituiti come interi a 8 bit.
I pixel RGB565 e YUV422 vengono restituiti come interi a 16 bit – la parola impacchettata.
I pixel binari vengono restituiti come
0o1.I pixel JPEG e PNG vengono restituiti come interi a 8 bit, un byte alla volta dello stream compresso. Tali valori sono opachi – sono frammenti di una codifica compressa piuttosto che pixel in un senso ordinario qualsiasi.
La forma a indice si adatta al codice che ragiona già in termini di offset del buffer: un ciclo che percorre ogni pixel una volta, un algoritmo che deve saltare di una riga alla volta, o una porzione di codice che traduce tra diverse disposizioni di buffer. Il codice che ragiona in termini di coordinate x e y è servito meglio da get_pixel e set_pixel; le due forme indirizzano gli stessi pixel attraverso modelli mentali differenti.
La Image è anche iterabile. for v in img: percorre il buffer nello stesso ordine row-major, restituendo i valori grezzi un pixel alla volta, e len(img) è il conteggio dei pixel per i formati non compressi o il conteggio dei byte per gli stream compressi.
5.4.3. Perché il Python per singolo pixel è il percorso lento¶
Una nota pratica su cui vale la pena essere onesti. Percorrere un’immagine un pixel alla volta da Python è lento. Un’immagine in scala di grigi 320 × 240 contiene 76.800 pixel; chiamare get_pixel() su ciascuno di essi in un ciclo for esegue milioni di istruzioni bytecode di MicroPython per svolgere un lavoro che un metodo nativo equivalente potrebbe completare in poche centinaia di microsecondi. Non è un fattore trascurabile. È la differenza tra uno script che elabora i frame in tempo reale e uno che arranca ben al di sotto del frame rate della camera.
Quasi ogni metodo sulla superficie di Image esiste perché c’è una versione nativa più veloce di un comune pattern per singolo pixel. Un ciclo che somma due immagini insieme diventa una singola chiamata nativa. Un ciclo che leviga ogni pixel facendone la media con i suoi vicini ne diventa un altro. Un ciclo che classifica ogni pixel rispetto a una soglia ne diventa un terzo. Il compito dell’applicazione, la maggior parte delle volte, è riconoscere quale metodo sull’intera immagine corrisponde al lavoro che il ciclo avrebbe svolto, e ricorrere a quello invece di scrivere il ciclo a mano.
La lettura e la scrittura a livello di pixel sono comunque lo strumento giusto quando nient’altro è adatto – riscrivere una misurazione specifica nel buffer, campionare una posizione per una fase di calibrazione, fare il debug di un valore in una posizione nota. Il punto è che esse sono il percorso lento, da usare quando i metodi sull’intera immagine non hanno la forma di cui l’applicazione ha bisogno, e non come modo predefinito per operare sui pixel.