Pygame/Introduction au module Surfarray

Un livre de Wikilivres.


Traduit de l'anglais, original par Pete Shinners :
http://www.pygame.org/docs/tut/surfarray/SurfarrayIntro.html

Remarquelink={{{link}}}

Note du traducteur :
Le projet Numeric, auquel il est fait référence dans ce tutoriel, a engendré le projet Numarray, qui lui-même a engendré le projet NumPy, lequel est maintenant intégré au projet SciPy (ouf!). D'après quelques lectures (1, 2, 3), NumPy devrait être entièrement compatible avec les 2 projets précédents.

Pygame, quant à lui, est compatible uniquement avec l'ancien projet Numeric qui est toujours disponible au téléchargement ainsi que sa documentation (HTML - PDF).

Il semblerait que la version 1.8.0 de Pygame ait apporté une compatibilité avec le projet NumPy. Je n'ai pas pu tester moi-même. Quoi qu'il en soit, le projet Numeric serait toujours compatible et même prioritaire par rapport au projet NumPy. → Référentiel Surfarray

Mise à jour 19/02/2014 : Pygame est maintenant totalement compatible avec Numpy et en conseille l'utilisation, Numeric étant quand à lui déconseillé.


Introduction[modifier | modifier le wikicode]

Ce tutoriel a pour objectif d'introduire les utilisateurs à Numeric et au module Surfarray de Pygame. Pour les débutants, le code utilisé par Surfarray peut être légèrement intimidant. Mais ici, il y a seulement quelques concepts à comprendre et vous serez opérationnel. En utilisant le module Surfarray, il devient possible de réaliser des opérations au niveau du pixel en utilisant du code python pur. Les compétences requises pour faire cela en C sont d'un niveau beaucoup plus difficilement accessible.

Vous pouvez avoir envie d'aller directement voir à la section Exemples pour vous faire une idée sur ce qu'il est possible de faire avec ce module, ensuite nous commencerons par le début pour vous montrer la manière d'y arriver.

Maintenant, je ne vais pas essayer de vous flouer en vous faisant penser que tout est simple. L'obtention d'effets puissants en modifiant les valeurs de chaque pixels est très complexe. Commencer à maîtriser Numeric constitue déjà un apprentissage ardu. Dans ce tutoriel, je serai rapide avec ce qui est facile et je vais utiliser beaucoup d'exemples avec pour objectif de semer les graines de la connaissance. Après avoir fini la lecture de ce tutoriel, vous devriez comprendre les bases du fonctionnement de Surfarray.

Numeric Python[modifier | modifier le wikicode]

Si la paquet python Numeric n'est pas installé, il est préférable de le faire maintenant. Vous pouvez télécharger le paquet depuis cette adresse. Pour être certain que Numeric fonctionne chez vous, vous devriez obtenir quelque chose de ce genre à partir du mode interactif de Python.

>>> from Numeric import *       #Importer Numeric
>>> a = array((1,2,3,4,5))      #Créer un tableau
>>> a                           #Afficher le tableau
array([1, 2, 3, 4, 5])
>>> a[2]                        #Un index dans le tableau
3
>>> a*2                         #Un nouveau tableau avec des valeurs doublées
array([ 2,  4,  6,  8, 10])

Comme vous pouvez le voir, le module Numeric nous fournit un nouveau type de données, le array. Cet objet contient un tableau de taille fixe, et toutes les valeurs qu'il contient sont du même type. Les tableaux peuvent aussi être multidimensionnels, et c'est de cette manière nous les utiliserons avec les images. Il y aurait un peu plus à dire à leur sujet, mais c'est suffisant pour commencer.

Si vous observez la dernière commande ci-dessus, vous verrez que les opérations mathématiques sur les tableaux du module Numeric s'appliquent à toutes les valeurs du tableau. Ce fonctionnement est appelé elementwise operations. Ces tableaux peuvent également être slicés (découpés) à la façon des listes normales. La syntaxe du découpage en slice est la même que celle utilisée avec les objets python standards (donc révisez-la si besoin). Voici quelques exemples de plus sur le fonctionnement des tableaux :

