Skip to content
Snippets Groups Projects
Select Git revision
  • 5dbc3e3559712103f0b87b641bfaebecec0e6cee
  • master default protected
2 results

math-multiplication-rec.py

Blame
  • Forked from Vuillemot Romain / INF-TC1
    Source project has a limited visibility.
    INF-TC1-td04.ipynb 26.65 KiB

    NAME:

    INF TC1 - TD4 (2h) - Images


    RAPPELS SUR L'UTILISATION DES NOTEBOOKS

    Comment utiliser ces notebooks ?

    Le but de votre travail est de répondre aux questions des exercices en remplissant certaines cellules de ce notebook avec votre solution. Ces cellules, une foit remplies et lancées au fur et à mesure de vos avancées, permettront de valider des tests écrits dans d'autres cellules de ce notebook. Il est donc important de bien suivre les instructions et répondre aux questions dans l'ordre, et ne pas changer le nom des fonctions et/ou les cellules. En particulier :

    1. Répondez aux questions dans les cellules en dessous des questions.

    2. Votre code devra remplacer le texte suivant :

    # YOUR CODE HERE
    raise NotImplementedError()
    

    (vous pouvez effacer ces deux lignes quand vous les rencontrez mais ne modifiez pas les noms de fonctions sinon les tests ne marchent plus).

    1. Exécuter enfin les cellules dans leur ordre d'apparition, de haut en bas et si votre code est correct alors les tests (sous forme d'assert seront validés (ils ne lanceront pas d'exception du type AssertionError ). Vous pouvez lancer plusieurs fois la même cellule, cela ne pose pas de soucis.

    2. Vous pouvez créer de nouvelles cellules comme bon vous semble.

    En cas de problème, une solution est de relancer les cellules depuis le début du notebook une par une. Pensez à bien sauvegarder ce notebook et ne pas le remplacer par un notebook qui a le même nom.

    Objectif du TD

    Ce TD vous fera manipuler des images en Python, et réaliser un algorithme de remplissage basé sur le contenu. Nous verrons en particulier la structure de données matricielle et les méthodes de parcours associées. Enfin ce TD sera une préparation au TD 5 qui fera l'objet d'un rendu à réaliser à partir des concepts et du code abordés.

    IMPORTANT : Dans le cadre de ce TD, nous n'autorisons pas l'utilisation des modules OpenCV ou NumPy (ou toute fonction de Pillow sauf celles indiquées).

    Exercice 1 : Charger une image et dessiner

    Une image en informatique est stockée sous forme d'une matrice de pixels qui contiennent les couleurs. Le model classique de couleurs est dit "RGB" (Red, Green, Blue) (doc) où chaque pixel contient une information colorimétrique encodée sous forme de triplets (r, g, b) (red, green, blue). Les valeurs de couleur peuvent varier de 0 à 255 pour chaque composante de couleur. Par exemple le rouge est encodé en (255, 0, 0), le gris en (128, 128, 128), etc. Ces couleurs sont organisées en matrice de dimension égale à celle de l'image, les couleurs sont indépendantes les unes des autres.

    Dans ce TD nous allons utiliser un module Python appelé PIL (Pillow doc). Ce module permettra également de créer des images. Il est possible de l'initialiser comme suit pour charger une image dans une variable px dite d'accès de pixel PixelAccess (doc). Le module Pillow est normalement installé, si ce n'est pas le cas, vous devez exécuter la commande suivante dans une fenêtre de terminal Anaconda (Menu Démarrer / Anaconda 64bit / Anaconda PowerShell Prompt) : pip3 install Pillow.

    Pour tester sur le module est présent sur votre ordinateur, charger une image comme suit :

    from PIL import Image
    from IPython.display import display
    
    im = Image.open("lyon.png")
    im = im.convert("RGB")  # important pour bien avoir 3 couleurs
    px = im.load()         
    
    W, H = im.size          # taille de l'image
    r, g, b = px[10, 20]    # on récupère un pixel
    px[10, 21] = r, g, b    # on change un pixel
    
    im = im.resize((W//2, H//2))
    
    display(im)             # on affiche l'image dans la cellule
    # im.show()             # on affiche l'image si vous n'utilisez pas de notebook

    Vous pouvez également créer une nouvelle image im2 vide (noire) de taille identique à im :

    im2 = Image.new('RGB', (im.width, im.height)) 
    px2 = im2.load()
    display(im2)

    Question 1.1 - Définissez une fonction de lecture qui renvoie la couleur d'un pixel à la position d'une image donnée. Inspirez vous du code précédent ou de la documentation Pillow (doc)).

    def getPixel(x: int, y: int, px) -> tuple:
        # YOUR CODE HERE
        raise NotImplementedError()
    assert getPixel(0, 0, px) == (69, 119, 170) # bleu
    assert getPixel(0, 0, px2) == (0, 0, 0) # noir

    Afin de vérifier visuellement votre résultat, nous vous fournissons la fonction draw_rectangle qui permet de dessiner la couleur d'un pixel dans une cellule :

    from IPython.display import display, HTML
    
    def draw_rectangle(rgb_color):
        color = f'rgb({rgb_color[0]}, {rgb_color[1]}, {rgb_color[2]})'
        html = f'<svg width="100" height="100"><rect width="100" height="100" fill="{color}" /></svg>'
    
        display(HTML(html))
    # utilisation : draw_rectangle((69, 119, 170)) 
    draw_rectangle(getPixel(0, 0, px)) # bleu
    draw_rectangle(getPixel(0, 0, px2)) # noir

    Question 1.2 - Définissez une fonction d'écriture d'un pixel à une position d'une image avec une couleur donnée en argument sous forme de tuple .

    def setPixel(x: int, y:int, color: tuple, px) -> None:
        # YOUR CODE HERE
        raise NotImplementedError()
    r, g, b = (0, 0, 0)
    setPixel(0, 0, (r, g, b), px2)
    assert getPixel(0, 0, px2) == (r, g, b)

    Question 1.3 - Écrire une fonction permettant de peindre un rectangle de l'image avec une même couleur. Coloriez avec la couleur moyenne de cette région (et donc définir une fonction qui calcule cette couleur moyenne).

    def moyenne(corner_x, corner_y, region_w, region_h, px) -> tuple: 
        # YOUR CODE HERE
        raise NotImplementedError()

    Dans la fonction de dessin de région setRegionla variable color contient le triplet de couleurs à utiliser.

    def setRegion(x, y, w, h, color, px) -> None:
        # YOUR CODE HERE
        raise NotImplementedError()
    assert moyenne(0, 0, 1, 1, px2) == (0.0, 0.0, 0.0) # région noire

    Le code ci-dessous doit dessiner un rectangle blanc au milieu d'une image noire.

    im2 = Image.new('RGB', (im.width, im.height)) 
    px2 = im2.load()
    W, H = im.size
    setRegion(W//3, H//3, W//3, H//3, (255, 255, 255), px2)
    display(im2)

    Nous allons maintenant réaliser un remplissage un peu plus intéressant de l'image à partir de son contenu (dans notre cas nous nous baserons sur les couleurs contenues dans l'image).

    Question 1.4 - Ecrire une fonction de calcul de distance Euclidienne entre deux couleurs RGB (Red, Green, Blue) et comme suit :

    $d_{\text{euclidienne}} = \sqrt{(R_2 - R_1)^2 + (G_2 - G_1)^2 + (B_2 - B_1)^2}$

    Attention il s'agit de distances entre les couleurs à comparer et non pas la distance entre les positions des pixels.

    def distance(c1: tuple, c2: tuple) -> float: 
        # YOUR CODE HERE
        raise NotImplementedError()
    assert distance((0, 0, 0), (0, 0, 0)) == 0.0

    Question 1.5 - Nous allons désormais travailler sur une méthode de remplissage de région basée sur l'homogénéité des couleurs dans la région. Pour cela nous allons ré-utiliser les méthodes ci-dessus en particulier la distance Euclidenne, en utilisant l'algorithme dit de flood fill (doc) et qui fonctionne comme suit :

    1. Charger une image et initialiser deux listes vides : une liste de pixels à visiter et une liste de pixels déjà visités

    2. Définir un pixel de départ et le rajouter dans la liste de pixels à visiter

    3. Extraire un pixel de la liste des pixels à visiter, il constituera la couleur de la région homogène et le rajouter dans une troisième liste de pixels homogènes à colorier avec cette couleur

    4. Tant que la liste de pixels homogènes n'est pas vide, extraire un pixel de cette liste :

      • Colorier le pixel avec la couleur et le rajouter dans la liste des pixels visités
      • Explorer les 4 voisins autour du pixel (haut, bas, gauche, droite) et pour chaque voisin :
        • Si la couleur du voisin est en dessous d'un seuil d'homogénéité alors l'inclure dans la liste de pixels homogènes
        • Sinon rajouter le pixel dans la liste de pixels à visiter
        • Répéter cela tant que la liste de pixels homogènes n'est pas vide
    5. Répéter cela tant que la liste des pixels non visités n'est pas vide

    Le résultat attendu est une image remplie coloriée avec un nombre de couleur inférieur au nombre initial de couleurs.

    def floodFill(w: int, h: int, start_x: int, start_y: int, c: tuple, s, px, px2) -> tuple:
        # YOUR CODE HERE
        raise NotImplementedError()

    Le code ci-dessous va tester votre solution avec une image fournie et générer quelques statistiques liés au nombre de couleurs utilisées.

    # YOUR CODE HERE
    raise NotImplementedError()

    Pour aller plus loin

    • Tester le remplissage dans 8 directions (en prenant en compte les diagonales) au moyen dans la fonction d'exploration de voisinage des pixels.

    Exercice 2 : Traitement d'image par filtre

    Nous allons aborder un deuxième aspect de manipulation d'image : le traitement d'image, afin d'en extraire des informations intéressantes (contours, formes, etc.). En particulier nous allons créer différents filtres dont le but sera de transformer les valeurs des pixels afin de par exemple réduire le bruit que les images peuvent contenir (à savoir les variations locales de valeur).

    La plupart de ces méthodes étant couteuses en temps, nous travaillerons sur une version en niveau de gris.

    Question 2.1 - Écrire une fonction de conversion d'image en niveaux de gris (soit la moyenne des triplets (r,b,g) ou en utilisant la formule suivante :

    $ C_{gray} = (0.3 \times R) + (0.59 \times G) + (0.11 \times B)$

    def conversion_gris(px, W: int, H: int) -> None:
        # YOUR CODE HERE
        raise NotImplementedError()
    im = Image.open("lyon.png")
    im = im.convert("RGB")
    px = im.load()
    W, H = im.size
    conversion_gris(px, W, H)
    display(im)

    Nous commençon avec le filtre dit de Flou Gaussien, basé sur une opération dite de convolution, permettant d'appliquer une fonction de distribution gaussienne aux voisins d'un pixel et d'en faire la moyenne. Autrement dit il s'agira de réaliser la moyenne pondérée de chaque pixel en réalisant la moyenne du pixel et de ses voisins en utilisant par exemple la matrice ci-dessous (dont les valeurs sont définies par la distribution gaussienne donnée en annexe pour une matrice ) :

    $G(x) = \frac{1}{\sqrt[]{2 \pi } \sigma} e^{- \frac{x^{2}}{2 \sigma ^{2}}}$

    gauss3 = [[1,2,1],
              [2,4,2],
              [1,2,1]]
    
    gauss7 = [[1,1,2,2,2,1,1],
              [1,2,2,4,2,2,1],
              [2,2,4,8,4,2,2],
              [2,4,8,16,8,4,2],
              [2,2,4,8,4,2,2],
              [1,2,2,4,2,2,1],
              [1,1,2,2,2,1,1]]

    Question 2.2 - Implémentez le filtre gaussien tel que défini ci-dessus en définissant les filtres donnés ci-dessous. La fonction doit effectuer la multiplication des pixels centrés sur le pixel en cours avec les valeurs de la matrice, puis la somme pondérée. Vous pourrez utiliser les matrices ci-dessus d'approximation du filtre (en commençant par le filtre Gaussien dont le total des valeurs est ). Utilisez la version de l'image en niveaux de gris afin de simplifier les traitements.

    Commencez tout d'abord par définir une fonction qui calcul la somme des valeurs d'une matrice (de type gauss3).

    def somme_matrice(m: list = []) -> list:
        # YOUR CODE HERE
        raise NotImplementedError()
    assert somme_matrice(gauss3) == 16

    Ecrire la fonction de convolution. Pensez à prendre en compte les bords de l'image. Conseil : ne vous approchez pas trop du bord afin de ne pas réaliser une convolution en dehors de l'image.

    def convolution(px, W: int, H: int, m: list) -> None:
        # YOUR CODE HERE
        raise NotImplementedError()
    im = Image.open("lyon.png")
    im = im.convert("RGB")
    px = im.load()
    W, H = im.size
    conversion_gris(px, W, H)
    convolution(px, W, H, gauss3)
    display(im)

    Vous pouvez comparer votre floutage avec celui de PIL :

    from PIL import Image, ImageFile, ImageDraw, ImageChops, ImageFilter
    _im = im.filter(ImageFilter.GaussianBlur)
    display(_im)

    Question 2.3 - Testez votre code avec ces filtres ci-dessous avec un facteur de normalisation/pondération de 1 : que se passe-t-il ? (doc).

    sobely3 = [[-1, 0, 1],
               [-2, 0, 2],
               [-1, 0, 1]]
    
    sobelx3 = [[-1, -2, -1],
               [0, 0, 0],
               [1, 2, 1]]
    def convolution_sobel(px, W: int, H: int, m: list, f=1) -> None:
    # YOUR CODE HERE
    raise NotImplementedError()

    Pour aller plus loin

    • Comparez vos méthodes avec les modules OpenCV ou NumPy.
    • Implémentez une fonction de calcul de distance entre la couleur choisie et la couleur initiale.
    def difference(px, px2):
        # YOUR CODE HERE
        raise NotImplementedError()