OpenCV на STM32F7-Discovery

OpenCV на STM32F7-Discovery Я один із розробників операційної системи Embox, і в цій статті я розповім про те, як мені вдалося запустити OpenCV на платі STM32746G.

Якщо вбити в пошукову систему щось на кшталт OpenCV on STM32 board, можна знайти чимало тих, хто цікавиться використанням цієї бібліотеки на платах STM32 або інших мікроконтролерах.
Є кілька відео, які, судячи з назви, повинні демонструвати те, що потрібно, але зазвичай (у всіх відео, які я бачив) на платі STM32 вироблялося лише отримання картинки з камери та виведення результату на екран, а сама обробка зображення робилася або на звичайному комп'ютері, або на платах потужніших (наприклад, Raspberry Pi).

Чому це складно?

Популярність пошукових запитів пояснюється тим, що OpenCV - найпопулярніша бібліотека комп'ютерного зору, а значить, з нею знайомо більше розробників, та й можливість запускати готовий для робочого столу код на мікроконтролері значно спрощує процес розробки. Але чому й досі немає якихось популярних готових рецептів вирішення цієї проблеми?

Проблема використання OpenCV на невеликих хустках пов'язана із двома особливостями:

  • Якщо скомпілювати бібліотеку навіть із мінімальним набором модулів, у флеш-пам'ять тієї ж STM32F7Discovery вона просто не влізе (навіть без урахування ОС) через дуже великий код (кілька мегабайт інструкцій)
  • Сама бібліотека написана на C++, отже
    • Потрібна підтримка плюсового рантайму (виключення тощо)
    • Мало підтримки LibC/Posix, які зазвичай є в ОС для вбудованих систем - потрібна стандартна бібліотека плюсів та стандартна бібліотека шаблонів STL (vector і т.д.)

Портування на Embox

Як завжди, перед портуванням будь-яких програм в операційну систему непогано спробувати зібрати її у вигляді, у якому це замислювали розробники. У нашому випадку проблем із цим не виникає — вихідники можна знайти на гітхабе, бібліотека збирається під GNU/Linux звичайним cmake-ом.

З хороших новин — OpenCV із коробки можна збирати у вигляді статичної бібліотеки, що робить простіше портування. Збираємо бібліотеку зі стандартним конфігом і дивимося, скільки місця вони займають. Кожен модуль збирається до окремої бібліотеки.

> 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)

Як видно з останнього рядка, .bss і .data займають не так багато місця, проте коду більше 70 МіБ. Зрозуміло, що якщо це злінкувати статично з конкретним додатком, коду стане менше.

Спробуємо викинути якнайбільше модулів, щоб зібрався мінімальний приклад (який, наприклад, просто виведе версію OpenCV), тож дивимося cmake .. -LA та відключаємо в опціях все, що відключається.

        -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)

З одного боку, це лише один модуль бібліотеки, з іншого боку, без оптимізації компілятором за розміром коду (-Os). ~3 МіБ коду - це все ще досить багато, але вже дає надію на успіх.

Запуск в емуляторі

На емуляторі налагоджуватись набагато простіше, тому спочатку переконаємося, що бібліотека працює на qemu. Як емульована платформа я вибрав Integrator/CP, т.к. по-перше, це теж ARM, а по-друге, Embox підтримує виведення графіки для цієї платформи.

У Embox є механізм для збирання зовнішніх бібліотек, з його допомогою додаємо OpenCV як модуль (передавши ті самі опції для «мінімального» збирання у вигляді статичних бібліотек), після цього додаю найпростіший додаток, який виглядає так:

version.cpp:

#include <stdio.h>
#include <opencv2/core/utility.hpp>

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

    return 0;
}

Збираємо систему, запускаємо - отримуємо очікуваний висновок.

