121а. OpenMV: камера с машинным зрением на STM32H7 — DCMI, MicroPython и поиск объектов
Разделы 116–121 работали с числами от датчиков: вибрация, ток, напряжение. Камера даёт другой объём: один кадр 320×240 в цвете — это 153 600 байт, больше, чем вся встроенная SRAM у многих STM32. Раздел 22б показал, как DCMI и DMA кладут кадр в память. Здесь — готовая открытая система, которая идёт дальше: принимает кадр, ищет на нём цветные пятна, метки AprilTag, QR-коды и запускает нейросеть, а управляется сценарием на MicroPython. Это проект OpenMV.
Раздел разбирает его исходники: какие платы и камни, как кадр проходит
от датчика до буфера, сколько кадров помещается в память, как цвет
переводится в LAB и как find_blobs находит пятна. Функция поиска пятен
собрана на ПК из исходников проекта и прогнана на синтетическом кадре
с известными ответами.
Опоры: исходники OpenMV (github.com/openmv/openmv,
коммит 62369938a94b). Лицензия: MIT — строка
SPDX-License-Identifier: MIT в заголовках imlib (blob.c,
framebuffer.c; таблицу lab_tab.c создаёт MIT-скрипт
tools/gen_rgb2lab.py), stm_dma.c и py_image.c. Но не везде. Слой камеры (common/omv_csi.c,
ports/stm32/omv_csi.c, modules/py_csi_ng.c), модуль нейросетей
modules/py_ml.c и загрузчик boot/ идут под другой лицензией: копировать
можно, но «solely for personal benefit and not for any commercial purpose»,
для коммерции — отдельный договор с OpenMV. Декодер штрихкодов
lib/imlib/zbar.c — LGPL 2.1, библиотека ST в cubeai/AI — лицензия ST
SLA0044. Регистры DCMI и деление кадра на
части для DMA — в разделе 22б, кэш и домены памяти H7 — в разделе 11б,
квантование и TensorFlow Lite Micro — в разделе 119.
Как это называется в документации
Оригинал Перевод machine vision машинное зрение: поиск объектов на кадре программой frame buffer буфер кадра line buffer строчный буфер: одна-две строки кадра MDMA (master DMA) DMA домена D1 у H7: копирует блоки между памятями UMA pool пул памяти OpenMV, из которого берутся кадры и модели blob пятно: связная область точек, прошедших порог threshold порог: шесть границ L, A, B CIELAB (L*a*b*) цветовое пространство: яркость L и две цветовые оси A и B flood fill заливка: обход связной области roundness, elongation округлость, вытянутость пятна AprilTag квадратная метка с кодом для поиска положения камеры
121а.1 Платы и камни
Каждая плата в OpenMV — каталог boards/<имя> с двумя файлами:
board_config.mk (камень, частота, какие модули собирать) и
board_config.h (выводы, память, датчики). Из них:
| Каталог | Камень | Частота | Пул UMA | Где пул |
|---|---|---|---|---|
| OPENMV2 | STM32F427 | 192 МГц | 160 КБ | SRAM |
| OPENMV3 | STM32F765 | 216 МГц | 383 КБ | SRAM |
| OPENMV4 (H7) | STM32H743 | 480 МГц | 480 КБ | SRAM с 0x2400 0000 |
| OPENMV4P (H7 Plus) | STM32H743 | 480 МГц | 27 МБ | SDRAM 32 МБ с 0xC000 0000 |
| ARDUINO_NICLA_VISION | STM32H747 | 400 МГц | 476 КБ | SRAM |
| ARDUINO_PORTENTA_H7 | STM32H747 | 400 МГц | 4 МБ | SDRAM |
| OPENMV_RT1060 | MIMXRT1062 | 600 МГц | 23 МБ | SDRAM |
| OPENMV_N6 | STM32N657 | 800 МГц | 31 МБ | внешняя память |
Дальше речь об OPENMV4 — STM32H743. Флеш у него 2 МБ: 128 КБ занимает загрузчик, 128 КБ — файловая система, прошивке остаётся 1664 КБ, последние 128 КБ — образ ROMFS. Стек — 64 КБ в ITCM. Кадры, модели нейросетей и временные массивы живут в пуле UMA: 480 КБ из 512 КБ SRAM по 0x2400 0000.
Датчиков в drivers/sensors — 20 файлов драйверов; OPENMV4 включает
десять: OV2640, OV5640, OV7725, MT9V0xx, OV9650, MT9M114, тепловизор
Lepton, PAG7920, PAJ6100 и FrogEye2020. Такт XCLK для датчика даёт
таймер TIM1 на выводе PA8: 12 МГц у H7, 6 МГц у OPENMV2, 9 МГц у OPENMV3
(OMV_CSI_CLK_FREQUENCY).
Аппаратный кодер JPEG есть в сборках H7 и N6
(OMV_JPEG_CODEC_ENABLE (1)), у F4 и F7 — нет.
Программа пользователя — сценарий на MicroPython. Кадр — одна строка,
поиск пятен — ещё одна (пример из
scripts/examples/02-Image-Processing/02-Color-Tracking/single_color_rgb565_blob_tracking.py,
MIT):
thresholds = [
(30, 100, 15, 127, 15, 127), # generic_red_thresholds
(30, 100, -64, -8, -32, 32), # generic_green_thresholds
(0, 30, 0, 64, -128, 0), # generic_blue_thresholds
]
Порог — шесть чисел: границы L, A и B. Под Python работает библиотека
imlib на C: сценарий вызывает img.find_blobs(...), а считает функция
imlib_find_blobs из lib/imlib/blob.c.
121а.2 Путь кадра: DCMI, строка и MDMA
Раздел 22б вёл DMA по всему кадру, частями до 65 535 слов. OpenMV на H7
делает иначе: DMA принимает одну строку по кругу, а раскладывает
строки по кадру второй контроллер — MDMA. Драйвер камеры для STM32
(ports/stm32/omv_csi.c) включает у потока DMA круговой режим, запускает
DCMI в непрерывном захвате и даёт DMA длину ровно в одну строку, в
словах, с адресом строчного буфера. DMA пишет строку, доходит до конца
буфера и начинает сначала — за это время MDMA должен забрать готовую
строку.
Поток DMA — DMA2_Stream1, запрос DCMI. Строчный буфер задан в
boards/OPENMV4/board_config.h:
#define OMV_LINE_BUF_SIZE (3 * 1024) // Image line buffer round(640 * 2BPP * 2 buffers).
Проверка перед запуском требует, чтобы строка была не длиннее половины
буфера: 1536 байт, то есть 768 точек RGB565. Строка VGA — 1280 байт,
проходит. По прерыванию конца передачи обработчик запускает MDMA: для
MDMA выделены каналы 0 и 1 (OMV_MDMA_CHANNEL_DCMI_0, _1), а у AXI
MDMA стоит наивысший приоритет 15 на чтение и запись.
Что это даёт:
- Обрезка по горизонтали без DCMI. Окно по вертикали задаёт DCMI
(
HAL_DCMI_ConfigCrop, раздел 22б), а по горизонтали — адрес источника MDMA: копия начинается с нужной точки строки. DCMI режет только строки, лишние точки слева и справа отбрасывает копирование. - Серый из цветного датчика. Многие датчики отдают серый как YUV 4:2:2 — два байта на точку. На H7 MDMA берёт из строки только байты яркости, по шине DCMI всё равно идут 2 байта. У N6 то же делает сам DCMI прореживанием байтов (поле BSM, раздел 22б).
- Поворот кадра. В режиме транспонирования строка ложится в кадр столбцом — копированием с шагом.
Поток по шине DCMI считается как в разделе 22б: 8 бит за такт PIXCLK, 2 байта на точку. Частоту PIXCLK задаёт настройка датчика, у платы известен только XCLK 12 МГц; вот что даёт пример на 24 и 48 МГц:
| PIXCLK | QVGA RGB565, данные кадра | VGA RGB565, данные кадра |
|---|---|---|
| 24 МГц | 6,4 мс — не больше 156 кадров/с | 25,6 мс — не больше 39 кадров/с |
| 48 МГц | 3,2 мс — не больше 312 кадров/с | 12,8 мс — не больше 78 кадров/с |
Это верхняя граница без гашения строк и кадров и без обработки: реальная частота кадров ниже.
121а.3 Буферы кадра и JPEG для IDE
Размер кадра:
| Кадр | RGB565 | Серый | Буфер в пуле |
|---|---|---|---|
| QQVGA 160×120 | 38 400 байт | 19 200 байт | 38 432 байт |
| QVGA 320×240 | 153 600 байт | 76 800 байт | 153 632 байт |
| VGA 640×480 | 614 400 байт | 307 200 байт | 614 432 байт |
Буфер в пуле больше кадра на заголовок vbuffer_t — смещение и флаги,
а данные с границы строки кэша: 32 байта, выравнивание тоже 32
(lib/imlib/framebuffer.h). Буферы ходят по двум очередям: свободные и
заполненные. Число буферов подбирает функция omv_csi_set_framebuffers
в common/omv_csi.c: если число не задано сценарием, она просит у
framebuffer_resize три буфера, при неудаче два, потом один; не
вышло и с одним — ошибка.
Размер кадра она считает как ширина окна × высота × 2 — по два байта на
точку при любом формате. framebuffer_resize добавляет две очереди
по 28 байт на три буфера (24 — на два, 20 — на один). Сколько буферов
помещается в пул, если он пуст:
| Плата | Пул | Буферов QVGA | Буферов VGA |
|---|---|---|---|
| OPENMV2 | 163 840 байт | 1 | 0 |
| OPENMV3 | 392 192 байт | 2 | 0 |
| OPENMV4 (H7) | 491 520 байт | 3 | 0 |
| ARDUINO_NICLA_VISION | 487 424 байт | 3 | 0 |
| OPENMV4P (H7 Plus) | 28 311 552 байт | 3 | 3 |
Три буфера QVGA — 460 952 байт из 491 520 у H7. VGA в 16 битах требует 614 472 байт на один буфер — у H7 это невозможно, у H7 Plus в SDRAM (раздел 41б) помещаются три.
Три буфера — это тройная буферизация: DMA пишет в один, программа читает
другой, третий держит последний готовый кадр. Когда свободный буфер
остался последним, framebuffer_release при трёх буферах меняет старый
готовый кадр на новый, а при двух новый кадр не отдаёт: следующий пишется в тот же буфер.
Предпросмотр в IDE. Кадр уходит в OpenMV IDE по USB сжатым в JPEG
(lib/imlib/framebuffer.c). Качество подстраивается: начальное —
середина между 50 и 90, то есть 70. Если сжатый кадр не влез в буфер
передачи, качество делится пополам, кадр пропускается, и 60 следующих
кадров качество не растёт: 70 → 35 → 17 → 8 → 4 при переполнениях подряд.
Без переполнений оно растёт на 1 за кадр до потолка: 90 для кадров до
153 600 байт включительно (QVGA RGB565 — ровно столько) и 50 для
больших. Потолок только останавливает рост: у VGA RGB565 качество
выше 50 не поднимется, но стартовые 70 само не снизит — только
переполнение.
121а.4 Цвет: RGB565 в LAB таблицей
Порог по цвету в RGB плохо переносит свет: тень меняет все три канала. В LAB яркость — отдельная ось L (0…100), а цвет — оси A (зелёный — красный) и B (синий — жёлтый). Поэтому пороги в OpenMV — в LAB.
Считать LAB на каждой точке дорого: степень 2,4 и кубический корень. imlib
берёт готовое из таблицы lab_table в lib/imlib/lab_tab.c, по три
байта L, A, B на вход. Входов не 65 536, а 32 768 — индекс pixel >> 1,
младший бит синего отброшен:
#define COLOR_RGB565_TO_L(pixel) lab_table[((pixel >> 1) * 3) + 0]
Таблица занимает 98 304 байт вместо 196 608. Генератор лежит в проекте —
tools/gen_rgb2lab.py: переводит каждую из 65 536 точек по формулам
sRGB и CIELAB (белая точка D65), округляет и усредняет соседние пары.
Прогон генератора на ПК:
- таблица совпала с
lab_tab.cво всех 98 304 байтах, таблица линеаризацииxyz_table— во всех 256 числах; - расхождение таблицы с точной формулой для отдельной точки — до 1 по L, до 3 по A и по B; точно совпадают 1761 точка из 65 536;
- значения в таблице: L от 0 до 100, A от −86 до 97, B от −106 до 94.
Цвета тестового кадра по этой таблице:
| Цвет, RGB | L | A | B |
|---|---|---|---|
| красный 200, 30, 30 | 44 | 65 | 51 |
| зелёный 30, 170, 40 | 61 | −60 | 54 |
| синий 20, 40, 170 | 25 | 42 | −69 |
| серый 128, 128, 128 | 55 | 2 | −3 |
Красный проходит «общий красный» порог примера (L 30…100, A и B от 15), синий — «общий синий» (L до 30, A 0…64, B до 0). А зелёный — нет: у него B = 54, а порог «общего зелёного» ограничивает B числом 32. Этот зелёный с жёлтым оттенком — ниже в прогоне его не найдёт.
121а.5 find_blobs: заливка строками
imlib_find_blobs проходит кадр строками с шагом x_stride по X (по
умолчанию 2) и y_stride по Y (1). Точку, прошедшую порог и ещё не
занятую, берёт за начало пятна и заливает его построчно: расширяет
отрезок влево и вправо, пока точки проходят порог, отмечает их в битовой
карте размером с кадр и ищет продолжение в строке выше и ниже. Вместо
рекурсии — стек LIFO из записей xylr_t (шесть чисел по 16 бит, 12
байт); под стек lifo_alloc_all забирает всё, что свободно в пуле UMA.
По ходу заливки каждый отрезок добавляет в пятно число точек и суммы
X, Y, X², X·Y и Y² — из этих моментов получаются центр,
угол поворота и округлость без второго прохода. Потом пятна меньше
pixels_threshold точек или area_threshold площади рамки отбрасываются
(по умолчанию оба — 10), а при merge=True пересекающиеся рамки
сливаются; margin расширяет рамку перед проверкой.
Прогон. Файлы blob.c, collections.c, fmath.c, lab_tab.c,
sincos_tab.c и три функции из imlib.c собраны gcc на ПК с заглушками
MicroPython и пула UMA. Кадр QVGA RGB565: серый фон с шумом ±12 по каждому
каналу, прямоугольники 60×40 (красный), 50×80 (зелёный), 70×30 (синий),
красная полоса 60×10 на 4 строки ниже красного прямоугольника, красный
круг радиусом 20 из 1257 точек и 40 одиночных красных точек.
| Запуск | Пятен | Что найдено |
|---|---|---|
| красный, по умолчанию | 3 | прямоугольник 2400 точек, центр 49,50; 49,50; полоса 600, центр 49,50; 78,50; круг 1257, центр 250,00; 180,00 |
красный, merge=True, margin=4 |
3 | то же: зазор 4 строки не закрыт |
красный, merge=True, margin=5 |
2 | прямоугольник и полоса слиты: рамка 60×54, 3000 точек, центр Y 55,30 |
| красный, шаг 2, порог 1 точка | 22 | три пятна и 19 одиночных точек из 40 |
| красный, шаг 1, порог 1 точка | 43 | три пятна и все 40 одиночных точек |
| зелёный, порог примера | 0 | — |
| зелёный, B до 64 | 1 | 4000 точек, центр 164,50; 139,50, угол 90,0° |
| синий, порог примера | 1 | 2100 точек, центр 264,50; 34,50 |
Площади и центры совпали с построенными точно: 60 × 40 = 2400, 70 × 30 = 2100, центр прямоугольника 60×40 с угла 20, 30 — 49,5; 49,5. Слитое пятно — взвешенный центр: (2400 · 49,5 + 600 · 78,5) / 3000 = 55,3. Округлость круга — 1,000, прямоугольника 60×40 — 0,445, полосы 60×10 — 0,028. Утечек памяти нет: все блоки пула освобождены.
Одиночные точки при пороге по умолчанию (10 точек) отброшены все — так фильтруется шум. При шаге 2 функция вообще не видит 21 точку из 40: начало пятна ищется только на каждой второй точке строки.
Другие поиски в imlib. find_apriltags — обёртка над библиотекой
AprilTag 3.4.5 (подмодуль lib/apriltag), на H7 включено семейство
TAG36H11. find_qrcodes — декодер quirc (MIT), встроенный в
lib/imlib/qrcode.c. Каскады Хаара (lib/imlib/haar.c, каскады лиц и
глаз в lib/haar) считают интегральное изображение не по всему кадру,
а в скользящем окне строк (imlib_integral_mw_*) — памяти нужно на
окно, а не на кадр.
121а.6 Нейросети: TensorFlow Lite Micro и ST Edge AI
Модуль ml (modules/py_ml.c) загружает модель .tflite в пул UMA —
в быструю память и насовсем, до выгрузки модели, — и запускает её через
движок сборки: у OPENMV4, OPENMV4P, Nicla Vision и Portenta — TensorFlow
Lite Micro (MICROPY_PY_ML_TFLM = 1), у N6 — ST Edge AI для
нейроускорителя (MICROPY_PY_ML_STAI = 1). Масштаб и нулевая точка
входа берутся из самой модели, и вход квантуется по формуле раздела 119:
значение делится на масштаб (в коде — умножается на заранее посчитанную
обратную величину), к нему прибавляется нулевая точка, результат
записывается как int8. Выход переводится назад: (q − zero_point) · scale.
Готовые модели в lib/models и их размеры:
| Модель | Байт | Что делает |
|---|---|---|
fomo_face_detection.tflite |
57 040 | лица: метки background, face |
person_detect.tflite |
300 568 | есть человек или нет: no_person, person |
yolov8n_192.tflite |
3 228 400 | YOLOv8n, метка person |
person_detect — 300 568 байт; вместе с тремя буферами QVGA (460 952)
это больше пула H7 в 491 520 байт, так что модели и кадрам приходится
делить память: меньше буферов, меньше кадр. YOLOv8n на 3,2 МБ в
пул H7 не помещается вовсе — это задача для H7 Plus с SDRAM или N6.
Каталог cubeai/ — вариант сборки с моделями STM32Cube.AI для H7 и
H7 Plus: обёртка nn_st.c, py_st_nn.c под MIT, а библиотека в
cubeai/AI — под лицензией ST SLA0044.
Сборка прошивки. Для этого раздела подмодули не выкачивались:
lib/micropython, lib/tflm/libtflm, lib/apriltag, lib/tinyusb,
modules/ulab пусты (git submodule status показывает их с «−»). Без
MicroPython прошивка не собирается — поэтому размер
прошивки здесь не приведён. На ARM собран только проверочный файл
структур буфера кадра: sizeof(vbuffer_t) = 32 и размеры очередей
28/24/20 байт подтверждены _Static_assert в arm-none-eabi-gcc для
Cortex-M7.
121а.7 Грабли
- Порог гуляет вместе с автоусилением. Автоусиление и автобаланс
белого сдвигают L, A и B от кадра к кадру. Пример OpenMV выключает оба
перед поиском по цвету:
auto_gain(False)иauto_whitebal(False)с пометкой «must be turned off for color tracking». - «Общий» порог не ловит свой цвет. Зелёный 30, 170, 40 имеет B = 54, порог примера — B до 32: ноль пятен. Порог подбирают по своему объекту и свету — в IDE есть редактор порогов.
- Слияние не срабатывает на зазоре, равном запасу. Рамки сравниваются
строго: при зазоре в 4 строки
margin=4оставляет два пятна, нужно 5. - Шаг 2 теряет мелочь. С
x_stride=2из 40 одиночных точек найдено 19. Для тонких линий и мелких объектов ставятx_stride=1. - Пороги у границы таблицы. Таблица LAB ошибается на отдельных точках до 3 единиц по A и B: граница порога ближе 3 единиц к цвету объекта — пятно будет «моргать».
- Серый кадр занимает место как цветной.
omv_csi_set_framebuffersрезервирует по 2 байта на точку при любом формате: серый VGA на H7 в пул не помещается так же, как цветной. - Модель и кадры делят один пул.
person_detectна 300 568 байт и три буфера QVGA вместе не помещаются в 480 КБ H7. - Картинка в IDE хуже, чем кадр. Предпросмотр — JPEG, после переполнений качество падает до единиц; обработка идёт по несжатому кадру. Судить о пороге по картинке IDE при сильном сжатии нельзя.
- Кэш H7. Буферы кадра в кэшируемой SRAM;
framebuffer_acquireс флагомFB_FLAG_INVALIDATEсбрасывает строки кэшаSCB_InvalidateDCache_by_Addr— без этого процессор читает старый кадр (раздел 11б). - Лицензии разные. imlib — MIT, но драйвер камеры (
omv_csi.c) и модульml— только для некоммерческого использования,zbar.c(штрихкоды) — LGPL 2.1, библиотека Cube.AI — SLA0044. Для прибора на продажу из OpenMV свободно берётся imlib, а приём кадра пишут сами (раздел 22б) или договариваются с OpenMV.
121а.8 Итоги
- OpenMV — открытая система машинного зрения: OPENMV4 на STM32H743
480 МГц, H7 Plus с SDRAM 32 МБ, платы Arduino на H747, N6 на STM32N657;
imlib — MIT, драйвер камеры и модуль
ml— некоммерческая лицензия, zbar — LGPL, Cube.AI — SLA0044. - Кадр идёт строками: DMA2 поток 1 по кругу в строчный буфер 3 КБ, MDMA каналами 0 и 1 — в буфер кадра; строка не длиннее 1536 байт.
- QVGA RGB565 — 153 600 байт; у H7 в пуле 480 КБ — три буфера (460 952 байт), VGA RGB565 — ни одного, у H7 Plus — три.
- JPEG для IDE: качество 70 на старте, рост до 90 для кадров до 153 600 байт и до 50 для больших, при переполнении — вдвое меньше и 60 кадров без роста.
- LAB — таблицей 98 304 байт на 32 768 входов; генератор проекта её воспроизводит байт в байт, ошибка против точной формулы — до 3 единиц.
find_blobsна синтетическом кадре: площади 2400, 1257, 2100 и центры совпали точно;margin=5сливает пятна через зазор 4 строки;x_stride=2находит 19 одиночных точек из 40.- Модели int8:
person_detect300 568 байт через TensorFlow Lite Micro; прошивку без подмодулей собрать нельзя — размер не приведён.
Дальше: раздел 121б «Звук на микроконтроллере: от PDM-микрофона к MFCC и распознаванию команды» — тракт от бита PDM до команды: CIC и КИХ, MFCC в целых числах, сеть int8 и решение по потоку.