>>> len(a)                                 #Obtenir la taille du tableau
5
>>> a[2:]                                  #Les éléments [2] et supérieurs
array([3, 4, 5])
>>> a[:-2]                                 #Tous exceptés les 2 derniers
array([1, 2, 3])
>>> a[2:] + a[:-2]                         #Ajout le début et la fin
array([4, 6, 8])
>>> array((1,2,3)) + array((3,4))          #Ajout de tableau de tailles différentes
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: frames are not aligned

On obtient une erreur avec la dernière commande, en essayant d'ajouter deux tableaux de tailles différentes. Pour réaliser des opérations impliquant deux tableaux (incluant les comparaisons et les assignations) les deux tableaux doivent avoir les mêmes dimensions. Il est très important de savoir que les valeurs contenues dans un tableau créé depuis le slice d'un original possède les mêmes références que les valeurs du tableau de départ. Donc modifier une valeur dans un slice issue d'un tableau original, modifiera la valeur correspondante du tableau original. Cette propriété des tableaux est très importante à retenir.

>>> a                   #Afficher le tableau de départ
array([1, 2, 3, 4, 5])
>>> aa = a[1:3]         #Slicer 2 éléments intermédiaires
>>> aa                  #Afficher le slice
array([2, 3])
>>> aa[1] = 13          #Modifier une valeur dans le slice
>>> a                   #Afficher le changement dans l'original
array([ 1, 2, 13,  4,  5])
>>> aaa = array(a)      #Faire une copie du tableau
>>> aaa                 #Afficher la copie
array([ 1, 2, 13,  4,  5])
>>> aaa[1:4] = 0        #Définir à 0 des valeurs intermédiaires
>>> aaa                 #Afficher la copie
array([1, 0, 0, 0, 5])
>>> a                   #Afficher l'original
array([ 1, 2, 13,  4,  5])

Maintenant, nous jetterons un coup d'œil à de petits tableau à deux dimensions. Ne soyez pas trop inquiets, c'est la même chose que d'avoir un tuple à deux dimensions (un tuple dans un tuple). Commençons avec des tableaux à deux dimensions.

>>> row1 = (1,2,3)                     #Créer un tuple
>>> row2 = (3,4,5)                     #Créer un second tuple
>>> (row1,row2)                        #Afficher comme un tuple 2D
((1, 2, 3), (3, 4, 5))
>>> b = array((row1, row2))            #Créer un tableau 2D
>>> b                                  #Afficher le tableau
array([[1, 2, 3],
       [3, 4, 5]])
>>> array(((1,2),(3,4),(5,6)))         #Afficher un nouveau tableau 2D
array([[1, 2],
       [3, 4],
       [5, 6]])

Maintenant, avec ce tableau à deux dimensions (que l'on appellera à partir de maintenant un 2D), nous pouvons récupérer des valeurs spécifiques par leur index et slicer dans les deux dimensions. L'utilisation d'une virgule pour séparer les indices, nous permet de chercher/slicer dans plusieurs dimensions. L'utilisation de ":" comme un index (afin de ne pas fournir tous les indices) nous renvoie toutes les valeurs contenues sur cette dimension. Voyons son fonctionnement :

>>> b                 #Afficher la tableau précédent
array([[1, 2, 3],
       [3, 4, 5]])
>>> b[0,1]            #Indexer une valeur unique
2
>>> b[1,:]            #Slicer la seconde rangée
array([3, 4, 5])
>>> b[1]              #Slicer la seconde rangée (idem ci-dessus)
array([3, 4, 5])
>>> b[:,2]            #Slicer la dernière colonne
array([3, 5])
>>> b[:,:2]           #Slicer en un tableau 2x2
array([[1, 2],
       [3, 4]])

Bon, restez avec moi, c'est à peu près aussi dur que ça. En utilisant Numeric, il existe une fonctionnalité supplémentaire pour effectuer des slices. Le slice de tableau nous permet de spécifier un incrément de slice. La syntaxe pour un slice avec incrément est index_debut : index_fin : increment.

>>> c = arange(10)                        #Comme range(), mais pour faire un tableau
>>> c                                     #Afficher le tableau
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> c[1:6:2]                              #Slicer les valuers impaires entre 1 et 6
array([1, 3, 5])
>>> c[4::4]                               #Slicer toutes les 4 valeurs en démarrant à l'index 4
array([4, 8])
>>> c[8:1:-1]                             #Slice de 1 à 8 inversé
array([8, 7, 6, 5, 4, 3, 2])

Voilà. Vous en savez suffisamment pour vous permettre de commencer à utiliser Numeric avec le module Surfarray. Les propriétés du module Numeric sont certainement plus consistantes, mais il ne s'agit que d'une introduction. Par ailleurs, on veut seulement faire des trucs marrants, pas vrai ?

Importer le module Surfarray[modifier | modifier le wikicode]

Pour utiliser le module Surfarray, nous avons besoin de l'importer. Les modules Surfarray et Numeric étant des composants optionnels de Pygame, il est judicieux de s'assurer de les importer correctement avant de les utiliser. Dans ces exemples, j'importerai le module Numeric dans une variable nommée N. Vous verrez ainsi quelles fonctions utilisées provient du module Numeric (et de plus c'est légèrement plus court que de taper Numeric devant chaque fonction).

