Mappatura in avanti e all’indietro per la visione artificiale

Forward and backward mapping for computer vision.

Mappatura in avanti e mappatura all’indietro applicate alla trasformazione di immagini

Foto di Vadim Bogulov su Unsplash

In questo articolo verranno presentati ed esplorati due algoritmi per la deformazione d’immagine: mappatura in avanti e mappatura all’indietro. Oltre a introdurre questi algoritmi a livello teorico, verranno anche applicati a immagini reali per vedere i risultati e le capacità di ogni algoritmo.

Per comprendere appieno tutto ciò che verrà spiegato in questo articolo, è necessario essere familiari con le matrici di trasformazione 2D, che sono state introdotte e spiegate nell’articolo precedente.

Trasformazioni di matrici 2D per la visione artificiale

Scaling, rotazione e traslazione tramite matrici di trasformazione per la visione artificiale

Nisoo.com

Introduzione

Come visto nell’articolo precedente, il modo di applicare le trasformazioni alle immagini consiste nell’iterare su ogni pixel dell’immagine e applicare la trasformazione su ognuno di essi individualmente. Tuttavia, ci sono alcuni casi d’uso in cui le trasformazioni non possono essere applicate direttamente, poiché ad esempio le nuove posizioni di alcuni pixel potrebbero trovarsi al di fuori del dominio dell’immagine. Un altro possibile problema è che la nuova immagine potrebbe avere pixel vuoti (strisce bianche), poiché è difficile mappare tutti i pixel dell’immagine originale su tutti i pixel della nuova immagine dopo la trasformazione.

Per evitare alcuni di questi problemi, vengono utilizzati i due algoritmi che verranno presentati in questo articolo, mappatura in avanti e mappatura all’indietro, che applicano tecniche diverse per trasformare correttamente le immagini.

Mappatura in avanti

Il processo di mappatura in avanti consiste nel semplice processo di trasformazione d’immagine che è stato discusso nell’introduzione e nell’articolo precedente: si itera su tutti i pixel dell’immagine e si applica la trasformazione corrispondente a ciascun pixel individualmente. Tuttavia, è necessario tenere conto di quei casi in cui la nuova posizione del pixel trasformato cade al di fuori del dominio dell’immagine, come mostrato nell’esempio di seguito.

I pixel dell'immagine trasformata cadono al di fuori del dominio dell'immagine originale. Immagine dell'autore

Per eseguire il processo di mappatura in avanti, si definisce innanzitutto una funzione che riceve come parametri le coordinate originali di un pixel. Questa funzione applicherà una trasformazione alle coordinate originali del pixel e restituirà le nuove coordinate del pixel dopo la trasformazione. L’esempio di codice seguente mostra la funzione per la trasformazione di rotazione.

def apply_transformation(original_x: int, original_y: int) -> Tuple[int, int]:    # Definisci la matrice di rotazione       rotate_transformation = np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0],                                      [np.sin(np.pi/4),  np.cos(np.pi/4), 0],                                      [0, 0, 1]])    # Applica la trasformazione dopo aver impostato la coordinata omogenea a 1 per il vettore originale.    new_coordinates = rotate_transformation @ np.array([original_x, original_y, 1]).T    # Arrotonda le nuove coordinate al pixel più vicino       return int(np.rint(new_coordinates[0])), int(np.rint(new_coordinates[1]))

Una volta che si ha questa funzione, è sufficiente iterare su ogni pixel dell’immagine, applicare la trasformazione e verificare se le nuove coordinate del pixel sono all’interno del dominio dell’immagine originale. Se le nuove coordinate si trovano all’interno del dominio, il pixel sulle nuove coordinate della nuova immagine assumerà il valore che il pixel originale aveva nell’immagine originale. Se cade al di fuori dell’immagine, il pixel viene omesso.