root@embox:/#opencv_version                                                     
OpenCV: 
General configuration for OpenCV 4.0.1 =====================================
  Version control:               bd6927bdf-dirty

  Platform:
    Timestamp:                   2019-06-21T10:02:18Z
    Host:                        Linux 5.1.7-arch1-1-ARCH x86_64
    Target:                      Generic arm-unknown-none
    CMake:                       3.14.5
    CMake generator:             Unix Makefiles
    CMake build tool:            /usr/bin/make
    Configuration:               Debug

  CPU/HW features:
    Baseline:
      requested:                 DETECT
      disabled:                  VFPV3 NEON

  C/C++:
    Built as dynamic libs?:      NO
< Дальше идут прочие параметры сборки -- с какими флагами компилировалось,
  какие модули OpenCV включены в сборку и т.п.>

Наступний крок — запустити якийсь приклад, найкраще якийсь стандартний із тих, що пропонують самі розробники у себе на сайті. Я обрав детектор кордонів Кенні.

Приклад довелося трохи переписати, щоб відображати картинку з результатом безпосередньо у фрейм-буфер. Зробити це довелося, т.к. функція imshow() вміє малювати зображення через інтерфейси QT, GTK і Windows, яких, зрозуміло, в конфізі для STM32 точно не буде. Насправді QT теж можна запустити на STM32F7Discovery, але про це буде розказано вже в іншій статті 🙂

Після недовгого з'ясування, в якому форматі зберігається результат роботи детектора кордонів, отримуємо зображення.

OpenCV на STM32F7-Discovery

Оригінальна картинка

OpenCV на STM32F7-Discovery

Результат

Запуск STM32F7Discovery

На 32F746GDISCOVERY є кілька апаратних розділів пам'яті, які ми можемо так чи інакше використовувати

  1. 320KiB оперативної пам'яті
  2. 1MiB флеш-пам'яті для образу
  3. 8MiB SDRAM
  4. 16MiB QSPI NAND-флешка
  5. Роз'єм для microSD-картки