try:
    import Numeric as N
    import pygame.surfarray as surfarray
except ImportError:
    raise ImportError, "Numeric and Surfarray are required."

Introduction à Surfarray[modifier | modifier le wikicode]

Il y a deux principaux types de fonctions dans Surfarray. Un des jeu de fonctions concerne la création d'un tableau qui est une copie des données de pixels d'une surface. L'autre jeu de fonctions crée une copie par référence d'un tableau de pixels, donc changer le tableau modifie directement la surface originale. Il y a d'autres fonctions qui vous permettent d'accéder aux valeurs du canal alpha de chaque pixel à l'aide de tableaux et de plusieurs autres fonctions très utiles. Nous étudierons ces fonctions plus tard.

En utilisant ces tableaux de surface, il y a deux moyens de représenter les valeurs des pixels. La première, peut être de les représenter comme un carte de nombres entiers. Ce type de tableau est un simple tableau 2D avec un unique entier qui représente la couleur du pixel correspondant. Ce type de tableau est pratique pour déplacer des parties d'une image. L'autre type de tableaux utilise trois valeurs pour représenter chaque pixel en codage RGB. Ce type de tableau rend extrêmement simple la réalisation d'effets qui modifie la couleur de chaque pixel. Ce type de matrice est également un peu délicat à manipuler, puisqu'il s'agit en fait d'un tableau à 3 dimensions. Si vous parvenez malgré tout à comprendre le truc, ce n'est pas plus difficile que d'utiliser un tableau 2D normal.

Le module Numeric utilise une machine de nombres naturels pour représenter les données numériques, donc un tableau Numeric peut être constitué d'entier de 8bits, 16bits, et 32bits. (les tableaux peuvent également utiliser d'autres types comme des flottants et des doubles, mais pour notre manipulation d'image nous n'utilisons pratiquement que des entiers). Du fait de la limitation en taille de certains entiers, vous devez veiller à ce que les tableaux contenant les données des pixels soient des tableaux dont les données sont du type adéquat. Les fonctions fabriquant ces tableaux à partir de surfaces sont :

  • surfarray.pixels2d(surface)
Crée un tableau 2D (valeur des pixels entière) qui référence les données originales de la surface. Ceci fonctionnera pour tous les formats de surface excepté celles en 24 bits.
  • surfarray.array2d(surface)
Crée un tableau 2D (valeur des pixels entière) copié depuis n'importe quel type de surface.
  • surfarray.pixels3d(surface)
Crée un tableau 3D (valeur des pixels codé en RGB) qui référence les données originales d'une surface. Cela va fonctionner exclusivement avec des surfaces sur 24 bits ou 32 bits qui ont un formatage RGB et BGR.
  • surfarray.array3d(surface)
Crée un tableau 3D (valeurs des pixels codé en RGB) copié depuis n'importe quel type de surface.

Voici un petit résumé qui devrait mieux illustrer quels types de fonctions devraient être utilisés sur quelles surfaces. Comme vous pouvez le voir, les fonctions de type arrayXD vont fonctionner avec tous les types de surface.

