3.11 TRAITEMENT D‘IMAGES

 

 

INTRODUCTION

 

Les humains interprètent une image comme une surface plane généralement rectangulaire comportant des formes colorées. En informatique et dans l’industrie de l’impression, une image est plutôt vue comme une grille de points colorés appelés pixels. Le nombre de pixels par unité de surface est appelé résolution de l’image et est souvent indiquée en ppp (points par pouces). En anglais, on parle de dpi (dots per inch).

Pour pouvoir stocker et traiter une image dans un ordinateur, il faut définir les couleurs par des valeurs numériques. Il y a plusieurs possibilités de le faire qui sont appelés modèles de couleurs ou systèmes colorimétriques . Un des modèles les plus populaires est RGB qui représente l’intensité de chacune des trois couleurs fondamentales rouge, vert et bleu par une échelle comprise entre 0 (pas de couleur) et 255 (intensité maximale). [plus...Ce modèle correspond à la perception des couleurs par l’œil humain.
La rétine est en effet formée de cellules photosensibles appelées
cônes qui sont sensibles au rouge, au vert et au bleu
]. Le modèle ARGB inclut encore un autre nombre compris entre 0 et 255 qui indique la transparence (valeur alpha) de la couleur [plus...La couleur d’un pixel est alors codée sur 32 bits : bits 0..7=bleu, bits 8..15=vert,
bits 16..23=rouge, bits 24..31=valeur alpha
].

En bref : une image est représentée dans un ordinateur comme un tableau bidimensionnel de nombres représentant chacun la couleur d’un pixel. Cette représentation d’une image est appelée un bitmap.

CONCEPTS DE PROGRAMMATION: Numérisation d’images, résolution, modèles de couleur, bitmap, format d’images.

 

 

MÉLANGES DE COULEURS DANS LE MODÈLE

 

TigerJython met à disposition des objets de type GBitmap pour simplifier la manipulation d’images bitmap. L’instruction bm = GBitmap(width, height) génère un bitmap comportant le nombre indiqué de pixels en hauteur et en largeur. Il est ensuite possible de modifier la couleur de chacun des pixels de manière individuelle avec la méthode setPixelColor(x, y, color) et de lire leur couleur avec la méthode getPixelColor(x, y). Finalement, la méthode image() permet de dessiner le bitmap dans un canevas GPanel.

 

Le programme ci-dessous utilise un objet GBitmap pour dessiner les fameux disques de la synthèse additive des trois couleurs fondamentales en parcourant le bitmap avec une boucle imbriquée. 

from gpanel import *

xRed = 200
yRed = 200
xGreen = 300
yGreen = 200
xBlue = 250
yBlue = 300

makeGPanel(Size(501, 501))
window(0, 501, 501, 0)    # y axis downwards
bm = GBitmap(500, 500)
for x in range(500):
  for y in range(500):
      red = green = blue = 0
      if (x - xRed) * (x - xRed) + (y - yRed) * (y - yRed) < 16000:
         red = 255
      if (x - xGreen) * (x - xGreen) + (y - yGreen) * (y - yGreen) < 16000:
         green = 255
      if (x - xBlue) * (x - xBlue) + (y - yBlue) * (y - yBlue) < 16000:
         blue = 255
      bm.setPixelColor(x, y, makeColor(red, green, blue))

image(bm, 0, 500)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Les couleurs sont définies par leurs composantes de rouge, vert et bleu. La fonction makeColor(red, green, blue) permet de combiner ces composantes pour former un objet couleur.

Les images utilisent typiquement un système de coordonnées où l’origine se trouve au coin supérieur gauche, l’axe y pointant vers le bas [plus... il est possible d’avoir des erreurs d’arrondis car GPanel utilise des coordonnées flottantes].

 

 

CREER UNE IMAGE EN NIVEAUX DE GRIS

 

Vous vous êtes peut-être déjà demandé comment un logiciel de traitement d’images tel que Photoshop fonctionne. On va voir ensemble quelques notions de base du traitement d’images qui permettront de comprendre les principes élémentaires. Le programme suivant va transformer une image colorée en nuances de gris. On utilise pour ce faire la moyenne des composantes de rouge, vert et bleu pour déterminer la valeur de la nuance de gris.

from gpanel import *

size = 300