def forward_mapping(original_image: np.ndarray) -> np.ndarray:    # Crea la nuova immagine con la stessa forma di quella originale    new_image = np.zeros_like(original_image)    for original_y in range(original_image.shape[1]):        for original_x in range(original_image.shape[0]):            # Applica la rotazione alle coordinate del pixel originale            new_x, new_y = apply_transformation(original_x, original_y)            # Verifica se le nuove coordinate cadono all'interno del dominio dell'immagine            if 0 <= new_y < new_image.shape[1] and 0 <= new_x < new_image.shape[0]:                new_image[new_x, new_y, :] = original_image[original_x, original_y, :]    return new_image

Il risultato dell’applicazione di una trasformazione di rotazione con mappatura in avanti può essere visto nell’immagine sottostante, dove a sinistra si trova l’immagine originale e a destra l’immagine trasformata. È importante notare che per questa immagine l’origine delle coordinate si trova nell’angolo in alto a sinistra, quindi l’immagine ruota intorno a quel punto in senso antiorario.

Risultati dell'applicazione della mappatura in avanti. Immagine a sinistra estratta dal dataset MNIST [1]. Immagine completa dell'autore

Riguardo al risultato della trasformazione, si può notare come l’immagine trasformata non abbia lo sfondo completamente nero dell’originale, ma invece presenti molte strisce bianche. Questo accade, come già detto nell’introduzione, perché i pixel dell’immagine originale non vengono sempre mappati su tutti i pixel della nuova immagine. Poiché le nuove coordinate sono calcolate arrotondando al pixel più vicino, ne risulta che molti pixel intermedi non ricevono mai un valore. In questo caso, poiché la nuova immagine è inizializzata con tutti i pixel vuoti, i pixel che non hanno ricevuto un valore durante la trasformazione rimarranno vuoti, generando quelle strisce bianche nell’immagine trasformata.

Inoltre, va notato che c’è un altro problema notevole: sovrapposizioni. Questo problema si verifica quando due pixel dell’immagine originale vengono trasformati nello stesso pixel della nuova immagine. Per il codice utilizzato in questo articolo, se ci sono due pixel dell’immagine originale che vengono mappati sullo stesso pixel della nuova immagine, il nuovo pixel prenderà il valore dell’ultimo pixel originale che è stato trasformato, sovrascrivendo il valore del primo che era già stato impostato.

Mappatura all’indietro

L’algoritmo di mappatura all’indietro nasce dalla necessità di eliminare le strisce bianche che si generano nell’immagine a causa della trasformazione, così come le possibili sovrapposizioni. Come detto, queste strisce appaiono quando non tutti i pixel dell’immagine trasformata assumono un valore, a causa dell’arrotondamento durante il calcolo delle nuove coordinate nel processo di mappatura in avanti, e le sovrapposizioni si verificano quando due o più pixel dell’immagine originale vengono mappati sullo stesso pixel della nuova immagine.

La logica di questo algoritmo è semplice: invece di trasformare ogni pixel dell’immagine originale nelle sue nuove coordinate nella nuova immagine (in avanti), questa volta tutti i pixel della nuova immagine vengono trasformati inversamente in pixel dell’immagine originale (all’indietro). In questo modo, non ci sarà mai alcun pixel senza valore nella nuova immagine, poiché tutti prenderanno il valore di un singolo pixel dell’immagine originale, risolvendo entrambi i problemi.

Felizmente, la trasformazione che viene applicata alle coordinate di un pixel utilizzando una matrice di trasformazione può essere annullata seguendo lo stesso processo con la matrice di trasformazione inversa. Questa proprietà delle matrici di trasformazione, insieme alla sua prova, può essere vista nella figura sottostante.

Trasformazioni con matrice di trasformazione inversa e prova. Immagine dell'autore

Tenendo conto di questa proprietà, l’algoritmo consiste nell’iterare su ogni pixel della nuova immagine e applicare la trasformazione inversa a ciascuna delle coordinate di questi pixel per sapere da quale pixel dell’immagine originale devono prendere il valore.

