Introduction au module Surfarray

Contributeurs Wikibooks

Pygame utilise le module Python Numeric pour gérer de façon plus efficace le traitement des images par calcul des valeurs de pixel. L'utilisation des tableaux de surface est une fonctionnalité avancée pour permettre de créer des effets et des filtres personnalisés. Ce tutoriel montre l'application des petits exemples de Pygame contenus dans arraydemo.py.

Commentez

- Titre : Introduction au module Surfarray

- Auteur : Contributeurs Wikibooks

- Parution : 19 novembre 2013

- Licence : Le contenu de cet article est rédigé par Contributeurs Wikibooks et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. Numeric Python

- III. Importer le module Surfarray

- IV. Introduction à Surfarray

- V. Exemples

- VI. Verrouillage de surface

- VII. Transparence

- VIII. Autres fonctions du module Surfarray

- IX. Utilisation plus avancée de Numeric

- X. Remise du diplôme

- XI. Remerciements

I - Introduction

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 deux 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

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. En fait, il n'y a que quelques concepts à comprendre pour que vous soyez 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 se rapprochent à celles nécessaires pour faire du C.

Vous pouvez avoir envie d'aller directement voir les 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 pixel 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.

II - Numeric Python

Si le 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 : 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 découpés de la même façon que les listes. La syntaxe du découpage en morceaux est la même que celle utilisée avec les objets Python standards (donc révisez-la si besoin). Voici quelques autres exemples 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 deux 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 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 morceau d'un original possèdent les mêmes références que les valeurs du tableau de départ. Donc modifier une valeur dans un morceau issu 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 morceau
array([2, 3])
>>> aa[1] = 13          #Modifier une valeur dans le morceau
>>> 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 tableaux à deux dimensions. Ne soyez pas trop inquiet, c'est la même chose que d'avoir un tuple à deux dimensions (un tuple dans un tuple). Commençons par des tableaux à deux dimensions.

>>> row1 = (1,2,3)                     #Créer un morceau
>>> row2 = (3,4,5)                     #Créer un second morceau
>>> (row1,row2)                        #Afficher comme un morceau 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 simplement « 2D »), nous pouvons récupérer des valeurs spécifiques par leur index et découper dans les deux dimensions. L'utilisation d'une virgule pour séparer les indices, nous permet de chercher/découper 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 le 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 découpages. Le morceau de tableau nous permet de spécifier un incrément de découpe. La syntaxe pour un morceau 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]                              #Découper les valeurs impaires entre 1 et 6
array([1, 3, 5])
>>> c[4::4]                               #Découper toutes les 4 valeurs en démarrant à l'index 4
array([4, 8])
>>> c[8:1:-1]                             #Segment 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 ?

III - Importer le module Surfarray

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 proviennent 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."

IV - Introduction à Surfarray

Il y a deux principaux types de fonctions dans Surfarray. Un des jeux 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 une 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 modifient 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 à trois 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 un type de nombres naturels natif pour représenter les données numériques, donc un tableau Numeric peut être constitué d'entier de 8 bits, 16 bits, et 32 bits. (Les tableaux peuvent également utiliser d'autres types comme des flottants et des doubles, mais pour notre manipulation d'images 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ée 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 (valeur des pixels codée en RGB) copié depuis n'importe quel type de surface.

Voici un petit résumé qui devrait mieux illustrer quels types de fonctions doivent ê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

V - Exemples

Avec ces informations, nous sommes parés pour 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.

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 une 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.

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

Ici nous manipulons un tableau à trois 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).

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.

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

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

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 découpes est utilisée pour conserver seulement un pixel sur deux à la fois verticalement et horizontalement.

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ée en utilisant les découpes. 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é.

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.

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 un 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.

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 un 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.

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 des 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 vous imaginez pas que vous avez terminé avec ce module, il vous reste encore beaucoup à apprendre.

VI - Verrouillage de surface

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ées. Il est important de se le 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 (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 rapides.

VII - Transparence

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é.

VIII - Autres fonctions du module Surfarray

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 ceux-ci sont très optimisés.

IX - Utilisation plus avancée de Numeric

Voici deux dernières choses qu'il est bon de connaître à propos des tableaux de Numeric. En manipulant des tableaux de très grande taille, comme des 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 fournis 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ées sur 16 bits, Numeric n'utilise pas les 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ées. Certains tableaux (particulièrement les surfaces de pixels mappées, en codage RGB) retournent 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 contenu dans 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.

X - Remise du diplôme

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 de « feu » 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.

XI - Remerciements

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

Cette traduction est aussi disponible sur Wikibooks. La liste des contributeurs est disponible ici.

Merci à ClaudeLELOUP pour sa relecture orthographique.