makeGPanel(Size(2 * size, size))
window(0, 2 * size, size, 0)    # y axis downwards
img = getImage("sprites/colorfrog.png")
w = img.getWidth()
h = img.getHeight()
image(img, 0, size)
for x in range(w):
    for y in range(h):
        color = img.getPixelColor(x, y)
        red = color.red
        green = color.green
        blue = color.blue
        intensity = (red + green + blue) / 3
        gray = makeColor(intensity, intensity, intensity)
        img.setPixelColor(x, y, gray)
image(img, size, size)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

On peut déterminer les composantes d’un objet couleur en utilisant les méthodes color.red, color.green, color.blue

Le fond doit être blanc et non transparent. Pour permettre la transparence, on peut déterminer la valeur de transparence avec alpha = getAlpha() et utiliser ensuite cette valeur comme paramètre supplémentaire de makeColor(red, green, blue, alpha).

 

 

RÉUTILISABILITÉ

 

Dans la plupart des programmes de traitement d’images, l’utilisateur doit être en mesure de sélectionner une portion rectangulaire de l’image. Pour cela, on peut dessiner un rectangle temporaire « élastique » en glissant la souris. Lorsque le bouton de la souris est relâché, la zone est définitivement choisie. Un programmeur avisé commencera par résoudre ce problème récurrent avant de s’attaquer au développement du logiciel dans son ensemble puisque cette fonctionnalité sera réutilisée pour implémenter de nombreuses fonctions et applications différentes. La réutilisabilité est un des critères de qualité majeurs dans tous les domaines du développement logiciel.

Comme nous l’avons déjà vu, le dessin de lignes « élastiques » peut être considéré comme une animation. Dans le cas qui nous intéresse, cela impliquerait cependant de redessiner l’image dans sa totalité à chaque changement du rectangle de sélection. Une astuce très performante pour éviter cela consiste à utiliser le mode de dessin XOR. Ce mode a la particularité de combiner la figure en cours de dessin avec les pixels déjà présents dans le canevas. Deux dessins successifs de la même figure en mode XOR la font disparaître sans pour autant affecter les pixels sous-jacents. Le petit bémol de ce procédé réside dans le changement de couleur incontrôlable pendant le dessin, ce qui ne se remarque toutefois presque pas pour une surface aussi faible que le bord d’un rectangle de sélection.

Dans le code ci-dessous, la fonction doIt() est appelée seulement après que le rectangle de sélection a été déterminé. Elle écrit les coordonnées du coin supérieur gauche du rectangle de sélection, ulx et uly (upper left x, y) et de son coin inférieur droit lrx, lry (lower right x, y). Cette fonction doIt() accueillera ultérieurement le code du traitement à appliquer sur la sélection.

Vous devriez être en mesure de comprendre sans problème ce code avec vos connaissances préalables à propos des événements.
 
from gpanel import *

size = 300

def onMousePressed(e):
    global x1, y1
    global x2, y2
    setColor("blue")
    setXORMode(Color.white) # set XOR paint mode
    x1 = x2 = e.getX()
    y1 = y2 = e.getY()

def onMouseDragged(e):
    global x2, y2
    rectangle(x1, y1, x2, y2) # erase old
    x2 = e.getX()
    y2 = e.getY()
    rectangle(x1, y1, x2, y2) # draw new

def onMouseReleased(e):
    rectangle(x1, y1, x2, y2) # erase old
    setPaintMode() # establish normal paint mode
    ulx = min(x1, x2)
    lrx = max(x1, x2)
    uly = min(y1, y2)
    lry = max(y1, y2)
    doIt(ulx, uly, lrx, lry)

def doIt(ulx, uly, lrx, lry):
    print("ulx = ", ulx, "uly = ", uly)
    print("lrx = ", lrx, "lry = ", lry)
    
x1 = y1 = 0
x2 = y2 = 0

makeGPanel(Size(size, size), 
    mousePressed = onMousePressed, 
    mouseDragged = onMouseDragged, 
    mouseReleased = onMouseReleased)
window(0, size, size, 0)    # y axis downwards

img = getImage("sprites/colorfrog.png")
image(img, 0, size)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

On peut récupérer le bitmap d’une image enregistrée sur l’ordinateur avec la fonction getImage() qui demande soit le chemin d’accès complet au fichier ou uniquement un chemin relatif par rapport au dossier contenant le programme Python. Pour charger les images se trouvant dans l’archive JAR de la distribution TigerJython, il faut utiliser le dossier spécial sprites.

