5.4. Lecture et écriture de pixels¶
La plupart des opérations sur une image dissimulent leur travail pixel par pixel à l’intérieur d’un seul appel de méthode, où les boucles qui parcourent chaque pixel s’exécutent à la vitesse native. Il existe cependant des cas où le code applicatif souhaite accéder directement à un pixel précis : pour lire ce qui se trouve à une position donnée, pour y écrire une nouvelle valeur, pour échantillonner un seul point lors d’une étape de calibration, ou pour déboguer une valeur à un emplacement connu. Le module image expose ce niveau d’accès à travers deux formes d’adressage, chacune correspondant à une manière différente de concevoir l’emplacement d’un pixel.
5.4.1. Adressage par coordonnée¶
La forme la plus naturelle est celle dont Coordinates a déjà développé le vocabulaire : nommer un pixel par ses coordonnées cartésiennes (x, y). get_pixel() prend (x, y) et renvoie la valeur à cette position ; set_pixel() prend les mêmes (x, y) accompagnés d’une valeur et l’écrit.
Ce que ces appels renvoient ou acceptent dépend du format de l’image. Les images en niveaux de gris, binaires et Bayer portent une seule valeur par pixel – une luminosité pour les niveaux de gris, un 0 ou un 1 pour le binaire, un échantillon d’un seul canal de couleur pour Bayer – de sorte que get_pixel() renvoie un seul entier. RGB565 porte trois canaux de couleur compactés sur 16 bits, et get_pixel les décompacte par défaut en un tuple (r, g, b), chaque canal étant ramené dans la plage 0 – 255.
Le comportement par défaut peut être inversé dans les deux sens. Passer rgbtuple=False à get_pixel sur une image RGB565 revient au mot compacté brut de 16 bits – la même forme que celle renvoyée par l’indice linéaire, et la forme efficace lorsque l’application va réécrire telle quelle la même valeur compactée. Passer rgbtuple=True sur une image à un seul canal fait l’inverse : la valeur stockée est convertie en un tuple RGB888 avant d’être renvoyée, les images Bayer passant par une étape de débayérisation à la volée. L’argument existe pour que le code appelant puisse demander les pixels dans un espace colorimétrique uniforme, quelle que soit la manière dont l’image sous-jacente les stocke.
Les images compressées – JPEG et PNG – ne sont pas prises en charge par get_pixel ni set_pixel. Leurs octets ne représentent pas des pixels à des positions connues, et les méthodes lèvent une erreur plutôt que de renvoyer une valeur qui n’aurait aucun sens.
En pratique, les schémas ressemblent à ceci :
v = img.get_pixel(40, 30) # grayscale: int 0..255
img.set_pixel(40, 30, 255) # write white
r, g, b = img.get_pixel(40, 30) # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0)) # write red
Si les (x, y) demandés se trouvent en dehors de l’image, get_pixel renvoie None et set_pixel ne fait rien. C’est indulgent à dessein : de nombreux algorithmes parcourent les abords des bords d’une image et indexent brièvement des positions hors limites, et une absence d’opération silencieuse est moins perturbante qu’une exception à chaque fois.
5.4.2. Adressage par indice linéaire¶
L’autre forme consiste à adresser les pixels par leur position dans le tampon sous-jacent. Rappelez-vous la disposition du tampon : les pixels sont stockés ligne par ligne, d’abord tous les pixels de la ligne du haut, puis tous ceux de la ligne suivante, et ainsi de suite jusqu’en bas. Cet agencement signifie que chaque pixel possède un unique indice entier comptant à partir de 0 en haut à gauche et s’incrémentant le long de chaque ligne tour à tour. Le pixel à la coordonnée (x, y) a pour indice linéaire y * width + x.
Les pixels sont adressés à la fois par les coordonnées cartésiennes (x, y) et par un indice linéaire qui parcourt le tampon ligne par ligne, de gauche à droite.¶
Le module image expose cet indice à travers la notation d’indexation ordinaire de Python : img[i] lit le pixel à l’indice linéaire i, img[i] = value en écrit un. Ce que la forme par indice renvoie est la valeur brute stockée pour le format, et non le tuple décompacté que get_pixel() renvoie par défaut. Cette distinction est importante, car le format choisi précédemment détermine à quoi ressemble la valeur brute :
Les pixels en niveaux de gris et Bayer reviennent sous forme d’entiers 8 bits.
Les pixels RGB565 et YUV422 reviennent sous forme d’entiers 16 bits – le mot compacté.
Les pixels binaires reviennent sous forme de
0ou de1.Les pixels JPEG et PNG reviennent sous forme d’entiers 8 bits, un octet à la fois du flux compressé. Ces valeurs sont opaques – ce sont des morceaux d’un encodage compressé plutôt que des pixels au sens ordinaire.
La forme par indice convient au code qui raisonne déjà en termes de décalages dans le tampon : une boucle qui parcourt chaque pixel une fois, un algorithme qui doit sauter d’une ligne à la fois, ou un fragment de code qui traduit entre différentes dispositions de tampon. Le code qui raisonne en termes de coordonnées x et y est mieux servi par get_pixel et set_pixel ; les deux formes adressent les mêmes pixels à travers des modèles mentaux différents.
L”Image est aussi itérable. for v in img: parcourt le tampon dans le même ordre par ligne, en livrant les valeurs brutes un pixel à la fois, et len(img) est le nombre de pixels pour les formats non compressés ou le nombre d’octets pour les flux compressés.
5.4.3. Pourquoi le traitement Python pixel par pixel est la voie lente¶
Une remarque pratique qu’il vaut la peine d’aborder honnêtement. Parcourir une image un pixel à la fois depuis Python est lent. Une image en niveaux de gris de 320 × 240 contient 76 800 pixels ; appeler get_pixel() sur chacun d’eux dans une boucle for exécute des millions d’instructions de bytecode MicroPython pour accomplir un travail qu’une méthode native équivalente pourrait terminer en quelques centaines de microsecondes. Ce n’est pas un facteur négligeable. C’est la différence entre un script qui traite les trames en temps réel et un autre qui se traîne bien en dessous de la cadence d’images de la caméra.
Presque toutes les méthodes de la surface Image existent parce qu’il y a une version native plus rapide d’un schéma pixel par pixel courant. Une boucle qui additionne deux images devient un unique appel natif. Une boucle qui lisse chaque pixel en le moyennant avec ses voisins en devient un autre. Une boucle qui classe chaque pixel par rapport à un seuil en devient un troisième. Le travail de l’application, la plupart du temps, consiste à reconnaître quelle méthode appliquée à l’image entière correspond au travail qu’aurait fait la boucle, et à utiliser celle-ci plutôt que d’écrire la boucle à la main.
La lecture et l’écriture au niveau du pixel restent le bon outil lorsque rien d’autre ne convient – réinjecter une mesure précise dans le tampon, échantillonner une position pour une étape de calibration, déboguer une valeur à un emplacement connu. Le point essentiel est qu’elles constituent la voie lente, utilisée lorsque les méthodes appliquées à l’image entière n’ont pas la forme dont l’application a besoin, et non comme manière par défaut d’opérer sur les pixels.