5.3. Formats de pixels¶
Un algorithme qui détecte les contours s’attend à ce que chaque pixel contienne une valeur de luminosité. Un algorithme qui suit un objet coloré s’attend à ce que chaque pixel porte une couleur. Un algorithme qui effectue une fermeture morphologique s’attend à ce que chaque pixel soit soit activé, soit désactivé. Le format de pixel que porte une Image – l’un de ceux énumérés dans le catalogue des Vision Sensors – est ce qui rend ces attentes vérifiables d’emblée : le format indique, à l’avance, sous quelle forme se présentent les pixels, et donc quels algorithmes peuvent s’exécuter sur eux sans étape de conversion.
Cette page traite de la manière dont cette contrainte se manifeste en pratique. Le bon choix de format dépend de ce que le pipeline va faire, et les méthodes de conversion entre formats sont ce qui permet à un pipeline ayant besoin de plusieurs d’entre eux d’enchaîner les étapes.
Les cinq formats de pixels non compressés et la manière dont leurs octets sont agencés. JPEG et PNG ne sont pas représentés ici car ce sont des flux compressés de longueur variable plutôt que des grilles de pixels de taille fixe.¶
5.3.1. Le format de travail : niveaux de gris¶
L’essentiel de la vision industrielle classique se résume à travailler avec des valeurs de luminosité. La détection de contours, la mise en correspondance de modèles, le décodage d’AprilTag, l’estimation du flux optique, les opérateurs morphologiques, l’analyse de blobs – tous, au niveau où opèrent les algorithmes, examinent la luminosité de chaque pixel et la manière dont cette luminosité se compare à celle des pixels voisins. La couleur de la scène est souvent utile à l’application qui les appelle, mais les algorithmes eux-mêmes n’en ont pas besoin.
Le format niveaux de gris fournit aux algorithmes exactement cela, sans surcharge. Un octet par pixel contient une valeur de luminosité allant de 0 (noir) à 255 (blanc). Le format fait la moitié de la taille de RGB565 et de YUV422 et le tiers de celle de RGB888, de sorte que chaque opération traite moins de données – à la fois plus rapidement et avec moins de pression sur le tampon. Sur les caméras plus petites, où le tampon d’image se dispute la RAM avec le reste du script, cette différence d’empreinte peut être ce qui détermine si un pipeline tient ou non. Si la couleur n’est pas l’indice dont l’algorithme a besoin, les niveaux de gris sont la bonne réponse.
5.3.2. La couleur avec RGB565¶
Lorsque la couleur est l’indice – suivre un marqueur coloré, distinguer les pommes rouges des vertes, repérer un élément d’interface par sa teinte – deux octets par pixel procurent assez de couleur pour les types de classification que les algorithmes effectuent. RGB565 est le format couleur par défaut sur la caméra, et celui qu’attendent les méthodes sensibles à la couleur exposées par l’interface.
Le rendu d’une trame annotée – dessiner des boîtes de détection, écrire du texte de diagnostic, afficher la trame à l’écran ou l’envoyer vers un visualiseur distant – fait également naturellement appel à RGB565. La prévisualisation de l’IDE, les contrôleurs d’affichage embarqués et la plupart des destinations réseau consomment soit directement le format, soit le convertissent à moindre coût.
5.3.3. Bayer comme format de stockage¶
Une image Bayer est la sortie brute du capteur, avant que l’ISP ne la dématrice en une représentation couleur finie. Chaque pixel est un octet contenant un seul canal de couleur – celui que le filtre coloré à cette position de la mosaïque a laissé passer. Cela fait d’une image Bayer la même taille qu’une image en niveaux de gris et le tiers de celle de RGB888, ce qui correspond à l’utilité réelle de Bayer : stocker de nombreuses trames à la fois lorsque la RAM est la contrainte déterminante.
Le hic est que les algorithmes du module image n’opèrent pas directement sur les images Bayer. Sans dématriçage, aucun pixel ne porte assez d’informations pour porter un jugement de couleur à lui seul, et les motifs que recherchent les algorithmes – contours, coins, blobs – seraient déformés par la mosaïque. Les seules façons de lire ou de modifier une image Bayer sont get_pixel() et set_pixel() ; tout le reste attend une représentation finie.
Le schéma qui en découle consiste à stocker les trames au format Bayer aussi longtemps qu’elles doivent rester dans une file d’attente et à convertir chacune en niveaux de gris ou en RGB565 au moment où son traitement commence réellement. La conversion coûte des cycles processeur mais économise la RAM qui serait autrement immobilisée à conserver des trames finies pendant toute la durée de vie de l’application.
Note
Les seules opérations du module image directement sur les pixels Bayer sont get_pixel(), set_pixel(), et le chemin d’encodage JPEG qui alimente la prévisualisation de l’IDE ou un visualiseur distant. Le dessin, l’analyse et le filtrage nécessitent tous une conversion préalable en niveaux de gris, en RGB565 ou en binaire.
5.3.4. YUV422 pour les pipelines qui veulent les deux¶
YUV422 sépare les informations de chaque pixel en un canal de luminance (Y) et deux canaux de chrominance (U et V), et sous-échantillonne la chrominance de sorte que les paires de pixels adjacentes partagent un seul U et un seul V. Le nombre d’octets par pixel s’établit en moyenne à deux – comme pour RGB565 – mais ils sont disposés de telle sorte que le canal Y constitue déjà une image continue en niveaux de gris sur 8 bits, située à des décalages connus dans le tampon.
Cette disposition est exactement ce qu’un pipeline souhaite lorsque certaines de ses étapes relèvent du travail en niveaux de gris et d’autres ont besoin de couleur. Lire directement les valeurs Y pour les étapes en niveaux de gris évite le coût d’une conversion explicite ; les canaux U et V sont là quand une étape ultérieure a réellement besoin de couleur. En dehors de ce schéma précis, RGB565 est généralement le choix le plus simple pour la couleur et les niveaux de gris le choix le plus simple pour le travail axé sur la seule luminosité – la valeur de YUV422 vient de sa capacité à bien faire les deux en même temps.
Note
Le module image opère sur YUV422 de manière plus limitée que sur les niveaux de gris, RGB565 ou le binaire – lectures directes du canal Y pour le travail en niveaux de gris et chemin d’encodage JPEG qui alimente la prévisualisation de l’IDE ou un visualiseur distant. Les méthodes sensibles à la couleur attendent RGB565 ; les trames YUV422 nécessitent une conversion explicite avant toute analyse de couleur ou tout dessin.
5.3.5. Binaire, masques et sortie seuillée¶
Une image binaire correspond à un bit par pixel : chaque pixel vaut soit 0, soit 1. Le format apparaît rarement comme une capture du capteur ; il apparaît plutôt comme la sortie naturelle du seuillage (où un test de couleur ou de luminosité classe chaque pixel en « oui, correspond » ou « non, ne correspond pas ») et comme l’entrée naturelle des opérations morphologiques et de l’argument mask qu’acceptent de nombreuses méthodes.
L’avantage pratique du format est sa taille. Une image binaire occupe un huitième de l’empreinte d’une image en niveaux de gris, de sorte que transporter un grand masque – un choix par pixel des positions qu’une opération en aval doit toucher – est peu coûteux. Le fait que de nombreuses opérations acceptent une image binaire comme argument nommé mask= est l’autre facette du même point : le format est petit, et enchaîner la sortie binaire d’une étape vers l’entrée masque d’une autre est un schéma de pipeline courant.
5.3.6. JPEG et PNG à la frontière¶
Les objets Image JPEG et PNG diffèrent des autres du catalogue. Ce ne sont pas des grilles de pixels ; ce sont des flux d’octets compressés qui encodent les données de pixels sous une forme que les opérations au niveau du pixel ne peuvent pas lire. Appeler get_pixel() sur un JPEG ne renvoie pas le pixel à une position donnée ; le pixel n’est décompressé nulle part dans le tampon pour que la méthode aille le chercher.
JPEG et PNG apparaissent à la frontière du traitement d’image, là où les données de pixels quittent ou entrent dans la caméra sous forme compressée. Enregistrer une trame sur disque au format JPEG garde le fichier petit ; envoyer une trame sur un réseau au format JPEG garde la transmission peu coûteuse ; charger une trame de référence depuis un fichier JPEG lui permet de résider sur disque sous une forme bien plus compacte que ne le seraient les pixels bruts. Pour chacun de ces cas d’usage, la représentation compressée est la bonne réponse. Cependant, pour effectuer un véritable traitement sur un JPEG, l’application le convertit d’abord en un format exploitable – et c’est lors de cette conversion que les octets compressés se déploient en pixels et que le gonflement du tampon (un JPEG de 30 Ko peut devenir 600 Ko de RGB565) se produit réellement.
5.3.7. Convertir entre formats¶
Le chemin de conversion est ce qui assemble différents formats en un pipeline unique. Cinq méthodes de la classe Image prennent une image existante et en renvoient une nouvelle dans un format différent :
to_grayscale()produit une image d’un octet par pixel, le format que veulent les algorithmes classiques.to_rgb565()produit le format couleur de deux octets par pixel que parlent à la fois les méthodes sensibles à la couleur et la prévisualisation de l’IDE.to_bitmap()produit une image binaire d’un bit, le format qu’acceptent la morphologie et les argumentsmask.to_jpeg()produit une image compressée en JPEG, adaptée à l’enregistrement ou à la transmission.to_png()produit une image compressée en PNG lorsqu’un encodage sans perte est préféré aux fichiers plus petits du JPEG.
Chaque conversion s’exécute sur place par défaut : le tampon de l’image source est écrasé par le résultat converti, et les pixels d’origine de la source ont disparu une fois l’appel terminé. C’est l’option la plus économe à la fois en processeur et en mémoire, et c’est la bonne réponse lorsque la trame source ne sera nécessaire à rien d’autre.
Lorsque la source est encore nécessaire – lorsqu’une étape ultérieure du pipeline doit voir la trame d’origine – deux arguments nommés remplacent le comportement par défaut sur place. copy=True alloue un tampon distinct pour l’image convertie sur le tas Python et laisse la source intacte. copy_to_fb=True effectue la même allocation mais la place dans le tampon d’image au lieu du tas – ce vers quoi une application se tourne lorsque l’image convertie doit aboutir dans la prévisualisation de l’IDE, puisque l’IDE lit depuis le tampon d’image.
Deux méthodes supplémentaires produisent des images RGB565 colorées via une palette plutôt que par une conversion directe. to_rainbow() associe chaque valeur d’entrée à canal unique à une couleur le long d’un dégradé continu parcourant le spectre visible. to_ironbow() associe chaque valeur d’entrée à la palette non linéaire d’imageur thermique qui va du noir aux rouges et oranges foncés jusqu’au blanc. Ce sont toutes deux des outils de visualisation plutôt que de mesure ; l’objectif est de rendre lisible d’un coup d’œil une image à canal unique dont les valeurs brutes seraient autrement invisibles à l’œil.
5.3.8. Taille du tampon¶
Un dernier détail concernant les formats qu’il vaut la peine de préciser. size() indique toujours la taille du tampon d’octets, et non le nombre de pixels. Pour les formats non compressés, cela découle directement des dimensions et du nombre d’octets par pixel : width * height * bytes_per_pixel. Pour JPEG et PNG, il s’agit de la taille du flux compressé, qui varie d’une trame à l’autre selon le contenu de la scène. Le code qui alloue des tampons à partir de budgets en octets utilise size() pour le premier cas ; le code qui diffuse des trames compressées hors de la caméra le lit après chaque compression pour connaître le nombre d’octets que le flux contient réellement.