Dans le gestionnaire de l’événement de clic de la souris, on bascule le système de dessin en XOR mode pour permettre au gestionnaire de glissé de la souris onMouseDragged de supprimer le vieux rectangle en le redessinant une deuxième fois pour ensuite le redessiner avec les nouvelles coordonnées de la souris. Les coordonnées x1, y1, x2, y2 doivent être stockées dans des variables globales pour permettre la communication entre les gestionnaires d’événements. D’autre part, si l’on redessinait le rectangle de sélection lorsque le bouton de la souris est relâché avant de repasser au mode de dessin normal, il disparaîtrait. Il faut donc commencer par repasser au mode de dessin normal pour éviter qu’il ne disparaisse.

Le programme est assez flexible pour retourner les coordonnées ulx,uly et lrx, lry quelle que soit la manière de dessiner le rectangle de sélection, même si l’on commence par exemple par le coin supérieur droit. Ainsi, on aura toujours ulx < lrx et uly < lry. Notez que le programme n’effectue aucune conversion entre les coordonnées de la souris (mouse coordinates) et les coordonnées fenêtre (window coordinates) puisque les deux systèmes coïncident dans le mesure où l’on choisit une taille identique pour la fenêtre avec size() et le système de coordonnées avec window().Même lorsque la souris est déplacée à l’extérieur de la fenêtre, le programme continue de recevoir des événements de type « glissé de la souris ». Il faut être très prudent à l’utilisation de ces coordonnées extérieures à la fenêtre, sans quoi le programme pourrait planter de manière inattendue.

 

 

SUPPRESSION DES YEUX ROUGES

 

Le traitement d’image joue un rôle très important dans la retouche de photos prises avec un appareil numérique. Internet regorge de tels programmes mais pourrez bientôt vous en passer et, armés de Python,  d’une bonne dose d’imagination et de persévérance, développer des programmes qui correspondent exactement à vos besoins. Le programme ci-dessous supprime l’effet des yeux rouges sur une photo numérique survenant à cause de la réflexion du flash sur le fond d’œil (fundus oculi). On utilisera en l’occurrence l’image d’une grenouille qui est intéressante puisqu’elle comporte d’autres zones rouges que les yeux.

 


from gpanel import *

size = 300

def onMousePressed(e):
    global x1, y1
    global x2, y2
    setColor("blue")
    setXORMode("white")
    x1 = x2 = e.getX()
    y1 = y2 = e.getY()

def onMouseDragged(e):
    global x2, y2
    rectangle(x1, y1, x2, y2) # erase old
    x2 = e.getX()
    y2 = e.getY()
    rectangle(x1, y1, x2, y2) # draw new

def onMouseReleased(e):
    rectangle(x1, y1, x2, y2) # erase old
    setPaintMode()
    ulx = min(x1, x2)
    lrx = max(x1, x2)
    uly = min(y1, y2)
    lry = max(y1, y2)

    doIt(ulx, uly, lrx, lry)    

def doIt(ulx, uly, lrx, lry):
    for x in range(ulx, lrx):
        for y in range(uly, lry):
            col = img.getPixelColor(x, y)
            red = col.red
            green = col.green
            blue = col. blue
            col1 = makeColor(3 * red / 4, green, blue)
            img.setPixelColor(x, y, col1)
    image(img, 0, size)
        
x1 = y1 = 0
x2 = y2 = 0

makeGPanel(Size(size, size), 
    mousePressed = onMousePressed, 
    mouseDragged = onMouseDragged, 
    mouseReleased = onMouseReleased)
window(0, size, size, 0)    # y axis downwards

img = getImage("sprites/colorfrog.png")
image(img, 0, size)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Le code effectuant le traitement de l’image est contenu dans la fonction doIt(). Le reste du code n’est que réutilisation des programmes précédents. Le traitement se fait simplement en parcourant tous les pixels de la zone rectangulaire sélectionnée et en diminuant de 25% la composante rouge. Remarquez le double slash qui représente une division entière permettant d’obtenir à nouveau un nombre entier.
Le programme présente quelques bugs dérangeants facilement corrigeables. Premièrement, le traitement altère les zones qui ne sont pas rouges et fait par exemple virer le blanc vers le bleu. Deuxièmement, il plante lorsque le rectangle de sélection est tiré en dehors de la fenêtre.

Il serait évidemment très pratique que le programme détecte lui-même la zone à laquelle appliquer le filtre des yeux rouges. Réaliser une telle fonctionnalité demande cependant d’être un barbu du traitement d’images car il faut pour cela que le programme soit capable d’analyser l’image et d’effectuer une certaine reconnaissance de forme qui est un problème difficile en informatique [plus... La reconnaissance d’image est une branche de la reconnaissance de formes, également appelée reconnaissance de motifs (pattern recognition)].

 

 

