Jak navrhnout Language-Agnostic Cross-Platform Computer Vision SDK: A Hands-On Tutorial

(Cyrus Behroozi) (21. října 2020)

Nedávno jsem měl příležitost představit se na setkání Venice Computer Vision . Pokud nejste obeznámeni, jedná se o akci sponzorovanou Trueface , kde mohou vývojáři i nadšenci počítačového vidění předvést špičkový výzkum, aplikace a praktické ukázky počítačového vidění výukové programy.

V tomto článku se budu věnovat své výukové prezentaci o tom, jak navrhnout jazykově agnostickou softwarovou vývojovou sadu pro počítačové vidění (SDK) pro nasazení napříč platformami a maximální rozšiřitelnost. Pokud chcete zobrazit živý záznam prezentace, můžete tak učinit zde . Také jsem vytvořil celý projekt jako open source , takže jej můžete použít jako šablonu pro svůj další projekt počítačového vidění.

cyrusbehr / sdk_design

Jak navrhnout jazykovou agnostickou SDK pro nasazení na různé platformy a maximální rozšiřitelnost. Benátské počítačové vidění…

github.com

Proč je tento výukový program důležitý

Podle mých zkušeností jsem nikdy nenašel všeobjímajícího průvodce, který by shrnuje všechny příslušné kroky potřebné k vytvoření jazykově agnostické sady SDK pro různé platformy. Musel jsem prohledávat různorodou dokumentaci, abych získal ty správné kousky informací, naučit se každou složku zvlášť a poté to všechno po částech sám. Bylo to frustrující. Trvalo to hodně času. A nyní vy, drahý čtenáři, budete mít prospěch z celé mé práce. Dozvíte se, jak vytvořit jazykově agnostickou sadu SDK pro různé platformy. Všechno podstatné je tam. Žádný z chmýří, uložte si pár memů. Užijte si to.

V tomto výukovém programu můžete očekávat, že:

  • Vytvoření základní knihovny počítačového vidění v C ++
  • Kompilace a křížové zkompilujte knihovnu pro AMD64, ARM64 a ARM32
  • Zabalte knihovnu a všechny závislosti do jedné statické knihovny
  • Automatizujte testování jednotek
  • Nastavte kontinuální pipeline integration (CI)
  • Psaní vazeb pythonu pro naši knihovnu
  • Generování dokumentace přímo z našeho API

Kvůli této ukázce jsme vytvoří sadu SDK pro detekci tváří a orientačních bodů pomocí detektoru obličeje s otevřeným zdrojovým kódem s názvem MTCNN.

Příklad rámečků ohraničujících obličej a orientačních bodů obličeje

Naše funkce API provede cestu obrázku a vrátí souřadnice rámečku ohraničujícího obličej a orientačních bodů obličeje. Schopnost detekovat obličeje je v počítačovém vidění velmi užitečná, protože je prvním krokem v mnoha kanálech, včetně rozpoznávání obličeje, predikce věku a automatického rozmazání obličeje.

Poznámka: K tomu tutoriál, budu pracovat na Ubuntu 18.04.

Proč používat C ++ pro naši knihovnu?

Spuštění efektivního kódu C ++ se může cítit takto

Většina naší knihovny bude napsána v C ++, kompilovaném a staticky zadaném jazyce. Není žádným tajemstvím, že C ++ je velmi rychlý programovací jazyk; je dostatečně nízká na to, aby nám poskytla požadovanou rychlost, a má minimální přidanou runtime režii.

V aplikacích počítačového vidění obecně manipulujeme se spoustou obrázků, provádíme maticové operace, provádíme odvození strojového učení, což vše zahrnuje obrovské množství výpočetní techniky. Rychlost provádění je proto kritická. To je obzvláště důležité v aplikacích v reálném čase, kde potřebujete snížit latenci, abyste dosáhli požadované rychlosti snímků – často máme jen milisekundy na spuštění celého našeho kódu.