32 bits 24 bits 16 bits 8 bits(c-map)
pixel2D yes yes yes
array2D yes yes yes yes
pixel3D yes yes
array3D yes yes yes yes

Exemples[modifier | modifier le wikicode]

Avec ces informations, nous sommes parés pour commencer à essayer diverses choses avec les tableaux de surface. Les petites démonstrations suivantes créent un tableau Numeric et l'affichent dans pygame. Ces différents tests sont issus des exemples contenus dans le fichier arraydemo.py. Il y a une fonction simple nommée surfdemo_show() qui affiche un tableau à l'écran.

Création d'une image noire[modifier | modifier le wikicode]

allblack = N.zeros((128, 128))
surfdemo_show(allblack, 'allblack')

Dans notre premier exemple, nous créons un tableau entièrement noir de 128 lignes sur 128 colonnes. Pour créer un tableau numérique avec un taille déterminée, il est préférable d'utiliser la fonction N.zeros(). Ici, le tableau de zéros forme une surface noire.

Tableaux de 3 dimensions (séparation des composantes RGB)[modifier | modifier le wikicode]

striped = N.zeros((128, 128, 3))
striped[:] = (255, 0, 0)
striped[:,::3] = (0, 255, 255)
surfdemo_show(striped, 'striped')

Ici nous manipulons un tableau à 3 dimensions. On commence par créer une image rouge. Ensuite nous extrayons une ligne sur trois et nous lui donnons la couleur bleu/vert. Comme vous pouvez le constater, nous pouvons traiter les tableaux à trois dimensions presque comme un tableau à deux dimensions, seulement on lui assigne des 3-uplets au lieu de valeurs uniques (scalaires).

Extraction des données d'une image depuis un fichier[modifier | modifier le wikicode]

imgsurface = pygame.image.load('surfarray.jpg')
imgarray = surfarray.array2d(imgsurface)
surfdemo_show(imgarray, 'imgarray')

Ici nous chargeons une image avec la fonction image.load() qui la convertit en un tableau 2D d'entiers. Nous utiliserons cette image comme base dans le reste de nos exemples.

Retournement miroir vertical[modifier | modifier le wikicode]

flipped = imgarray[:,::-1]
surfdemo_show(flipped, 'flipped')

Voici un retournement vertical de l'image, réalisé en utilisant la notation en slices à l'aide d'un incrément négatif pour l'indice des colonnes.

Miniaturisation d'une image[modifier | modifier le wikicode]

scaledown = imgarray[::2,::2]
surfdemo_show(scaledown, 'scaledown')

Diminuer une image repose sur le même principe que l'exemple précédent. Ici, la notation en slices est utilisée pour conserver seulement un pixel sur deux à la fois verticalement et horizontalement.

Augmentation de la taille d'une image[modifier | modifier le wikicode]

size = N.array(imgarray.shape)*2
scaleup = N.zeros(size)
scaleup[::2,::2] = imgarray
scaleup[1::2,::2] = imgarray
scaleup[:,1::2] = scaleup[:,::2]
surfdemo_show(scaleup, 'scaleup')

Augmenter la taille d'une image n'est pas aussi radicalement simple, mais s'inspire de la diminution que nous avons réalisé en utilisant les slices. D'abord, nous créons un tableau qui est de deux fois la taille de l'original. On réalise une copie du tableau original, pixel par pixel, en écrivant seulement sur les colonnes paires du tableau de destination, puis on réalise à nouveau l'opération en écrivant seulement sur les colonnes impaires du tableau de destination. À ce stade, nous avons image redimensionnée correctement, mais toutes les lignes impaires sont noires. Il nous suffit alors de recopier chaque ligne paire sur la ligne du dessous. On obtient ainsi une image dont la taille a doublé.

Filtrage de canaux[modifier | modifier le wikicode]

rgbarray = surfarray.array3d(imgsurface)
redimg = N.array(rgbarray)
redimg[:,:,1:] = 0
surfdemo_show(redimg, 'redimg')

