«Le but de ce travail est de générer automatiquement une image couleur à partir des plaques de verre numérisées de la collection Prokudin-Gorskii, et ce, avec le minimum d'artifacts visuels possible. Pour ce faire, il vous faudra extraire les trois canaux de couleurs, les chevaucher l'un «par-dessus» l'autre, et les aligner pour que leur combinaison forme une image couleur en RGB. Dans ce TP, nous ferons l'hypothèse qu'un simple modèle de translation (en x,y) est suffisant pour aligner les images correctement. Par contre, puisque les plaques de verre numérisées ont une très grande résolution, votre procédure d'alignement devra être rapide et efficace.»
L'alignement à une seule échelle permet d'aligner les 3 canaux de l'image pour une image à résolution relativement faible. Le décalage est limité à $[-10, 10]$ pixels en $x$ et $y$. L'algorithme consiste à appliquer un déplacement de toutes les permutations possibles de décalage sur le canal R et, par la suite, sur le canal G. À chaque décalage, on calcule la norme L2 entre le canal décalé et le canal B. Pour chacun des canaux (R et G), on conserve le déplacement optimal, celui avec l'erreur la plus basse.
Au niveau du code source, l'image de base est importée et divisée en 3 canaux dans le constructeur de la classe Image
. Par la suite, l'appel de la fonction Image.align()
sur cet objet effectue l'alignement à une seule échelle.
Résultats | ||
---|---|---|
L'approche à échelles multiples réutilise la technique d'alignement à une seule échelle, mais avec des images de taille réduite sur plusieurs itérations. Ceci permet de trouver le décalage optimal sans essayer toutes les possiblités et accelère grandement le processus.
L'approche, avec 4 itérations, est la suivante:
Cette approche permet de trouver le décalage optimal dans une plage de $[-150, 150]$ en $x$ et $y$ en évitant de calculer la norme L2 sur toutes les possibilités ($4\times21^2$ au lieu de $301^2$ pour chaque axe).
Images fournis | ||
---|---|---|
Images suplémentaires | ||
---|---|---|
Le script rgb2pgpy
permet de convertir trois images RGB en une image du même style que celles de Prokudin-Gorskii. Ce script a été utilisé pour produire les images personnelles suivante.
| Images personnelles |
:---: | :---: | :---:
|
|
Sur l'image de gauche, on voit que la position de la caméra c'est décalé entre chaque photo car on peut distinguer les canaux B et R. Pour l'image du centre, la caméra était plus stable et on le remarque sur la photo. Pour celle de droite, le sujet et le repère de la caméra été déplacer entre chaque photo. On remarque que les canaux sont bien alignés au centre mais moins sur les côtés. Ceci peut s'expliquer car l'algorithme d'alignement calcul la norme L2 seulement sur le centre de la photo.
Pour obtenir un gain de performance, la majorité des opérations sont effectuée sur GPU au lieu du CPU. L'approche utilisée est d'employer PyTorch au lieu de NumPy. L'idée est d'effectuer les opérations sur des PyTorch Tensors
au lieu des NumPy arrays
car ceux-ci supportent l'accélération GPU.
La difficulté avec cette approche est que plusieurs fonctions implémentées pour traiter les Numpy arrays
n'ont pas d'équivalent avec les PyTorch Tensors
. Par example, le filtre de Prewitt utilisé dans la section 4.4
est disponible depuis SciPy mais pas depuis PyTorch
. Le problème est contourné en appliquant la convolution directement sur l'image.
L'utilisation du GPU permet de réduire le temps d'éxécution de plus de 90% pour aligner une image de taille $3238\times3722$ avec l'approche à échelles multiples.
Pour obtenir une meilleure superposition des différents canaux, une transformation affine est appliquée sur le canal R et G. La transformation affine est une matrice de $2x3$ qui permet d'appliquer une translation, une mise à l'échelle, une rotation et une distorsion à l'image dépendamment de ses valeurs. Pour trouver les valeurs optimales de la matrice, une approche par descente du gradient est utilisée. En partant de la matrice identité, celle-ci est appliquée au canal et la norme L2 est calculée par rapport au canal B. Par la suite, l'erreur est rétropropagée dans la matrice initiale selon la descente de gradient. Cette étape est répétée jusqu'à convergence.
L'implémentation est relativement simple car PyTorch
permet de calculer automatiquement les gradients. Ainsi, un Model PyTorch applique la transformation affine et celui-ci possède la matrice de transformation comme paramètre. Après chaque itération, les poids de la matrice sont mis à jour.
Avant | Après |
---|---|
Pour détecter les bordures, l'approche est de conserver seulement le blanc et le noir car les coutours de l'image originale sont principalement de cette couleur. Concrètement, sur chaque canal, tous les pixels entre 0.1 et 0.9 sont mis à 0 et les autres sont mis à 1. Par la suite, on additionne les trois canaux ensemble et limite la valeur maximale d'un pixel à 1.
Pour détecter les 4 bordures, on itère de l'extérieur vers l'intérieur de l'image en se limitant à $10\%$ de la taille de l'image sur chacune des bordures. On retient, les deux colonnes et les deux lignes qui sont les plus proches du centre avec une moyenne supérieure à $0.85$.
La figure suivante montre l'image originale (gauche), la détection des bordures (centre) et le résultat après recadrage (droite).
Avant |
Détection |
Après |
---|---|---|
Avant | Après |
---|---|
On voit rapidement que le découpage n'est pas parfait et que dans plusieurs cas, l'image devrait être encore plus réduite. Ceci est souvent causé par des taches d'un autre canal sur la bordure (bordure gauche de la dernière image) mais aussi par une bordure qui n'est pas suffisamment noire ou blanche à l'origine.
Pour la correction des imperfections sur les images, l'approche est basée sur le fait que les imperfections sont souvent seulement présentes sur un seul des canaux. Pour les détecter, un filtre un Prewitt est appliqué en $x$ et $y$ individuellement sur chaque canal. Par la suite, en itèrant sur chaque canal, on soustrait les deux autres canaux sur le canal courant en appliquant une dilatation) sur les canaux soustraits. Sur le canal courant, les pixels avec valeurs positives sont considérés comme des imperfections. Ceux-ci sont remplacés par la valeur médiane depuis le canal original. L'opération est répétée pour les deux autres canaux.
La figure suivante montre les différentes étapes.
Original |
||
---|---|---|
Prewitt R |
Prewitt G |
Prewitt B |
R - (G + B) |
G - (R + B) |
B - (R + G) |
Correction |
Avant | Après |
---|---|
On remarque que cette approche est efficace pour les endroits uniformes (comme le ciel) mais moins où il y a beaucoup de détails. Ceci s'explique car, pour la détection des imperfections, un filtre de prewitt est utilisé. Celui-ci ne fait pas la distinction entre une bordure et une tache. Pour obtenir de meilleur résultat, il faut envisager un autre algorithme de détection.
De plus, on remarque que les imperfections de plus grandes tailles ne sont pas corrigées même quand elles sont détectées. Ceci s'explique par le fait qu'elles sont encore visibles sur le canal avec le filtre médian appliqué.