Mini-cours au groupe de travail "Images"
MMI, Lyon
Arnaud Chéritat

Retour


29 janvier 2016
2e partie
C'était une séance de travaux pratiques. J'ai demandé aux personnes présentes d'installer Python 2 avant de venir.

Le choix de Python pour la séance est basé sur sa grande lisibilité, vertu pédagogique qui a prévalu. Pour mon travail, j'utilise C++, qui est un peu moins agréable mais s'exécute plus rapidement.
Il y a des différences notables entre Python 3 et Python 2, la syntaxe de la version 3 est plus stricte, du coup on perd en lisibilité. C'est pour cela que j'ai choisi Python 2. La dernière version de Python 2 est 2.7.11.

Il fallait également installer le package pypng, avec la commande

pip install pypng
ou
sudo pip install pypng

Sur Windows on peut aussi y arriver, j'ai testé mais je ne me souviens plus de la procédure.

Ce TP a été formaté pour des gens qui ont peu pratiqué la programmation. Les programmeurs aguerris peuvent donc sauter un grand nombre de paragraphes au début.

Parmi les autres aspects traités, il y a la notion d'ensemble de Julia, l'introduction progressive de méthode de dessins en dynamique holomorphe, et une illustration assez réaliste d'un workflow typique quand je produis des images.


Créons une première image formée de 2 lignes de 3 pixels, la première ligne en rouge et la seconde en blanc.

On stocke d'abord les couleurs des pixels dans une zone de mémoire (vraisemblablement) contigue. L'adressage de la mémoire est linéaire (c.à.d. 1 seule coordonnée) il faut donc choisir l'ordre dans lequel on écrit les données.

Par convention, les octets codant les valeurs RVB de chaque pixel d'une ligne sont juxtaposées ainsi : R,V,B,R,V,B,... On parcourt les pixels d'une ligne de la gauche vers la droite. On commence par la ligne du haut et quand on a fini on juxtapose la ligne suivante, etc...

rouge : R,V,B = 255,0,0

blanc : R,V,B = 255,255,255

Dans le cas du module pypng de Python, on a un certain nombre de façons de procéder, j'ai opté pour la suivante. On crée un objet que j'ai appelé "pixels", de type liste (terminologie Python) dont les éléments représentent les lignes successives. Ces représentations sont elles-mêmes des listes, les valeurs R,V,B,R,V,B,... des pixels de la ligne. La commande png.from_array() est une fonction qui crée un objet auquel on attache une étiquette dont on choisit le nom, ici : "image". Le fonctionnement interne de cet objet, en particulier la représentation interne des donnée, est complètement caché. La fonction .save() de l'objet va encoder les données en PNG et sauver le résultat dans un fichier.



Pour utiliser le module pypng il faut le "charger", on le fait avec la ligne import png.

Ici le programme est exécuté ligne par ligne, dans l'ordre dans lequel elles se présentent. Un programme plus élaboré peut sauter d'une ligne à l'autre, en arrière ou en avant, ou d'un morceau à l'autre, c'est ce qu'on appelle le flux/flot d'exécution (control flow). On parle parfois de pointeur d'exécution pour parler de l'endroit où se trouve le flot à un moment donné.

Maintenant, on aimerait faire calculer l'image au lieu d'écrire nous-même la valeur de chaque pixels. La variante ci-dessous produit la même image mais le programme contient des éléments de systématisation. On y introduit les boucles "for", la notion de bloc, la définition d'une fonction, le test "if", éléments communs à presque tous les langages de programmation. On y voit aussi quelques fonctions de manipulation de listes.




[] désigne une liste vide

for i in xrange(n) : fait exécuter n fois le bloc d'instructions qui suit. La première fois la variable i contient 0, la 2e fois elle contient 1, etc.. et la n-iè fois elle contient n-1.

Le bloc est déterminé par indentation. Celle-ci est définie par le nombre d'espaces situés avant le premier caractère de la ligne. Le bloc commence juste après le symbole ":" et se termine quand l'indentation redescend à celle du for ou en dessous.

for i in xrange(height) :
  row = []
  for j in xrange(width) :
    row.extend(compute(i,j))
  pixels.append(row)
  
image = ...
Notez que le bloc en vert contient lui-même une boucle : il y a une boucle dans la boucle, un bloc dans le bloc.
if condition :
  bloc 1
else :
  bloc 2
exécute le bloc 1 si la condition donne le résultat "True" et le bloc 2 en cas de "False". La partie "else" est facultative. Le test d'égalité s'écrit == et non pas = qui est l'opérateur d'affectation. Si vous vous trompez, au mieux ça vous donne un message d'erreur, au pire ça met le bazar dans votre programme.

def ma_fonction(nom du 1er argument, nom du 2e, etc...) : définit une fonction, le bloc qui suit est appelé son corps. Les instructions ne sont pas exécutées quand le flot rencontre la définition, elle sont simplement stockées. Pour exécuter ces instructions, il faut appeler la fonction ma_fonction(1er arg, 2e arg, ...). On ne peut pas appeler une fonction avant qu'elle ait été définie. Par contre le corps d'une fonction f peut utiliser des fonction pas encore définies, pourvu que celles-ci l'aient été avant avant le premier appel de f. Une fonction peut recevoir des valeurs en entrée, les arguments, et effectuer des calculs avec des variables temporaires et/ou des variables globales, et éventuellement avoir une ou des valeurs de sortie. Je ne rentre pas dans les détails, il y a des subtilités dont on n'a pas besoin de se soucier pour l'instant. L'appel return fait quitter le bloc prématurément, return valeur renvoie en plus "valeur" en sortie. On récupère cette valeur au niveau de l'appel de fonction en lui attachant un nom de variable nom_de_var = ma_fonction(...) ou bien en l'utilisant directement comme argument d'une autre fonction ou d'un calcul print ma_fonction(34,autre_fonction(2))+256.


Suite de la séance