Retour vers les tableaux 3D, on utilisera le codage RGB pour modifier les couleurs. On fait un simple tableau 3D à partir de l'image originale, en utilisant la méthode surfarray.array3D(), puis toutes les valeurs pour le bleu et le vert sont mises à zéro. Il nous reste alors, uniquement le canal rouge.

Filtrage par convolution[modifier | modifier le wikicode]

soften = N.array(rgbarray)
soften[1:,:]  += rgbarray[:-1,:]*8
soften[:-1,:] += rgbarray[1:,:]*8
soften[:,1:]  += rgbarray[:,:-1]*8
soften[:,:-1] += rgbarray[:,1:]*8
soften /= 33
surfdemo_show(soften, 'soften')

On réalise ici une convolution à l'aide d'un filtre 3x3 qui va adoucir les reliefs de l'image. Cela paraît lourd en calculs, mais ce qui est fait est en fait de décaler l'image de 1 pixel dans toutes les directions, et de sommer toutes ces images (en multipliant par un certain coefficient de poids). Alors, on moyenne toutes les valeurs obtenues. Ce n'est pas un filtre gaussien, mais c'est rapide.


Décoloration[modifier | modifier le wikicode]

src = N.array(rgbarray)
dest = N.zeros(rgbarray.shape)
dest[:] = 20, 50, 100
diff = (dest - src) * 0.50
xfade = src + diff.astype(N.Int)
surfdemo_show(xfade, 'xfade')