def apply_inverse_transformation(new_x: int, new_y: int) -> Tuple[int, int]:    # Definire la matrice di rotazione inversa    rotate_transformation = np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0],                                      [np.sin(np.pi/4),  np.cos(np.pi/4), 0],                                      [0, 0, 1]])    inverse_rotate_transformation = np.linalg.inv(rotate_transformation)    # Applicare la trasformazione dopo aver impostato la coordinata omogenea a 1 per il vettore di posizione.    original_coordinates = inverse_rotate_transformation @ np.array([new_x, new_y, 1]).T    # Arrotondare le coordinate originali al pixel più vicino    return int(np.rint(original_coordinates[0])), int(np.rint(original_coordinates[1]))

Si noti che la funzione apply_inverse_transformation() prende in input le coordinate nella nuova immagine e restituisce le coordinate nell’immagine originale, invece di ricevere le coordinate originali e restituire quelle nuove, come nel caso della mappatura in avanti.

def backward_mapping(original_image: np.ndarray) -> np.ndarray:    # Creare la nuova immagine con lo stesso formato dell'originale    new_image = np.zeros_like(original_image)    for new_y in range(new_image.shape[1]):        for new_x in range(new_image.shape[0]):            # Applicare la rotazione inversa alle coordinate del nuovo pixel            original_x, original_y = apply_inverse_transformation(new_x, new_y)            # Verificare se le coordinate originali cadono all'interno del dominio dell'immagine            if 0 <= original_y < original_image.shape[1] and 0 <= original_x < original_image.shape[0]:                new_image[new_x, new_y, :] = original_image[original_x, original_y, :]    return new_image

Il risultato dell’applicazione della trasformazione di rotazione con backward mapping può essere visto nell’immagine qui sotto, dove a sinistra si trova l’immagine originale, e a destra l’immagine trasformata. Come già anticipato, l’immagine ruota intorno all’origine delle coordinate, situata nell’angolo in alto a sinistra.

Risultati dell'applicazione di Backward Mapping. Immagine a sinistra estratta dal dataset MNIST [1]. Immagine completa dell'autore

Nell’immagine si può notare come tutte le strisce bianche che sono apparse con l’applicazione del forward mapping siano scomparse con il backward mapping. Infatti si può vedere come la qualità dell’immagine trasformata sia piuttosto buona (dobbiamo tenere presente che la qualità dell’immagine originale non è molto buona), quindi possiamo considerare l’algoritmo di backward mapping molto migliore del forward mapping per quei casi in cui durante la trasformazione compaiono strisce bianche.

Conclusioni

Il forward mapping è un algoritmo semplice da implementare e facile da capire, poiché consiste nella trasformazione diretta di ogni pixel dell’immagine originale in quella nuova. Tuttavia, questo algoritmo presenta il problema dell’overlapping e il problema di lasciare molti pixel senza valore, il che abbassa considerevolmente la qualità dell’immagine trasformata. L’algoritmo di backward mapping, la cui implementazione è altrettanto semplice come quella del forward mapping, ha risultati molto migliori e risolve entrambi i problemi, poiché fornisce un singolo valore a tutti i pixel della nuova immagine.

Per quanto riguarda il tempo di esecuzione degli algoritmi, entrambi hanno la stessa complessità, quindi in generale sarà sempre una buona idea utilizzare l’algoritmo di backward mapping, a causa dei suoi migliori risultati. In uno scenario ideale, la funzione responsabile dell’applicazione delle singole trasformazioni su ogni pixel (chiamata apply_transformation() e apply_inverse_transformation() in questo articolo) non costruirà la matrice di trasformazione, ma la riceverà come parametro. Questo risparmierà il tempo di esecuzione necessario dall’algoritmo di forward mapping per costruire la matrice di trasformazione, e dall’algoritmo di backward mapping per costruire la matrice e invertirla.

In conclusione, l’algoritmo di backward mapping ottiene ottimi risultati rispetto al forward mapping, entrambi avendo praticamente lo stesso tempo di esecuzione. Tuttavia, va notato che entrambi gli algoritmi impiegano molto tempo per effettuare la trasformazione per immagini ad alta risoluzione, anche se sono ancora molto utili per stabilire le basi su cui sono costruiti altri algoritmi di trasformazione più potenti.

Riferimenti

[1] http://yann.lecun.com/exdb/mnist/