OpenCV na STM32F7-Discovery

OpenCV na STM32F7-Discovery Jestem jednym z programistów systemu operacyjnego Embox, a w tym artykule opowiem, jak udało mi się uruchomić OpenCV na płycie STM32746G.

Jeśli wpiszesz w wyszukiwarce coś w stylu „OpenCV na płycie STM32”, możesz znaleźć sporo osób zainteresowanych wykorzystaniem tej biblioteki na płytach STM32 lub innych mikrokontrolerach.
Jest kilka filmów, które, sądząc po tytule, powinny demonstrować to, czego potrzeba, ale zazwyczaj (we wszystkich filmach, które widziałem) na płycie STM32 jedynie uzyskiwano obraz z kamery i wyświetlano wyniki na ekranie, a sama obróbka obrazu odbywała się albo na zwykłym komputerze, albo na mocniejszych płytach (na przykład Raspberry Pi).

Dlaczego to jest skomplikowane?

Popularność zapytań w wyszukiwarkach tłumaczy się tym, że OpenCV jest najpopularniejszą biblioteką do komputerowego widzenia, co oznacza, że więcej programistów jest z nią zaznajomionych, a możliwość uruchomienia kodu przygotowanego na desktopie na mikrokontrolerze znacznie upraszcza proces tworzenia. Ale dlaczego do tej pory nie ma jakichś popularnych gotowych przepisów na rozwiązanie tego problemu?

Problem używania OpenCV na małych płytkach wiąże się z dwiema cechami:

  • Nawet jeśli skompilujesz bibliotekę z minimalnym zestawem modułów, w pamięci flash tej samej STM32F7Discovery po prostu się nie zmieści (nawet bez liczenia systemu operacyjnego) z powodu bardzo dużego kodu (kilka megabajtów instrukcji)
  • Sama biblioteka jest napisana w C++, co oznacza
    • Potrzebne jest wsparcie dla runtime'u C++ (wyjątki itd.)
    • Brakuje wsparcia LibC/Posix, które zazwyczaj występuje w systemach operacyjnych dla systemów wbudowanych — potrzebna jest standardowa biblioteka C++ oraz standardowa biblioteka szablonów STL (vector itd.)

Portowanie na Embox

Jak zwykle przed portowaniem jakichkolwiek programów do systemu operacyjnego, warto spróbować zbudować go w takiej formie, w jakiej przewidzieli go programiści. W naszym przypadku nie ma z tym problemu — źródła można znaleźć na githubie, biblioteka jest kompilowana na GNU/Linux zwykłym cmake'em.

Dla dobrych wiadomości — OpenCV można budować jako statyczną bibliotekę, co upraszcza portowanie. Budujemy bibliotekę ze standardową konfiguracją i sprawdzamy, ile miejsca zajmuje. Każdy moduł jest kompilowany do oddzielnej biblioteki.

