Comment concevoir un Kit de développement logiciel (SDK) de vision par ordinateur multiplateforme indépendant du langage: didacticiel pratique

(Cyrus Behroozi) (21 octobre 2020)

Jai récemment eu loccasion de faire une présentation au meetup Venice Computer Vision . Si vous nêtes pas familier, il sagit dun événement parrainé par Trueface où les développeurs et les passionnés de vision par ordinateur peuvent présenter des recherches, des applications et des activités pratiques en matière de vision par ordinateur. tutoriels.

Dans cet article, je vais passer en revue ma présentation de tutoriel sur la façon de concevoir un kit de développement de logiciel de vision par ordinateur (SDK) indépendant du langage pour un déploiement multiplateforme et une extensibilité maximale. Si vous souhaitez afficher l enregistrement en direct de la présentation, vous pouvez le faire ici . Jai également rendu lensemble du projet open source , alors nhésitez pas à lutiliser comme modèle pour votre prochain projet de vision par ordinateur.

cyrusbehr / sdk_design

Comment concevoir un SDK indépendant du langage pour un déploiement multiplateforme et une extensibilité maximale. Une vision par ordinateur de Venise…

github.com

Pourquoi ce didacticiel est-il important?

Daprès mon expérience, je nai jamais trouvé de guide complet qui résume toutes les étapes pertinentes nécessaires pour créer un SDK multi-plateforme indépendant du langage. Jai dû parcourir une documentation disparate pour trouver les bonnes informations, apprendre chaque composant séparément, puis tout fragmenter moi-même. Cétait frustrant. Cela a pris beaucoup de temps. Et maintenant vous, cher lecteur, bénéficiez de tout mon travail. Vous apprendrez ensuite à créer un SDK multiplateforme et indépendant du langage. Tous les éléments essentiels sont là. Aucune des peluches, sauf quelques mèmes. Amusez-vous bien.

Dans ce didacticiel, vous pouvez vous attendre à apprendre comment:

  • Créer une bibliothèque de base de vision par ordinateur en C ++
  • Compiler et croiser- compilez la bibliothèque pour AMD64, ARM64 et ARM32
  • Empaquetez la bibliothèque et toutes les dépendances en une seule bibliothèque statique
  • Automatisez les tests unitaires
  • Configurez une pipeline dintégration (CI)
  • Écrire des liaisons python pour notre bibliothèque
  • Générer de la documentation directement à partir de notre API

Pour les besoins de cette démo, nous va créer un SDK de détection de visage et de repère à laide dun détecteur de visage open-source appelé MTCNN.

Exemple des cadres de délimitation du visage et des repères faciaux

Notre fonction API prendra un chemin dimage puis retournera les coordonnées du cadre de délimitation du visage et des repères faciaux. La capacité de détecter les visages est très utile en vision par ordinateur car cest la première étape de nombreux pipelines, y compris la reconnaissance faciale, la prédiction de lâge et le flou automatique du visage.

Remarque: Pour cela tutoriel, je vais travailler sur Ubuntu 18.04.

Pourquoi utiliser C ++ pour notre bibliothèque?

Lexécution de code C ++ efficace peut ressembler à ça

La majorité de notre bibliothèque sera écrite en C ++, un langage compilé et typé statiquement. Ce n’est un secret pour personne que C ++ est un langage de programmation très rapide; son niveau est suffisamment bas pour nous donner la vitesse que nous souhaitons et a une surcharge dexécution minimale.

Dans les applications de vision par ordinateur, nous manipulons généralement beaucoup dimages, effectuons des opérations matricielles, exécutons des inférences dapprentissage automatique, ce qui implique une énorme quantité de calcul. La vitesse dexécution est donc critique. Ceci est particulièrement important dans les applications en temps réel où vous devez réduire la latence afin datteindre la fréquence dimages souhaitée – souvent, nous navons que des millisecondes pour exécuter tout notre code.

Un autre avantage du C ++ est que si nous compilez pour une certaine architecture et liez toutes les dépendances de manière statique, puis nous pouvons lexécuter sur ce matériel sans nécessiter dinterprètes ou de bibliothèques supplémentaires. Croyez-le ou non, nous pouvons même fonctionner sur un appareil embarqué sans système dexploitation!