Další výhodou C ++ je, že pokud zkompilovat pro určitou architekturu a staticky propojit všechny závislosti, pak ji můžeme spustit na daném hardwaru bez nutnosti dalších tlumočníků nebo knihoven. Věřte tomu nebo ne, můžeme dokonce běžet na holém kovovém zařízení bez operačního systému!

Adresářová struktura

Pro náš projekt budeme používat následující adresářovou strukturu.

3rdparty bude obsahovat knihovny závislostí třetích stran požadované naším projektem.

dist bude obsahovat soubory, které budou distribuovány koncovým uživatelům sady SDK. V našem případě to bude samotná knihovna a přidružený soubor záhlaví.

docker bude obsahovat ukotvitelný soubor, který bude použit ke generování ukotvitelného obrazu pro sestavení CI.

docs bude obsahovat skripty pro sestavení potřebné pro generování dokumentace přímo z našeho souboru záhlaví.

include bude obsahovat všechny soubory pro zahrnutí pro veřejné API.

models bude obsahovat soubory modelu hlubokého učení s detekcí obličeje.

python bude obsahovat kód potřebný pro generování vazeb pythonu.

src bude obsahovat všechny soubory cpp, které budou kompilovány, a také všechny soubory záhlaví, které nebudou distribuovány pomocí SDK (interní soubory záhlaví).

test bude obsahovat naše testy jednotek.

tools bude obsahovat naše soubory nástrojů CMake potřebné pro křížové kompilace.

Instalace závislých knihoven

U tohoto projektu třetí strana povinné knihovny závislostí jsou ncnn , lehká odvozovací knihovna pro strojové učení, OpenCV , knihovna pro zvětšení obrázků, Catch2 , knihovna pro testování jednotek a nakonec pybind11 , knihovna používá se pro generování vazeb pythonu. První dvě knihovny budou muset být zkompilovány jako samostatné knihovny, zatímco poslední dvě jsou pouze záhlaví, a proto vyžadujeme pouze zdroj.

Jedním ze způsobů, jak tyto knihovny přidat do našich projektů, je pomocí podmodulů git . Ačkoli tento přístup funguje, osobně jsem fanouškem používání skriptů prostředí, které stahují zdrojový kód a poté vytvářejí požadované platformy: v našem případě AMD64, ARM32 a ARM64.

Zde je příklad toho, co z těchto sestavovacích skriptů vypadá takto:

Skript je celkem přímočarý. Začíná to tažením požadovaného zdrojového kódu vydání z úložiště git. Dále se CMake používá k přípravě sestavení, poté se vyvolá make, aby kompilátor vytvořil zdrojový kód.

Všimnete si, že hlavní rozdíl mezi sestavením AMD64 a sestavením ARM spočívá v tom, že sestavení ARM předávají další parametr CMake s názvem CMAKE_TOOLCHAIN_FILE. Tento argument se používá k určení CMake, že cílová architektura sestavení (ARM32 neboARM64) se liší od architektury hostitele (AMD64 / x86_64). CMake se proto instruuje, aby k sestavení knihovny použil křížový kompilátor zadaný ve vybraném souboru sady nástrojů (více o souborech nástrojů později v tomto kurzu). Aby tento skript prostředí fungoval, budete muset mít na svém počítači s Ubuntu nainstalovány příslušné křížové kompilátory. Lze je snadno nainstalovat pomocí apt-get a pokyny, jak to udělat, jsou zobrazeny zde .

Naše API knihovny

Naše API knihovny vypadá takto:

Protože jsem super kreativní, rozhodl jsem se pojmenovat svoji SDK MySDK. V našem API máme enum s názvem ErrorCode, máme strukturu s názvem Point a konečně máme jednu veřejnou členskou funkci s názvem getFaceBoxAndLandmarks. V rámci tohoto kurzu se nebudu zabývat podrobnostmi implementace SDK. Podstatou je, že jsme načetli obraz do paměti pomocí OpenCV a poté provedli odvození strojového učení pomocí ncnn s modely s otevřeným zdrojovým kódem k detekci ohraničujícího rámečku a orientačních bodů. Pokud byste se chtěli ponořit do implementace, můžete tak učinit zde .

