5.6. Dessiner des formes et du texte

Un algorithme qui décide quelque chose à propos d’une image a souvent besoin que cette décision soit visible. Un détecteur de blobs trouve une région qui intéresse l’application ; l’application veut que la région soit dessinée sur la trame afin qu’un opérateur – ou le développeur qui exécute le script – puisse voir ce qui a été trouvé. Une transformation de coordonnées met en correspondance une position d’entrée avec une position de sortie ; déboguer cela signifie généralement marquer les deux positions sur la même image. L’aperçu de l’IDE lit ce qui se trouve dans le tampon d’image au moment où il l’interroge, de sorte que la manière la plus simple de rendre visible la sortie d’un algorithme est d’écrire des annotations dans le tampon d’image lui-même. La famille de dessin de la classe Image est la boîte à outils dédiée précisément à ce travail.

5.6.1. Les primitives

Chaque méthode de dessin place une sorte spécifique de marque sur l’image. Le catalogue est restreint et reste proche des primitives géométriques dont une annotation a réellement besoin :

  • draw_line() – un segment de droite entre deux extrémités.

  • draw_rectangle() – un rectangle aligné sur les axes, creux ou plein.

  • draw_circle() – un cercle autour d’un centre, creux ou plein.

  • draw_ellipse() – une ellipse avec une rotation arbitraire.

  • draw_cross() – un signe plus en un point, la marque habituelle pour un centroïde ou une cible de clic.

  • draw_arrow() – une flèche d’un point de départ vers un point d’arrivée.

  • draw_edges() – les quatre côtés d’un quadrilatère arbitraire, à partir des quatre points de coin ; la manière naturelle de tracer le contour d’un tag détecté ou d’une région déformée en perspective.

  • draw_string() – du texte à partir d’une police bitmap intégrée.

Chacune de ces méthodes modifie l’image source sur place et renvoie la même image pour le chaînage, suivant la convention des méthodes d’opération établie précédemment.

Une grille de petits panneaux montrant chacune des huit primitives de dessin appliquée une fois. Chaque panneau contient une ligne, un rectangle, un cercle, une ellipse, une croix, une flèche, un quadrilatère ou une courte chaîne de texte, avec le nom de la méthode qui l'a produit indiqué en dessous.

Les huit primitives de dessin, une par panneau. Chaque méthode réalise un type de marque.

5.6.2. Couleur

Chaque méthode de dessin prend un argument color qui détermine quelle valeur écrire dans chaque pixel peint. La forme que prend cet argument dépend du format de l’image. Pour une image RGB565, la forme naturelle est un tuple (r, g, b) avec chaque canal dans 0255 ; le module empaquette cela dans le mot RGB565 16 bits avant de l’écrire. Pour une image en niveaux de gris, la forme naturelle est un entier unique de luminosité de 0 (noir) à 255 (blanc). Les méthodes acceptent aussi la valeur brute stockée du format – un mot empaqueté 16 bits pour RGB565, un entier 8 bits pour les niveaux de gris – qui est la forme efficace lorsque la couleur a été calculée ailleurs et se trouve déjà sous la forme stockée.

Omettre l’argument color peint en blanc. Cette valeur par défaut est pratique pour le travail en niveaux de gris, où le blanc est la valeur maximale et se lit clairement sur la plupart des arrière-plans. Pour les superpositions de débogage en RGB565, c’est presque toujours un mauvais choix : le vert ou le rouge se lit généralement mieux sur le genre de scène qu’une caméra observe réellement, et une couleur explicite communique l’intention.

5.6.3. Épaisseur et remplissage

La plupart des méthodes géométriques prennent deux indicateurs qui déterminent comment la marque est dessinée :

  • thickness=N définit la largeur de trait en pixels. La valeur par défaut est 1, ce qui convient pour la plupart des superpositions ; une valeur plus grande est utile lorsqu’une annotation doit rester visible sur une scène chargée ou après qu’une étape ultérieure du pipeline a modifié l’image davantage.

  • fill=True fait passer la marque d’un contour à une forme pleine, peignant chaque pixel intérieur avec la couleur choisie. La valeur par défaut est False.

Ces indicateurs ne s’appliquent pas aux primitives qui n’ont pas d’intérieur à remplir – la ligne, la croix, la flèche, le quadrilatère – où seul thickness a un sens.

5.6.4. Dessiner du texte

draw_string() écrit des caractères à partir d’une police bitmap intégrée de 8 par 10 pixels. x et y sont le coin en haut à gauche de la cellule du premier caractère, text est la chaîne à dessiner, et color suit la même convention que les méthodes géométriques. La police porte toute la plage ASCII imprimable et n’a pas de crénage – chaque caractère occupe la même cellule de 8 pixels de large – ce qui rend la sortie facile à positionner.

img.draw_string(10, 10, "blobs: 3", color=(0, 255, 0))

La chaîne peut inclure des sauts de ligne (\n) ; chaque saut de ligne déplace le caractère suivant au début d’une nouvelle ligne dix pixels en dessous de la précédente. L’argument scale dessine chaque caractère à une taille plus grande selon un facteur à virgule flottante, et x_spacing et y_spacing ajoutent un espacement autour de chaque caractère. Un petit ensemble d’indicateurs de rotation / miroir / retournement s’applique soit à la chaîne entière, soit à chaque caractère individuellement – assez de contrôle pour disposer le texte selon un angle ou contre un bord non horizontal lorsque la mise en page l’exige.

5.6.5. Effacer le canevas

Une méthode de la famille ne dessine aucune marque spécifique. Elle remet simplement chaque pixel de l’image à zéro :

  • clear() – met chaque pixel à zéro, éventuellement restreint à une ROI ou délimité par un masque.

clear() est la bonne réponse lorsqu’une application compose une annotation à partir de zéro à chaque trame – commencer avec un canevas noir, dessiner les nouvelles annotations, transmettre le résultat à l’affichage – plutôt que de superposer par-dessus la trame capturée. C’est aussi la manière la moins coûteuse de préparer une image temporaire destinée à servir de tampon de masque.

Une image fraîchement allouée est déjà à zéro dès le constructeur, donc clear() importe spécifiquement pour les tampons réutilisés entre les trames.