COUPER ET STOCKER DES IMAGES

 

Découper des portions d’une image fait également partie des fonctions de base d’un programme de traitement d’images. Le programme suivant permet de sélectionner une zone rectangulaire de l’image qui sera ensuite copiée dans une autre fenêtre et enregistrée au format JPEG une fois le bouton de la souris relâché.


 

from gpanel import *

size = 300

def onMousePressed(x, y):
    global x1, y1
    global x2, y2
    setColor("blue")
    setXORMode("white")
    x1 = x2 = int(x)
    y1 = y2 = int(y)

def onMouseDragged(x, y):
    global x2, y2
    rectangle(x1, y1, x2, y2) # erase old
    x2 = int(x)
    y2 = int(y)
    rectangle(x1, y1, x2, y2) # draw new

def onMouseReleased(x, y):
    rectangle(x1, y1, x2, y2) # erase old
    setPaintMode()
    ulx = min(x1, x2)
    lrx = max(x1, x2)
    uly = min(y1, y2)
    lry = max(y1, y2)
    doIt(ulx, uly, lrx, lry)    

def doIt(ulx, uly, lrx, lry):
    width = lrx - ulx
    height = lry - uly
    if ulx < 0 or uly < 0 or lrx > size or lry > size:
        return
    if width < 20 or height < 20:
        return
    
    cropped = GBitmap.crop(img, ulx, uly, lrx, lry)
    p = GPanel(Size(width, height))  # another GPanel
    p.window(0, width, 0, height)
    p.image(cropped, 0, 0)
    rc = save(cropped, "mypict.jpg", "jpg") 
    if rc:
        p.title("Saving OK")
    else:
        p.title("Saving Failed")

    
x1 = y1 = 0
x2 = y2 = 0

makeGPanel(Size(size, size), 
    mousePressed = onMousePressed, 
    mouseDragged = onMouseDragged, 
    mouseReleased = onMouseReleased)
window(0, size, size, 0)    # y axis downwards

img = getImage("sprites/colorfrog.png")
image(img, 0, size)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Il est possible d’afficher plusieurs fenêtres GPanel en créant plusieurs objets GPanel. Pour spécifier dans laquelle il faut effectuer les instructions de dessin, il faut invoquer les opérations graphiques avec l’opérateur point. Si la région sélectionnée est trop petite, par exemple si on clique avec la souris sans la déplacer, ou si elle se situe en partie en dehors de la fenêtre, la fonction doIt() se termine avec une instruction return vide.

Pour sauvegarder une image, on peut utiliser la méthode save() où le dernier paramètre détermine le format de l’image. Les valeurs permises sont "bmp", "gif", "jpg", "png".

 

 

EXERCICES

 

1.


Développer un programme qui inverse les composantes rouge et vert de colorfrog.png.

 


2.


Développer un programme permettant de tourner l’image en glissant la souris. Utiliser la fonction atan2(y, x) qui renvoie l’angle (en radians) entre le segment OP et l’horizontale pour n’importe quel point P(x, y) du canevas.  Il ne faut pas oublier de convertir les radians en degrés avec la fonction degrees du module math avant de pouvoir tourner l’image avec GBitmap.scale().

Prendre l’image colorfrog.png comme image de test.
 



3.


Développer un programme de retouche de photos permettant de mémoriser la couleur du pixel survolé par la souris lors d’un clic (pipette de couleur). Ensuite, chaque glissé de la souris devrait dessiner un disque rempli de la couleur mémorisée, dont le centre correspond au premier clic et tel que le point auquel la souris est relâchée se trouve sur le bord du cercle. Il faudra faire usage des événements press, drag, et click events. Utiliser à nouveau colorfrog.png comme image de test. Écrire les trois composantes fondamentales de la couleur mémorisée dans la barre de titre de la fenêtre.

 

 

   

MATÉRIEL BONUS


 

FILTRER DES IMAGES PAR CONVOLUTION

 

Vous connaissez certainement des programmes de traitement d’images mettant à disposition de nombreux filtres tels que le lissage, l’accentuation des contours, le floutage, etc … L’implémentation de ces filtres fait systématiquement appel à une opération mathématique appelée convolution sur laquelle vous pouvez sans problème vous renseigner plus en détails sur le Web [plus... Cla convolution est un principe mathématique utilisé très souvent en mathématiques, dans les sciences naturelles ou dans les sciences de l’ingénieur]. In this process, you change the color values of each pixel by calculating a new value from it and its eight neighboring pixels, according to a filtering rule.