Structure de répertoire

Nous utiliserons la structure de répertoire suivante pour notre projet.

3rdparty contiendra les bibliothèques de dépendances tierces requises par notre projet.

dist contiendra les fichiers qui seront distribués aux utilisateurs finaux du SDK. Dans notre cas, ce sera la bibliothèque elle-même, et le fichier den-tête associé.

docker contiendra le fichier docker qui sera utilisé pour générer une image docker pour les builds CI.

docs contiendra les scripts de construction requis pour générer la documentation directement à partir de notre fichier den-tête.

include contiendra tous les fichiers dinclusion pour lAPI publique.

models contiendra les fichiers du modèle dapprentissage en profondeur de détection de visage.

python contiendra le code requis pour générer des liaisons python.

src contiendra tous les fichiers cpp qui seront compilés, ainsi que tous les fichiers den-tête qui ne seront pas distribués avec le SDK (fichiers den-tête internes).

test contiendra nos tests unitaires.

tools contiendra nos fichiers de chaîne doutils CMake requis pour la compilation croisée.

Installation des bibliothèques de dépendances

Pour ce projet, le tiers les bibliothèques de dépendances requises sont ncnn , une bibliothèque dinférence de machine learning légère, OpenCV , une bibliothèque daugmentation dimage, Catch2 , une bibliothèque de tests unitaires et enfin pybind11 , une bibliothèque utilisé pour générer des liaisons python. Les deux premières bibliothèques devront être compilées en tant que bibliothèques autonomes, alors que les deux dernières sont uniquement en-tête et donc nous navons besoin que de la source.

Une façon dajouter ces bibliothèques à nos projets est via les sous-modules git . Bien que cette approche fonctionne, je suis personnellement fan de lutilisation de scripts shell qui extraient le code source puis construisent pour les plates-formes souhaitées: dans notre cas AMD64, ARM32 et ARM64.

Voici un exemple de ce que lon de ces scripts de construction ressemble à:

Le script est assez simple. Il commence par extraire le code source de la version souhaitée du référentiel git. Ensuite, CMake est utilisé pour préparer la construction, puis make est appelé pour conduire le compilateur à construire le code source.

Ce que vous remarquerez, cest que la principale différence entre la version AMD64 et les versions ARM est que les builds ARM passent un paramètre CMake supplémentaire appelé CMAKE_TOOLCHAIN_FILE. Cet argument est utilisé pour spécifier à CMake que larchitecture cible de construction (ARM32 ou ARM64) est différente de larchitecture hôte (AMD64 / x86_64). CMake est donc invité à utiliser le compilateur croisé spécifié dans le fichier de chaîne doutils sélectionné pour créer la bibliothèque (plus dinformations sur les fichiers de chaîne doutils plus loin dans ce didacticiel). Pour que ce script shell fonctionne, vous devrez avoir les compilateurs croisés appropriés installés sur votre machine Ubuntu. Ceux-ci peuvent être installés facilement en utilisant apt-get et des instructions sur la façon de le faire sont affichées ici .

Notre API de bibliothèque

Notre API de bibliothèque ressemble à ceci:

Comme je suis super créatif, jai décidé de nommer mon SDK MySDK. Dans notre API, nous avons une énumération appelée ErrorCode, nous avons une structure appelée Point, et enfin, nous avons une fonction membre publique appelée getFaceBoxAndLandmarks. Pour la portée de ce tutoriel, je nentrerai pas dans les détails de limplémentation du SDK. Lessentiel est que nous lisons limage en mémoire à laide dOpenCV, puis effectuons une inférence dapprentissage automatique à laide de ncnn avec des modèles open source pour détecter la zone de délimitation du visage et les points de repère. Si vous souhaitez vous plonger dans limplémentation, vous pouvez le faire ici .