Enfin, Nous réalisons une décoloration croisée entre l'image originale et une fond entièrement en bleu. Ce n'est pas très folichon, mais l'image de destination peut être n'importe quoi, et en modifiant le coefficient multiplicateur (0.50 dans l'exemple), vous pouvez choisir chaque étape pour un fondu linéaire entre deux images.

Conclusion[modifier | modifier le wikicode]

J'espère qu'à partir de maintenant vous commencez à voir comment le module Surfarray peut-être utilisé pour réaliser des effets spéciaux et/ou transformations qui ne sont possibles qu'à partir d'une manipulation de pixels. Au minimum, vous pouvez utiliser Surfarray pour faire un grand nombre d'opérations très rapides de type Surface.set_at() et Surface.get_at(). Mais ne pensez pas que vous en ayez terminé avec ce module, il vous reste encore beaucoup à apprendre.

Verrouillage de surface[modifier | modifier le wikicode]

Comme le reste de Pygame, Surfarray va verrouiller tout objet de type Surface lors de l'accès aux données de pixels. C'est une chose dont il faut être conscient dans tout ce que vous faites. En créant un tableau de données de pixels, la surface originale sera verrouillée pendant le temps d'existence du tableau de donnée. Il est important de s'en rappeler. Soyez certain d'avoir supprimé le tableau de pixels soit explicitement avec l'instruction Python : del, soit implicitement en sortant de l'espace de nom et ainsi faire intervenir le garbage collector (comme par exemple après un retour de fonction).

Faites attention à ne pas accéder directement à des surfaces en hardware (HWSURFACE). Car les données de ces surfaces résident dans la mémoire de la carte graphique, et le transfert de modifications de pixels à travers le bus PCI/AGP n'est pas des plus rapide.

Transparence[modifier | modifier le wikicode]

Le module Surfarray possède plusieurs méthodes pour accéder aux valeurs du canal alpha/couleur clé d'une surface. Aucune des fonctions qui gèrent le canal alpha, n'a d'effet sur le reste des données de la surface, uniquement sur les valeurs du canal alpha des pixels. Voici la liste de ces fonctions :

  • surfarray.pixels_alpha(surface)
Crée un tableau 2D de valeurs entières qui référence les valeurs du canal alpha des pixels d'une surface. Ceci fonctionne uniquement avec les images codées sur 32 bits par pixel, avec un canal alpha sur 8 bits.
  • surfarray.array_alpha(surface)
Crée un tableau 2D de valeurs entières qui copie les valeurs du canal alpha des pixels d'une surface. Ceci fonctionne avec tous les types de surface. Si l'image d'origine ne contient aucun canal alpha, les valeurs du tableau sont initialisées à 255, qui est la valeur maximale d'opacité.
  • surfarray.array_colorkey(surface)
Crée un tableau 2D de valeurs entières qui met la transparence à 0 (valeur maximale de transparence) pour chaque pixel de la surface dont la couleur correspond à la couleur clé.

Autres fonctions du module Surfarray[modifier | modifier le wikicode]

Il existe quelques autres fonctions disponibles dans le module Surfarray. Vous pouvez en obtenir une liste exhaustive ainsi qu'une description plus complète sur la page de référence. Notez malgré tout cette fonction très utile :

  • surfarray.blit_array(surface, array)
Ceci va transférer tout type de tableau 2D ou 3D sur une surface possédant les mêmes dimensions. Ce blit de Surfarray sera généralement beaucoup plus rapide que d'assigner un tableau qui contiendrait les pixels de référence. Néanmoins, ça ne devrait pas être plus rapide qu'un blit normal d'une surface, puisque celui-ci est très optimisé.

Utilisation plus avancée de Numeric[modifier | modifier le wikicode]

Voici deux dernière choses qu'il est bon de connaître à propos des tableaux de Numeric. En manipulant des tableaux de très grande taille, comme par exemple des grandes surfaces de 640x480, vous devrez veiller à certaines choses en particulier. D'abord, même si les opérateurs + et * utilisés avec les tableaux sont très pratiques, ils sont également très coûteux en temps de calcul sur les grands tableaux. Ces opérateurs doivent réaliser des nouvelles copies temporaires des tableaux, qui sont alors habituellement copiées dans un autre tableau. Cela peut prendre énormément de temps. Heureusement, tous les opérateurs du module Numeric sont fournit avec des fonctions spéciales qui sont plus performantes et peuvent être utilisées en lieu et place des opérateurs. Par exemple, vous pourriez remplacer screen[:] = screen + brightmap par la fonction plus rapide add(screen, brightmap, screen). Toutefois, lisez la documentation concernant les Numeric Ufuncs pour en savoir plus à leur sujet. C'est important lors de la manipulation des tableaux.

En manipulant les tableaux avec des valeurs de pixel codés sur 16 bits, Numeric n'utilise pas des entiers non signés sur 16 bits, donc certaines de vos valeurs seront des nombres négatifs signés. Heureusement ça ne pose pas de problème.

Une autre chose à laquelle il faut veiller en utilisant des tableaux est le type de données manipulé. Certains tableaux (particulièrement les surfaces de pixels mappées, en codage RGB) retourne des tableaux avec des valeurs sur 8 bits non signés. Ces tableaux peuvent facilement provoquer un dépassement de capacité si vous n'êtes pas très attentifs.Le module Numeric possède les mêmes contraintes que vous trouverez dans le langage C, c'est à dire qu'une opération avec un nombre en 8 bits et un nombre en 32 bits va renvoyer un nombre en 32 bits. Vous pouvez toujours convertir le type de donnée d'un tableau, mais soyez toujours certain du type que contienne les tableaux que vous manipulez. S'il arrive une situation dans laquelle un dépassement de capacité est provoqué, Numeric va lever une exception.

Enfin, vous devez faire attention lorsque vous assignez des valeurs dans un tableau à trois dimensions, celles-ci doivent être comprises entre 0 et 255, sinon vous obtiendrez des erreurs de troncatures indéfinies.

Remise du Diplôme[modifier | modifier le wikicode]

Ok, vous l'avez, ma formation rapide sur Numeric python et Surfarray. Espérons que maintenant vous voyez ce qu'il est possible de faire, et que si vous ne l'avez jamais utilisé vous-même, vous ne serez pas effrayé à la vue de ces codes. Regardez dans l’exemple vgrade.py pour plus d'actions sur les tableaux Numeric. Il existe également quelques démonstrations "enflammées" qui utilisent Surfarray pour créer un effet de liquide en temps réel. Le mieux est toujours d'essayer des choses par vous même. Allez-y tranquillement au début, et construisez au fur et à mesure. J'ai vu des choses très intéressantes faites avec Surfarray comme des gradients radiaux et d'autres choses dans le genre. Bonne Chance.