Tableaux 2D

Images numériques

Le but de cette partie est de travailler sur les images numériques, en tant que tableaux à deux dimensions

Pour cela nous allons utiliser les module PIL et numpy de Python

Lorsqu'on a travaillé sur la matrice de leds du microbit on a utilisé les fonctions get_pixel(x,y) et set_pixel(x,y) pour créer une cible mouvante sur la matrice en fonction de l'inclinaison du microbit .

Voir l'API ici

L'API du module PIL propose les mêmes fonctions pour travailler sur les images numériques, où chaque pixel est repéré de la même manière qu'une led sur la matrice de leds du microbit

Le module numpy va nous permettre de travailler avec des tableaux à deux dimensions exploitables ensuite par le module PIL pour visualiser les images

Négatif d'une image en nuances de gris

Une image en nuances de gris est composée de pixels où chaque pixel a une nuance de gris entière,comprise entre 0 (noir) et 255 (blanc).

Une image a une hauteur h entière et une largeur l entière

On peut donc voir une image en nuances de gris comme un tableau tab de h tableaux contenant l octets

Ainsi un pixel peut être repéré par un indice de ligne lig, et un indice de colonne col où

$0 \leqslant \text{lig} \leqslant h - 1$ et $0 \leqslant \text{col} \leqslant l - 1$

Enfin tab[lig][col] contient la nuance de gris de ce pixel

Le négatif d'une image en nuances de gris s'obtient en remplaçant l'intensité n d'un pixel par 255 - n

On obtient par exemple

-->
  1. Récupérer dans le cahier de textes l'image chat.png
  2. Le module numpy permet de transformer cette image en nuances de gris en un tableau 2D, de la manière suivante

            
            import numpy as np
            from PIL import Image
            img = Image.open("chat.png")
            tab = np.array(img)
            # affichage de quelques informations
            print(img.size)
    
            print(len(tab))
    
            print(len(tab[0]))
    
            print(tab)
            
            
  3. Analyser les informations affichées en console
  4. Compléter le code de la fonction negatif_image(img) qui prend une image, en entrée, la transforme en tableau grâce à numpy, puis change chaque valeur v du tableau en 255 - v, transforme le tableau en image, grâce à PIL, et renvoie cette image

            
            def negatif_image(img):
                tab = .. 
                for lig in range(... ):
                    for col in range(... ):
                        tab[.. ][.. ] = .. 
                return Image. .. 
            
            
  5. Finalement exécuter la fonction negatif_image sur l'image nommée chat.png et visualiser cette nouvelle image avec la méthode show()

            
            img = Image.open("chat.png")
            nouvelle_image = negatif_image(img)
            nouvelle_image.show()
            
            
  6. Si on veut sauvegarder le négatif de l'image on peut procéder ainsi

            
            img = Image.open("chat.png")
            nouvelle_image = negatif_image(img)
            nouvelle_image.save("negatif_chat.png")
            
            

Binarisation d'une image en nuances de gris

Il s'agit de transformer une images en nuances de gris en une image en noir et blanc de la manière suivante

On remplace l'intensité d'un pixel n par 0 si n <= 127 et par 255 sinon

On obtient par exemple

-->
  1. Ecrire une fonction binarisation(img) qui prend en paramètre une image en nuances de gris et renvoie une image en noir et blanc
  2. Sauvegarder l'image

Séparer les couleurs d'une image en couleurs

Pour une image en couleurs en mode RVB, l'intensité d'un pixel est un triplet d'entiers compris entre 0 et 255, sous la forme d'un tableau par exemple

[226, 137, 125]

Le premier entier 226 est l'intensité du rouge, le second 137 l'intensité du vert, et enfin 125 est l'intensité du bleu

On veut remplacer chaque triplet par un autre triplet où les intensités du vert et du bleu sont nulles et conserver l'intensité du rouge

Sur cet exemple on remplace [226, 137, 125] par [226, 0, 0]

On obtient par exemple

-->
  1. Récupérer dans le cahier de textes l'image en couleurs lena.png
  2. Ecrire une fonction composante_rouge(img) qui prend en paramètre une image img et qui renvoie la composante rouge de l'image.
  3. Ecrire une fonction composante_verte(img) qui prend en paramètre une image img et qui renvoie la composante verte de l'image.
  4. Ecrire une fonction composante_bleue(img) qui prend en paramètre une image img et qui renvoie la composante bleue de l'image.

Transformer une image en couleurs en une image en nuance de gris

On obtient par exemple