Ce à quoi je veux que vous soyez attentif est le modèle de conception que nous utilisons. Nous utilisons une technique appelée Pointer to implementation, ou pImpl en abrégé, qui supprime essentiellement les détails dimplémentation dune classe en les plaçant dans une classe séparée. Dans le code ci-dessus, ceci est réalisé en déclarant en avant la classe Impl, puis en ayant un unique_ptr à cette classe en tant que variable membre privée. Ce faisant, non seulement nous cachons limplémentation aux regards indiscrets de lutilisateur final (ce qui peut être très important dans un SDK commercial), mais nous réduisons également le nombre den-têtes dont dépend notre en-tête API (et empêchons ainsi notre En-tête API de #include en-têtes de bibliothèque de dépendances).

Une note sur les fichiers de modèle

Jai dit que nous nallions pas aller plus loin les détails de la mise en œuvre, mais il y a quelque chose que je pense quil vaut la peine de mentionner. Par défaut, le détecteur de visage open source que nous utilisons, appelé MTCNN, charge les fichiers de modèle de machine learning lors de lexécution. Ce n’est pas idéal, car cela signifie que nous devrons distribuer les modèles à l’utilisateur final. Ce problème est encore plus important avec les modèles commerciaux où vous ne voulez pas que les utilisateurs aient un accès gratuit à ces fichiers de modèles (pensez aux innombrables heures consacrées à la formation de ces modèles). Une solution consiste à crypter les fichiers de ces modèles, ce que je vous conseille absolument de faire.Cependant, cela signifie toujours que nous devons expédier les fichiers de modèle avec le SDK. En fin de compte, nous voulons réduire le nombre de fichiers que nous envoyons à un utilisateur pour lui faciliter lutilisation de notre logiciel (moins de fichiers signifie moins dendroits où se tromper). Nous pouvons donc utiliser la méthode ci-dessous pour convertir les fichiers de modèle en fichiers den-tête et les intégrer réellement dans le SDK lui-même.

La commande xdd bash est utilisée pour générer des vidages hexadécimaux et peut être utilisée pour générer un fichier den-tête à partir dun fichier binaire. Nous pouvons donc inclure les fichiers de modèle dans notre code comme des fichiers den-tête normaux et les charger directement depuis la mémoire. L’une des limites de cette approche est qu’elle n’est pas pratique avec des fichiers de modèle très volumineux car elle consomme trop de mémoire au moment de la compilation. À la place, vous pouvez utiliser un outil tel que ld pour convertir ces fichiers de modèle volumineux directement en fichiers objet.

CMake et compiler notre bibliothèque

Nous pouvons maintenant utiliser CMake pour générer les fichiers de construction de notre projet. Si vous nêtes pas familier, CMake est un générateur de système de construction utilisé pour gérer le processus de construction. Ci-dessous, vous verrez à quoi ressemble une partie de la racine CMakeLists.txt (fichier CMake).

En gros, nous créons une bibliothèque statique appelée my_sdk_static avec les deux fichiers source qui contiennent notre implémentation, my_sdk.cpp et mtcnn.cpp. La raison pour laquelle nous créons une bibliothèque statique est que, daprès mon expérience, il est plus facile de distribuer une bibliothèque statique aux utilisateurs et elle est plus conviviale pour les périphériques embarqués. Comme je lai mentionné ci-dessus, si un exécutable est lié à une bibliothèque statique, il peut être exécuté sur un appareil intégré qui na même pas de système dexploitation. Ce nest tout simplement pas possible avec une bibliothèque dynamique. De plus, avec les bibliothèques dynamiques, nous devons nous soucier des versions de dépendance. Nous pourrions même avoir besoin dun fichier manifeste associé à notre bibliothèque. Les bibliothèques liées statiquement ont également un profil de performances légèrement meilleur que leurs homologues dynamiques.

La prochaine chose que nous faisons dans notre script CMake est dindiquer à CMake où trouver les fichiers den-tête dinclusion nécessaires dont nos fichiers source ont besoin. Quelque chose à noter: bien que notre bibliothèque compile à ce stade, lorsque nous essaierons de créer un lien avec notre bibliothèque (avec un exécutable par exemple), nous obtiendrons une tonne absolue de références indéfinies à des erreurs de symboles. Cest parce que nous navons lié aucune de nos bibliothèques de dépendances. Donc, si nous voulions réussir à lier un exécutable à libmy_sdk_static.a, alors nous devrons également localiser et lier toutes les bibliothèques de dépendances (modules OpenCV, ncnn, etc.) Contrairement aux bibliothèques dynamiques, les bibliothèques statiques ne peuvent pas résoudre leurs propres dépendances. Il sagit essentiellement dune collection de fichiers objets empaquetés dans une archive.

Plus tard dans ce didacticiel, je montrerai comment nous pouvons regrouper toutes les bibliothèques de dépendances dans notre bibliothèque statique afin que lutilisateur nait pas besoin de vous inquiétez de la liaison avec lune des bibliothèques de dépendances.

Compilation croisée de nos fichiers de bibliothèque et de chaîne doutils

Edge computing est tellement… avant-gardiste

De nombreuses applications de vision par ordinateur sont déployées à la périphérie. Ceci généralement implique lexécution du code sur des périphériques embarqués basse consommation qui ont généralement des processeurs ARM. Puisque C ++ est un langage compilé, nous devons compiler notre code pour larchitecture CPU sur laquelle lapplication sera exécutée (chaque architecture utilise des instructions dassemblage différentes).

Avant de nous y plonger, abordons également le différence entre ARM32 et ARM64, également appelé AArch32 et AArch64. AArch64 fait référence à lextension 64 bits de larchitecture ARM et dépend à la fois du processeur et du système dexploitation. Ainsi, par exemple, même si le Raspberry Pi 4 dispose dun processeur ARM 64 bits, le système dexploitation par défaut Raspbian est 32 bits. Par conséquent, un tel périphérique nécessite un binaire compilé AArch32. Si nous devions exécuter un système dexploitation 64 bits tel que Gentoo sur cet appareil Pi, alors nous aurions besoin dun binaire compilé AArch64. Un autre exemple de périphérique intégré populaire est le NVIDIA Jetson qui a un GPU intégré et exécute AArch64.

Afin de faire une compilation croisée, nous devons spécifier à CMake que nous ne compilons pas pour larchitecture du machine sur laquelle nous construisons actuellement. Par conséquent, nous devons spécifier le compilateur croisé que CMake doit utiliser. Pour AArch64, nous utilisons le compilateur aarch64-linux-gnu-g++, et pour AArch32 nous utilisons le compilateur arm-linux-gnuebhif-g++ (hf signifie hard float ).

Ce qui suit est un exemple de fichier de chaîne doutils. Comme vous pouvez le voir, nous spécifions dutiliser le compilateur croisé AArch64.

De retour à notre racine CMakeLists.txt, nous pouvons ajoutez le code suivant en haut du fichier.

Fondamentalement, nous ajoutons des options CMake qui peut être activé à partir de la ligne de commande pour effectuer une compilation croisée. Lactivation des options BUILD_ARM32 ou BUILD_ARM64 sélectionnera le fichier de chaîne doutils approprié et configurera la construction pour une compilation croisée.

Empaquetage de notre SDK avec des bibliothèques de dépendances

Comme mentionné précédemment, si un développeur souhaite établir un lien avec notre bibliothèque à ce stade, il devra également établir un lien avec toutes les bibliothèques de dépendances afin de résoudre tous les symboles de bibliothèques de dépendances. Même si notre application est assez simple, nous avons déjà huit bibliothèques de dépendances! Le premier est ncnn, puis nous avons trois bibliothèques de modules OpenCV, puis nous avons quatre bibliothèques utilitaires qui ont été construites avec OpenCV (libjpeg, libpng, zlib, libtiff). Nous pourrions demander à lutilisateur de créer lui-même les bibliothèques de dépendances ou même de les expédier avec notre bibliothèque, mais en fin de compte, cela demande plus de travail à lutilisateur et nous sommes tous sur le point dabaisser la barrière dutilisation. La situation idéale est de savoir si nous pouvons envoyer à lutilisateur une bibliothèque unique contenant notre bibliothèque avec toutes les bibliothèques de dépendances tierces autres que les bibliothèques système standard. Il savère que nous pouvons y parvenir en utilisant un peu de magie CMake.

Nous ajoutons dabord une cible personnalisée à notre CMakeLists.txt, puis exécutez ce quon appelle un script IRM. Ce script MRI est passé à la commande bash ar -M, qui combine essentiellement toutes les bibliothèques statiques dans une seule archive. Ce qui est bien avec cette méthode, cest quelle gérera gracieusement les noms de membres qui se chevauchent dans les archives dorigine, nous navons donc pas à nous soucier des conflits. Construire cette cible personnalisée produira libmy_sdk.a qui contiendra notre SDK ainsi que toutes les archives de dépendances.

Attendez une seconde: faisons le point sur ce que nous Je lai fait jusquici.

Respirez. Prenez une collation. Appelez votre maman.

À ce stade, nous avons une bibliothèque statique appelée libmy_sdk.a qui contient notre SDK et toutes les bibliothèques de dépendances, que nous avons regroupées dans une seule archive. Nous avons également la possibilité de compiler et de recompiler (en utilisant des arguments de ligne de commande) pour toutes nos plates-formes cibles.

Tests unitaires

Lorsque vous exécutez vos tests unitaires pour la première fois

Je nai probablement pas besoin de expliquer pourquoi les tests unitaires sont importants, mais fondamentalement, ils sont une partie cruciale de la conception du SDK qui permet au développeur de sassurer que le SDK fonctionne comme en retrait. De plus, si des modifications majeures sont apportées sur toute la ligne, cela aide à les suivre et à pousser les correctifs plus rapidement.

Dans ce cas précis, la création dun exécutable de test unitaire nous donne également la possibilité détablir un lien avec le bibliothèque combinée que nous venons de créer pour nous assurer que nous pouvons lier correctement comme prévu (et nous nobtenons aucune de ces erreurs de référence à symbole indéfinies).

Nous utilisons Catch2 comme cadre de test unitaire . La syntaxe est décrite ci-dessous:

Le fonctionnement de Catch2 est que nous avons cette macro appelée TEST_CASE et une autre macro appelée SECTION. Pour chaque SECTION, le TEST_CASE est exécuté depuis le début. Ainsi, dans notre exemple, mySdk sera dabord initialisé, puis la première section nommée «Non face image» sera exécutée. Ensuite, mySdk sera déconstruit avant dêtre reconstruit, puis la deuxième section nommée «Faces in image» sexécutera. Cest génial car cela garantit que nous avons un nouvel objet MySDK sur lequel opérer pour chaque section. Nous pouvons ensuite utiliser des macros telles que REQUIRE pour faire nos assertions.

Nous pouvons utiliser CMake pour construire un exécutable de test unitaire appelé run_tests. Comme nous pouvons le voir dans lappel à target_link_libraries à la ligne 3 ci-dessous, la seule bibliothèque avec laquelle nous devons établir un lien est notre libmy_sdk.a et aucune autre bibliothèques de dépendances.

Documentation

Si seulement les utilisateurs lisaient la fichue documentation.

Nous utiliserons doxygen pour générer la documentation directement à partir de notre fichier den-tête. Nous pouvons continuer et documenter toutes nos méthodes et types de données dans notre en-tête public en utilisant la syntaxe indiquée dans lextrait de code ci-dessous.Assurez-vous de spécifier tous les paramètres dentrée et de sortie pour toutes les fonctions.

Afin de générer réellement la documentation , nous avons besoin de quelque chose qui sappelle un doxyfile, qui est essentiellement un plan pour indiquer à doxygen comment générer la documentation. Nous pouvons générer un doxyfile générique en exécutant doxygen -g dans notre terminal, en supposant que doxygen soit installé sur votre système. Ensuite, nous pouvons éditer le doxyfile. Au minimum, nous devons spécifier le répertoire de sortie ainsi que les fichiers dentrée.

Dans notre cas, nous voulons uniquement générer de la documentation à partir de notre fichier den-tête API, cest pourquoi nous avons spécifié le répertoire dinclusion. Enfin, vous utilisez CMake pour construire la documentation, ce qui peut être fait comme ça .

Liaisons Python

Êtes-vous fatigué de voir des gifs semi-pertinents? Ouais, moi non plus.

Soyons honnêtes. Le C ++ nest pas le langage le plus simple ou le plus convivial à développer. Par conséquent, nous souhaitons étendre notre bibliothèque pour prendre en charge les liaisons de langage afin de la rendre plus facile à utiliser pour les développeurs. Je vais le démontrer en utilisant Python car cest un langage de prototypage de vision par ordinateur populaire, mais dautres liaisons de langage sont tout aussi faciles à écrire. Nous utilisons pybind11 pour y parvenir:

Nous commençons par utiliser le PYBIND11_MODULE macro qui crée une fonction qui sera appelée lorsquune instruction dimportation est émise depuis python. Ainsi, dans lexemple ci-dessus, le nom du module python est mysdk. Ensuite, nous sommes en mesure de définir nos classes et leurs membres en utilisant la syntaxe pybind11.

Voici quelque chose à noter: en C ++, il est assez courant de passer des variables en utilisant une référence mutable qui permet à la fois laccès en lecture et en écriture. Cest exactement ce que nous avons fait avec notre fonction membre API avec les paramètres faceDetected et fbAndLandmarks. En python, tous les arguments sont passés par référence. Cependant, certains types python de base sont immuables, y compris bool. Par coïncidence, notre paramètre faceDetected est un booléen passé par référence mutable. Nous devons donc utiliser la solution de contournement indiquée dans le code ci-dessus aux lignes 31 à 34 où nous définissons le bool dans notre fonction wrapper python, puis le passons à notre fonction C ++ avant de renvoyer la variable dans le cadre dun tuple.

Une fois que nous avons construit la bibliothèque de liaisons python, nous pouvons facilement lutiliser en utilisant le code ci-dessous:

Intégration continue

Pour notre pipeline dintégration continue, nous utiliserons un outil appelé CircleCI que jaime beaucoup car il sintègre directement à Github. Une nouvelle compilation sera automatiquement déclenchée chaque fois que vous poussez un commit. Pour commencer, accédez au site Web CircleCI et connectez-le à votre compte Github, puis sélectionnez le projet que vous souhaitez ajouter. Une fois ajouté, vous devrez créer un répertoire .circleci à la racine de votre projet et créer un fichier appelé config.yml dans ce répertoire.

Pour tous ceux qui ne sont pas familiers, YAML est un langage de sérialisation couramment utilisé pour les fichiers de configuration. Nous pouvons lutiliser pour indiquer quelles opérations nous voulons que CircleCI effectue. Dans lextrait YAML ci-dessous, vous pouvez voir comment nous construisons dabord lune des bibliothèques de dépendances, ensuite construisons le SDK lui-même, et enfin construisons et exécutons les tests unitaires.

Si nous sommes intelligents (et je suppose que vous lêtes si vous êtes arrivé jusquici), nous pouvons utiliser la mise en cache pour réduire considérablement les temps de construction. Par exemple, dans le YAML ci-dessus, nous mettons en cache la construction OpenCV en utilisant le hachage du script de construction comme clé de cache. De cette façon, la bibliothèque OpenCV ne sera reconstruite que si le script de construction a été modifié – sinon, la construction mise en cache sera utilisée. Une autre chose à noter est que nous exécutons la construction à lintérieur dune image docker de notre choix. Jai sélectionné une image Docker personnalisée ( ici est le Dockerfile) dans laquelle jai installé toutes les dépendances système.

Fin.

Et voilà. Comme tout produit bien conçu, nous voulons prendre en charge les plates-formes les plus demandées et le rendre facile à utiliser pour le plus grand nombre de développeurs. À laide du didacticiel ci-dessus, nous avons créé un SDK accessible en plusieurs langues et déployable sur plusieurs plates-formes. Et vous n’avez même pas eu à lire vous-même la documentation de pybind11. Jespère que vous avez trouvé ce didacticiel utile et divertissant. Bonne construction.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *