6.1. Perché gli array¶
La classe Image è lo strumento giusto per il lavoro sui pixel perché ogni suo metodo opera direttamente sul buffer di pixel nativo della camera in un’unica chiamata veloce. La maggior parte di ciò che l’applicazione fa a un frame – soglia, ricerca di blob, rilevamento di AprilTag, filtri sui bordi – vive già lì.
Ciò che la libreria delle immagini non espone è il resto del lavoro numerico in cui si imbatte un’applicazione OpenMV:
buffer di sensori che non sono pixel – campioni ADC, assi da una IMU (unità di misura inerziale), audio del microfono,
numeri derivati dall’immagine che nessun metodo integrato restituisce – una colonna di istogramma, una fusione personalizzata di due frame, una trasformazione per pixel che il catalogo non copre,
piccola algebra lineare – la matrice di calibrazione che rettifica la lente, la rotazione che fonde l’IMU,
matematica di elaborazione del segnale – il contenuto in frequenza di un buffer di vibrazioni, lo smoothing applicato all’uscita di un sensore, un vettore di caratteristiche che un classificatore vuole come input.
Tutti questi vogliono la stessa forma: un buffer di numeri con un’operazione applicata a ogni elemento. Un ciclo for Python è il modo ovvio per scriverlo:
for i in range(len(samples)):
samples[i] = samples[i] * cal
Il ciclo funziona. È anche lento. Python è un linguaggio interpretato e ogni iterazione di un ciclo Python comporta il costo di eseguire una volta l’interprete: cercare samples, leggere l’elemento i, moltiplicare, riscrivere, avanzare il contatore del ciclo, verificare la condizione del ciclo. Su un buffer di mille campioni di un sensore questi costi dell’interprete si sommano a decine di millisecondi per quella che è fondamentalmente un’operazione rapida.
Quel sovraccarico morde ogni volta che uno script raggiunge un buffer. Un frame QVGA in scala di grigi ha 76.800 pixel; un accelerometro a 100 Hz fornisce un centinaio di campioni a tre assi al secondo; un microfono riempie un buffer da 1024 campioni ogni 64 ms. Un ciclo for in puro Python su uno qualsiasi di questi trasforma un compito che dovrebbe richiedere pochi microsecondi in uno che richiede decine di millisecondi – e all’incirca dieci volte di più ancora su un buffer della dimensione di un’immagine.
6.1.1. Le funzioni di libreria sono più veloci dei cicli¶
La soluzione è esprimere l’operazione come un’unica chiamata di funzione sull’intero buffer, invece di un ciclo Python sui suoi elementi. numpy è esattamente questo: una libreria di matematica su array in cui ogni operazione è un’unica funzione già ottimizzata che percorre il buffer una sola volta dall’inizio alla fine. np.multiply(samples, cal) moltiplica ogni elemento di samples per cal in un’unica chiamata – la stessa aritmetica che faceva il ciclo, senza il costo dell’interprete per ogni iterazione. La stessa moltiplicazione di 1000 elementi che richiedeva decine di millisecondi come ciclo Python richiede decine di microsecondi come chiamata numpy.
Questo è il patto che numpy offre a tutto campo: somma, media, seno, esponenziale, moltiplicazione tra matrici, primitive di elaborazione del segnale – ognuna è un’unica funzione di libreria che opera su un intero buffer in una volta sola. Il compromesso è che i dati devono vivere nel tipo array di numpy e l’operazione deve essere espressa rispetto a quell’array, non rispetto ai suoi elementi uno alla volta.
6.1.2. Perché una lista non basta¶
Una list Python non può sostituirla. Una lista può contenere qualsiasi misto di oggetti – interi, float, stringhe, altre liste – e una funzione di libreria che la legge deve comunque esaminare ogni slot per scoprire cosa contiene ed estrarne il valore prima che avvenga qualsiasi operazione aritmetica. Quel sovraccarico per slot è esattamente il costo che paga il ciclo Python. Le liste sono inadatte alla matematica veloce su array.
6.1.3. Perché nemmeno bytearray è sufficiente¶
Un bytearray è la forma giusta – un buffer tipizzato, un byte per elemento, tutto in un unico blocco contiguo. È ciò che la maggior parte delle API delle periferiche orientate ai byte restituisce. Ciò che manca è la matematica. bytearray * 2 ripete il buffer invece di raddoppiare ogni valore, e non c’è alcun significato sensato per bytearray + bytearray elemento per elemento.
La struttura dati che combina un buffer tipizzato con la matematica elemento per elemento è l”ndarray. Cosa c’è dentro la scatola e come ciascun campo modella il comportamento del percorso veloce sono le fondamenta su cui poggia il resto di questo capitolo.