SD-карту можна використовувати для зберігання зображень, але в контексті запуску мінімального прикладу це не дуже корисно.
Дисплей має роздільну здатність 480×272, отже, пам'ять під фреймбуффер складе 522 240 байт при глибині 32 біта, тобто. це більше, ніж розмір оперативної пам'яті, так що фреймбуффер і купу (яка буде потрібна в тому числі для OpenCV, щоб зберігати дані для зображень і допоміжних структур) будемо розташовувати в SDRAM, все інше (пам'ять під стеки та інші системні потреби) відправиться в RAM .

Якщо взяти мінімальний конфіг для STM32F7Discovery (викинути всю мережу, всі команди, зробити стеки якнайменше і т.д.) і додати туди OpenCV з прикладами, з пам'яттю буде наступне:

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

Для тих, хто не дуже знайомий з тим, які секції куди складається, поясню: .text и .rodata лежать інструкції та константи (грубо кажучи, readonly-дані), в .data лежать дані, що змінюються, в .bss лежить «занулені» змінні, яким, тим не менш, потрібне місце (ця секція «відправиться» до RAM).

Хороша новина в тому, що .data/.bss повинні поміщатися, а от з .text біда - під образ є лише 1MiB пам'яті. Можна викинути з .text картинку з прикладу і читати її, наприклад, з SD-карти на згадку при запуску, але fruits.png важить приблизно 330KiB, так що проблему це не вирішить: більша частина .text складається саме із коду OpenCV.

За великим рахунком, залишається лише одне - завантаження частини коду на QSPI-флешку (у неї є спец. режим роботи для мепування пам'яті на системну шину, так що процесор зможе звертатися до цих даних безпосередньо). При цьому виникає проблема: по-перше, пам'ять QSPI-флешки недоступна відразу після перезавантаження пристрою (потрібно окремо ініціалізувати memory-mapped-режим), по-друге, не можна "прошити" цю пам'ять звичним завантажувачем.

У результаті було вирішено з'єднати весь код в QSPI, а прошивати його самописним завантажувачем, який отримуватиме потрібний бінарник по TFTP.

Результат

Ідея портувати цю бібліотеку на Embox з'явилася ще приблизно рік тому, але щоразу це відкладалося з різних причин. Одна з них – підтримка libstdc++ та standart template library. Проблема підтримки C++ в Embox виходить за межі цієї статті, тому тут тільки скажу, що нам вдалося досягти цієї підтримки в потрібному обсязі для роботи цієї бібліотеки 🙂

У результаті ці проблеми були подолані (принаймні, достатньо для роботи прикладу OpenCV), і приклад запустився. 40 тривалих секунд займає у плати пошук кордонів фільтром Кенні. Це, звичайно, занадто довго (є міркування, як цю справу оптимізувати, про це можна буде написати окрему статтю у разі успіху).

OpenCV на STM32F7-Discovery

Тим не менш, проміжною метою було створення прототипу, який покаже принципову можливість запуску OpenCV на STM32, відповідно, цієї мети було досягнуто, ура!

tl;dr: покрокова інструкція

0: Качаємо вихідні коди Embox, наприклад так:

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

1: Почнемо зі складання завантажувача, який «прошиє» QSPI-флешку.

    make confload-arm/stm32f7cube

Тепер необхідно налаштувати мережу, т.к. завантажувати образ будемо по TFTP. Для того, щоб задати IP-адресу плати та хоста, потрібно змінити файл conf/rootfs/network.

Приклад конфігурації:

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

gateway - адреса хоста, звідки завантажуватиметься образ, address - адреса плати.

Після цього збираємо завантажувач:

    make

2: Звичайне завантаження завантажувача (вибачте за каламбур) на плату - тут нічого специфічного, потрібно це зробити як для будь-якої іншої програми для STM32F7Discovery. Якщо ви не знаєте, як це робиться, можна почитати про це тут.
3: Компіляція образу з конфігом для OpenCV.

    make confload-platform/opencv/stm32f7discovery
    make

4: Вилучення з секцій ELF, які потрібно записати в QSPI, в 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

У директорії conf лежить скрипт, який це робить, тож можна запустити його

    ./conf/qspi_objcopy.sh # Нужный бинарник -- build/base/bin/qspi.bin

5: За допомогою tftp завантажуємо qspi.bin.bin на QSPI-флешку. На хості для цього потрібно скопіювати qspi.bin у кореневу папку tftp-сервера (зазвичай це /srv/tftp/ або /var/lib/tftpboot/; пакети для відповідного сервера є у більшості популярних дистрибутивів, зазвичай називається tftpd або tftp-hpa, іноді потрібно зробити systemctl start tftpd.service для старту).

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

На Embox-і (тобто в завантажувачі) потрібно виконати таку команду (припускаємо, що сервер адресу 192.168.2.1):

    embox> qspi_loader qspi.bin 192.168.2.1

6: За допомогою команди goto потрібно «стрибнути» у QSPI-пам'ять. Конкретна локація варіюватиметься залежно від того, як образ з'єднується, подивитися цю адресу можна командою mem 0x90000000 (Адреса старту вкладається в друге 32-бітне слово образу); також потрібно виставити стек прапором -s, адреса стека лежить за адресою 0x90000000, приклад:

    embox>mem 0x90000000
    0x90000000:     0x20023200  0x9000c27f  0x9000c275  0x9000c275
                      ↑           ↑
              это адрес    это  адрес 
                стэка        первой
                           инструкции

    embox>goto -i 0x9000c27f -s 0x20023200 # Флаг -i нужен чтобы запретить прерывания во время инициализации системы

    < Начиная отсюда будет вывод не загрузчика, а образа с OpenCV >

7: Запускаємо

    embox> edges 20

та насолоджуємось 40-секундним пошуком кордонів 🙂

Якщо щось піде не так - пишіть issue нашому репозиторії, або в розсилку [захищено електронною поштою], або у коментарі тут.

Джерело: habr.com

Додати коментар або відгук