Я один из разработчиков операционной системы 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, но об этом будет рассказано уже в другой статье :)


После недолгого выяснения, в каком именно формате хранится результат работы детектора границ, получаем изображение.



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



Результат


Запуск на STM32F7Discovery


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


  1. 320KiB оперативной памяти
  2. 1MiB флэш-памяти для образа
  3. 8MiB SDRAM
  4. 16MiB QSPI NAND-флэшка
  5. Разъём для microSD-карточки

SD-карту можно использовать для хранения изображений, но в контексте запуска минимального примера это не очень полезно.
Дисплей имеет разрешение 480x272, а значит, память под фреймбуффер составит 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 на 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 в нашем репозитории, или в рассылку embox-devel@googlegroups.com, или в комментарии здесь.

Комментарии (8)


  1. Mirn
    26.06.2019 17:12
    +2

    Спасибо за хорошую статью!
    44 секунды вышло только потому что код находился в QSPI памяти а она очень медленная.
    А ещё лучше QSPI и SDRAM памятью не пользоваться — она всего 100-150мб/сек когда внутренняя доходит до 4200мб/сек.
    Пример влияния этих видов памяти:
    Я например запускал в таком конфиге MobileNetV2 224х224 на 1000 классов, выходило 30-40секунд.
    А когда запустил полностью из DTCM подгружая по необходимости всего один раз нужные куски W и B то скорость возросла почти в десятки раз, речь уже зашла о FPS и сетка заработала в реалтайме (входа, выхода и промежуточные расчёты — 16бит квантизации, а W и B сжаты адуиокодеком до 8 бит с расжатие до 16 бит в DTCM памяти — чтоб быстрее загружалось, загрузка по DMA пока считается текущая свёртка).

    Я тоже портировал OpenCV но кусками, вышло в десятки раз быстрее чем оригинальный OpenCV даже на ПК. т.к. я затачивал только под монохром и конкретные задачи.

    Например вот поиск точек по равнобедренному треугольнику (10мс на кадр 480х240)
    аналог ConnectedComponentsWithStats
    www.youtube.com/watch?v=210UZUjZwBs

    Или вот детектор вторжения просто сравнение с фоном при помощи фильров (так же около 20 мс на кадр)
    аналог вычитания, монохромотизации и фильтров на эрозию и размытие.
    www.youtube.com/watch?v=kkZLZnUYn9E

    Во всех случая использовал SIMD инструкции — они реально ускоряют в разы, особенно SMLAD и сумма четырёх разниц по абсолюту.


    1. sim2q
      27.06.2019 08:27
      +1

      Спасибо автору и вам за не менее интересный комментарий!


    1. abondarev
      27.06.2019 12:24

      Спасибо. Очень интересные результаты. Было бы классно, если бы Вы написали об этом статью, а то в последнее время, как то мало технических статей.

      По поводу, медленной памяти, абсолютно согласны. Это очевидное место для ускорения. Мы параллельно работаем над запуском Qt на данной плате, и на данный момент часть кода (обработку прерываний, потоки, системную библиотеку, ..) перенесли во внутреннюю флешь, стало заметно быстрее. Но нам хочется, чтобы именно из коробки запускались данные библиотеки, соотвественно пока уместить их внутрь не получается.

      Можете ответить на пару вопросов, по вашим результатам.

      • Правильно я понимаю, что видео память располагается тоже во внутней sram и использует 16 битный формат. 32 битный у нас просто не влезает во внутреннюю память (320кб) (480х240x4 = 460800)
      • Проект на который ссылка это бареметальный проект для stm-ки, а для малинки как измеряли? просто на линуксе скомпилили приложение и запустили? Или тоже бареметальное что то?


      Отдельный вопрос, а почему openCV считается стандартом для распознавания, может есть лучшие проекты, и нам лучше с ними поиграться? Мы то сами системщики больше!


      1. Mirn
        28.06.2019 03:51

        Правильно я понимаю, что видео память располагается тоже во внутней sram?

        нет, я использовал HALовский пример и дма сохраняла видеопоток в SDRAM (где то 640х480) и из SDRAM уже во внутренюю память я вырезал только нужное окно с пред обработкой уже в виде битового образа (1 бит на пиксель). Даже софтверного быстродействия хватало чтоб разово считать из SDRAM блок памяти. При желании в STM есть 2д видеоускоритель и его можно было бы задействовать например для клипинга и преобразования битов.

        Проект на который ссылка это бареметальный проект для stm-ки, а для малинки как измеряли? просто на линуксе скомпилили приложение и запустили?
        Да просто на люниксе и OpenCV скомпилировал тот же исходник и запустил: от люникса требовался только файловый ввод вывод и замер скорости, а от OpenCV только сырой видео поток с камеры. Аналогично на ПК. Я обычно стараюсь писать сразу хороший код который отлично работает что под ПК что под МК что под FPGA. Спец инструкци же стараюсь применять так чтоб «не вляпаться в ассемблер», т.е. например всё специфически необходимое есть например у GCC в группе функций _buildinXXXX. Моя жизненная позиция — изучить инструмент до конца. И только когда я вообще ничего не нашёл и консультации не помогли только тогда опускаться ниже огородив это западло кучей тестов, доскональной документацией и дебрями условных компиляций чтоб недайбог эта чёрная магия не выстрелила тебе же через лет 10.

        а почему openCV считается стандартом для распознавания, может есть лучшие проекты, и нам лучше с ними поиграться?

        Извиняюсь но это не в моей компетенции что либо рекомендовать т.к. я не ИТшник вообще, я вообще инженер-схемотехник, у меня оба образования схемотехнические, и ИжГТУ (радиоапаратостроение) и MiT (разработка цифровых микросхем и ASIC). Просто так вышло что интересные проекты и страны требуют научных исследований в области глубокой математики чтоб ускорить видео обработку и нейронок. Я почему то умудряюсь парой строк кода сделать лучше других применив математику по месту понимая смысл. За что и платят.


        1. abondarev
          28.06.2019 10:47

          Спасибо!


      1. Mirn
        28.06.2019 04:03

        По поводу, медленной памяти, абсолютно согласны. Это очевидное место для ускорения. Мы параллельно работаем над запуском Qt на данной плате, и на данный момент часть кода (обработку прерываний, потоки, системную библиотеку, ..) перенесли во внутреннюю флешь, стало заметно быстрее. Но нам хочется, чтобы именно из коробки запускались данные библиотеки, соотвественно пока уместить их внутрь не получается.

        Ммм… а оно того стоит QT на МК запускать? Я вот подсчитывал бюджет разработки (не только BOM list а себестоимость труда) для разработки на QT для МК граф интерфейсов с нормальным экраном и контролами — вышло почти столько же сколько стоит малинка с экраном но сроки и риски на порядок выше.
        Может стоит адаптировать QT на другие стмки, которые хотябы предназначены для этого?
        например stm32mp157
        STM32MP157 microprocessors are based on the flexible architecture of a Dual Arm® Cortex®-A7 core running at 650 MHz and Cortex®-M4 at 209 MHz combined with a dedicated 3D graphics processing unit (GPU) and MIPI-DSI display interface and a CAN FD interface.

        Specifically designed to accelerate 3D graphics in applications such as graphical user interfaces (GUI), menu displays or animations, the STM32MP157 3D OpenGL ES 2.0 graphics engine works together with an optimized software stack design for industry-standard APIs with support for Android™ and Linux® embedded development platforms.

        извиняюсь, но мне кажется что вы пытаетесь микроскопом гвоздь забить.

        stm32 на базе CortexM7 хороши своей запредельной скоростью арифметики для потоковой обработки видео и звука там где пинг нужен минимальный, например на ДСП процессоре для авто (я писал ранее в своих статьях), или реалтайм обработку видео с задержкой меньше кадра (специально же добавили внутрь CortexM7 заточенные именно для RAW-RGB цветовой интерполяции и поиска оптического потока инструкции).

        А менюшами рулить — ну он офигенно справляется если на baremetal написать в лоб
        (более того я делал классный плавный интерфейс на 2к озу и 8мгц, если интересно могу скинуть видео), а вот либы обросшие мхом тянуть — это какой то садизм как мне кажется.
        Их надо очень сильно урезать и модифицировать на корню — тогда будет толк. А пытаться вытянуть только сборкой, это очень странно. Они слишком гигантские для бедного МК и это факт.

        Как другой вариант: сделать динамическую подкачку кода QT в ITCM т.е. как исполнение из EMS памяти под ДОС через оверлеи. Но это ещё тот ниокр.


        1. abondarev
          28.06.2019 10:59

          извиняюсь, но мне кажется что вы пытаетесь микроскопом гвоздь забить.

          Возможно. Но речь шла не о проекте, а о памяти. Если OpenCV немного не влазит внутрь быстрой памяти, то Qt точно требует внешней. И хорошо заметно ускорение выполнения кода из внутренней памяти.

          Qt это конечно не только менюшки, менюшки можно и по другому рисовать. Это очень мощная платформа программирования. И хочется показать принципиальную возможность использовать подобные библиотеки (Qt, OpenCV, ...) не зависимо от платформы. конечно можно взять и большую платформу чем STM32MP157 поставить Linux, и наслаждаться жизнью, но это будет как минимум дороже и больше потреблять.

          Как другой вариант: сделать динамическую подкачку кода QT в ITCM т.е. как исполнение из EMS памяти под ДОС через оверлеи.

          Спасибо!


  1. AVI-crak
    27.06.2019 01:25

    Работающий проект должен вызвать зуд, с желанием проверить в пошаговом режиме — чего там так сильно тормозит…