Voici une explication plus précise du fonctionnement de la convolution sur une image en nuances de gris où chaque pixel possède une valeur de gris v comprise entre 0 et 255. La règle de filtrage est définie par neuf nombres disposés en carré :

m00   m01   m02
m10   m11   m12
m20   m21   m22

Cette représentation est appelée matrice de convolution (ou masque). En Python, cette dernière est implémentée comme une liste de lignes de la matrice, chaque ligne étant elle-même représentée par une liste :

mask = [[0, -1, 0], [-1, 5, 1], [0, -1, 0]]

Cette structure de données offre un accès facile à chaque élément de la matrice de convolution avec un double indice. Par exemple m12 = mask[1][2] = 1. Ces neuf nombres sont utilisés comme des facteurs de pondération entre le pixel central en cours de traitement et ses huit voisins qui permettent de calculer la nouvelle valeur vnew que prendra le pixel en fonction des valeurs actuelles v(x, y) du pixel et de ses huit voisins. Le calcul est effectué de la manière suivante :

 

vnew(x, y) = m00 * v(x - 1, y -1) + m01 * v(x, y - 1) + m02 * v(x + 1, y - 1) +
  m10 * v(x - 1, y) + m11 * v(x, y) + m12 * v(x + 1, y) +
  m20 * v(x - 1, y + 1) + m21 * v(x , y + 1) + m22 * v(x + 1, y + 1)

Pour simplifier, on pourrait dire que pour calculer la valeur de gris d’un pixel (rouge sur le schéma), on place le centre de la matrice de convolution au-dessus du pixel à recalculer, que l’on multiplie chacun de ses éléments avec la valeur de gris du pixel sous-jacent et que l’on fait la somme de ces neufs produits.
Le programme suivant effectue cette opération de convolution pour chacun des pixels excepté ceux qui se trouvent dans les bords et enregistre les valeurs de gris résultant dans un nouveau bitmap qui est ensuite affiché. Pour cela, la matrice de convolution est déplacée ligne par ligne, de gauche à droite et du haut vers le bas à l’intérieur d’une boucle for. La matrice de convolution utilisée correspond à un filtre d’accentuation de la netteté appliqué à l’image frogbw.png de la grenouille.

from gpanel import *

size = 300

makeGPanel(Size(2 * size, size))
window(0, size, size, 0)    # y axis downwards

bmIn = getImage("sprites/frogbw.png")
image(bmIn, 0, size)
w = bmIn.getWidth()
h = bmIn.getHeight()
bmOut = GBitmap(w, h)

#mask = [[1/9, 1/9, 1/9], [1/9, 1/9, 1/9], [1/9, 1/9, 1/9]]  # smoothing
mask = [[ 0, -1,  0], [-1,  5, -1], [0,  -1,  0]] #sharpening
#mask = [[-1, -2, -1], [ 0,  0,  0], [ 1,  2,  1]] #horizontal edge extraction
#mask = [[-1,  0,  1], [-2,  0,  2], [-1,  0,  1]] #vertical edge extraction

for x in range(0, w):
    for y in range(0, h):
        if x > 0 and x < w - 1 and y > 0 and y < h - 1:
            vnew = 0
            for k in range(3):
                for i in range(3):
                    c = bmIn.getPixelColor(x - 1 + i, y - 1 + k)
                    v = c.getRed()
                    vnew +=  v * mask[k][i]
            # Make int in 0..255        
            vnew = int(vnew)
            vnew = max(vnew, 0)
            vnew = min(vnew, 255)
            gray = Color(vnew, vnew, vnew)
        else:
            c = bmIn.getPixelColor(x, y)
            v = c.getRed()
            gray = Color(v, v, v)
        
        bmOut.setPixelColor(x, y, gray)

image(bmOut, size / 2, size)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Lors d’une convolution, la valeur du pixel central est remplacée par une moyenne pondérée de la valeur actuelle du pixel et celle de ses huit voisins. La pondération des différents facteurs est indiquée par la matrice de convolution qui détermine le type de filtre dont il s’agit.

 

Pourquoi ne pas expérimenter avec les matrices de convolution bien connues ci-dessous ou inventer vos propres matrices de convolution ?

Type de filtre

Matrice de convolution

Filtre de netteté

Filtre de lissage

Filtre de détection de contours horizontal

Filtre de détection de contours vertical