> size lib/*so --totals
   text    data     bss     dec     hex filename
1945822   15431     960 1962213  1df0e5 lib/libopencv_calib3d.so
17081885     170312   25640 17277837    107a38d lib/libopencv_core.so
10928229     137640   20192 11086061     a928ed lib/libopencv_dnn.so
 842311   25680    1968  869959   d4647 lib/libopencv_features2d.so
 423660    8552     184  432396   6990c lib/libopencv_flann.so
8034733   54872    1416 8091021  7b758d lib/libopencv_gapi.so
  90741    3452     304   94497   17121 lib/libopencv_highgui.so
6338414   53152     968 6392534  618ad6 lib/libopencv_imgcodecs.so
21323564     155912  652056 22131532    151b34c lib/libopencv_imgproc.so
 724323   12176     376  736875   b3e6b lib/libopencv_ml.so
 429036    6864     464  436364   6a88c lib/libopencv_objdetect.so
6866973   50176    1064 6918213  699045 lib/libopencv_photo.so
 698531   13640     160  712331   ade8b lib/libopencv_stitching.so
 466295    6688     168  473151   7383f lib/libopencv_video.so
 315858    6972   11576  334406   51a46 lib/libopencv_videoio.so
76510375     721519  717496 77949390    4a569ce (TOTALS)

Jak widać z ostatniego wiersza, .bss i .data zajmują niewiele miejsca, natomiast kod zajmuje ponad 70 MiB. Oczywiście, jeśli to zostanie skompilowane statycznie z konkretną aplikacją, kod stanie się mniejszy.

Spróbujmy usunąć jak najwięcej modułów, aby stworzyć minimalny przykład (na przykład, który po prostu wyświetli wersję OpenCV), więc patrzymy cmake .. -LA i wyłączamy we opcjach wszystko, co można wyłączyć.

        -DBUILD_opencv_java_bindings_generator=OFF 
        -DBUILD_opencv_stitching=OFF 
        -DWITH_PROTOBUF=OFF 
        -DWITH_PTHREADS_PF=OFF 
        -DWITH_QUIRC=OFF 
        -DWITH_TIFF=OFF 
        -DWITH_V4L=OFF 
        -DWITH_VTK=OFF 
        -DWITH_WEBP=OFF

> size lib/libopencv_core.a --totals
   text    data     bss     dec     hex filename
3317069   36425   17987 3371481  3371d9 (TOTALS)

Z jednej strony, to tylko jeden moduł biblioteki, z drugiej strony, to bez optymalizacji kompilatora pod względem rozmiaru kodu (-Os). ~3 MiB kodu — to wciąż dość dużo, ale daje już nadzieję na sukces.

Uruchamianie w emulatorze

W emulatorze łatwiej się debugować, więc najpierw upewnijmy się, że biblioteka działa na qemu. Jako emulowana platforma wybrałem Integrator/CP, ponieważ po pierwsze, to również ARM, a po drugie, Embox wspiera wyjście graficzne dla tej platformy.

W Embox znajduje się mechanizm do budowania zewnętrznych bibliotek, za jego pomocą dodajemy OpenCV jako moduł (przekazując te same opcje dla „minimalnej” budowy w postaci bibliotek statycznych), po tym dodaję najprostsze aplikację, która wygląda tak:

version.cpp:

#include 
#include 

int main() {
    printf("OpenCV: %s", cv::getBuildInformation().c_str());

    return 0;
}

Kompilujemy system, uruchamiamy — otrzymujemy oczekiwany wynik.

root@embox:/>opencv_version                                                     
OpenCV: 
Ogólna konfiguracja dla OpenCV 4.0.1 =====================================
  Kontrola wersji:               bd6927bdf-dirty

  Platforma:
    Znacznik czasu:               2019-06-21T10:02:18Z
    Host:                        Linux 5.1.7-arch1-1-ARCH x86_64
    Cel:                          Ogólny arm-unknown-none
    CMake:                       3.14.5
    Generator CMake:             Unix Makefiles
    Narzędzie budowlane CMake:   /usr/bin/make
    Konfiguracja:                Debug

  Funkcje CPU/HW:
    Podstawa:
      żądane:                   DETECT
      wyłączone:                VFPV3 NEON

  C/C++:
    Zbudowany jako biblioteki dynamiczne?:   NIE

Następnym krokiem jest uruchomienie jakiegoś przykładu, najlepiej standardowego z tych, które oferują sami twórcy na swojej stronie. Wybrałem detektor krawędzi Canny'ego.

Przykład musiałem nieco przerobić, aby wyświetlać obrazek z wynikiem bezpośrednio w buforze ramki. Musiałem to zrobić, ponieważ funkcja imshow() potrafi rysować obrazy przez interfejsy QT, GTK i Windows, których oczywiście w konfiguracji dla STM32 na pewno nie będzie. W rzeczywistości, QT również można uruchomić na STM32F7Discovery, ale o tym będzie mowa w innym artykule 🙂

Po krótkim ustaleniu, w jakim dokładnie formacie przechowywany jest wynik działania detektora krawędzi, otrzymujemy obraz.

OpenCV na STM32F7-Discovery

Oryginalny obrazek

OpenCV na STM32F7-Discovery

Wynik

Uruchamianie na STM32F7Discovery

Na 32F746GDISCOVERY jest kilka sekcji pamięci, które możemy w ten czy inny sposób wykorzystać

  1. 320KiB pamięci RAM
  2. 1MiB pamięci flash dla obrazu
  3. 8MiB SDRAM
  4. 16MiB pamięci flash QSPI NAND
  5. Złącze dla karty microSD

Kartę SD można wykorzystać do przechowywania obrazów, ale w kontekście uruchomienia minimalnego przykładu nie jest to zbyt przydatne.
Wyświetlacz ma rozdzielczość 480×272, więc pamięć na bufor ramki wyniesie 522 240 bajtów przy głębokości 32 bity, co oznacza, że to więcej niż wielkość pamięci RAM, więc bufor ramki i stos (który będzie potrzebny również dla OpenCV do przechowywania danych obrazów i struktur pomocniczych) umieścimy w SDRAM, podczas gdy reszta (pamięć na stosy i inne potrzeby systemowe) trafia do RAM.

Jeśli weźmiemy minimalną konfigurację dla STM32F7Discovery (wyrzucimy całą sieć, wszystkie komendy, zmniejszymy stosy do minimum itp.) i dodamy do tego OpenCV z przykładami, wymagania pamięci będą następujące:

   text    data     bss     dec     hex filename
2876890  459208  312736 3648834  37ad42 build/base/bin/embox

Dla tych, którzy nie są zbyt zaznajomieni z tym, jakie sekcje gdzie się umieszczają, wyjaśnię: w .text i .rodata znajdują się instrukcje i stałe (ogólnie mówiąc, dane tylko do odczytu), w .data znajdują się dane zmienne, w .bss znajdują się „zerowane” zmienne, którym mimo wszystko potrzebne jest miejsce (ta sekcja „trafi” do RAM).

Dobrą wiadomością jest to, że .data/.bss muszą być umieszczone, ale z .text problemem — obraz zajmuje tylko 1MiB pamięci. Można usunąć z .text zdjęcie z przykładu i odczytać je, na przykład, z karty SD do pamięci przy starcie, ale fruits.png waży około 330KiB, więc to nie rozwiązuje problemu: dużą część .text stanowi dokładnie kod OpenCV.

W dużym skrócie, pozostaje tylko jedno — załadowanie części kodu na pamięć QSPI-flash (ma ona specjalny tryb pracy do mapowania pamięci na magistralę systemową, dzięki czemu procesor może bezpośrednio uzyskiwać dostęp do tych danych). Jednak pojawia się problem: po pierwsze, pamięć QSPI-flash nie jest dostępna od razu po ponownym uruchomieniu urządzenia (trzeba ją osobno zainicjować w trybie pamięci-mapped), po drugie, nie można 'wflashować' tej pamięci za pomocą standardowego bootloadera.

Ostatecznie zdecydowano się na zlinkowanie całego kodu w QSPI i wgranie go własnym bootloaderem, który pobierze potrzebny binarny plik przez TFTP.

Wynik

Pomysł przeniesienia tej biblioteki na Embox pojawił się już około rok temu, ale nieustannie był odkładany z różnych powodów. Jednym z nich była obsługa libstdc++ i standardowej biblioteki szablonów. Problem wsparcia C++ w Embox wykracza poza ramy tego artykułu, dlatego tutaj tylko powiem, że udało nam się osiągnąć to wsparcie w wymaganym zakresie do działania tej biblioteki 🙂

Ostatecznie te problemy zostały przezwyciężone (przynajmniej w wystarczającym stopniu do działania przykładu OpenCV), a przykład uruchomił się. 40 długich sekund zajmuje płycie znalezienie krawędzi filtrem Canny'ego. To, oczywiście, zbyt długo (są pomysły, jak to zoptymalizować, o tym można by napisać osobny artykuł w przypadku sukcesu).

OpenCV na STM32F7-Discovery

Niemniej jednak, pośrednim celem było stworzenie prototypu, który pokaże zasadniczą możliwość uruchomienia OpenCV na STM32, w związku z tym cel został osiągnięty, hurra!

tl;dr: krok po kroku instrukcja

0: Pobieramy źródła Embox, na przykład w ten sposób:

    git clone https://github.com/embox/embox && cd ./embox

1: Zaczniemy od zbudowania bootloadera, który 'wflashuje' pamięć QSPI.

    make confload-arm/stm32f7cube

Teraz należy skonfigurować sieć, ponieważ obraz będziemy ładować przez TFTP. Aby ustawić adresy IP dla płytki i hosta, należy zmienić plik conf/rootfs/network.

Przykład konfiguracji:

iface eth0 inet static
    address 192.168.2.2
    netmask 255.255.255.0
    gateway 192.168.2.1
    hwaddress aa:bb:cc:dd:ee:02

brama — adres hosta, z którego będzie ładowany obraz, address — adres płytki.

Po tym zbieramy bootloader:

    make

2: Zwykłe wgranie bootloadera (przepraszam za kalambur) na płytkę — tutaj nie ma nic specyficznego, należy to zrobić jak dla każdej innej aplikacji dla STM32F7Discovery. Jeśli nie wiesz, jak to zrobić, można o tym poczytać tutaj.
3: Kompilacja obrazu z konfiguracją dla OpenCV.

    make confload-platform/opencv/stm32f7discovery
    make

4: Ekstrakcja sekcji z ELF, które należy zapisać w QSPI, do qspi.bin

    arm-none-eabi-objcopy -O binary build/base/bin/embox build/base/bin/qspi.bin 
        --only-section=.text --only-section=.rodata 
        --only-section='.ARM.ex*' 
        --only-section=.data

W katalogu conf znajduje się skrypt, który to robi, więc możesz go uruchomić

    ./conf/qspi_objcopy.sh # Potrzebny binarny -- build/base/bin/qspi.bin

5: Za pomocą tftp ładujemy qspi.bin na QSPI-flash. Na hoście w tym celu należy skopiować qspi.bin do katalogu głównego serwera tftp (zwykle to /srv/tftp/ lub /var/lib/tftpboot/; pakiety dla odpowiedniego serwera są w większości popularnych dystrybucji, zazwyczaj nazywa się tftpd lub tftp-hpa, czasami trzeba uruchomić systemctl start tftpd.service aby wystartować).

    # вариант для tftpd
    sudo cp build/base/bin/qspi.bin /srv/tftp
    # вариант для tftp-hpa
    sudo cp build/base/bin/qspi.bin /var/lib/tftpboot

Na Embox (tj. w bootloaderze) należy wykonać takie polecenie (zakładając, że adres serwera to 192.168.2.1):

    embox> qspi_loader qspi.bin 192.168.2.1

6: Wykonujemy polecenie goto aby «skoczyć» do pamięci QSPI. Konkretna lokalizacja będzie się różnić w zależności od tego, jak obraz jest linkowany, adres ten można zobaczyć poleceniem mem 0x90000000 (adres startowy leży w drugim 32-bitowym słowie obrazu); należy również ustawić stos flagą -s, adres stosu znajduje się pod adresem 0x90000000, przykład:

    embox>mem 0x90000000
    0x90000000:     0x20023200  0x9000c27f  0x9000c275  0x9000c275
                      ↑           ↑
              to jest adres    to jest adres 
                stosu        pierwszej
                           instrukcji

    embox>goto -i 0x9000c27f -s 0x20023200 # Flaga -i jest potrzebna, aby zapobiec przerwom podczas inicjalizacji systemu

7: Uruchamiamy

    embox> edges 20

i cieszymy się 40-sekundowym poszukiwaniem krawędzi 🙂

Jeśli coś pójdzie nie tak — napisz zgłoszenie do naszego repozytorium, lub na listę embox-devel@googlegroups.com, lub w komentarzach tutaj.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster