6.1. Pourquoi des tableaux

La classe Image est le bon outil pour le travail sur les pixels car chacune de ses méthodes opère directement sur le tampon de pixels natif de la caméra en un seul appel rapide. La plupart de ce que l’application fait à une trame – seuillage, recherche de blobs, détection d’AprilTag, filtres de contours – vit déjà là.

Ce que la bibliothèque d’images n’expose pas, c’est le reste du travail numérique qu’une application OpenMV rencontre :

  • des tampons de capteur qui ne sont pas des pixels – échantillons ADC, axes d’une IMU (centrale inertielle), audio de microphone,

  • des nombres dérivés de l’image qu’aucune méthode intégrée ne renvoie – une colonne d’histogramme, un mélange personnalisé de deux trames, une transformation par pixel que le catalogue ne couvre pas,

  • un peu d’algèbre linéaire – la matrice de calibration qui rectifie l’objectif, la rotation qui fusionne l’IMU,

  • des calculs de traitement du signal – le contenu fréquentiel d’un tampon de vibration, le lissage appliqué à la sortie d’un capteur, un vecteur de caractéristiques qu’un classifieur attend en entrée.

Tout cela veut la même forme : un tampon de nombres avec une opération appliquée à chaque élément. Une boucle for Python est la façon évidente de l’écrire

for i in range(len(samples)):
    samples[i] = samples[i] * cal

La boucle fonctionne. Elle est aussi lente. Python est un langage interprété, et chaque itération d’une boucle Python supporte le coût d’exécuter l’interpréteur une fois : rechercher samples, lire l’élément i, multiplier, réécrire, faire avancer le compteur de boucle, vérifier la condition de boucle. Sur un tampon de mille échantillons de capteur, ces coûts d’interpréteur s’accumulent jusqu’à des dizaines de millisecondes pour ce qui est fondamentalement une opération rapide.

Ce surcoût mord à chaque fois qu’un script atteint un tampon. Une trame QVGA en niveaux de gris fait 76 800 pixels ; un accéléromètre à 100 Hz délivre une centaine d’échantillons à trois axes par seconde ; un microphone remplit un tampon de 1024 échantillons toutes les 64 ms. Une boucle for purement Python sur l’un de ces éléments transforme une tâche qui devrait prendre quelques microsecondes en une qui en prend des dizaines de millisecondes – et environ dix fois plus longtemps encore sur un tampon de la taille d’une image.

6.1.1. Les fonctions de bibliothèque sont plus rapides que les boucles

La solution consiste à exprimer l’opération comme un seul appel de fonction sur l’ensemble du tampon, au lieu d’une boucle Python sur ses éléments. numpy est exactement cela : une bibliothèque de calcul matriciel où chaque opération est une fonction déjà optimisée qui parcourt le tampon une fois du début à la fin. np.multiply(samples, cal) multiplie chaque élément de samples par cal à l’intérieur d’un seul appel – la même arithmétique que faisait la boucle, sans le coût de l’interpréteur par itération. La même multiplication de 1000 éléments qui prenait des dizaines de millisecondes en boucle Python prend des dizaines de microsecondes en appel numpy.

C’est l’accord que numpy propose dans tous les domaines : somme, moyenne, sin, exp, multiplication matricielle, primitives de traitement du signal – chacun est une seule fonction de bibliothèque qui opère sur tout un tampon à la fois. La contrepartie est que les données doivent vivre dans le type tableau de numpy et que l’opération doit être exprimée sur ce tableau, et non sur ses éléments un à un.

6.1.2. Pourquoi une liste ne convient pas

Une list Python ne peut pas faire l’affaire. Une liste peut contenir n’importe quel mélange d’objets – entiers, flottants, chaînes, autres listes – et une fonction de bibliothèque qui la lit doit encore examiner chaque emplacement pour découvrir ce qu’il contient et en extraire la valeur avant qu’une quelconque arithmétique ne se produise. Ce surcoût par emplacement est exactement le coût que paie la boucle Python. Les listes sont inadaptées au calcul matriciel rapide.

6.1.3. Pourquoi bytearray ne suffit pas non plus

Un bytearray est la bonne forme – un seul tampon typé, un octet par élément, le tout dans un unique bloc contigu. C’est ce que la plupart des API de périphériques orientées octets renvoient. Ce qui lui manque, c’est le calcul. bytearray * 2 répète le tampon plutôt que de doubler chaque valeur, et il n’y a pas de sens raisonnable pour bytearray + bytearray élément par élément.

La structure de données qui combine un tampon typé avec le calcul par élément est le ndarray. Ce qu’il y a à l’intérieur de la boîte et la façon dont chaque champ façonne le comportement du chemin rapide sont les fondations sur lesquelles repose le reste de ce chapitre.