TD – Informatique – PSI Traitement d’images 1. Notions de bases & vocabulaire Une image numérique est une fonction à support discret et borné, et à valeurs discrètes. Le support est multidimensionnel, en général 2d ou 3d. Les valeurs peuvent être scalaires (images en niveaux de gris), ou bien vectorielles (imagerie multi-composante, imagerie couleur). La gamme de valeurs possibles varie en fonction du type d'images considéré ; le Tableau 1 présente les types les plus courants, les grandeurs physiques associées et les capteurs utilisés. Phénomène physique Lumière visible Rayonnement Infra-rouge Echo ultrasonore Résonance magnétique Echo électromagnétique Absorption des rayons X Grandeur mesurée Flux photonique émis ou réfléchi Luminance Infra-rouge (Chaleur) Distance, densité de tissus,. . . Présence d'un corps chimique…. Distance, spécularité de surfaces… Densité de tissus…. Capteur CCD, CMOS, Scanner Bolomètres Echographie, sonar IRM, RMN,… Radar, SAR… Radiographie… Tableau 1 : Quelques types d’images Une image numérique est associée à un pavage de l'espace, en général rectangulaire. Chaque élément du pavage, appelé pixel, est désigné par ses coordonnées entières : Figure 1 – gauche. L'échantillonnage est le procédé de discrétisation spatiale d'une image consistant à associer à chaque pixel une unique valeur : Figure 1 – droite. On parle de sous-échantillonnage lorsque l'image est déjà discrétisée et que l’on diminue le nombre de pixels. Figure 1 Pavage (à gauche) ; Echantillonnage (à droite) La quantification désigne la discrétisation tonale correspondant à la limitation du nombre de valeurs différentes que peut prendre chaque pixel. Une image numérique est donc une image échantillonnée et quantifiée. Une image numérique 2D est représentée par un tableau T de h lignes et w colonnes. Le pixel est désigné par un couple (i,j) où j est l'indice de colonne j ∈ {0,w−1}, et i l'indice de ligne i ∈ {0,h−1}. w est la largeur, h la hauteur de l'image T. Par convention, le pixel origine (0,0) est en général en haut à gauche (voir Figure 2). Le nombre T(i,j) est la valeur (ou le niveau de gris) du pixel (i,j), T(i,j) ∈ {0,Nmax−1}. Nmax est le nombre de niveaux de gris. On appelle dynamique de l'image le logarithme en base 2 de Nmax, soit encore le nombre de bits utilisés pour coder l'ensemble des valeurs possibles. Figure 2 Conventions de notations TD – Informatique – PSI Une image numérique ne constitue donc qu'une version approchée de « l’image réelle » formée par l'image (au sens mathématique) de la projection de la scène 3D sur la portion de plan correspondant à la surface photosensible du capteur. La qualité de l'approximation dépend de la quantité d'information portée par l'image numérique, en particulier du nombre de pixels utilisés : la résolution spatiale. La Figure 3 montre un exemple d'une même image acquise à des Figure 3 Résolution spatiale : échantillonnage résolutions différentes. Le changement de résolution tonale, correspondant à la quantification, fait aussi apparaître une perte d'information dans les images : voir Figure 4, pour la représentation d'une même image avec différentes dynamiques. La bonne dynamique dépend de la qualité des éléments photosensibles du capteur, mais aussi de la richesse du contenu informationnel de l'image, qui est lié à la distribution statistique de ses valeurs. Figure 4 Résolution tonale : Quantification TD – Informatique – PSI 2. Manipulations de base sur les images en noir et blanc Nous allons nous intéresser dans cette première partie à l’image « damier-ng.jpeg » qui se trouve dans un dossier nommé « Image » disponible sur le réseau. Copier-coller ce dossier dans le répertoire de travail courant libre d’accès en écriture. Nous aurons besoin dans toute la suite des bibliothèques « numpy », « image » et « pyplot » de matplotlib. Les importer de la façon suivante : import matplotlib.image as im import matplotlib.pyplot as plt import numpy as np Commencer par ouvrir l’image en lecture : pixels = im.imread("damier-ng.jpeg") Pour afficher l’image: plt.figure(1) plt.imshow(pixels, cmap='gray') plt.show() L'image du fichier « damier-ng.jpeg » étant en noir et blanc l'argument cmap='gray' est nécessaire pour afficher l'image correctement. Q1. Que représente pixels ? Que représente uint8 ? Que se passe-t-il si on calcule la somme suivante : np.uint8(10)+np.uint8(250) Toutes les valeurs du tableau n'ont pas été affichées ci-dessus (elles ne sont pas toutes à 255). La taille du tableau est accessible par pixels.shape qui fournit dans l'ordre le nombre de lignes et le nombre de colonnes, ou par np.size(pixels,0) et np.size(pixels,1). Comme on travaille dans « numpy », il est facile grâce au concept de diffusion que propose cette librairie de modifier un ensemble de valeurs d’un tableau par une valeur unique. Par exemple : l’instruction pixels[50:250, 150:250] = 128 est équivalente à pixels[50:250, 150:250] = table où table est un tableau de la même taille que pixels[50:250, 150:250] avec toutes ses valeurs à 128. Q2. Mettre en œuvre une méthode permettant de créer une nouvelle image avec un bord gris de 50 pixels d’épaisseur tout autour de l’image de départ. Nous allons maintenant modifier l'image en inversant l'intensité des pixels. En particulier le blanc et le noir seront inversés. Q3. Réaliser cette modification et afficher l’image obtenue. Proposer une méthode alternative permettant cette inversion d’intensité de pixels, sans utiliser de boucles. 3. Matrices de convolutions Une opération courante en traitement d’image consiste à recalculer la valeur d’un pixel en fonction des valeurs des pixels environnants. Ceci s'applique par exemple lorsque l'on souhaite flouter une image. Souvent, la nouvelle valeur du pixel est une combinaison linéaire des valeurs environnantes. On peut alors décrire la transformation par une matrice. Considérons la matrice suivante : 1 2 4 2 1 1 2 4 8 4 2 𝐾= 4 8 16 8 4 100 2 4 8 4 2 (1 2 4 2 1) Hormis les pixels trop près du bord de l'image, chaque pixel est le centre d'un carré de 5 pixels de côté. Nous nous proposons de remplacer la valeur de ce pixel par la moyenne des 25 pixels du carré, en utilisant la matrice K comme coefficients de pondération. Les pixels proches du centre auront ainsi plus d'influence sur la valeur moyenne. TD – Informatique – PSI Pour cela nous allons écrire la fonction filtrer(T,K) qui prend en entrée deux tableaux Numpy : T : l’image à modifier et K : une matrice carrée de taille impaire 2n+1. La fonction filtrer(pixels,K) effectue un moyennage des pixels de pixels avec les coefficients de la matrice K. Les pixels situés sur le cadre extérieur de largeur n ne sont pas modifiés. Q4. Programmer une cette fonction filtrer(T,K) qui retournera une matrice filtrée de même taille que T, en n’oubliant pas que T est un objet mutable (il faudra donc faire une copie superficielle de la matrice T dans cette fonction…). Q5. L'image « ecole-ng.jpeg » contient une scène où apparaît le visage d'une petite fille. Modifier cette image de manière à flouter le visage. On pourra appliquer plusieurs fois de suite la fonction filtrer afin d'obtenir un flou suffisant pour masquer l'identité de la fillette. Les matrices de convolution peuvent aussi être utilisées pour repérer des contours d'objets dans une image. On peut par exemple utiliser l'une des deux matrices. −1 0 1 −1 2 −1 𝐺𝑥 = (−2 0 2) OU 𝐺𝑦 = ( 0 0 0 ) −1 0 1 1 2 1 On remarquera que les sommes des coefficients de ces matrices sont nulles. Si on applique la fonction filtrer avec elles, on ne calcule donc pas vraiment une moyenne. D'ailleurs, le tableau qui en résulte peut contenir des valeurs négatives. Il ne peut donc pas être interprété comme étant une image, du moins pas directement. Soient les lignes de programmes suivantes, saisies directement dans la console : >>> T = imread("ecole-ng.jpeg") >>> T [94:97,149:152] array([[117, 174, 251], [116, 184, 250], [119, 207, 249]], dtype=uint8) >>> T[470:473,600:603] array([[214, 212, 210], [214, 213, 212], [213, 213, 211]], dtype=uint8) >>> Gx = array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=int32) >>> Px = filtrer(T, Gx) Q6. Calculer à la main Px[95,150] et Px[471,601]. Que mesurent les valeurs du tableau Px ? Dans quel cas obtient-on de grandes valeurs ? Les valeurs calculées par filtrer(T,Gx) n'ont pas de raison de rester comprises entre 0 et 255. Le tableau retourné doit être du type « float64 ». Q7. Modifier en conséquence la fonction filtrer en remplaçant la ligne faisant la copie superficielle de la matrice initiale par la création d’une matrice remplie de zeros, de type « float64 ». Nous allons maintenant analyser le contenu du tableau Px. On exécutera pour cela les lignes de code suivantes : Px = filtrer2(pixels, Gx) plt.figure() plt.hist(Px.flat, bins=200, range=(-100, 100)) plt.figure() plt.imshow(pixels, cmap='gray') plt.figure() plt.imshow(Px, cmap='gray') plt.show() Q8. Que représentent chacune des figures obtenues ? Existe-t-il une corrélation entre elles ? L’opération de filtrage précédente a mis en évidence les régions où l’intensité varie fortement dans la direction horizontale. Nous allons maintenant procéder de même, en effectuant le calcul dans la direction verticale TD – Informatique – PSI Q9. Créer de même le tableau Py et le représenter par une image en niveau de gris. L’association des deux tableaux (Px, Py) peut être interprétée comme un vecteur qui indique la direction dans laquelle la variation d'intensité est maximale pour chaque pixel. La norme de ce vecteur correspond à cette intensité maximale. Q10. Créer alors un tableau P de même taille de Px (ou Py) dont les composantes seront les normes des vecteurs définis cidessus. Dans quel cas obtient-on de grandes valeurs dans les cellules de ce tableau et le représenter par une image en niveau de gris. Nous avons au final trouvé une méthode qui permet de détecter les contours dans une image. 4. Images en couleur Le codage informatique des couleurs est l'ensemble des conventions permettant l'affichage ou l'impression par un périphérique informatique d'une image en couleurs, plutôt qu'en noir et blanc. Le codage se base sur la synthèse additive trichrome des couleurs. On s’intéresse alors à une image codée au format RGB (Red-Green-Blue). Pour cela, on a trois images nommées « canal0.jpeg », « canal1.jpeg » et « canal2.jpeg » qui sont les trois canaux RGB d’une même image. Les canaux 0, 1 et 2 représentent respectivement les intensités de rouge, de vert et de bleu. Chaque combinaison d'intensités correspond à une couleur particulière. Q11. Observer la structure de chacune de ces images, après les avoirs ouvertes et les avoir affichées. En déduire de combien de couleurs différentes on peut coder dans le format RGB. De quelle couleur sont les manches des pinceaux ? Une image RGB complète est représentée par un tableau Numpy à trois dimensions (une dimension par canal) : >>> pixels[2, 5, 0] # Canal 0 du pixel (2, 5) 165 >>> pixels[2, 5] # Les trois canaux du pixel (2, 5) array([165, 168, 180], dtype=uint8) >>> pixels[:, :, 2] # Canal 2 complet array([[180, 179, 178, ..., 161, 160, 159], [180, 180, 180, ..., 162, 161, 160], [180, 180, 181, ..., 162, 161, 160], ..., [162, 163, 164, ..., 100, 100, 101], [165, 165, 166, ..., 96, 98, 101], [167, 167, 167, ..., 88, 94, 101]], dtype=uint8) L’objectif va être de reconstruire l’image à partir des 3 canaux. Pour cela, on va créer un tableau Numpy de taille (h,w,3), où (h,w) est la taille d’un tableau correspondant à un canal. Q12. Créer alors un tel tableau, que l’on pourra nommer pixels et l’afficher sur une nouvelle figure. (Quand on utilisera imshow, il ne faudra bien entendu ne plus spécifier cmap). Q13. Assembler de manière différente les canaux pour obtenir une image avec des manches de pinceaux jaunes. 5. Photomaton Soit l’image « mona-lisa.jpeg » contenue dans le dossier Images. L’afficher. Nous allons trouver une méthode pour réaliser la transformation suivante : TD – Informatique – PSI Sachant que l'image de droite est composée de 256×256 pixels et que l'image de gauche a été obtenue à partir de l'image de droite en déplaçant uniquement les pixels. Plus précisément : - les pixels des lignes paires sont dans la partie haute et les pixels des lignes impaires dans la partie basse ; - les pixels des colonnes paires sont dans la partie gauche et les pixels des colonnes impaires dans la partie droite. Notons (i,j) les coordonnées d’un pixel dans l’image initiale, et (u,v) les nouvelles coordonnées de ce même pixel dans l’image transformée. Q14. Si i=124 et j=91 ,combien valent u et v ? Q15. Ecrire une fonctions new_coords(i,j) prenant en argument les coordonnées i et j d’un pixel et qui retourne les nouvelles coordonnées. Q16. Ecrire la fonction photomaton(pixels) qui effectue la transformation sur le tableau pixels et qui retourne le résultat dans un nouveau tableau. Afficher la nouvelle image. Q17. Réitérer cette expérience plusieurs fois de suite et expliquer ce qui se passe. 6. Stéganographie Le fichier tranquility.png est une photographie d'un jardin dans un temple Zen. La visualiser et observer sa structure. Cependant, le fichier contient une seconde image cachée dans la première. Chaque pixel d'une image RGB est codé sur 3 octects (un par canal). Au lieu d'utiliser les 8 bits pour coder un entier quelconque entre 0 et 255, seuls les quatre bits de poids forts ont été utilisés pour coder l'image apparente. Les quatre bits de poids faibles peuvent alors être utilisés pour l'image cachée. Intéressons par exemple au canal bleu d'un pixel particulier. Supposons que sa valeur soit A=105 dans l'image apparente originale et C=96 dans l'image cachée originale. Q18. Écrire les nombres A et C en binaire. Q19. Si on met à 0 les 4 bits de poids faibles de A, quel nombre obtient-on ? Q20. Si on remplace ces quatre bits de poids faibles par les quatre bits de poids forts de C, quel nombre B obtient-on ? C'est ce nombre B qui remplace A dans le fichier « tranquility.png ». Q21. Faire apparaître l'image cachée dans le fichier « tranquility.png ». Support : - Support de cours d’A. Manzanera (ENSTA) - Sujet de TP de D. Pinsard - Sujet de D. Prevost