-->
  1. Dans la documentation de PIL on peut lire au sujet de la transformation d'une image (RGB) en une nouvelle image en intensité de gris (L):

    "When translating a color image to greyscale (mode “L”), the library uses the ITU-R 601-2 luma transform:

    L = R * 299/1000 + G * 587/1000 + B * 114/1000"

    Que vaut 299 + 587 + 114 ? Expliquer la relation mathématique ci-dessus Pourquoi le coefficient de l'intensité de Vert (G) est le plus élevé ?

  2. Ecrire une fonction Python rvb_l(img) qui prend en paramètre une image en couleurs et renvoie une image en nuances de gris en utilisant la transformation ci-dessus (attention, la formule ci-dessus renvoie un nombre à virgule qu'il faudra transformer en entier)
  3. Ecrire une deuxième fonction rvb_l2(img) de telle sorte que cette fois

    L = (R + G + B)/3

  4. Y-a-t-il une différence entre les images obtenues ?

Renverser une image

On obtient par exemple

-->

Proposer une méthode pour réaliser cela

Tourner une image carrée d'un quart de tour dans le sens horaire

On obtient par exemple

-->

Proposer une méthode pour réaliser cela

Pour initialiser un tableau n_tab de même caractéristique qu'un autre tab, faire

n_tab = np.empty_like(tab)

Photomaton

A partir d'une image carrée dont le côté est une puissance de 2 on veut créer quatre images identiques comme ceci

-->

On parcourt l'image de départ comme précédemment et on lit les pixels par groupe de 4, les pixels (2i,2j), (2i + 1,2j), (2i, 2j + 1) (2i + 1,2j + 1) pour i variant de 0 à largeur//2 et j variant de 0 à largeur//2 et ensuite

Ecrire une fonction photo_maton(img) et qui prend en paramètre une image carrée 512 x 512 et qui renvoie une nouvelle image suivant la transformation ci-dessus

Réalisation d'un dégradé

On veut construire une image constituée de bandes verticales de couleur grise, l'ensemble donnant l'impression d'un dégradé où, la bande la plus à gauche est noire et la plus à droite est blanche.

  1. Construire un tableau Python commençant par la valeur 250 se terminant par 0, chaque valeur est répétée 5 fois, et on passe d'une valeur à la suivante en diminuant de 5

    Indication : Pour concaténer deux tableaux t1 et t2 on peut faire t1 + t2

        
            tab = [0, 0, 0, 0, 0, 5, 5, 5, 5, 5,
             ...,250, 250, 250, 250, 250]
    
    
  2. Construire un tableau à deux dimensions T en compréhension constitué de 200 lignes égales à tab
  3. T est un tableau d'entiers codés sur 64 bits. La classe Image du module PIL a une méthode Image.fromarray(T) qui va transformer T en une image que l'on pourra visualiser, à condition qu'on transforme chaque entier codé sur 64 bits en un entier non signé sur 8 bits

    Pour réaliser cela la classe numpy a une méthode np.uint8(T)

    Compléter le code ci-dessous pour visualiser un dégradé semblable à l'image ci-dessous

        
            import numpy as np
            from PIL import Image
    
            # compléter tab et T
            
            tab = .. 
            for i in range(.. ,.. ,.. ):
                tab = .. 
            T = .. 
            n_image = Image.fromarray(np.uint8(T))
            n_image.show()
    
    
    

Jeu du morpion

Le jeu se joue à la console entre deux joueurs qui entrent alternativement soit le caractère X, soit le caractère O

Un joueur gagne s'il réussit à avoir un alignement sur une des lignes, ou une des colonnes, ou une des diagonales

Si au bout des neuf coups joués il n'y a pas d'alignement, le match est considéré comme nul

Le but de ce projet, au delà de coder un jeu de morpion, est de comprendre l'intérêt de la programmation modulaire et des tests

Dans l'E.N.T vous a été fourni deux fichiers, le fichier tictac.py et morpion.py

Pourquoi deux fichiers ?

Le fichier tictac.py doit contenir toutes les fonctions qui permettent de jouer à la console le jeu du Morpion

Le module doctest importé dans le fichier tictac.py permet de tester les fonctions une par une

Ensuite une fois les tests terminés on peut jouer en exécutant le fichier morpion.py qui importe le module tictac.py

Questions

Il convient de suivre pas à pas et scrupuleusement les questions suivantes pour la mise au point du TP. La méthode tout au long du TP est:
  1. Annoter la fonction
  2. Spécifier la fonction
  3. Insérer des tests dans la spécification de la fonction
  4. Ecrire le code de la fonction
  5. Exécuter le fichier tictactoe.py (autrement dit vérifier la fonction)
  1. Commencer par annoter chaque fonction
  2. Après avoir bien lu et compris la spécification et les tests de la fonction alignementLigne(tableau,numLigne), écrire le code de la fonction alignementLigne(tableau,numLigne)
  3. Exécuter le fichier tictac.py revient à tester pour l'instant la fonction alignementLigne(tableau,numLigne)

    Vous devez observer ceci

    Que signifient ces lignes ?

    Après le symbole >>> le module doctest va exécuter la fonction alignementLigne(tableau,numLigne) sur le cas particulier où la valeur de plateau est [[ '#','#','O'],['X','X','X'],['#','O','O']] et la valeur de numLigne est 1 et comparer le résultat qu'il a obtenu avec le résultat espéré que vous avez mis juste en dessous c'est à dire True

    En effet sur ce cas particulier il y a un alignement dans la ligne numéro 1 où il n'y a que des X

    S'il obtient le même résultat que celui que vous avez mis alors il dit que le test est ok

    Cependant à la fin du rapport de tests on observe aussi que beaucoup de fonctions n'ont pas été testées

    C'est normal certaines n'ont pas été définies et d'autres comme affichePlateau(plateau) qui retourne None ne seront pas testées

    A la fin de la mise au point du TP vous devrez avoir comme rapport de tests

    Le message "Test passed" vous assure maintenant que si vos exemples de test ont bien été choisis votre programme est "presque" correct

    Il reste peut-être des erreurs mais la méthode que vous avez utilisé en a éliminé un grand nombre au cours des tests

  4. En adaptant la spécification de la fonction alignementLigne(tableau,numLigne) écrire celle de la fonction alignementColonne(tableau,numColonne)

    Puis ajouter trois lignes de tests

    Puis définir la fonction alignementColonne(tableau,numColonne)

    Tester ensuite la fonction

  5. La méthode utilisée est la suivante
    1. Annoter et spécifier la fonction en ajoutant des tests
    2. Définir la fonction
    3. Passer à la fonction suivante uniquement si les tests sont réussis
  6. Documenter, définir et tester la fonction alignementColonnes(plateau) (regarder l'exemple de alignementLignes(plateau))
  7. Documenter, définir et tester les fonctions alignementDiagonale1(plateau) et alignementDiagonale2(plateau)
  8. Documenter, définir et tester la fonction alignementDiagonales(plateau)
  9. Documenter, définir et tester la fonction alignement(plateau) qui retourne True s'il y a un alignement dans une des trois lignes ou une des trois colonnes ou une des deux diagonales, False sinon
  10. La fonction sontValides(plateau,x,y) est déjà documentée avec des tests. Il reste à la définir et à la tester. A quoi sert cette fonction ?

    La façon choisie de programmer le jeu du Morpion est la suivante :

    • Le premier joueur qui joue a le numéro 0, le second le numéro 1
    • Le premier joueur joue avec le symbole X, le second avec le symbole O

    Le joueur doit préciser en quel endroit du plateau il veut mettre le caractère et cet endroit est repéré par des coordonnées (x,y) où x et y sont deux entiers

    (x,y) sont valides si 0 <= x <= 2 et 0 <= y <= 2 et si plateau[x][y] == '#'

    On ne traitera pas le cas où l'utilisateur entre n'importe quoi à la place de deux entiers

    Tester sontValides(plateau,x,y)

  11. La fonction partieEstFinie(plateau,nbCoups) est déjà documentée avec des exemples de tests. Bien comprendre la documentation pour définir cette fonction

    Tester

  12. A ce stade le jeu est presque fini d'être mis au point et les deux dernières fonctions jouer(plateau,numJoueur) et unePartie(plateau) seront testées en exécutant morpion.py

    La fonction jouer(plateau,numJoueur) consiste dans un premier temps, tant que les coordonnées entrées par un joueur ne sont pas valides, à lui demander d'entrer les coordonnées par l'instruction

    
    i,j  = tuple(input("NumLigne,NumColonne ? -->  "))
    
    
    On demande au joueur d'entrer au clavier une suite de deux nombres,sans espace, où le premier est le numéro de ligne, le second le numéro de colonne par exemple
    
    11
    
    

    Dans un second temps, une fois que les coordonnées sont valides, on affecte à plateau le symbole car associé au numéro du joueur

    
    plateau[lig][col] = car
    
    
  13. Enfin il reste à définir la fonction unePartie(plateau)

    Tant que la partie n'est pas finie on fait jouer un joueur de numéro numJoueur avec la fonction jouer(plateau,numJoueur)

    On fait évoluer le numéro du joueur numJoueur à chaque tour par l'instruction

    
    numJoueur = (numJoueur + 1) % 2
    
    

    Enfin pour que le jeu soit compréhensible on demande à ce qu'il y ait un affichage dans la console :

    • du plateau de jeu à chaque tour
    • un message pour annoncer le tour de X ou O
    • l'affichage du nombre de coups déjà joués

    A la fin du jeu on demande à ce que soit affiché soit un message disant quel joueur a gagné X ou O

    soit un message annonçant un match nul

  14. Arrivé à ce stade vous avez fini !