Chci vám však věnovat pozornost designový vzor, ​​který používáme. Používáme techniku ​​nazvanou Ukazatel na implementaci, nebo zkráceně pImpl , která v zásadě odstraní podrobnosti implementace třídy jejich umístěním do samostatné třídy. Ve výše uvedeném kódu je toho dosaženo předáním deklarace třídy Impl a následným použitím unique_ptr této třídy jako soukromé členské proměnné. Přitom nejen skryjeme implementaci před zvědavými očima koncového uživatele (což může být v komerční sadě SDK docela důležité), ale také snížíme počet záhlaví, na kterých naše záhlaví API závisí (a tím zabráníme Záhlaví API z #include ing záhlaví knihovny závislostí).

Poznámka o modelových souborech

Řekl jsem, že nebudeme procházet podrobnosti implementace, ale je tu něco, co si myslím, že stojí za zmínku. Ve výchozím nastavení otevřený detektor tváře, který používáme, nazvaný MTCNN, načítá soubory modelu strojového učení za běhu. To není ideální, protože to znamená, že budeme muset distribuovat modely koncovému uživateli. Tento problém je ještě významnější u komerčních modelů, u kterých nechcete, aby uživatelé měli k těmto souborům modelů bezplatný přístup (pomyslete na nespočet hodin, které byly tyto modely procvičovány). Jedním z řešení je šifrování souborů těchto modelů, což naprosto doporučuji.To však stále znamená, že musíme dodávat soubory modelu spolu s SDK. Nakonec chceme snížit počet souborů, které uživateli pošleme, abychom mu usnadnili používání našeho softwaru (méně souborů se rovná menšímu počtu chybných míst). Můžeme tedy použít níže uvedenou metodu k převodu modelových souborů na soubory záhlaví a ve skutečnosti je vložit do samotné sady SDK.

Příkaz xdd bash se používá ke generování hexadecimálních výpisů a lze jej použít ke generování souboru záhlaví z binárního souboru. Můžeme tedy zahrnout modelové soubory do našeho kódu jako normální hlavičkové soubory a načíst je přímo z paměti. Omezení tohoto přístupu je, že to není praktické u velmi velkých modelových souborů, protože v době kompilace spotřebovává příliš mnoho paměti. Místo toho můžete použít nástroj jako ld k převodu těchto velkých souborů modelu přímo na soubory objektů.

CMake a kompilace naší knihovny

Nyní můžeme použít CMake ke generování souborů sestavení pro náš projekt. V případě, že nejste obeznámeni, CMake je generátor systému sestavení používaný ke správě procesu sestavení. Níže uvidíte, jak vypadá část kořenového CMakeLists.txt (souboru CMake).

V zásadě vytvoříme statickou knihovnu s názvem my_sdk_static se dvěma zdrojovými soubory, které obsahují naši implementaci, my_sdk.cpp a mtcnn.cpp. Důvod, proč vytváříme statickou knihovnu, je ten, že podle mých zkušeností je jednodušší distribuovat statickou knihovnu uživatelům a je přátelštější vůči vestavěným zařízením. Jak jsem zmínil výše, je-li spustitelný soubor propojen se statickou knihovnou, lze jej spustit na vloženém zařízení, které nemá ani operační systém. U dynamické knihovny to jednoduše není možné. Navíc s dynamickými knihovnami si musíme dělat starosti s verzemi závislostí. Možná budeme potřebovat soubor manifestu přidružený k naší knihovně. Staticky propojené knihovny mají také o něco lepší výkonnostní profil než jejich dynamické protějšky.

Další věc, kterou děláme v našem skriptu CMake, je říct CMake, kde najdete potřebné hlavičkové soubory, které naše zdrojové soubory vyžadují. Něco k poznámce: ačkoliv se naše knihovna v tomto okamžiku zkompiluje, při pokusu o propojení s naší knihovnou (například se spustitelným souborem) získáme absolutní tunu nedefinovaného odkazu na chyby symbolů. Důvodem je, že jsme nepropojili žádnou z našich knihoven závislostí. Pokud bychom tedy chtěli úspěšně propojit spustitelný soubor s libmy_sdk_static.a, museli bychom vystopovat a propojit také všechny knihovny závislostí (moduly OpenCV, ncnn atd.). Na rozdíl od dynamických knihoven statické knihovny nemohou vyřešit své vlastní závislosti. Jsou to v podstatě jen sbírka objektových souborů zabalených do archivu.

Později v tomto tutoriálu ukážu, jak můžeme spojit všechny knihovny závislostí do naší statické knihovny, takže uživatel nebude muset starat se o propojení s některou ze závislých knihoven.

Křížová kompilace našich knihoven a souborů nástrojů

Edge computing je tak… ostrý

Mnoho aplikací pro počítačové vidění je nasazeno na okraji. To obecně zahrnuje spuštění kódu na nízkoenergetických vestavěných zařízeních, která mají obvykle procesory ARM. Protože C ++ je kompilovaný jazyk, musíme zkompilovat náš kód pro architekturu CPU, na které bude aplikace spuštěna (každá architektura používá jiné pokyny pro sestavení).

Než se do toho ponoříme, dotkneme se také rozdíl mezi ARM32 a ARM64, také nazývaný AArch32 a AArch64. AArch64 odkazuje na 64bitové rozšíření architektury ARM a je závislý jak na CPU, tak na operačním systému. Například například Raspberry Pi 4 má 64bitový procesor ARM, výchozí operační systém Raspbian je 32bitový. Proto takové zařízení vyžaduje kompilovaný binární soubor AArch32. Pokud bychom na tomto zařízení Pi měli provozovat 64bitový operační systém, například Gentoo , pak bychom potřebovali binární soubor zkompilovaný AArch64. Dalším příkladem populárního integrovaného zařízení je NVIDIA Jetson, který má integrovaný grafický procesor a běží na něm AArch64.

Aby bylo možné provést kompilaci napříč, je třeba specifikovat CMake, že nekompilaujeme architekturu stroj, na kterém v současné době stavíme. Proto musíme určit křížový kompilátor, který by měl CMake použít. Pro AArch64 používáme kompilátor aarch64-linux-gnu-g++ a pro AArch32 kompilátor arm-linux-gnuebhif-g++ (hf znamená hard float ).

Následuje příklad souboru nástrojů. Jak vidíte, specifikujeme použití křížového kompilátoru AArch64.

Zpět na kořenový adresář CMakeLists.txt můžeme přidejte následující kód do horní části souboru.

V zásadě přidáváme možnosti CMake, které lze povolit z příkazového řádku za účelem křížové kompilace. Povolením možností BUILD_ARM32 nebo BUILD_ARM64 vyberete vhodný soubor řetězců nástrojů a nakonfigurujete sestavení pro křížovou kompilaci.

Balení naší sady SDK s knihovnami závislostí

Jak již bylo zmíněno dříve, pokud chce vývojář v tomto okamžiku propojit naši knihovnu, bude muset také propojit všechny knihovny závislostí, aby vyřešil všechny symboly z závislostní knihovny. I když je naše aplikace velmi jednoduchá, máme již osm knihoven závislostí! První je ncnn, pak máme tři knihovny modulů OpenCV, pak máme čtyři obslužné knihovny, které byly vytvořeny s OpenCV (libjpeg, libpng, zlib, libtiff). Mohli bychom požadovat, aby si uživatel sám vytvořil knihovny závislostí nebo je dokonce dodal spolu s naší knihovnou, ale nakonec to bude pro uživatele vyžadovat více práce a všichni se snažíme snížit bariéru pro použití. Ideální situace je, pokud můžeme uživateli odeslat jednu knihovnu, která obsahuje naši knihovnu, spolu se všemi nezávislými knihovnami jiných stran, než jsou standardní systémové knihovny. Ukázalo se, že toho můžeme dosáhnout pomocí nějaké magie CMake.

Nejprve přidáme vlastní cíl do naše CMakeLists.txt, poté proveďte takzvaný skript MRI. Tento skript MRI je předán ar -M příkazu bash, který v podstatě kombinuje všechny statické knihovny do jednoho archivu. Na této metodě je skvělé, že elegantně zvládne překrývající se názvy členů z původních archivů, takže se tam nemusíme obávat konfliktů. Budování tohoto vlastního cíle vytvoří libmy_sdk.a, která bude obsahovat naši sadu SDK spolu se všemi archivy závislostí.

Počkejte na okamžik: Pojďme se podívat na to, co jsme Dosud jsme udělali.

Nadechněte se. Chyťte občerstvení. Zavolej své matce.

V tomto okamžiku máme statickou knihovnu s názvem libmy_sdk.a, která obsahuje naši sadu SDK a všechny knihovny závislostí, které jsme zabalili do jednoho archivu. Máme také schopnost kompilovat a překládat (pomocí argumentů příkazového řádku) pro všechny naše cílové platformy.

Testy jednotek

Při prvním spuštění testů jednotky

Pravděpodobně nemusím vysvětlete, proč jsou testy jednotek důležité, ale v zásadě jsou zásadní součástí designu SDK, který umožňuje vývojáři zajistit, aby SDK fungovala jako odsazená. Kromě toho, pokud dojde k nějakým zlomovým změnám, pomůže to je sledovat a rychleji vytlačovat opravy.

V tomto konkrétním případě nám vytvoření spustitelného souboru testu jednotky také dává možnost propojit se s kombinovaná knihovna, kterou jsme právě vytvořili, abychom zajistili, že se můžeme správně propojit, jak bylo zamýšleno (a nedostaneme žádnou z těch ošklivých nedefinovaných chyb odkazu na symbol).

Jako rámec testování jednotek používáme Catch2 . Syntaxe je uvedena níže:

Jak funguje Catch2, je to, že máme toto makro s názvem TEST_CASE a další makro s názvem SECTION. U každého SECTION se TEST_CASE provádí od začátku. V našem příkladu bude tedy mySdk nejprve inicializován, poté bude spuštěna první sekce s názvem „Non face image“. Poté bude mySdk před rekonstrukcí dekonstruován, poté bude spuštěna druhá část s názvem „Tváře v obraze“. To je skvělé, protože to zajišťuje, že máme pro každou sekci nový MySDK objekt, se kterým budeme pracovat. Potom můžeme použít makra jako REQUIRE k provedení našich tvrzení.

Pomocí CMake můžeme vytvořit spustitelný soubor testování jednotky s názvem run_tests. Jak vidíme ve volání target_link_libraries na řádku 3 níže, jedinou knihovnou, proti které musíme odkazovat, je naše libmy_sdk.a a žádná jiná závislostní knihovny.

dokumentace

Kéž by si uživatelé přečetli tu zatracenou dokumentaci.

Použijeme doxygen ke generování dokumentace přímo z našeho souboru záhlaví. Můžeme pokračovat a zdokumentovat všechny naše metody a datové typy v naší veřejné hlavičce pomocí syntaxe zobrazené v úryvku kódu níže.Určitě u všech funkcí zadejte všechny vstupní a výstupní parametry.

Chcete-li skutečně vygenerovat dokumentaci , potřebujeme něco, čemu se říká doxyfile, což je v podstatě plán pro instruování doxygenu, jak generovat dokumentaci. Můžeme generovat obecný doxyfile spuštěním doxygen -g v našem terminálu za předpokladu, že máte ve svém systému nainstalovaný doxygen. Dále můžeme upravit soubor doxyfile. Minimálně musíme určit výstupní adresář a také vstupní soubory.

V našem v případě, že chceme vygenerovat pouze dokumentaci z našeho souboru záhlaví API, proto jsme určili adresář include. Nakonec použijete CMake ke skutečnému vytvoření dokumentace, což lze provést takhle .

Vazby Pythonu

Už vás nebaví vidět polorelevantní gify? Ano, ani já.

Buďme upřímní. C ++ není nejjednodušší nebo nejpřátelštější jazyk, ve kterém se dá vyvíjet. Proto chceme rozšířit naši knihovnu o podporu jazykových vazeb, abychom vývojářům usnadnili používání. Ukážu to pomocí pythonu, protože je to populární prototypovací jazyk počítačového vidění, ale stejně snadné je i psaní dalších jazykových vazeb. K dosažení tohoto cíle používáme pybind11:

Začneme používáním PYBIND11_MODULE makro, které vytváří funkci, která bude volána, když je vydán příkaz pro import z prostředí pythonu. Ve výše uvedeném příkladu je tedy název modulu pythonu mysdk. Dále jsme schopni definovat naše třídy a jejich členy pomocí syntaxe pybind11.

Je třeba poznamenat: V C ++ je docela běžné předávat proměnné pomocí proměnlivé reference, která umožňuje přístup pro čtení i zápis. To je přesně to, co jsme udělali s naší členskou funkcí API s parametry faceDetected a fbAndLandmarks. V pythonu jsou všechny argumenty předávány odkazem. Některé základní typy pythonu jsou však neměnné, včetně bool. Shodou okolností je náš faceDetected parametr bool, který je předáván proměnlivým odkazem. Proto musíme použít řešení zobrazené v kódu výše na řádcích 31 až 34, kde definujeme bool v rámci naší funkce obálky pythonu, poté jej předat naší funkci C ++ před vrácením proměnné jako součásti n-tice.

Jakmile vytvoříme knihovnu vazeb pythonu, můžeme ji snadno využít pomocí níže uvedeného kódu:

Continuous Integration

Pro náš kanál kontinuální integrace budeme používat nástroj s názvem CircleCI, který se mi opravdu líbí, protože se integruje přímo s Github. Nové sestavení se automaticky spustí pokaždé, když stisknete potvrzení. Začněte tím, že přejdete na web CircleCI a připojíte jej ke svému účtu Github a poté vyberete projekt, který chcete přidat. Po přidání budete muset v kořenovém adresáři projektu vytvořit adresář .circleci a v tomto adresáři vytvořit soubor s názvem config.yml.

Pro každého, kdo není obeznámen, je YAML jazyk pro serializaci běžně používaný pro konfigurační soubory. Můžeme jej použít k pokynu, jaké operace má CircleCI provádět. V níže uvedeném fragmentu YAML můžete vidět, jak nejprve sestavíme jednu ze závislostních knihoven, dále sestavíme samotnou sadu SDK a nakonec sestavíme a spustíme testy jednotek.

Pokud jsme inteligentní (a předpokládám, že jste, pokud jste se dostali až sem), můžeme pomocí mezipaměti výrazně zkrátit dobu sestavení. Například ve výše uvedeném YAML ukládáme do mezipaměti sestavení OpenCV pomocí hash skriptu sestavení jako klíče mezipaměti. Tímto způsobem bude knihovna OpenCV znovu vytvořena pouze v případě, že byl upraven skript sestavení – jinak bude použito sestavení v mezipaměti. Další věc, kterou je třeba si uvědomit, je, že provozujeme sestavení uvnitř dokovacího obrazu podle našeho výběru. Vybral jsem vlastní obrázek ukotvitelného panelu ( zde je soubor Dockerfile), do kterého jsem nainstaloval všechny systémové závislosti.

Fin.

A tady to máte. Jako každý dobře navržený produkt, i my chceme podporovat nejžádanější platformy a umožnit jeho snadné použití pro největší počet vývojářů. Pomocí výše uvedeného tutoriálu jsme vytvořili SDK, která je přístupná v několika jazycích a je nasaditelná na více platforem. A nemuseli jste si ani sami přečíst dokumentaci k pybind11. Doufám, že vám tento návod připadal užitečný a zábavný. Šťastné budování.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *