Аппаратные наборы
Развёртывание машинного обучения на реальном встраиваемом оборудовании. Платы стоят от $25, а если бюджета нет, интерактивные лабораторные работы воспроизводят весь опыт работы с оборудованием в симуляции.
Эти практические лабораторные работы сопровождают учебник Machine Learning Systems и переносят теорию на настоящее оборудование. Вы развернёте модели машинного обучения на встраиваемых устройствах, которые помещаются в ладони: от классификации изображений до распознавания голоса и обнаружения движения. Профессиональные отладочные платы стоимостью $25–100 дают мгновенную ощутимую обратную связь: когда модель работает успешно, светодиоды загораются, моторы вращаются, а зуммеры звучат.
Работа в условиях ограниченных ресурсов встраиваемых устройств (обычно 2 МБ оперативной памяти и 1 МБ флеш-памяти) заставляет сталкиваться с теми же инженерными компромиссами, которые определяют крупномасштабные ML-системы, но в осязаемой среде, где каждое решение по оптимизации сразу даёт наблюдаемый результат.
Разработка лабораторных работ. Эти практические лабораторные работы разработаны совместно проф. Vijay Janapa Reddi и Marcelo Rovai; разработкой руководил Marcelo. Его многолетний опыт в области встраиваемых систем позволил создать доступные практические занятия, которые связывают теорию с реальной реализацией.
Marcelo также написал открытые книги-компаньоны о платах Nicla Vision, XIAO и Raspberry Pi. Прямые ссылки и описания см. в разделе «Companion Books» Хаба для преподавателей.
Аппаратные платформы
- Grove Vision AI V2, ~$25. Лучший выбор для начинающих: готовое решение для компьютерного зрения «подключи и работай».
- XIAOML Kit, ~$40. Лучшее соотношение цены и возможностей: зрение, звук и движение.
- Raspberry Pi, ~$60–80. Продвинутые проекты: LLM, VLM и периферийный ИИ.
- Nicla Vision, ~$95. Профессиональный уровень и два датчика в компактном корпусе.
Что вы создадите
- Компьютерное зрение. Классификация изображений и обнаружение объектов на микроконтроллерах. Обучайте модели распознавать объекты, находить лица или классифицировать сцены, а затем запускайте их на устройствах, работающих от батареи.
- Обработка звука. Распознавание ключевых слов и голосовых команд. Создавайте детекторы слова пробуждения и простые голосовые интерфейсы, которые работают полностью на устройстве, без облака.
- Классификация движения. Распознавание активности и жестов по данным IMU. Создавайте носимые приложения, которые определяют ходьбу, бег или пользовательские жесты с помощью акселерометра и гироскопа.
- Большие языковые модели. Запускайте LLM и VLM на периферийных устройствах. Познакомьтесь с передовыми возможностями ИИ на устройстве: модели понимают и генерируют текст.
Начало работы
- Выберите оборудование. Подберите платформу по бюджету и учебным целям. Подробное сравнение см. в разделе «Платформы».
- Настройте среду. Установите Arduino IDE или инструменты для конкретной платформы. Пошаговые инструкции см. в руководстве по настройке среды разработки.
- Соберите и разверните. Выполняйте лабораторные работы для выбранной платформы. Начните с раздела «Начало работы», где описаны все доступные упражнения.
Часть экосистемы MLSysBook
- Учебник — полная теория по всему стеку систем машинного обучения.
- Слайды лекций — презентации Beamer и учебные материалы для каждой главы.
- Лабораторные работы — интерактивные блокноты Marimo, которые проверяют утверждения книги.
- Аппаратные наборы — практическое развёртывание машинного обучения на реальных встраиваемых устройствах.
- Хаб для преподавателей — карты курсов, учебные программы и материалы для внедрения курса в преподавание.
- TinyTorch — постройте собственный фреймворк машинного обучения с нуля, модуль за модулем.
- MLSys·im — аналитический движок моделирования, на котором основаны количественные рисунки книги.
Начало работы
Это руководство проведёт вас через выбор оборудования, настройку среды разработки и запуск первого приложения машинного обучения на встраиваемом устройстве. Большинство студентов завершают настройку меньше чем за час.
Шаг 1. Выберите платформу
Выбор зависит от бюджета, учебных целей и типов приложений, которые вы хотите создавать.
Для начинающих и тех, кто ограничен в бюджете:
| Платформа | Стоимость | Почему стоит выбрать |
|---|---|---|
| Grove Vision AI V2 | ~$25 | Интерфейс без кода, самый быстрый путь к запуску моделей |
| XIAOML Kit | ~$40 | Лучшее соотношение цены и возможностей: зрение, звук и движение |
Для продвинутых приложений:
| Платформа | Стоимость | Почему стоит выбрать |
|---|---|---|
| Raspberry Pi | ~$60–80 | Полноценная среда Linux, LLM и VLM |
| Nicla Vision | ~$95 | Профессиональный уровень, сверхнизкое энергопотребление |
Подробные характеристики и технические сравнения см. на странице «Платформы».
Шаг 2. Настройте среду разработки
Настройка среды зависит от платформы, но всегда следует одной схеме: установить программные инструменты, настроить связь с оборудованием и проверить, что всё работает.
Ориентировочное время: 30–60 минут в зависимости от платформы и скорости интернета.
Полные инструкции см. в руководстве по настройке среды разработки. В нём описаны:
- требования к компьютеру для разработки;
- установка Arduino IDE для платформ на микроконтроллерах;
- настройка среды Python для Raspberry Pi;
- веб-интерфейс SenseCraft AI для Grove Vision AI V2;
- настройка последовательной связи (Serial) и проверка оборудования.
Шаг 3. Выберите первую лабораторную работу
Каждая платформа поддерживает разные категории упражнений. Выбирайте работы, которые подходят и вашему оборудованию, и учебным целям.
| Категория работы | Grove Vision | XIAOML Kit | Nicla | Raspberry Pi |
|---|---|---|---|---|
| Классификация изображений | ✓ | ✓ | ✓ | ✓ |
| Обнаружение объектов | ✓ | ✓ | ✓ | ✓ |
| Распознавание ключевых слов | ✓ | ✓ | ||
| Классификация движения | ✓ | ✓ | ||
| Большие языковые модели (LLM) | ✓ | |||
| Визуально-языковые модели (VLM) | ✓ |
Доступность упражнений по платформам
Шаг 4. Начните первую лабораторную работу
Grove Vision AI V2: начните с раздела «Настройка и приложения без кода». Вы развернёте предобученную модель за несколько минут в визуальном интерфейсе.
XIAOML Kit: начните с раздела «Настройка», затем перейдите к разделу «Классификация изображений», чтобы обучить и развернуть свою первую модель.
Nicla Vision: пройдите «Настройку», чтобы подготовить плату, и попробуйте «Классификацию изображений».
Raspberry Pi: выполните «Настройку», затем выберите путь:
- «Классификация изображений» — основы компьютерного зрения;
- «Развёртывание LLM» — запуск языковых моделей на периферийных устройствах.
Предварительные требования
Лабораторные работы рассчитаны на следующую подготовку:
- Программирование: уверенное владение Python. Знание C/C++ полезно для платформ на микроконтроллерах, но не обязательно.
- Математика: рабочее знание линейной алгебры и основ теории вероятностей на уровне бакалавриата.
- Оборудование: опыт работы со встраиваемыми системами не нужен. Каждая лабораторная работа содержит полные инструкции по настройке и поиску неисправностей.
Связь с учебником «Machine Learning Systems»
Эти лабораторные работы дополняют отдельные главы учебника «Machine Learning Systems»:
- Лабораторные работы по классификации изображений закрепляют материал глав о компьютерном зрении и оптимизации моделей;
- Лабораторные работы по распознаванию ключевых слов связаны с главами об обработке аудио и инференсе в реальном времени;
- Лабораторные работы по классификации движения демонстрируют слияние данных датчиков и анализ временных рядов;
- Лабораторные работы по LLM и VLM переносят развёртывание больших моделей в среду с ограниченными ресурсами.
Каждая лабораторная работа указывает соответствующие разделы учебника для более глубокого теоретического изучения.
Настройка среды разработки
Настройка среды разработки — важный первый шаг, который определяет ваш успех на протяжении всей серии лабораторных работ. В отличие от облачной разработки ML, где инфраструктура скрыта, встраиваемые системы требуют понимания всей цепочки инструментов: от компиляции кода до развёртывания на оборудовании.
Настройка среды обычно занимает 30–60 минут в зависимости от выбранной платформы и скорости интернета. Эти процедуры рассчитаны на студентов, у которых нет опыта работы со встраиваемыми системами.
Системные требования
Прежде чем начинать установку, убедитесь, что ваш компьютер для разработки соответствует следующим требованиям:
Компьютер для разработки:
- Операционная система: Windows 10/11, macOS 10.15+ или Linux (Ubuntu 18.04+)
- Память: минимум 8 ГБ ОЗУ (для разработки на Raspberry Pi рекомендуется 16 ГБ)
- Накопитель: 10 ГБ свободного места для инструментов разработки и библиотек
- Порты USB: хотя бы один порт USB 2.0/3.0 для подключения устройства
- Интернет: необходим для установки программ и загрузки библиотек
Программные требования:
- Arduino IDE 2.0+ для платформ на базе Arduino (XIAO, Nicla Vision)
- Python 3.8+ для разработки на Raspberry Pi
- Git для управления версиями и доступа к примерам кода
- Текстовый редактор или IDE, например VS Code или PyCharm
Аксессуары для оборудования:
- USB-кабели: USB-C или Micro-USB (кабель должен передавать данные, а не только питание)
- SD-карта: 32 ГБ и больше, класс 10, для Raspberry Pi
- Блоки питания: подходящие для каждой платформы
- Модули камеры: входят в состав большинства наборов или продаются отдельно
Установка программного обеспечения для конкретных платформ
Каждая аппаратная платформа требует собственного подхода к разработке, который отражает практику реальной встраиваемой инженерии. Системы на базе Arduino сосредоточены на эффективном использовании ресурсов и ограничениях реального времени, Raspberry Pi демонстрирует полноценные возможности периферийных вычислений, а специализированное ИИ-оборудование показывает методы аппаратного ускорения.
Выберите процедуры установки, подходящие для вашей платформы.
Платформы на базе Arduino (Nicla Vision, XIAOML Kit)
Встраиваемые системы на базе Arduino дают прямой контроль над оборудованием с минимумом программных прослоек, что делает их идеальными для понимания ограничений ресурсов и методов оптимизации. Среда разработки ориентирована на быструю обратную связь между изменениями кода и поведением системы.
Установка Arduino IDE:
- Скачайте Arduino IDE 2.0 с сайта arduino.cc/software
- Установите программу, следуя мастеру установки для вашей платформы
- Запустите Arduino IDE и откройте меню File → Preferences (Файл → Настройки)
- Добавьте ссылки на пакеты поддержки плат:
- для XIAOML Kit:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - для Nicla Vision: ссылка указана в Boards Manager (менеджере плат) Arduino IDE
- для XIAOML Kit:
Установка пакетов поддержки плат:
- Откройте Tools → Board → Boards Manager (Инструменты → Плата → Менеджер плат)
- Найдите свою платформу:
- XIAOML Kit: введите «ESP32» и установите «esp32 by Espressif Systems»
- Nicla Vision: введите «Arduino Mbed OS Nicla Boards» и установите пакет
- Выберите свою плату в меню Tools → Board (Инструменты → Плата)
- Установите нужные библиотеки через Library Manager (менеджер библиотек)
Основные библиотеки:
- TensorFlow Lite Micro
- драйверы камер для конкретной платы
- библиотеки интерфейсов датчиков (I2C, SPI)
Платформа Grove Vision AI V2
Эта платформа демонстрирует ИИ с аппаратным ускорением на выделенных нейропроцессорных блоках и показывает, как специализированный кремний достигает производительности, недоступной универсальным процессорам. Визуальный интерфейс программирования позволяет быстро прототипировать, а традиционные среды разработки дают больше возможностей для настройки.
Настройка SenseCraft AI:
- Создайте учётную запись на сайте sensecraft.seeed.cc
- Подключите Grove Vision AI V2 через USB
- Откройте устройство в веб-интерфейсе SenseCraft AI
- Для визуального программирования локальная установка программ не требуется
Настройка Arduino IDE (для собственной разработки):
Следуйте инструкциям для платформ на базе Arduino, описанным выше, и используйте в менеджере плат ссылку на пакет Seeed Studio.
Платформа Raspberry Pi
Среда Raspberry Pi соединяет ограничения встраиваемых систем с полноценными вычислительными возможностями: студенты знакомятся и с оптимизацией ресурсов, и с продвинутыми ML-фреймворками. Такой двойной взгляд показывает, как вычислительные ресурсы влияют на выбор алгоритмов и архитектуру системы.
Установка операционной системы:
- Скачайте Raspberry Pi Imager
- Запишите Raspberry Pi OS (рекомендуется 64-разрядная версия) на microSD-карту объёмом не меньше 32 ГБ
- Во время записи настройте доступ по SSH и учётные данные Wi-Fi
- Вставьте записанную SD-карту и включите Raspberry Pi
Настройка программной среды:
Следующие команды создают полноценную среду разработки ML на Python с правильным управлением зависимостями:
# Обновление системных пакетов
sudo apt update && sudo apt upgrade -y
# Установка инструментов разработки Python
# python3-pip: установщик пакетов Python
# python3-venv: создание виртуальных окружений
# python3-dev: заголовочные файлы для разработки на Python
sudo apt install python3-pip \
python3-venv \
python3-dev -y
# Установка зависимостей ML-фреймворков
# libatlas-base-dev: библиотека линейной алгебры (BLAS/LAPACK)
# libhdf5-dev: библиотека формата данных HDF5
# libhdf5-serial-dev: последовательная версия HDF5
sudo apt install libatlas-base-dev \
libhdf5-dev \
libhdf5-serial-dev -y
# Установка зависимостей компьютерного зрения
# libcamera-dev: библиотека интерфейса камеры
# python3-libcamera: привязки Python для libcamera
# python3-kms++: библиотека установки режима ядра (kernel mode setting)
sudo apt install libcamera-dev \
python3-libcamera \
python3-kms++ -y
# Создание виртуального окружения для проектов
python3 -m venv ~/ml_projects
source ~/ml_projects/bin/activate
# Установка основных пакетов ML
# tensorflow: основной фреймворк ML
# tensorflow-lite: оптимизирован для периферийных и мобильных устройств
# opencv-python: библиотека компьютерного зрения
# numpy: основа численных вычислений
pip install tensorflow \
tensorflow-lite \
opencv-python \
numpy
Настройка инструментов разработки
Правильная настройка инструментов обеспечивает надёжную связь между рабочей станцией разработчика и встраиваемым оборудованием. Эти параметры составляют основу для загрузки кода, отладки и мониторинга производительности на протяжении всех лабораторных упражнений.
Настройка последовательной связи (Serial)
Последовательная связь — основной интерфейс для отладки и сбора данных во встраиваемых системах. Она показывает поведение системы, без чего невозможно понять ограничения производительности и возможности оптимизации.
Windows:
- установите подходящие драйверы USB-to-serial (CH340, FTDI или драйверы конкретной платы);
- проверьте в диспетчере устройств, что плата распознаётся.
macOS/Linux:
- большинство адаптеров USB-to-serial работают без дополнительных драйверов;
- проверить распознавание устройства можно командой
ls /dev/tty*(macOS/Linux); - добавьте пользователя в группу dialout:
sudo usermod -a -G dialout $USER(Linux).
Настройка IDE
Параметры среды разработки напрямую влияют на эффективность цикла «написать — проверить — развернуть», который характерен для разработки встраиваемых систем. Правильная настройка сокращает время отладки и даёт ясную обратную связь о производительности системы.
Параметры Arduino IDE:
- выберите нужный COM-порт в меню Tools → Port (Инструменты → Порт);
- задайте правильную плату и процессор;
- проверьте скорость загрузки (обычно 115200 бод);
- включите подробный вывод при компиляции для отладки.
Разработка на Raspberry Pi:
- настройте SSH-ключи для удалённой разработки;
- установите VS Code с расширениями Python и Remote SSH;
- настройте доступ к Jupyter Notebook для интерактивной работы.
Проверка среды
Проверочные процедуры подтверждают, что среда разработки может обмениваться данными с оборудованием и выполнять базовые операции. Эти тесты задают исходную точку перед более сложными лабораторными упражнениями.
Тесты распознавания оборудования
Ниже приведены проверочные процедуры, которые тестируют основные функции, необходимые для лабораторных работ: обмен данными с оборудованием и корректную работу программных библиотек.
Платформы Arduino:
void setup() {
// Запуск последовательного порта на скорости 115200 бод
Serial.begin(115200);
Serial.println("Development environment test");
Serial.print("Board: ");
Serial.println(ARDUINO_BOARD);
}
void loop() {
Serial.println("Environment operational");
delay(1000);
}
Raspberry Pi:
# Проверка интерфейса камеры
libcamera-hello --timeout 5000
# Проверка среды ML на Python
python3 -c \
"import tensorflow as tf; print('TensorFlow:', tf.__version__)"
python3 -c \
"import cv2; print('OpenCV:', cv2.__version__)"
Grove Vision AI V2:
- проверьте, что устройство видно в веб-интерфейсе SenseCraft AI;
- протестируйте базовое развёртывание модели через визуальный интерфейс программирования.
Типичные проблемы при настройке и их решения
Трудности при настройке — обычное дело, и они дают ценный опыт в понимании ограничений встраиваемых систем и методов отладки. Ниже описаны решения для наиболее частых проблем.
Проблемы с подключением устройства:
- убедитесь, что USB-кабель передаёт данные, а не только питание;
- установите драйверы USB для платформы, если устройство не распознаётся;
- попробуйте другие порты USB или USB-хаб, если соединение нестабильно.
Ошибки компиляции:
- проверьте, что в IDE выбраны правильная плата и процессор;
- убедитесь, что все нужные библиотеки установлены в совместимых версиях;
- проверьте, достаточно ли места на диске для компиляции.
Ошибки во время работы:
- убедитесь, что блок питания достаточно мощный (особенно для работы камеры);
- проверьте совместимость и формат SD-карты (Raspberry Pi);
- проверьте распределение памяти под ML-модели в рамках ограничений платформы.
Сетевое подключение (платформы с Wi-Fi):
- проверьте учётные данные сети и протоколы безопасности;
- проверьте настройки брандмауэра для доступа инструментов разработки;
- убедитесь, что сеть разрешает обмен данными между устройством и компьютером разработчика.
Поиск и устранение неисправностей
Типичные аппаратные проблемы:
- Устройство не распознаётся: убедитесь, что USB-кабель передаёт данные, попробуйте другие порты;
- Ошибки загрузки: проверьте выбор платы и настройки порта в IDE;
- Проблемы с питанием: проверьте достаточность питания, особенно для работы камеры;
- Ошибки памяти: убедитесь, что модель укладывается в ограничения платформы.
Программные проблемы:
- Конфликты библиотек: используйте совместимые версии, указанные в руководствах по настройке;
- Ошибки компиляции: проверьте, что все зависимости установлены правильно;
- Сетевое подключение: проверьте настройки брандмауэра и разрешения сети.
Ресурсы для конкретных платформ:
- XIAOML Kit: документация Seeed Studio
- Arduino Nicla Vision: документация Arduino
- Grove Vision AI V2: платформа SenseCraft AI
- Raspberry Pi: официальная документация
Поддержка сообщества:
- GitHub Issues: сообщайте об ошибках и предлагайте функции через репозиторий проекта;
- Форумы: сообщества конкретных платформ на сайтах Arduino, Raspberry Pi и Seeed Studio;
- Stack Overflow: задавайте вопросы с тегами соответствующих платформ.
Готовность к лабораторным работам
Когда среда разработки настроена и проверена, у вас есть базовые инструменты для программирования встраиваемых ML-систем. Навыки, полученные при настройке среды (понимание цепочек инструментов, управление зависимостями и проверка работоспособности системы), пригодятся во всех последующих лабораторных работах.
Настроенная среда поддерживает весь рабочий процесс: от реализации алгоритма до развёртывания на оборудовании и оптимизации производительности. В обзоре лабораторных работ собраны категории упражнений по сложности и учебным целям; они систематически опираются на эти базовые навыки.
Рекомендуемая последовательность:
- Начните с простых упражнений на датчики, чтобы проверить работу оборудования.
- Перейдите к приложениям ML с одним типом данных (изображения или звук).
- Затем переходите к мультимодальным упражнениям и задачам оптимизации.
Каждое лабораторное упражнение включает подробные процедуры реализации, ожидаемые показатели производительности и советы по поиску неисправностей с учётом требований конкретного проекта. Созданная вами среда разработки открывает путь к полному спектру встраиваемых ML-приложений и методов оптимизации.
Платформы
Примечание о переводе. Цены указаны в долларах США, как в оригинале, и ориентировочны: перед покупкой проверьте актуальную стоимость у продавца.
В этой главе приведены подробные технические характеристики четырёх аппаратных платформ, которые используются в лабораторных работах. Каждая из них занимает собственное место на шкале возможностей встраиваемых вычислений: от микроконтроллеров со сверхнизким энергопотреблением до полноценных периферийных компьютеров.
Эти платформы выбраны потому, что каждая из них наглядно показывает свой инженерный компромисс между энергопотреблением, вычислительной мощностью и сложностью разработки. Все они широко применяются в коммерческих проектах, поэтому навыки, полученные здесь, напрямую переносятся на профессиональную работу со встраиваемыми системами.
Платформа в центре внимания

Полный комплект XIAOML Kit со всеми компонентами
XIAOML Kit — самая новая из наших учебных аппаратных платформ (выпущена 31 июля 2025 года). Это полноценная среда разработки TinyML для изучения ML-систем: встроенная беспроводная связь, камера, несколько датчиков и обширная документация. Эта компактная плата показывает, как современные встраиваемые системы могут эффективно обеспечивать продвинутые возможности машинного обучения в недорогом исполнении.
Обзор платформ
В нашем курсе четыре тщательно отобранные платформы, которые охватывают весь спектр возможностей встраиваемых вычислений. Каждая платформа из таблицы ниже выбрана, чтобы показать конкретные инженерные компромиссы и учебные цели.
| Платформа | Основная тема обучения | Стоимость | Энергопрофиль | Лучше всего для |
|---|---|---|---|---|
| XIAOML Kit | IoT и беспроводное ML | ~$40 | Низкое | Проекты с ограниченным бюджетом |
| Arduino Nicla | Проектирование со сверхнизким энергопотреблением | ~$95 | Сверхнизкое | Устройства с питанием от батареи |
| Grove Vision AI | Аппаратное ускорение | ~$25 | Среднее | Промышленные применения |
| Raspberry Pi | Полноценные ML-фреймворки | $60–145 | Высокое | Сложные периферийные вычисления |
Стратегия выбора платформ
Сравнение платформ
В таблице ниже приведено подробное техническое сравнение всех четырёх платформ.
| Характеристика | XIAOML Kit | Raspberry Pi | Arduino Nicla | Grove Vision AI V2 |
|---|---|---|---|---|
| Диапазон стоимости (USD) | ~$40 | $60–145 | ~$95 | ~$25 |
| Энергопотребление | Низкое | Высокое | Сверхнизкое | Среднее |
| Вычислительная мощность | Средняя | Очень высокая | Низкая | Высокая (NPU) |
| Объём памяти | 8 МБ | 1–16 ГБ | 2 МБ | 16 МБ |
| Основной сценарий | IoT-сети | Периферийные вычисления | Устройства от батареи | Промышленный ИИ |
| ML-фреймворк | TF Lite | TensorFlow, PyTorch | TensorFlow Lite | SenseCraft AI |
| Среда разработки | Arduino/PlatformIO | Python/Linux | Arduino IDE | Визуальный интерфейс / код |
Матрица сравнения платформ
Рекомендации по выбору платформы
Выбор подходящей платформы зависит от конкретных учебных целей и требований проекта. Таблица ниже систематизирует эти соответствия и помогает принять решение.
| Учебная задача / применение | XIAOML Kit | Raspberry Pi | Arduino Nicla | Grove Vision AI V2 |
|---|---|---|---|---|
| Основы встраиваемых систем | ✓ | Ограниченно | ✓ | ✓ |
| Беспроводная связь | ✓ | ✓ | ✓ | |
| Сверхнизкое энергопотребление | ✓ | |||
| Полноценные ML-фреймворки | ✓ | |||
| Аппаратное ускорение | ✓ | |||
| Компьютерное зрение в реальном времени | Ограниченно | ✓ | ✓ | ✓ |
| Интеграция периферийных устройств с облаком | ✓ | ✓ | ✓ | |
| Промышленное развёртывание | ✓ | ✓ | ✓ |
Матрица возможностей платформ
Технические характеристики платформ
В этом разделе приведены подробные технические характеристики каждой платформы: архитектура процессора, иерархия памяти, возможности датчиков и требования к инструментам разработки.
XIAOML Kit (Seeed Studio)
Лучше всего для: IoT и беспроводного ML. XIAOML Kit хорошо справляется с беспроводной связью и проектами с ограниченным бюджетом. Он идеально подходит для изучения сетей IoT-датчиков, систем удалённого мониторинга и беспроводного ML-инференса, когда нужна надёжная связь в компактном и недорогом исполнении.
XIAOML Kit объединяет плату XIAO ESP32S3 Sense и плату расширения, образуя полноценную среду разработки TinyML. Одна только XIAO ESP32S3 Sense обеспечивает камеру и микрофон (зрение и звук), а плата расширения добавляет 6-осевой инерциальный измерительный блок (IMU) для классификации движения. Название «XIAO» (小) в переводе с китайского означает «крошечный» и отражает форм-фактор платы: мм.

XIAOML Kit с платой расширения
Архитектура процессора: Двухъядерный ESP32-S3 с ядрами Xtensa LX7 на частоте 240 МГц
Иерархия памяти: 8 МБ PSRAM и 8 МБ флеш-памяти
Связь: Wi-Fi 802.11 b/g/n и Bluetooth 5.0
Встроенные датчики:
- XIAO ESP32S3 Sense: камера OV2640, цифровой микрофон
- Плата расширения: 6-осевой инерциальный измерительный блок (IMU), 0,42-дюймовый OLED-дисплей
Характеристики питания: Питание 3,3 В с несколькими режимами пониженного энергопотребления
Среда разработки: Поддерживаются Arduino IDE и PlatformIO, есть обширная экосистема библиотек. Программирование ведётся на C/C++ с абстракциями в стиле Arduino; опытные пользователи могут работать напрямую с ESP-IDF.
Область применения: Сети IoT-датчиков, системы удалённого мониторинга, беспроводный ML-инференс, проекты с ограниченным бюджетом
Arduino Nicla Vision
Лучше всего для: проектирования со сверхнизким энергопотреблением. Arduino Nicla Vision оптимизирована для устройств с питанием от батареи и систем непрерывного сенсорного наблюдения. Она идеально подходит для изучения проектирования со сверхнизким энергопотреблением, систем классификации изображений и обнаружения объектов, где время работы от батареи измеряется месяцами, а не часами.
Nicla Vision — пример профессиональных встраиваемых систем технического зрения, построенных на микроконтроллере STM32H7. Эта платформа показывает, как специализированная аппаратная архитектура позволяет выполнять сложный ML-инференс в жёстких ограничениях ресурсов.

Arduino Nicla Vision с модулем камеры
Архитектура процессора: Двухъядерный STM32H747: ARM Cortex-M7 и Cortex-M4 на частоте 480 МГц
Иерархия памяти: 1 МБ встроенной SRAM и 2 МБ флеш-памяти, а также 16 МБ внешней QSPI-флеш-памяти
Встроенные датчики: Камера GC2145, цифровой микрофон MP34DT05, 6-осевой IMU
Характеристики питания: Питание 3,3 В, оптимизированное для работы от батареи
Среда разработки: Поддерживаются Arduino IDE и OpenMV IDE со специализированными библиотеками компьютерного зрения. Для быстрого прототипирования доступен MicroPython, а для промышленного развёртывания — C/C++.
Область применения: Устройства с питанием от батареи, системы классификации изображений, обнаружение объектов, непрерывное сенсорное наблюдение
Grove Vision AI V2
Лучше всего для: аппаратного ускорения. Grove Vision AI V2 содержит специализированный аппаратный блок для нейронных сетей, благодаря которому производительность растёт на порядки. Плата идеально подходит для изучения систем промышленного контроля, аналитики видео в реальном времени и продвинутого обнаружения объектов, где нужен инференс с ускорением на NPU.
Grove Vision AI V2 включает выделенный блок нейронной обработки (NPU), который демонстрирует аппаратно ускоренный ML-инференс. Эта платформа показывает, как специализированные ИИ-процессоры дают прирост производительности на порядки по сравнению с реализациями, работающими только программно.

Grove Vision AI V2 с NPU
Архитектура процессора: ARM Cortex-M55 со встроенным NPU Ethos-U55
Иерархия памяти: 16 МБ внешней памяти для хранения модели и данных
Нейропроцессорный блок: Выделенный аппаратный ускоритель для ML-инференса
Интерфейс камеры: Стандартный разъём CSI, поддерживающий различные модули камер
Аудиовход: Встроенный цифровой микрофон
Среда разработки: Визуальная платформа программирования SenseCraft AI для разработки без написания кода, а также поддержка Arduino IDE для собственных приложений. Доступны как графическое программирование, так и традиционная разработка на C/C++.
Область применения: Системы промышленного контроля, аналитика видео в реальном времени, продвинутое обнаружение объектов, инференс с ускорением на NPU
Raspberry Pi (модели 4/5 и Zero 2W)
Лучше всего для: полноценных ML-фреймворков. Raspberry Pi соединяет встраиваемые системы и традиционные вычисления, предоставляя полноценную среду Linux для продвинутых ML-приложений. Она идеальна для изучения периферийных ИИ-шлюзов, сложных систем компьютерного зрения, развёртывания языковых моделей и мультимодальных ИИ-приложений, где нужны полные вычислительные возможности.
Семейство Raspberry Pi соединяет встраиваемые системы и традиционные вычисления: оно даёт полноценную среду Linux и при этом остаётся доступным для обучения. Эта платформа показывает, как увеличение вычислительных ресурсов открывает путь к сложным ML-приложениям.

Raspberry Pi 5 и Pi Zero 2W для сравнения
Архитектура процессора: ARM Cortex-A76 (Pi 5) или Cortex-A53 (Zero 2W)
Иерархия памяти: Оперативная память DDR4 объёмом 1–16 ГБ в зависимости от модели
Накопитель: Основное хранилище — microSD-карта, расширение через USB 3.0
Связь: Gigabit Ethernet, Wi-Fi, Bluetooth, несколько портов USB
Интерфейс камеры: Выделенный разъём CSI и поддержка USB-камер
Операционная система: Raspberry Pi OS на основе Debian (полноценный дистрибутив Linux)
Среда разработки: Полноценная среда разработки Linux с нативной поддержкой Python, C/C++ и JavaScript. Менеджеры пакетов (apt, pip) дают доступ к обширному набору ML-библиотек, включая TensorFlow, PyTorch и OpenCV.
Область применения: Периферийные ИИ-шлюзы, системы компьютерного зрения, развёртывание языковых моделей, мультимодальные ИИ-приложения
Начало работы
Чтобы начать работу с аппаратными наборами, которые используются в курсе, их можно купить напрямую у следующих официальных поставщиков:
- Seeed Studio — XIAOML Kit и модуль Grove Vision AI V2
- Магазин Arduino — Nicla Vision
- Raspberry Pi Foundation — платы и наборы
- DigiKey, Mouser, SparkFun — альтернативные дистрибьюторы с широким ассортиментом компонентов и наборов
Уточняйте на сайтах наличие учебных скидок, комплектов и доступность в вашем регионе. Большинство наборов продаются как стартовые комплекты, в которые входят плата и базовые аксессуары.
Обзор
Эти лабораторные работы дают уникальную возможность получить практический опыт работы с системами машинного обучения (ML). В отличие от работы с большими моделями, которым нужны ресурсы уровня дата-центра, эти упражнения позволяют напрямую работать с оборудованием и программным обеспечением с помощью TinyML. Такой практический подход даёт осязаемое понимание проблем и возможностей развёртывания ИИ, пусть и в крошечном масштабе. При этом принципы в целом те же, что и при работе с более крупными системами.

Nicla Vision. Источник: Arduino.
Где купить
Arduino Nicla Vision можно купить в официальном магазине Arduino:
- Arduino Store (~$95)
Предварительные требования
- Плата Nicla Vision: убедитесь, что плата у вас есть.
- USB-кабель: для подключения платы к компьютеру.
- Сеть: доступ в интернет для загрузки необходимого программного обеспечения.
Настройка
Упражнения
| Модальность | Задача | Описание | Ссылка |
|---|---|---|---|
| Зрение | Классификация изображений | Научитесь классифицировать изображения | Открыть |
| Зрение | Обнаружение объектов | Реализуйте обнаружение объектов | Открыть |
| Звук | Распознавание ключевых слов | Изучите системы распознавания голоса | Открыть |
| IMU | Классификация движения и обнаружение аномалий | Классифицируйте данные движения и находите аномалии | Открыть |
Классификация изображений

Промпт DALL·E 3: мультфильм в стиле 1950-х с компактным электронным устройством и модулем камеры на деревянном столе. На экране слева синие роботы, справа зелёные периквито. Светодиоды на устройстве показывают классы, а персонажи в ретро-одежде с интересом наблюдают.
Обзор
Начиная изучение встраиваемого машинного обучения, или TinyML, невозможно не заметить, как сильно компьютерное зрение (CV) и искусственный интеллект (ИИ) меняют нашу жизнь. Эти две взаимосвязанные дисциплины переопределяют то, что машины способны воспринимать и делать: от автономных автомобилей и робототехники до здравоохранения и систем наблюдения.
Всё чаще мы сталкиваемся с революцией в области искусственного интеллекта, где, как отмечает Gartner, Edge AI имеет очень большой потенциал влияния, и это уже сейчас!
В «яблочке» радара находится периферийное компьютерное зрение (Edge Computer Vision). Когда речь заходит о машинном обучении (ML) применительно к зрению, в первую очередь приходит на ум классификация изображений — своего рода «Hello World» для ML!
В этой лабораторной работе мы разберём проект компьютерного зрения, который использует свёрточные нейронные сети (CNN) для классификации изображений в реальном времени. Опираясь на экосистему TensorFlow, мы возьмём предобученную модель MobileNet и адаптируем её для развёртывания на периферийном устройстве. Основное внимание будет уделено оптимизации модели, чтобы она эффективно работала на аппаратном обеспечении с ограниченными ресурсами и при этом не теряла точности.
Мы применим такие приёмы, как квантизация и прунинг (удаление избыточных весов), чтобы снизить вычислительную нагрузку. К концу руководства у вас будет работающий прототип, который классифицирует изображения в реальном времени и работает на маломощной встраиваемой системе на базе платы Arduino Nicla Vision.
Компьютерное зрение
По своей сути компьютерное зрение позволяет машинам интерпретировать визуальные данные из окружающего мира и принимать решения на их основе, по сути повторяя возможности человеческой оптической системы. ИИ, в свою очередь, — более широкая область, которая включает машинное обучение, обработку естественного языка, робототехнику и другие технологии. Когда вы добавляете алгоритмы ИИ в проекты компьютерного зрения, возможности системы по пониманию и интерпретации визуальных стимулов и реакции на них заметно возрастают.
Когда речь заходит о проектах компьютерного зрения для встраиваемых устройств, на ум прежде всего приходят две задачи: классификация изображений и обнаружение объектов.
Обе модели можно реализовать на крошечных устройствах вроде Arduino Nicla Vision и использовать в реальных проектах. В этой главе мы разберём классификацию изображений.
Цель проекта классификации изображений
Первый шаг любого проекта ML — определить цель. В данном случае цель — обнаружить и классифицировать два конкретных объекта, присутствующих на одном изображении. Для проекта мы возьмём две небольшие игрушки: робота и маленького бразильского попугая (по имени Periquito). Также мы соберём изображения фона, на которых этих двух объектов нет.
Сбор данных
Как только мы определили цель проекта ML, следующий и самый важный шаг — сбор набора данных. Для съёмки изображений можно использовать:
- инструмент Web Serial Camera;
- Edge Impulse Studio;
- OpenMV IDE;
- смартфон.
Здесь мы будем использовать OpenMV IDE.
Сбор набора данных в OpenMV IDE
Сначала создадим на компьютере папку, в которую будут сохраняться данные, например «data». Затем в OpenMV IDE откроем Tools > Dataset Editor и выберем New Dataset, чтобы начать сбор набора данных:
IDE попросит открыть файл, в который будут сохраняться данные. Выберите созданную папку «data». Обратите внимание: на левой панели появятся новые значки.
Используя верхний значок (1), введите имя первого класса, например «periquito»:
Запустите скрипт dataset_capture_script.py и нажмите значок камеры (2), чтобы начать съёмку изображений:
Повторите ту же процедуру для остальных классов.
Рекомендуем около 50–60 изображений для каждой категории. Старайтесь снимать с разных ракурсов, на разном фоне и при разном освещении.
Сохраняемые изображения имеют размер кадра QVGA и формат цвета RGB565 (формат цветных пикселей).
Когда набор данных собран, закройте инструмент Dataset Editor через Tools > Dataset Editor.
В итоге на компьютере появится набор данных с тремя классами: periquito, robot и background.
Теперь вернитесь в Edge Impulse Studio и загрузите набор данных в созданный проект.
Обучение модели в Edge Impulse Studio
Для обучения модели мы воспользуемся Edge Impulse Studio. Войдите в аккаунт и создайте новый проект:
Здесь можно клонировать похожий проект: NICLA-Vision_Image_Classification.
Набор данных
Работая в EI Studio (или просто Studio), мы пройдём четыре основных шага, чтобы подготовить модель к использованию на плате Nicla Vision: Dataset (набор данных), Impulse (конвейер), Tests (тесты) и Deploy (развёртывание на периферийном устройстве, в данном случае на NiclaV).
Что касается набора данных, важно отметить: исходный набор, собранный в OpenMV IDE, будет разделён на обучающую, валидационную и тестовую выборки. Тестовая выборка с самого начала откладывается и используется только на этапе тестирования после обучения. Валидационная выборка применяется во время обучения.
EI Studio сам отберёт часть обучающих данных для валидации.
В Studio откройте вкладку Data acquisition, а в разделе UPLOAD DATA загрузите файлы выбранных категорий с компьютера:
Разделение исходного набора на обучающую и тестовую выборки оставьте Studio и выберите метку для этих данных:
Повторите процедуру для всех трёх классов.
Можно выбрать папку и загрузить все файлы сразу.
В итоге в Studio должны появиться ваши «сырые данные» (raw data):
Обратите внимание: при начале загрузки может появиться всплывающее окно с вопросом, создаёте ли вы проект обнаружения объектов (Object Detection). Выберите [NO].
Тип проекта всегда можно изменить в разделе Dashboard: One label per data item (классификация изображений):
При желании Studio позволяет просмотреть все данные проекта целиком. Можно очищать, проверять или менять метки, щёлкая по отдельным элементам. В нашем случае данные выглядят корректно.
Проектирование Impulse
На этом этапе нужно определить, как:
- предобработать данные: изменить размер отдельных изображений и выбрать
color depth(RGB или оттенки серого); - задать модель: в данном случае это
Transfer Learning (Images)— дообучение предобученной модели классификации изображений MobileNet V2 на наших данных. Этот метод хорошо работает даже с небольшими наборами изображений (в нашем случае около 150 изображений).
Перенос обучения (Transfer Learning) с MobileNet упрощает обучение модели. Это особенно полезно в условиях ограниченных ресурсов и при малом количестве размеченных данных. MobileNet, известная своей лёгкой архитектурой, — предобученная модель, которая уже выучила полезные признаки на большом наборе данных (ImageNet).
Используя уже выученные признаки, можно обучить новую модель для своей задачи меньшим количеством данных и вычислительных ресурсов, при этом получив конкурентную точность.
Такой подход заметно сокращает время обучения и вычислительные затраты, поэтому он идеален для быстрого прототипирования и развёртывания на встраиваемых устройствах, где важна эффективность.
Перейдите на вкладку Impulse Design и создайте impulse, задав размер изображения и сжатие к квадратной форме (без обрезки). Выберите блоки Image и Transfer Learning. Сохраните Impulse.
Предобработка изображений
Все входные изображения QVGA/RGB565 будут преобразованы в 27 648 признаков ().
Нажмите [Save parameters] и сгенерируйте все признаки:
Проектирование модели
В 2017 году Google представил MobileNetV1 — семейство универсальных нейронных сетей для компьютерного зрения, созданных с расчётом на мобильные устройства и поддерживающих классификацию, обнаружение и другие задачи. MobileNet — компактные модели с малой задержкой и низким энергопотреблением, параметры которых подобраны под ресурсные ограничения разных сценариев. В 2018 году Google выпустил MobileNetV2: Inverted Residuals and Linear Bottlenecks.
MobileNet V1 и MobileNet V2 ориентированы на эффективность в мобильных устройствах и во встраиваемом зрении, но различаются по сложности архитектуры и производительности. Обе используют разделяемые по глубине свёртки (depthwise separable convolutions), чтобы снизить вычислительные затраты, однако MobileNet V2 вводит инвертированные остаточные блоки (Inverted Residual Blocks) и линейные бутылочные горлышки (Linear Bottlenecks), чтобы повысить производительность. Эти новшества позволяют V2 выделять более сложные признаки меньшим количеством параметров, поэтому она вычислительно эффективнее и, как правило, точнее предшественницы. Кроме того, V2 использует нелинейную активацию в промежуточном слое расширения, а для слоя бутылочного горлышка по-прежнему применяет линейную активацию: такое решение, как показали исследования, помогает сохранять важную информацию по всей сети. MobileNet V2 предлагает оптимизированную архитектуру с более высокой точностью и эффективностью, её мы и будем использовать в проекте.
Хотя базовая архитектура MobileNet уже компактна и быстра, в некоторых задачах модель должна быть ещё меньше и быстрее. MobileNet вводит простой параметр (альфа) — множитель ширины (width multiplier), с помощью которого строятся меньшие и менее требовательные к вычислениям модели. Множитель ширины равномерно «утончает» сеть на каждом слое.
Edge Impulse Studio может использовать как MobileNetV1 (изображения ), так и V2 (изображения или ) с несколькими значениями (от 0,05 до 1,0). Например, наибольшая точность достигается у V2 с изображениями и . Однако есть компромисс: чем выше точность, тем больше памяти (около 1,3 МБ ОЗУ и 2,6 МБ ПЗУ) нужно для работы модели и тем выше задержка. Наименьший объём обеспечивает MobileNetV1 с (около 53,2 КБ ОЗУ и 101 КБ ПЗУ).
Для этого проекта мы используем MobileNetV2 96x96 0.1 (или 0.05) с ожидаемым расходом памяти 265,3 КБ ОЗУ. Этой модели должно хватить для Nicla Vision с 1 МБ SRAM. На вкладке Transfer Learning выберите эту модель:
Обучение модели
Ещё одна полезная техника в глубоком обучении — аугментация данных (Data Augmentation). Это метод повышения точности моделей машинного обучения за счёт создания дополнительных искусственных данных. Система аугментации вносит небольшие случайные изменения в обучающие данные в процессе обучения (например, отражает, обрезает или поворачивает изображения).
Заглянем «под капот» и посмотрим, как Edge Impulse реализует политику аугментации данных:
# Реализация политики аугментации данных
def augment_image(image, label):
# Случайно отражаем изображение по горизонтали
image = tf.image.random_flip_left_right(image)
# Увеличиваем размер изображения, затем случайно обрезаем его до
# исходных размеров
resize_factor = random.uniform(1, 1.2)
new_height = math.floor(resize_factor * INPUT_SHAPE[0])
new_width = math.floor(resize_factor * INPUT_SHAPE[1])
image = tf.image.resize_with_crop_or_pad(image, new_height,
new_width)
image = tf.image.random_crop(image, size=INPUT_SHAPE)
# Меняем яркость изображения
image = tf.image.random_brightness(image, max_delta=0.2)
return image, label
Такие вариации во время обучения помогают модели не срезать углы, «запоминая» поверхностные признаки в обучающих данных. Благодаря этому она лучше отражает глубинные закономерности набора данных.
Последний слой модели будет содержать 12 нейронов с dropout 15 % для предотвращения переобучения. Вот результат обучения:
Результат отличный: оценочная задержка составляет 77 мс, что должно обеспечить около 13 кадров в секунду (FPS) при инференсе.
Тестирование модели
Теперь возьмём набор данных, отложенный в начале проекта, и подадим его на вход обученной модели:
Результат снова отличный.
Развёртывание модели
Теперь обученную модель можно развернуть как прошивку (FW) и запустить через OpenMV IDE на MicroPython, либо развернуть как библиотеку C/C++ или Arduino.
Библиотека Arduino
Сначала развернём модель как библиотеку Arduino:
Библиотеку нужно установить в Arduino IDE как архив .zip, а затем запустить скетч nicla_vision_camera.ino, который находится в разделе Examples под именем библиотеки.
По умолчанию Arduino Nicla Vision выделяет 512 КБ ОЗУ для ядра M7 и ещё 244 КБ в адресном пространстве M4. В коде это выделение изменено на 288 КБ, чтобы модель гарантированно работала на устройстве (
malloc_addblock((void*)0x30000000, 288 * 1024);).
Результат хороший: измеренная задержка составила 86 мс.
Короткое видео с результатами инференса:
OpenMV
Обученную модель можно использовать с OpenMV двумя способами: как библиотеку и как прошивку (FW). При выборе прошивки Edge Impulse Studio генерирует оптимизированные модели, библиотеки и фреймворки, которые нужны для инференса. Рассмотрим этот вариант.
На вкладке Deploy выберите OpenMV Firmware и нажмите [Build].
На компьютере появится ZIP-файл. Откройте его:
Используйте инструмент Bootloader в OpenMV IDE, чтобы загрузить прошивку на плату (1):
Выберите нужный файл (.bin для Nicla-Vision):
Когда загрузка завершится, нажмите OK:
Если появится сообщение, что прошивка устарела, НЕ ОБНОВЛЯЙТЕ её. Выберите [NO].
Теперь откройте скрипт ei_image_classification.py, скачанный из Studio, и файл .bin для Nicla.
Запустите его. Направляя камеру на объекты, которые нужно классифицировать, вы увидите результат инференса в Serial Terminal.
Результат классификации появляется в Serial Terminal. Если результат трудно читать, добавьте в код новую строку с задержкой:
import time
while True:
...
time.sleep_ms(200) # Задержка 0,2 секунды
Изменение кода для вывода меток
Код от Edge Impulse можно изменить так, чтобы для проверки видеть результат инференса прямо на изображении в OpenMV IDE.
Загрузите код с GitHub или измените его, как показано ниже:
# Marcelo Rovai — NICLA Vision — классификация изображений
# Адаптировано из примера Edge Impulse — OpenMV Image Classification
# @24March25
import sensor
import time
import ml
sensor.reset() # Сброс и инициализация датчика.
# Формат пикселей RGB565 (или GRAYSCALE)
sensor.set_pixformat(sensor.RGB565)
# Размер кадра QVGA (320x240)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((240, 240)) # Окно 240x240.
sensor.skip_frames(time=2000) # Даём камере настроиться.
model = ml.Model("trained")#mobilenet, load_to_fb=True)
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot()
fps = clock.fps()
lat = clock.avg()
print("**********\nPrediction:")
# Объединяем метки и уверенность в список кортежей и сортируем его
# по значениям уверенности.
sorted_list = sorted(
zip(model.labels, model.predict([img])[0].flatten().tolist()),
key=lambda x: x[1], reverse=True
)
# Выводим только класс с наибольшей вероятностью
max_val = sorted_list[0][1]
max_lbl = sorted_list[0][0]
if max_val < 0.5:
max_lbl = 'uncertain'
print("{} with a prob of {:.2f}".format(max_lbl, max_val))
print("FPS: {:.2f} fps ==> latency: {:.0f} ms".format(fps, lat))
# Выводим метку с наибольшей вероятностью на просмотрщике изображения
img.draw_string(
10, 10,
max_lbl + "\n{:.2f}".format(max_val),
mono_space = False,
scale=3
)
time.sleep_ms(500) # Пауза 0.5 секунды
Вот результат:
Обратите внимание: задержка (136 мс) почти вдвое больше, чем при работе напрямую через Arduino IDE (86 мс). Это происходит потому, что мы используем IDE как интерфейс, а также из-за времени ожидания готовности камеры. Если запустить таймер непосредственно перед инференсом, задержка должна снизиться примерно до 70 мс.
При подключении к IDE NiclaV работает примерно вдвое медленнее. После отключения FPS должен вырасти.
Пост-обработка со светодиодами
Работая со встраиваемым машинным обучением, мы ищем устройства, которые могут непрерывно выполнять инференс и выдавать результат, действуя непосредственно в физическом мире, а не выводя результат на подключённый компьютер. Чтобы это смоделировать, мы будем зажигать разный светодиод для каждого возможного результата инференса.
Чтобы это сделать, загрузите код с GitHub или дополните предыдущий код светодиодами:
# Marcelo Rovai — NICLA Vision — классификация изображений со светодиодами
# Адаптировано из примера Edge Impulse — OpenMV Image Classification
# @24Aug23
import sensor, time, ml
from machine import LED
ledRed = LED("LED_RED")
ledGre = LED("LED_GREEN")
ledBlu = LED("LED_BLUE")
sensor.reset() # Сброс и инициализация датчика.
# Формат пикселей RGB565 (или GRAYSCALE)
sensor.set_pixformat(sensor.RGB565)
# Размер кадра QVGA (320x240)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((240, 240)) # Окно 240x240.
sensor.skip_frames(time=2000) # Даём камере настроиться.
model = ml.Model("trained")#mobilenet, load_to_fb=True)
ledRed.off()
ledGre.off()
ledBlu.off()
clock = time.clock()
def setLEDs(max_lbl):
if max_lbl == 'uncertain':
ledRed.on()
ledGre.off()
ledBlu.off()
if max_lbl == 'periquito':
ledRed.off()
ledGre.on()
ledBlu.off()
if max_lbl == 'robot':
ledRed.off()
ledGre.off()
ledBlu.on()
if max_lbl == 'background':
ledRed.off()
ledGre.off()
ledBlu.off()
while True:
img = sensor.snapshot()
clock.tick()
fps = clock.fps()
lat = clock.avg()
print("**********\nPrediction:")
sorted_list = sorted(
zip(model.labels, model.predict([img])[0].flatten().tolist()),
key=lambda x: x[1], reverse=True
)
# Выводим только класс с наибольшей вероятностью
max_val = sorted_list[0][1]
max_lbl = sorted_list[0][0]
if max_val < 0.5:
max_lbl = 'uncertain'
print("{} with a prob of {:.2f}".format(max_lbl, max_val))
print("FPS: {:.2f} fps ==> latency: {:.0f} ms".format(fps, lat))
# Выводим метку с наибольшей вероятностью на просмотрщике изображения
img.draw_string(
10, 10,
max_lbl + "\n{:.2f}".format(max_val),
mono_space = False,
scale=3
)
setLEDs(max_lbl)
time.sleep_ms(200) # Пауза 0.2 секунды
Теперь каждый раз, когда какой-либо класс получает результат выше 0,8, загорается соответствующий светодиод:
- Красный светодиод горит: Uncertain (ни один класс не выше 0,8);
- Зелёный светодиод горит: Periquito > 0,8;
- Синий светодиод горит: Robot > 0,8;
- Все светодиоды выключены: Background > 0,8.
Вот результат:
Подробнее:
Неофициальный сравнительный тест классификации изображений
Для встраиваемого машинного обучения (TinyML) можно использовать несколько отладочных плат. Чаще всего для задач компьютерного зрения с малым энергопотреблением применяют ESP32-CAM, Seeed XIAO ESP32S3 Sense, Arduino Nicla Vision и Arduino Portenta.
Воспользовавшись возможностью, мы развернули одну и ту же обученную модель на ESP-CAM, XIAO и Portenta (для последней модель обучили заново на изображениях в оттенках серого, чтобы она подходила к её камере). Вот результат, когда модели развёрнуты как библиотеки Arduino:
Итоги
Прежде чем завершить, учтите: компьютерное зрение — это больше, чем классификация изображений. Например, на основе зрения можно разрабатывать проекты периферийного машинного обучения во многих областях:
- Автономный транспорт: слияние датчиков, данные лидара и алгоритмы компьютерного зрения для навигации и принятия решений.
- Здравоохранение: автоматическая диагностика заболеваний по снимкам МРТ, рентгена и КТ.
- Розничная торговля: автоматические кассовые системы, которые распознают товары при проезде через сканер.
- Безопасность и наблюдение: распознавание лиц, обнаружение аномалий и отслеживание объектов в видеопотоке в реальном времени.
- Дополненная реальность: обнаружение и классификация объектов для наложения цифровой информации на реальный мир.
- Промышленная автоматизация: визуальный контроль продукции, предиктивное обслуживание, наведение роботов и дронов.
- Сельское хозяйство: мониторинг посевов с дронов и автоматизированная уборка урожая.
- Обработка естественного языка: создание подписей к изображениям и визуальные вопросно-ответные системы.
- Распознавание жестов: для игр, перевода жестового языка и взаимодействия человека с машиной.
- Рекомендательные системы: рекомендации товаров на основе изображений в электронной коммерции.
Ресурсы
Распознавание ключевых слов (KWS)

Промпт DALL·E 3: мультфильм в стиле 1950-х в старинной лаборатории аудиоисследований. В центре две учёные-женщины. Одна держит увеличительное стекло и внимательно рассматривает старую схему, другая делает записи. На деревянном столе несколько плат с датчиками, в том числе с микрофоном. За платами стоит компьютер с округлой задней панелью, на экране которого открыта Arduino IDE. В IDE видны код назначения выводов светодиодов и инференс модели машинного обучения для распознавания голосовых команд. В окне Serial Monitor выводятся распознанные команды «yes» и «no». Атмосфера комнаты ностальгическая: старинные лампы, классические инструменты анализа звука и графики БПФ и временных кривых.
Обзор
Мы уже исследовали плату Nicla Vision в приложениях классификации изображений и обнаружения объектов. Теперь переходим к голосовым приложениям: проект распознавания ключевых слов (Keyword Spotting, KWS).
Как рассказано в практическом руководстве «Инженерия признаков для KWS», распознавание ключевых слов (KWS) используется во многих системах распознавания речи и позволяет устройствам реагировать на определённые слова или фразы. Хотя эта технология лежит в основе популярных устройств вроде Google Assistant или Amazon Alexa, её можно реализовать и на маленьких маломощных устройствах. Это руководство проведёт вас через реализацию системы KWS с помощью TinyML на отладочной плате Nicla Vision с цифровым микрофоном.
Наша модель будет распознавать ключевые слова, которые могут будить устройство или запускать определённые действия, тем самым оживляя голосовое управление.
Как работает голосовой помощник?
Как уже говорилось, голосовые помощники на рынке, например Google Home или Amazon Echo Dot, реагируют на человека, только когда их «будят» определённые ключевые слова: «Hey Google» в первом случае и «Alexa» во втором.
Иначе говоря, распознавание голосовых команд основано на многоступенчатой модели, или каскадном обнаружении (Cascade Detection).
Этап 1: маленький микропроцессор внутри Echo Dot или Google Home постоянно слушает, ожидая, когда будет распознано ключевое слово, с помощью модели TinyML на периферии (приложение KWS).
Этап 2: данные отправляются в облако и обрабатываются более крупной моделью, только если приложение KWS на этапе 1 сработало.
Видео ниже показывает пример программирования Google Assistant на Raspberry Pi (этап 2) с устройством TinyML Arduino Nano 33 BLE (этап 1).
Чтобы изучить описанный проект Google Assistant, см. руководство: Building an Intelligent Voice Assistant From Scratch.
В этом проекте KWS мы сосредоточимся на этапе 1 (KWS, или Keyword Spotting): здесь мы используем Nicla Vision, у которой есть цифровой микрофон для распознавания ключевого слова.
Практический проект KWS
Схема ниже показывает, как должно работать итоговое приложение KWS (во время инференса):
Наше приложение KWS будет распознавать четыре класса звуков:
- YES (ключевое слово 1);
- NO (ключевое слово 2);
- NOISE (слов нет, есть только фоновый шум);
- UNKNOWN (смесь слов, отличных от YES и NO).
Для реальных проектов всегда полезно включать в набор, помимо ключевых слов, и другие звуки, например «Noise» (фон) и «Unknown» (неизвестные слова).
Рабочий процесс машинного обучения
Главный компонент приложения KWS — его модель. Поэтому нужно обучить модель на наших ключевых словах, шуме и других словах («unknown»):
Набор данных
Важнейший компонент любого рабочего процесса машинного обучения — набор данных. Выбрав конкретные ключевые слова (в нашем случае YES и NO), мы можем воспользоваться набором данных, который разработал Пит Уорден (Pete Warden): «Speech Commands: A Dataset for Limited-Vocabulary Speech Recognition». В этом наборе 35 ключевых слов (по более чем 1000 образцов каждого), например yes, no, stop и go. Для таких слов, как yes и no, можно получить по 1500 образцов.
Небольшую часть набора данных можно скачать из Edge Impulse Studio (готовый набор данных для распознавания ключевых слов). Он включает образцы четырёх классов, которые мы будем использовать в этом проекте: yes, no, noise и background. Для этого выполните следующие шаги:
- Скачайте набор ключевых слов.
- Распакуйте файл в удобное место.
Загрузка набора данных в Edge Impulse Studio
Создайте новый проект в Edge Impulse Studio (EIS) и выберите инструмент Upload Existing Data в разделе Data Acquisition. Выберите файлы для загрузки:
Задайте метку, выберите Automatically split between train and test и нажмите Upload data, чтобы загрузить данные в EIS. Повторите для всех классов.
Теперь набор данных появится в разделе Data acquisition. Обратите внимание: примерно 6000 образцов (по 1500 на каждый класс) разделены на обучающую (4800) и тестовую (1200) выборки.
Сбор дополнительных аудиоданных
Хотя у нас много данных из набора Пита, рекомендуется записать и слова, произнесённые нами. При работе с акселерометрами важно, чтобы набор данных был снят датчиком того же типа. Для звука это необязательно, потому что мы классифицируем, по сути, аудиоданные.
Ключевое различие между звуком и аудио — в типе энергии. Звук — механическое возмущение (продольные звуковые волны), которое распространяется в среде и вызывает колебания давления. Аудио — электрический (аналоговый или цифровой) сигнал, представляющий звук.
Когда мы произносим ключевое слово, звуковые волны нужно преобразовать в аудиоданные. Преобразование выполняется дискретизацией сигнала с микрофона с частотой 16 кГц и разрядностью 16 бит на отсчёт.
Поэтому подойдёт любое устройство, которое может создавать аудиоданные с такими базовыми параметрами (16 кГц / 16 бит). В качестве устройства можно использовать NiclaV, компьютер или даже телефон.
Использование NiclaV и Edge Impulse Studio
Как мы узнали в главе «Настройка Nicla Vision», EIS официально поддерживает Nicla Vision, что упрощает сбор данных с её датчиков, в том числе с микрофона. Поэтому создайте новый проект в EIS и подключите к нему Nicla, выполнив следующие шаги:
-
Скачайте последнюю версию прошивки EIS и распакуйте её.
-
Откройте ZIP-архив на компьютере и выберите загрузчик, соответствующий вашей ОС:
-
Переведите NiclaV в режим загрузки, дважды нажав кнопку reset.
-
Загрузите двоичный файл arduino-nicla-vision.bin на плату, запустив пакетный файл для своей ОС.
Откройте свой проект в EIS и на вкладке Data Acquisition выберите WebUSB. Откроется окно; выберите вариант, в котором видно, что Nicla is paired, и нажмите [Connect].
В разделе Collect Data на вкладке Data Acquisition можно выбрать, данные какого датчика собирать. Выберите Built-in microphone, задайте label (например, yes), частоту дискретизации Frequency [16000 Гц] и длительность образца Sample length (in milliseconds), например [10 с]. Нажмите Start sampling.
Данные набора Пита имеют длительность 1 с, а записанные образцы — по 10 с, и их нужно разбить на отрезки по 1 с. Нажмите три точки после названия образца и выберите Split sample.
Откроется окно инструмента разбиения (Split).
В инструменте разбейте данные на записи по 1 секунде (1000 мс). При необходимости добавьте или удалите сегменты. Эту процедуру нужно повторить для всех новых образцов.
Использование смартфона и EI Studio
Аудиоданные можно также записывать на ПК или смартфон с частотой дискретизации 16 кГц и разрядностью 16 бит.
Откройте Devices, отсканируйте QR Code телефоном и перейдите по ссылке. В браузере откроется приложение для сбора данных. Выберите Collecting Audio, затем задайте Label, длительность записи Length и Category.
Повторите ту же процедуру, что и с NiclaV.
Для записи звука можно использовать любое приложение, например Audacity, при условии что образцы записаны с частотой 16 кГц и разрядностью 16 бит.
Создание Impulse (предобработка и определение модели)
Impulse получает сырые данные, с помощью обработки сигнала извлекает признаки, а затем использует блок обучения для классификации новых данных.
Проектирование Impulse
Сначала мы возьмём точки данных с окном в 1 секунду, применим аугментацию и будем сдвигать окно с интервалом 500 мс. Обратите внимание, что включён параметр zero-pad data (дополнение нулями). Он важен: образцы короче 1 секунды дополняются «нулями» (в некоторых случаях образцы могут быть короче окна 1000 мс в инструменте разбиения — чтобы избежать шумов и выбросов).
Каждый образец аудио длиной 1 секунда предобрабатывается и преобразуется в изображение (например, ). Как обсуждалось в практическом руководстве «Инженерия признаков для KWS», мы будем использовать Audio (MFCC), который извлекает признаки из аудиосигналов с помощью мел-частотных кепстральных коэффициентов, хорошо подходящих для человеческого голоса, то есть для нашей задачи.
Затем выберем блок Classification, чтобы построить модель с нуля на основе свёрточной нейронной сети (CNN).
Вместо этого можно использовать блок
Transfer Learning (Keyword Spotting), который дообучает предобученную модель распознавания ключевых слов на ваших данных. Такой подход хорошо работает с относительно небольшими наборами ключевых слов.
Предобработка (MFCC)
Следующий шаг — создать признаки для обучения на следующем этапе.
Можно оставить значения параметров по умолчанию, но мы воспользуемся опцией DSP Autotune parameters.
Мы возьмём Raw features (наши аудиоданные длительностью 1 секунда с частотой 16 кГц) и с помощью блока обработки MFCC вычислим Processed features. На каждые 16 000 сырых признаков (16 000 1 секунда) мы получим 637 обработанных признаков .
Результат показывает, что предобработка данных потребовала мало памяти (16 КБ) и имеет задержку 34 мс, что отлично. Например, на Arduino Nano (Cortex-M4f @ 64 МГц) та же предобработка займёт около 480 мс. Выбранные параметры, например FFT length [512], существенно влияют на задержку.
Теперь нажмём Save parameters и перейдём на вкладку Generated features, где будут созданы реальные признаки. Используя UMAP, метод снижения размерности, Feature explorer показывает, как признаки распределены на двумерном графике.
Результат выглядит хорошо: признаки yes (красные) и no (синие) визуально чётко разделены. Признаки unknown расположены ближе к области no, чем к yes. Это говорит о том, что ключевое слово no склонно давать больше ложных срабатываний.
Заглядываем под капот
Чтобы лучше понять, как обрабатывается сырой звук, изучите главу «Инженерия признаков для KWS». Поэкспериментировать с генерацией признаков MFCC можно, скачав блокнот с GitHub или открыв его в Colab.
Проектирование модели и обучение
Мы используем простую свёрточную нейронную сеть (CNN), протестированную с одномерными (1D) и двумерными (2D) свёртками. Базовая архитектура содержит два блока «свёртка + MaxPooling» (с [8] и [16] фильтрами соответственно) и Dropout [0,25] для 1D и [0,5] для 2D. Последний слой после Flatten содержит [4] нейрона — по одному на каждый класс:
Гиперпараметры: Learning Rate — [0,005], модель обучается за [100] эпох. Также включим метод аугментации данных на основе SpecAugment. Модели 1D и 2D обучались с одинаковыми гиперпараметрами. Архитектура 1D дала лучший общий результат (точность 90,5 % против 88 % у 2D), поэтому мы используем 1D.
Свёртки 1D эффективнее, потому что требуют меньше параметров, чем 2D, и поэтому лучше подходят для сред с ограниченными ресурсами.
Также стоит посмотреть на матрицу ошибок для 1D. F1-мера для yes составляет 95 %, для no — 91 %. Этого и следовало ожидать по результатам Feature Explorer (no и unknown находятся на близком расстоянии). Чтобы улучшить результат, можно внимательно изучить образцы с ошибками.
Послушайте образцы, которые распознаны неверно. Например, для yes большинство ошибок связано с тем, что yes произносилось как «yeh». Вы можете записать дополнительные образцы и переобучить модель.
Заглядываем под капот
Если вы хотите понять, что происходит «под капотом», можно скачать предобработанный набор данных (MFCC training data) на вкладке Dashboard и запустить этот блокнот Jupyter — можно экспериментировать с кодом или открыть его в Colab. Например, можно проанализировать точность по эпохам:
Тестирование
Тестирование модели на данных, отложенных для проверки (тестовых данных), дало точность примерно 76 %.
Если посмотреть F1-меру, для YES получилось 0,90 — отличный результат, ведь мы ожидаем использовать это слово как основной «триггер» в проекте KWS. Худший результат (0,70) — у UNKNOWN, и это нормально.
Для NO получилось 0,72. Этого и следовало ожидать, но чтобы улучшить результат, можно перенести неверно классифицированные образцы в обучающий набор и повторить обучение.
Классификация в реальном времени
Можно перейти к следующему шагу проекта, но учтите, что возможна классификация Live Classification с помощью NiclaV или смартфона: она позволяет захватывать образцы в реальном времени и тестировать обученную модель до её развёртывания на устройстве.
Развёртывание и инференс
EIS упакует все нужные библиотеки, функции предобработки и обученные модели и загрузит их на ваш компьютер. Перейдите в раздел Deployment, выберите Arduino Library, внизу выберите Quantized (Int8) и нажмите Build.
Когда вы нажмёте Build, будет создан ZIP-файл и скачан на компьютер. В Arduino IDE откройте вкладку Sketch, выберите Add .ZIP Library и укажите ZIP-файл, скачанный из EIS:
Теперь пора провести настоящий тест. Мы будем делать инференс, полностью отключившись от EIS. Используем пример кода NiclaV, созданный при развёртывании библиотеки Arduino.
В Arduino IDE откройте вкладку File/Examples, найдите свой проект и выберите nicla-vision/nicla-vision_microphone (или nicla-vision_microphone_continuous):
Дважды нажмите кнопку reset, чтобы перевести NiclaV в режим загрузки, загрузите скетч на плату и протестируйте реальный инференс:
Постобработка
Теперь, когда мы знаем, что модель работает и распознаёт наши ключевые слова, изменим код, чтобы увидеть результат на NiclaV полностью автономно — без подключения к ПК, от батареи, пауэрбанка или отдельного источника 5 В.
Идея такая: когда распознано ключевое слово YES, загорается зелёный светодиод; при NO — красный; если звучит UNKNOWN — синий; при наличии шума (без ключевого слова) все светодиоды выключены.
Изменим один из примеров кода. Сделаем это на примере nicla-vision_microphone_continuous.
Начнём с инициализации светодиодов:
...
void setup()
{
// Когда отладка кода завершена, можно закомментировать или удалить
// часть кода, связанную с Serial
Serial.begin(115200);
while (!Serial);
Serial.println("Inferencing - Nicla Vision KWS with LEDs");
// Выводы встроенных RGB-светодиодов Arduino NiclaV
pinMode(LEDR, OUTPUT);
pinMode(LEDG, OUTPUT);
pinMode(LEDB, OUTPUT);
// По умолчанию светодиоды выключены.
// Примечание: RGB-светодиоды Arduino Nicla Vision
// горят, когда вывод в состоянии LOW, и гаснут, когда в HIGH.
digitalWrite(LEDR, HIGH);
digitalWrite(LEDG, HIGH);
digitalWrite(LEDB, HIGH);
...
}
Создайте две функции. Первая, turn_off_leds(), выключает все RGB-светодиоды:
/*
* @brief Функция turn_off_leds — выключает все RGB-светодиоды
*/
void turn_off_leds(){
digitalWrite(LEDR, HIGH);
digitalWrite(LEDG, HIGH);
digitalWrite(LEDB, HIGH);
}
Вторая функция, turn_on_led(), включает RGB-светодиоды в соответствии с наиболее вероятным результатом классификатора:
/*
* @brief Функция turn_on_leds — включает RGB-светодиоды
* @param[in] pred_index
* no: [0] ==> горит красный
* noise: [1] ==> все выключены
* unknown: [2] ==> горит синий
* yes: [3] ==> горит зелёный
*/
void turn_on_leds(int pred_index) {
switch (pred_index)
{
case 0:
turn_off_leds();
digitalWrite(LEDR, LOW);
break;
case 1:
turn_off_leds();
break;
case 2:
turn_off_leds();
digitalWrite(LEDB, LOW);
break;
case 3:
turn_off_leds();
digitalWrite(LEDG, LOW);
break;
}
}
И измените фрагмент // print the predictions в функции loop():
...
if (++print_results >= (EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW)) {
// вывод предсказаний
ei_printf("Predictions ");
ei_printf("(DSP: %d ms., Classification: %d ms., "
"Anomaly: %d ms.)",
result.timing.dsp, result.timing.classification,
result.timing.anomaly);
ei_printf(": \n");
int pred_index = 0; // Инициализация pred_index
float pred_value = 0; // Инициализация pred_value
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
if (result.classification[ix].value > pred_value){
pred_index = ix;
pred_value = result.classification[ix].value;
}
// ei_printf(" %s: ",
// result.classification[ix].label);
// ei_printf_float(result.classification[ix].value);
// ei_printf("\n");
}
ei_printf(" PREDICTION: ==> %s with probability %.2f\n",
result.classification[pred_index].label,
pred_value);
turn_on_leds (pred_index);
#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf(" anomaly score: ");
ei_printf_float(result.anomaly);
ei_printf("\n");
#endif
print_results = 0;
}
}
...
Полный код можно найти в репозитории проекта на GitHub.
Загрузите скетч на плату и протестируйте реальный инференс. Идея такая: зелёный светодиод горит при распознавании ключевого слова YES, красный — при NO, а любое другое слово включает синий. Все светодиоды должны быть выключены при тишине или фоновом шуме. Учтите, что тот же приём может «запускать» внешнее устройство для выполнения нужного действия вместо включения светодиода, как мы видели во введении.
Итоги
Блокноты и код, использованные в этой практической работе, вы найдёте в репозитории GitHub.
Прежде чем завершить, учтите: классификация звуков — это больше, чем голос. Например, проекты TinyML со звуком можно развивать в самых разных областях:
- Безопасность (обнаружение битого стекла, выстрелов);
- Промышленность (обнаружение аномалий);
- Медицина (храп, кашель, заболевания лёгких);
- Природа (контроль ульев, звуки насекомых, борьба с браконьерством).
Ресурсы
Классификация движения и обнаружение аномалий

Промпт DALL·E 3: мультфильм в стиле 1950-х, изображающий лабораторию исследования движения. В центре комнаты стоит имитация контейнера для перевозки грузов на грузовиках, лодках и погрузчиках. Контейнер детализирован заклёпками и маркировкой, характерной для промышленных грузовых ящиков. Вокруг него — старинное оборудование: осциллограф, различные массивы датчиков и большие бумажные рулоны с записанными данными. На стенах висят учебные плакаты о безопасности перевозок и логистике. Общая атмосфера комнаты ностальгическая и научная, с оттенком промышленного стиля.
Обзор
Транспорт — основа мировой торговли. Миллионы контейнеров ежедневно перевозятся кораблями, грузовиками и поездами в города по всему миру. Обеспечить безопасную и эффективную перевозку этих контейнеров — громадная задача, которая требует современных технологий, и TinyML — одна из них.
В этой практической работе мы решим реальные задачи, связанные с транспортом. Мы создадим систему классификации движения и обнаружения аномалий на базе платы Arduino Nicla Vision, Arduino IDE и Edge Impulse Studio. Этот проект поможет понять, как контейнеры испытывают различные силы и движения на разных этапах перевозки: наземная и морская перевозка, вертикальное перемещение погрузчиком и периоды простоя на складе.
Цели обучения
- Настройка платы Arduino Nicla Vision
- Сбор и предобработка данных
- Построение модели классификации движения
- Реализация обнаружения аномалий
- Тестирование и анализ в реальных условиях
К концу этого руководства у вас будет работающий прототип, который классифицирует разные виды движения и обнаруживает аномалии при перевозке контейнеров. Этот опыт станет ступенькой к более сложным проектам быстро развивающейся области TinyML, связанным с вибрацией.
Установка и проверка IMU
В этом проекте мы будем использовать акселерометр. Как обсуждалось в практической работе «Настройка Nicla Vision», на плате Nicla Vision есть встроенный 6-осевой IMU: 3D-гироскоп и 3D-акселерометр LSM6DSOX. Проверьте, установлена ли библиотека IMU LSM6DSOX. Если нет, установите её.
Затем откройте Examples > Arduino_LSM6DSOX > SimpleAccelerometer и запустите тест акселерометра. Работоспособность можно проверить через Serial Monitor или Plotter в IDE. Значения выражены в g (земное ускорение) с диапазоном по умолчанию ±4 g:
Определение частоты дискретизации
Выбор подходящей частоты дискретизации важен для того, чтобы зафиксировать характеристики движения, которые мы изучаем. Теорема Найквиста — Шеннона утверждает, что частота дискретизации должна как минимум вдвое превышать самую высокую частотную составляющую сигнала, чтобы его можно было правильно восстановить. В контексте классификации движения и обнаружения аномалий при перевозке выбор частоты дискретизации зависит от нескольких факторов:
-
Характер движения. Разные виды транспорта (наземный, морской и т. д.) могут включать разные диапазоны частот движения. Более быстрые движения могут требовать более высокой частоты дискретизации.
-
Аппаратные ограничения. Плата Arduino Nicla Vision и связанные с ней датчики могут ограничивать скорость дискретизации данных.
-
Вычислительные ресурсы. Более высокая частота дискретизации создаёт больше данных, что может быть ресурсоёмко, особенно в среде TinyML.
-
Время работы от батареи. Более высокая частота дискретизации потребляет больше энергии. Если система питается от батареи, это важный фактор.
-
Хранение данных. Более частая дискретизация требует больше места для хранения — ещё одно важное соображение для встраиваемых систем с ограниченной памятью.
Во многих задачах распознавания физической активности человека обычно используют частоты дискретизации около 50–100 Гц. Поскольку мы моделируем сценарии перевозки, которые обычно не являются высокочастотными событиями, диапазон 50–100 Гц может быть разумной отправной точкой.
Определим скетч, который позволит собирать данные с заданной частотой дискретизации (например, 50 Гц):
/*
* Основано на примере Edge Impulse Data Forwarder (Arduino)
- https://docs.edgeimpulse.com/docs/cli-data-forwarder
* Разработано M.Rovai @11May23
*/
/* Подключение библиотек ----------------------------- */
#include <Arduino_LSM6DSOX.h>
/* Константы ----------------------------------------- */
#define CONVERT_G_TO_MS2 9.80665f
#define FREQUENCY_HZ 50
#define INTERVAL_MS (1000 / (FREQUENCY_HZ + 1))
static unsigned long last_interval_ms = 0;
float x, y, z;
void setup() {
Serial.begin(9600);
while (!Serial);
if (!IMU.begin()) {
Serial.println("Failed to initialize IMU!");
while (1);
}
}
void loop() {
if (millis() > last_interval_ms + INTERVAL_MS) {
last_interval_ms = millis();
if (IMU.accelerationAvailable()) {
// Считываем сырые измерения ускорения с устройства
IMU.readAcceleration(x, y, z);
// переводим в м/с2
float ax_m_s2 = x * CONVERT_G_TO_MS2;
float ay_m_s2 = y * CONVERT_G_TO_MS2;
float az_m_s2 = z * CONVERT_G_TO_MS2;
Serial.print(ax_m_s2);
Serial.print("\t");
Serial.print(ay_m_s2);
Serial.print("\t");
Serial.println(az_m_s2);
}
}
}
Загрузите скетч и посмотрите Serial Monitor: мы видим, что собираем 50 отсчётов в секунду.
Обратите внимание: когда плата Nicla лежит на столе (камерой вниз), ось показывает около 9,8 м/с — ожидаемое ускорение свободного падения.
Кейс: имитация перевозки контейнера
Чтобы сделать руководство понятнее и ближе к практике, мы смоделируем перевозку контейнера (точнее, пакета) в разных сценариях. С помощью встроенного акселерометра платы Arduino Nicla Vision мы будем записывать данные о движении, вручную имитируя условия:
- Наземная перевозка (автомобилем или поездом);
- Морская перевозка;
- Вертикальное перемещение погрузчиком (fork-lift);
- Стационарный период простоя на складе.
Из приведённых изображений для нашей симуляции следует, что преимущественно горизонтальные движения (по осям или ) относятся к классу «Наземный» (Terrestrial), вертикальные движения (по оси ) — к классу «Погрузчик» (Lift), отсутствие движения — к классу «Простой» (Idle), а движение по всем трём осям — к классу «Морской» (Maritime).
Сбор данных
Для сбора данных есть несколько вариантов. В реальной ситуации устройство можно подключить непосредственно к одному контейнеру, а данные записывать в файл (например, .CSV) на SD-карту (через SPI) или в офлайн-хранилище на компьютере. Данные также можно передавать удалённо на ближайшее устройство, например на телефон, по Bluetooth (как сделано в этом проекте: Sensor DataLogger). Когда набор данных собран и сохранён в файле .CSV, его можно загрузить в Studio с помощью CSV Wizard.
В этом видео вы можете узнать о других способах отправки данных в Edge Impulse Studio.
Подключение устройства к Edge Impulse
Мы подключим Nicla непосредственно к Edge Impulse Studio, которая также будет использоваться для предобработки данных, обучения модели, тестирования и развёртывания. Для этого есть два варианта:
- Скачать последнюю прошивку и подключить её напрямую в разделе
Data Collection. - Использовать CLI Data Forwarder, чтобы собирать данные датчика и отправлять их в Studio.
Первый вариант проще, как мы видели в практической работе «Настройка Nicla Vision», но второй даёт больше гибкости при сборе данных, например при задании частоты дискретизации. Выберем второй вариант.
Создайте новый проект в Edge Impulse Studio (EIS) и подключите к нему Nicla, выполнив следующие шаги:
- Установите Edge Impulse CLI и Node.js на компьютер.
- Загрузите скетч для сбора данных (тот, что обсуждался выше в этом руководстве).
- Используйте CLI Data Forwarder, чтобы собирать данные акселерометра Nicla и отправлять их в Studio, как показано на схеме:
Запустите CLI Data Forwarder в терминале. Если делаете это впервые, введите команду:
$ edge-impulse-data-forwarder --clean
Затем введите учётные данные EI, выберите проект, переменные (например, accX, accY и accZ) и имя устройства (например, NiclaV):
Откройте раздел Devices в своём проекте EI и проверьте, что устройство подключено (точка должна быть зелёной):
Вы можете клонировать проект, созданный для этой практической работы: NICLA Vision Movement Classification.
Сбор данных в Studio
В разделе Data Acquisition вы должны увидеть, что ваша плата [NiclaV] подключена. Датчик доступен: [sensor with 3 axes (accX, accY, accZ)], с частотой дискретизации [50 Hz]. Studio предлагает длительность образца [10000] мс (10 с). Осталось задать метку образца. Начнём с [terrestrial]:
Наземная перевозка (паллеты в грузовике или поезде): движение по горизонтали. Нажмите [Start Sample] и перемещайте устройство горизонтально, сохраняя одно направление над столом. Через 10 с данные загрузятся в Studio. Вот как выглядит записанный образец:
Как и ожидалось, движение зафиксировано в основном по оси (зелёная). Синяя линия — ось , около −10 м/с (камера Nicla смотрит вверх).
Как обсуждалось выше, нужно собрать данные для всех четырёх классов перевозки. Представьте, что у вас есть контейнер со встроенным акселерометром, который оказывается в следующих ситуациях:
Морская перевозка (паллеты на лодках в бушующем океане). Движение фиксируется по всем трём осям:
Погрузчик (паллеты, которые вертикально поднимает погрузчик). Движение фиксируется только по оси :
Простой (паллеты на складе). Акселерометр не фиксирует движения:
Например, можно записать по 2 минуты (двенадцать образцов по 10 секунд) для каждого из четырёх классов — всего 8 минут данных. Через меню three dots после каждого образца выберите 2 из них и отложите для тестовой выборки. Или можно воспользоваться автоматическим инструментом Train/Test Split в разделе Danger Zone на вкладке Dashboard. Ниже показан итоговый набор данных:
Когда набор данных собран, его можно детальнее изучить с помощью Data Explorer — визуального инструмента для поиска выбросов и неверно размеченных данных (с его помощью их можно исправить). Data Explorer сначала извлекает осмысленные признаки из данных (применяя обработку сигналов и встраивания нейронной сети), а затем использует алгоритм снижения размерности, например PCA или t-SNE, чтобы отобразить эти признаки в двумерное пространство. Так вы получаете обзор всего набора данных одним взглядом.
В нашем случае набор данных выглядит корректно (хорошее разделение классов). Однако PCA показывает возможные проблемы между классами «морской» (зелёный) и «погрузчик» (оранжевый). Это ожидаемо: на лодке движение иногда может быть только «вертикальным».
Проектирование Impulse
Следующий шаг — определение нашего Impulse. Он получает сырые данные, с помощью обработки сигналов извлекает признаки и передаёт их как входной тензор в блок обучения для классификации новых данных. Перейдите в Impulse Design и выберите Create Impulse. Studio предложит базовую конструкцию. Добавим также второй блок обучения для Anomaly Detection.
Эта вторая модель использует K-means. Если представить, что известные классы — это кластеры, то любой образец, который не вписывается ни в один из них, может быть выбросом, аномалией: например, контейнер, упавший с корабля в океан или сорвавшийся с погрузчика.
Частота дискретизации должна определиться автоматически; если нет — введите её: [50] Гц. Studio предлагает размер окна 2 секунды ([2000] мс) со скользящим окном шагом [20] мс. На этом шаге мы предобрабатываем захваченные данные (временные ряды), создавая табличный набор признаков, который будет входом для нейронной сети-классификатора (DNN) и модели обнаружения аномалий (K-Means), как показано ниже:
Разберём эти шаги и параметры подробнее, чтобы понять, что мы делаем.
Обзор предобработки данных
Предобработка данных — это извлечение признаков из набора данных, снятого акселерометром: обработка и анализ сырых данных. Акселерометры измеряют ускорение объекта по одной или нескольким осям (обычно по трём, обозначаемым , и ). Эти измерения позволяют понять различные аспекты движения объекта: характер перемещений и вибрации.
Сырые данные акселерометра могут быть зашумлёнными и содержать ошибки или лишнюю информацию. Шаги предобработки, например фильтрация и нормализация, очищают и стандартизируют данные, делая их более подходящими для извлечения признаков. В нашем случае данные нужно разбить на небольшие сегменты, или окна. Это помогает сосредоточиться на конкретных событиях или активности в наборе данных, делая извлечение признаков более управляемым и содержательным. Выбор размера окна и перекрытия (шага окна) зависит от приложения и частоты интересующих событий. Как правило, стоит стремиться захватить пару «циклов данных».
При частоте дискретизации (SR) 50 Гц и размере окна 2 секунды мы получим 100 отсчётов на ось, или 300 всего (3 оси 2 секунды 50 отсчётов). Окно будем сдвигать каждые 200 мс, создавая более крупный набор данных, где каждый экземпляр содержит 300 сырых признаков.
После предобработки и сегментации данных можно извлекать признаки, описывающие характеристики движения. Типичные признаки, извлекаемые из данных акселерометра:
- Признаки временной области описывают статистические свойства данных внутри каждого сегмента: среднее, медиану, стандартное отклонение, асимметрию (skewness), эксцесс (kurtosis) и частоту переходов через ноль (zero-crossing rate).
- Признаки частотной области получают, преобразуя данные в частотную область с помощью таких методов, как быстрое преобразование Фурье (FFT). К типичным признакам частотной области относятся спектр мощности, спектральная энергия, доминирующие частоты (амплитуда и частота) и спектральная энтропия.
- Признаки временно-частотной области объединяют информацию о времени и частоте, например кратковременное преобразование Фурье (STFT) или дискретное вейвлет-преобразование (DWT). Они дают более детальное представление о том, как частотное содержание сигнала меняется во времени.
Во многих случаях количество извлечённых признаков может быть большим, что приводит к переобучению или увеличению вычислительной сложности. Методы отбора признаков, такие как взаимная информация, корреляционные методы или метод главных компонент (PCA), помогают выявить наиболее значимые признаки для конкретного приложения и уменьшить размерность набора данных. Studio может помочь с расчётом важности признаков.
Спектральные признаки в EI Studio
Предобработка данных — сложная область для встраиваемого машинного обучения, но Edge Impulse помогает справиться с ней благодаря шагу цифровой обработки сигналов (DSP) и, в частности, блоку спектральных признаков (Spectral Features Block).
В Studio собранный сырой набор данных становится входом блока спектрального анализа, который отлично подходит для анализа повторяющихся движений, например данных акселерометров. Этот блок выполняет DSP (цифровую обработку сигналов) и извлекает признаки, такие как FFT или вейвлеты.
Для нашего проекта, поскольку временной сигнал непрерывен, стоит использовать FFT с длиной, например, [32].
Статистические признаки временной области для каждой оси (канала):
- RMS (среднеквадратичное значение): 1 признак;
- Skewness (асимметрия): 1 признак;
- Kurtosis (эксцесс): 1 признак.
Спектральные признаки частотной области для каждой оси (канала):
- Spectral Power (спектральная мощность): 16 признаков (длина FFT / 2);
- Skewness (асимметрия): 1 признак;
- Kurtosis (эксцесс): 1 признак.
Таким образом, для длины FFT 32 точки блок спектрального анализа выдаёт 21 признак на ось (всего 63 признака).
Подробнее о том, как вычисляется каждый признак, можно узнать из блокнота Edge Impulse — Spectral Features Block Analysis, из статьи TinyML under the hood: Spectral Analysis или открыв блокнот прямо в Google Colab.
Генерация признаков
Когда мы поняли, что делает предобработка, пора довести дело до конца. Возьмём сырые данные (временные ряды) и преобразуем их в табличные данные. Для этого перейдите в раздел Spectral Features на вкладке Parameters, задайте основные параметры, как обсуждалось в предыдущем разделе ([FFT] с [32] точками), и выберите [Save Parameters]:
В верхнем меню выберите опцию Generate Features и кнопку Generate Features. Каждое окно данных длительностью 2 секунды будет преобразовано в одну точку данных из 63 признаков.
Feature Explorer покажет эти данные в двумерном виде с помощью UMAP. Uniform Manifold Approximation and Projection (UMAP) — метод снижения размерности, который используется для визуализации, как и t-SNE, но применим и для общего нелинейного снижения размерности.
Визуализация позволяет убедиться, что после генерации признаков классы остаются хорошо разделимыми, что говорит о том, что классификатор должен работать хорошо. При желании можно проанализировать, насколько важен каждый признак для одного класса по сравнению с другими.
Обучение моделей
Наш классификатор будет полносвязной нейронной сетью (DNN): входной слой из 63 нейронов, два скрытых слоя с 20 и 10 нейронами и выходной слой из четырёх нейронов (по одному на каждый класс), как показано здесь:
Гиперпараметры: скорость обучения (Learning Rate) [0.005], размер батча (Batch size) [32], доля данных для валидации [20] % и [30] эпох. После обучения точность составляет 98,5 %. Затраты памяти и задержка при этом незначительны.
Для обнаружения аномалий мы выберем предложенные признаки — те самые наиболее важные признаки, отобранные при извлечении, плюс RMS по оси accZ. Количество кластеров составит [32], как предлагает Studio:
Тестирование
Проверить поведение модели на неизвестных данных можно, используя 20 % данных, отложенных при сборе. Результат составил почти 95 %, что хорошо. Результаты всегда можно улучшить, например разобравшись, что пошло не так в одном из неверных ответов. Если это уникальная ситуация, её можно добавить в обучающий набор и повторить обучение.
По умолчанию минимальный порог для неопределённого результата — [0.6] для классификации и [0.3] для аномалий. Поскольку классов четыре (их сумма должна быть равна 1,0), можно также задать более низкий порог, при котором класс считается допустимым (например, 0,4). Пороги уверенности можно задать через пункт Set confidence thresholds в меню three dots рядом с кнопкой Classify all.
Также можно выполнить классификацию в реальном времени с помощью вашего устройства (оно должно оставаться подключённым к Studio).
Учтите, что в этом случае вы будете записывать реальные данные с устройства и загружать их в Studio, где инференс выполняется с помощью обученной модели (но модель НЕ находится на вашем устройстве).
Развёртывание
Настало время развернуть блок предобработки и обученную модель на Nicla. Studio упакует все нужные библиотеки, функции предобработки и обученные модели и загрузит их на компьютер. Выберите опцию Arduino Library, внизу выберите Quantized (Int8) или Unoptimized (float32) и нажмите [Build]. Будет создан ZIP-файл, который скачается на компьютер.
В Arduino IDE откройте вкладку Sketch, выберите Add.ZIP Library и укажите ZIP-файл, скачанный из Studio. В терминале IDE появится сообщение: Library installed (библиотека установлена).
Инференс
Теперь пора провести настоящий тест. Инференс будем выполнять, полностью отключившись от Studio. Изменим один из примеров кода, созданных при развёртывании библиотеки Arduino.
В Arduino IDE откройте вкладку File/Examples, найдите свой проект и в примерах выберите Nicla_vision_fusion:
Обратите внимание: код, созданный Edge Impulse, использует подход слияния данных датчиков (sensor fusion), где применяются IMU (акселерометр и гироскоп) и ToF-датчик. В начале кода находятся библиотеки, связанные с нашим проектом: IMU и ToF:
/* Подключение библиотек ---------------------------------- */
#include <NICLA_Vision_Movement_Classification_inferencing.h>
#include <Arduino_LSM6DSOX.h> // IMU
#include "VL53L1X.h" // ToF
Для тестирования код можно оставить как есть, потому что обученная модель использует только признаки, предобработанные с акселерометра. Но в реальном проекте стоит оставить только те библиотеки, которые нужны.
Вот и всё!
Теперь можно загрузить код на устройство и проводить инференс. Нажмите кнопку [RESET] на Nicla дважды, чтобы перевести её в режим загрузки (если она всё ещё подключена к Studio, отключите её), и загрузите скетч на плату.
Теперь попробуйте разные движения с платой (похожие на те, что вы записывали при сборе данных), наблюдая результат инференса каждого класса в Serial Monitor:
- Простой и погрузчик:
- Морская и наземная перевозка:
Обратите внимание, что во всех описанных ситуациях значение anomaly score было меньше 0,0. Попробуйте новое движение, которого не было в исходном наборе данных: например, «перекатывание» Nicla, когда камера направлена вниз — как контейнер, упавший с лодки, или даже авария на лодке:
- Обнаружение аномалии:
В этом случае аномалия гораздо больше — свыше 1,00.
Постобработка
Теперь, когда мы знаем, что модель работает и распознаёт движения, предлагаем изменить код, чтобы увидеть результат на NiclaV полностью автономно — отключённой от ПК и питающейся от батареи, пауэрбанка или отдельного источника питания 5 В.
Идея та же, что и в проекте KWS: при распознавании определённого движения зажигается определённый светодиод. Например, для наземной перевозки загорится зелёный светодиод, для морской — красный, для погрузчика — синий, а если движения нет (простой), все светодиоды выключены. Можно также добавить условие для обнаружения аномалии: например, использовать белый цвет (все светодиоды горят одновременно).
Итоги
Блокноты и код, использованные в этой практической работе, можно найти в репозитории GitHub.
Прежде чем завершить, учтите: классификация движения и обнаружение аномалий могут применяться во многих областях. Вот некоторые потенциальные приложения:
Примеры применения
Промышленность и производство
- Предиктивное обслуживание: обнаружение аномалий в движении механизмов, чтобы предсказать поломки до их возникновения.
- Контроль качества: мониторинг движения сборочных линий или роботизированных манипуляторов для оценки точности и обнаружения отклонений от стандартного паттерна движения.
- Складская логистика: управление перемещением товаров и их отслеживание автоматизированными системами, которые классифицируют разные виды движения и обнаруживают аномалии при обращении с грузом.
Здравоохранение
- Мониторинг пациентов: обнаружение падений и ненормальных движений у пожилых людей или людей с нарушениями подвижности.
- Реабилитация: мониторинг прогресса пациентов, восстанавливающихся после травм, с помощью классификации паттернов движения во время сеансов физиотерапии.
- Распознавание активности: классификация видов физической активности для фитнес-приложений или наблюдения за пациентами.
Бытовая электроника
- Управление жестами: распознавание определённых движений для управления устройствами, например включение света взмахом руки.
- Игры: улучшение игрового опыта за счёт ввода, управляемого движениями.
Транспорт и логистика
- Телематика транспортных средств: мониторинг движения автомобиля для выявления необычного поведения, например резкого торможения, резких поворотов или аварий.
- Мониторинг груза: обеспечение сохранности товаров при перевозке путём обнаружения необычных движений, которые могут указывать на вмешательство или неправильное обращение.
Умные города и инфраструктура
- Мониторинг состояния конструкций: обнаружение вибраций или перемещений внутри сооружений, которые могут указывать на потенциальные неисправности или необходимость обслуживания.
- Управление дорожным движением: анализ потока пешеходов или транспорта для повышения мобильности и безопасности в городе.
Безопасность и наблюдение
- Обнаружение вторжений: распознавание паттернов движения, типичных для несанкционированного доступа или других нарушений безопасности.
- Мониторинг дикой природы: обнаружение браконьеров или необычного поведения животных на охраняемых территориях.
Сельское хозяйство
- Мониторинг техники: отслеживание производительности и использования сельскохозяйственной техники.
- Анализ поведения животных: мониторинг движения скота для выявления признаков проблем со здоровьем или стресса.
Экологический мониторинг
- Сейсмическая активность: обнаружение нерегулярных паттернов движения, которые предшествуют землетрясениям или другим геологически значимым событиям.
- Океанография: изучение волновых паттернов и морских движений для исследований и обеспечения безопасности.
Трёхмерный корпус для Nicla
Для реальных приложений, как было сказано выше, к устройству можно добавить корпус. Eoin Jordan из Edge Impulse разработал отличный носимый корпус для контроля здоровья человека и механизмов для семейства плат Nicla. Он работает с магнитом 10 мм, винтами M2 и ремешком 16 мм для сценариев использования, связанных со здоровьем человека и машин. Ссылка: Arduino Nicla Voice and Vision Wearable Case.
Применения классификации движения и обнаружения аномалий очень разнообразны, а Arduino Nicla Vision хорошо подходит для сценариев, где важны низкое энергопотребление и обработка на периферии. Компактный форм-фактор и эффективность обработки делают её идеальным выбором для портативных и удалённых приложений, где важна обработка в реальном времени, а связь может быть ограничена.
Ресурсы
Обнаружение объектов

Промпт DALL·E 3: мультфильм в стиле 1940–1950-х: просторный промышленный склад. На первом плане конвейер с игрушечными колёсами и коробками. У колёс яркие жёлтые центры и чёрные шины, коробки — белые кубики с чередующимся чёрно-белым узором. В конце движущегося конвейера стоит ретро-робот с инструментами и датчиками, который старательно классифицирует и пересчитывает прибывающие колёса и коробки. Общая эстетика напоминает анимацию середины века с чёткими линиями и классической палитрой.
Обзор
Это продолжение работы с классификацией изображений на Nicla Vision: теперь мы изучаем обнаружение объектов (Object Detection).
Обнаружение объектов и классификация изображений
Основная задача моделей классификации изображений — выдать список наиболее вероятных категорий объектов, присутствующих на изображении. Например, распознать полосатого кота сразу после ужина:
Но что будет, если кот прыгнет рядом с бокалом вина? Модель по-прежнему распознаёт только преобладающую категорию на изображении — полосатого кота:
А что, если на изображении нет доминирующей категории?
Модель совершенно неверно определяет это изображение как «ashcan» (мусорный ящик), возможно из-за цветовых оттенков.
Модель, которая использовалась во всех предыдущих примерах, — MobileNet, обученная на большом наборе данных ImageNet.
Чтобы решить эту проблему, нужен другой тип моделей, которые находят не только несколько категорий (или меток), но и где на изображении расположены объекты.
Такие модели гораздо сложнее и крупнее, например MobileNetV2 SSD FPN-Lite 320x320, обученная на наборе COCO. Эта предобученная модель обнаружения объектов умеет находить до 10 объектов на изображении и выдаёт ограничивающий прямоугольник (bounding box) для каждого обнаруженного объекта. Изображение ниже — результат работы такой модели на Raspberry Pi:
Модели для обнаружения объектов (такие как MobileNet SSD или YOLO) обычно занимают несколько мегабайт. Для Raspberry Pi это нормально, но для встраиваемых устройств, где ОЗУ обычно меньше 1 МБ, такие модели не подходят.
Инновационное решение для обнаружения объектов: FOMO
Edge Impulse представила в 2022 году FOMO (Faster Objects, More Objects) — новое решение для обнаружения объектов на встраиваемых устройствах. Оно работает не только на Nicla Vision (Cortex M7), но и на процессорах Cortex M4F (Arduino Nano33 и серии OpenMV M4), а также на устройствах Espressif ESP32 (ESP-CAM и XIAO ESP32S3 Sense).
В этой практической работе мы будем использовать FOMO для обнаружения объектов и не станем вдаваться в подробности самой модели. Чтобы больше узнать о её работе, можно прочитать официальное объявление FOMO от Edge Impulse, где Louis Moreau и Mat Kelcey подробно объясняют принцип работы.
Цель проекта обнаружения объектов
Любой проект машинного обучения должен начинаться с подробно сформулированной цели. Допустим, мы работаем на промышленном предприятии и должны сортировать и считать колёса и специальные коробки.
Иначе говоря, нам нужна многоклассовая классификация, где каждое изображение может относиться к трём классам:
- фон (объектов нет);
- коробка (Box);
- колесо (Wheel).
Вот несколько неразмеченных примеров изображений, по которым мы будем обнаруживать объекты (колёса и коробки):
Нас интересует, какой объект есть на изображении, его положение (центроид) и сколько таких объектов на нём присутствует. Размер объекта FOMO не определяет — в отличие от MobileNet SSD или YOLO, где ограничивающий прямоугольник является одним из выходных данных модели.
Проект мы будем разрабатывать с Nicla Vision для захвата изображений и инференса, а для машинного обучения использовать Edge Impulse Studio. Но прежде чем начинать проект обнаружения объектов в Studio, создадим сырой набор данных (без разметки) с изображениями, на которых есть объекты для обнаружения.
Сбор данных
Для съёмки изображений можно использовать:
- инструмент Web Serial Camera;
- Edge Impulse Studio;
- OpenMV IDE;
- смартфон.
Здесь мы будем использовать OpenMV IDE.
Сбор набора данных в OpenMV IDE
Сначала создаём на компьютере папку, в которую будут сохраняться данные, например «data». Затем в OpenMV IDE открываем Tools > Dataset Editor и выбираем New Dataset, чтобы начать сбор набора данных:
Edge Impulse рекомендует, чтобы объекты были похожего размера и не перекрывали друг друга, — так модель работает лучше. На промышленном предприятии это нормально: камера закреплена неподвижно и находится на одном расстоянии от объектов. Тем не менее мы попробуем и смешанные размеры и положения, чтобы посмотреть на результат.
Отдельные папки для изображений создавать не будем, потому что каждое изображение содержит несколько меток.
Подключите Nicla Vision к OpenMV IDE и запустите dataset_capture_script.py. Нажатие кнопки Capture Image начнёт съёмку изображений:
Рекомендуем около 50 изображений, с разным сочетанием объектов и разным их количеством в кадре. Старайтесь снимать с разных углов, на разном фоне и при разном освещении.
Сохраняемые изображения имеют размер кадра QVGA и формат RGB565 (формат цветных пикселей).
Когда набор данных собран, закройте инструмент Dataset Editor через Tools > Dataset Editor.
Edge Impulse Studio
Настройка проекта
Откройте Edge Impulse Studio, войдите в систему по кнопке Login (или создайте аккаунт) и начните новый проект.
Здесь можно клонировать проект, созданный для этой практической работы: NICLA_Vision_Object_Detection.
На панели проекта Dashboard откройте Project info и выберите Bounding boxes (object detection), а в правом верхнем углу страницы выберите Target — Arduino Nicla Vision (Cortex-M7).
Загрузка неразмеченных данных
В Studio откройте вкладку Data acquisition и в разделе UPLOAD DATA загрузите с компьютера снятые файлы.
Можно поручить Studio автоматически разделить данные на обучающую и тестовую выборки или сделать это вручную.
Все неразмеченные изображения (51) загружены, но перед использованием в наборе данных их нужно правильно разметить. Для этого в Studio есть инструмент, который открывается по ссылке Labeling queue (51).
В бесплатной версии Edge Impulse Studio доступны два способа разметки с помощью ИИ:
- с использованием YOLOv5;
- отслеживанием объектов между кадрами.
Для клиентов Enterprise Edge Impulse запустила функцию автоматической разметки, которая упрощает разметку в проектах обнаружения объектов.
Обычные объекты можно быстро распознать и разметить с помощью готовой библиотеки предобученных моделей обнаружения YOLOv5 (обученных на наборе COCO). Но в нашем случае объектов нет в наборах COCO, поэтому выберем опцию tracking objects. С ней, как только вы нарисуете рамки и разметите изображения в одном кадре, объекты будут автоматически отслеживаться от кадра к кадру, и новые объекты будут частично размечены (не все — корректно).
Если у вас уже есть размеченный набор данных с ограничивающими рамками, импортируйте его через загрузчик EI.
Разметка набора данных
Начните с первого изображения из неразмеченных данных: с помощью мыши обведите объект рамкой, чтобы добавить метку. Затем нажмите Save labels, чтобы перейти к следующему элементу.
Продолжайте, пока очередь не опустеет. В конце все изображения должны быть размечены так же, как примеры ниже:
Затем просмотрите размеченные примеры на вкладке Data acquisition. Если какая-то метка неверна, её можно отредактировать через меню три точки после названия образца:
Studio подскажет, как заменить неверную метку и исправить набор данных.
Проектирование Impulse
На этом этапе нужно определить, как:
-
Предобработка: изменить размер отдельных изображений с до , сжав их к квадратной форме (без обрезки). Затем изображения переводятся из RGB в оттенки серого (Grayscale).
-
Проектирование модели: в данном случае «Object Detection» (обнаружение объектов).
Предобработка всего набора данных
В этом разделе выберите Color depth — Grayscale, подходящий для моделей FOMO, и сохраните parameters.
Studio автоматически переходит к следующему разделу Generate features, где обрабатываются все образцы. В результате получается набор данных из отдельных изображений , или 9 216 признаков.
Просмотр признаков показывает, что после генерации все образцы хорошо разделяются.
Один из образцов (№ 46), судя по всему, находится не в том месте, но щелчок по нему подтверждает, что разметка верна.
Проектирование модели, обучение и тестирование
Мы будем использовать FOMO — модель обнаружения объектов на основе MobileNetV2 (альфа 0,35), которая грубо сегментирует изображение на сетку: фон и интересующие объекты (здесь коробки и колёса).
FOMO — инновационная модель машинного обучения для обнаружения объектов, которая может использовать в 30 раз меньше энергии и памяти, чем традиционные модели вроде Mobilenet SSD и YOLOv5. FOMO способна работать на микроконтроллерах с ОЗУ менее 200 КБ. Главная причина в том, что другие модели вычисляют размер объекта, рисуя вокруг него квадрат (ограничивающий прямоугольник), а FOMO не учитывает размер и выдаёт только информацию о том, где объект находится на изображении, — в виде координат его центроида.
Как работает FOMO
FOMO берёт изображение в оттенках серого и делит его на блоки пикселей с коэффициентом 8. Для входного изображения сетка будет . Затем FOMO запускает классификатор для каждого блока пикселей, чтобы вычислить вероятность того, что в нём есть коробка или колесо, и определяет области с наибольшей вероятностью наличия объекта (если в блоке пикселей объектов нет, он классифицируется как background). По перекрытию итоговой области FOMO выдаёт координаты (относительно размеров изображения) центроида этой области.
Для обучения нужно выбрать предобученную модель. Используем FOMO (Faster Objects, More Objects) MobileNetV2 0.35. Эта модель занимает около 250 КБ ОЗУ и 80 КБ ПЗУ (флеш-памяти), что хорошо подходит для нашей платы, у которой 1 МБ ОЗУ и ПЗУ.
Гиперпараметры обучения такие:
- эпох: 60;
- размер батча (batch size): 32;
- скорость обучения (learning rate): 0,001.
Для проверки во время обучения будут отложены 20 % набора данных (validation_dataset). Для оставшихся 80 % (train_dataset) применим аугментацию данных: случайно отразим, изменим размер и яркость изображений и обрежем их, искусственно увеличивая количество образцов для обучения.
В итоге модель получает F1-меру около 91 % (валидация) и 93 % (тестовые данные).
Обратите внимание: FOMO автоматически добавил третью метку background к двум заданным ранее (box и wheel).
В задачах обнаружения объектов точность, как правило, не является основной метрикой оценки. Обнаружение объектов включает их классификацию и ограничивающие рамки вокруг них, то есть это более сложная задача, чем простая классификация. Проблема в том, что у нас нет ограничивающих рамок, только центроиды. Коротко говоря, использование точности как метрики может вводить в заблуждение и не даёт полной картины того, насколько хорошо работает модель. Поэтому мы используем F1-меру.
Проверка модели в режиме «Live Classification»
Поскольку Edge Impulse официально поддерживает Nicla Vision, подключим её к Studio. Для этого выполните шаги:
-
Скачайте последнюю прошивку EI и распакуйте её.
-
Откройте ZIP-архив на компьютере и выберите загрузчик для своей ОС.
-
Переведите Nicla Vision в режим загрузки, дважды нажав кнопку reset.
-
Запустите пакетный файл для своей ОС, чтобы загрузить двоичный файл (
arduino-nicla-vision.bin) на плату.
Откройте раздел Live classification в EI Studio и с помощью webUSB подключите Nicla Vision:
Когда подключение установлено, Nicla может снимать реальные изображения, которые обученная модель в Edge Impulse Studio будет тестировать.
Учтите, что модель может давать ложные срабатывания и пропуски. Это можно уменьшить, задав подходящий Confidence Threshold (порог уверенности; настройка через меню три точки). Попробуйте значение 0,8 и выше.
Развёртывание модели
На вкладке Deploy выберите OpenMV Firmware и нажмите [Build].
При повторном подключении Nicla к OpenMV IDE она попытается обновить свою прошивку. Вместо этого выберите опцию Load a specific firmware. Либо откройте меню Tools > Run Bootloader (Load Firmware).
На компьютере появится ZIP-файл из Studio. Откройте его:
Загрузите файл .bin на плату:
Когда загрузка завершится, появится всплывающее сообщение. Нажмите Press OK и откройте скрипт ei_object_detection.py, скачанный из Studio.
Примечание: если появится всплывающее окно с сообщением, что прошивка устарела, нажмите
[NO], чтобы не обновлять её.
Перед запуском скрипта изменим несколько строк. Обратите внимание, что определение окна можно оставить как , а камера может снимать изображения в формате QVGA/RGB. Снимок будет предобработан прошивкой, развёрнутой из Edge Impulse.
import sensor
import time
import ml
from ml.utils import NMS
import math
import image
sensor.reset() # Сброс и инициализация датчика.
# Формат пикселей (RGB565 или GRAYSCALE)
sensor.set_pixformat(sensor.RGB565)
# Размер кадра QVGA (320x240)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time=2000) # Даём камере настроиться.
Задайте минимальную уверенность, например 0,8, чтобы уменьшить количество ложных срабатываний и пропусков.
min_confidence = 0.8
При необходимости измените цвет кругов, которыми отмечаются центроиды обнаруженных объектов, — для лучшего контраста.
threshold_list = [(math.ceil(min_confidence * 255), 255)]
# Загрузка встроенной модели
model = ml.Model("trained")
print(model)
# Вместо этого модели можно загрузить из
# файловой системы.
# model = ml.Model(
# '<object_detection_modelwork>.tflite',
# load_to_fb=True)
# labels = [line.rstrip('\n') for line in open("labels.txt")]
colors = [ # Добавьте цвета, если одновременно обнаруживаете более
# 7 классов.
(255, 255, 0), # background: жёлтый (не используется)
( 0, 255, 0), # cube (коробка): зелёный
(255, 0, 0), # wheel (колесо): красный
( 0, 0, 255), # не используется
(255, 0, 255), # не используется
( 0, 255, 255), # не используется
(255, 255, 255), # не используется
]
Остальной код оставьте без изменений:
# FOMO выдаёт изображение для каждого класса, где каждый пиксель —
# центроид обученного объекта. Поэтому мы получаем эти выходные
# изображения и запускаем на них find_blobs(), чтобы извлечь
# центроиды. Также запускаем get_stats() для найденных блобов,
# чтобы определить их оценку. Объект Non-Max-Suppression (NMS)
# затем отфильтровывает перекрывающиеся обнаружения и переносит
# их положение из выходного изображения обратно на исходное
# входное изображение. Функция возвращает список для каждого
# класса, в котором содержится список кортежей (rect, score),
# описывающих обнаруженные объекты.
def fomo_post_process(model, inputs, outputs):
n, oh, ow, oc = model.output_shape[0]
nms = NMS(ow, oh, inputs[0].roi)
for i in range(oc):
img = image.Image(outputs[0][0, :, :, i] * 255)
blobs = img.find_blobs(
threshold_list,
x_stride=1,
area_threshold=1,
pixels_threshold=1,
)
for b in blobs:
rect = b.rect()
x, y, w, h = rect
score = (
img.get_statistics(
thresholds=threshold_list, roi=rect
).l_mean()
/ 255.0
)
nms.add_bounding_box(x, y, x + w, y + h, score, i)
return nms.get_bounding_boxes()
clock = time.clock()
while True:
clock.tick()
img = sensor.snapshot()
for i, detection_list in enumerate(
model.predict([img], callback=fomo_post_process)
):
if i == 0:
continue # класс background
if len(detection_list) == 0:
continue # для этого класса нет обнаружений?
print("********** %s **********" % model.labels[i])
for (x, y, w, h), score in detection_list:
center_x = math.floor(x + (w / 2))
center_y = math.floor(y + (h / 2))
print(f"x {center_x}\ty {center_y}\tscore {score}")
img.draw_circle((center_x, center_y, 12), color=colors[i])
print(clock.fps(), "fps", end="\n")
Затем нажмите зелёную кнопку Play, чтобы запустить код:
В поле зрения камеры видны объекты, центроиды которых отмечены кругами фиксированного размера 12 пикселей (каждый круг имеет свой цвет в зависимости от класса). В Serial Terminal модель выводит обнаруженные метки и их положение в окне изображения .
Учтите, что начало координат находится в левом верхнем углу.
Частота кадров составляет около 8 кадров в секунду (примерно как в проекте классификации изображений). Это объясняется тем, что FOMO искусно построена на основе CNN-модели, а не модели обнаружения вроде SSD MobileNet или YOLO. Например, при запуске модели MobileNetV2 SSD FPN-Lite на Raspberry Pi 4 задержка примерно в 5 раз выше (около 1,5 кадра в секунду).
Короткое видео с результатами инференса:
Итоги
FOMO — важный скачок в области обработки изображений, как сказали Louis Moreau и Mat Kelcey при её запуске в 2022 году:
FOMO — революционный алгоритм, который впервые приносит обнаружение, отслеживание и подсчёт объектов в реальном времени на микроконтроллеры.
Существует множество возможностей для обнаружения объектов (а точнее, их подсчёта) на встраиваемых устройствах. Это может быть очень полезно, например, в проектах по подсчёту пчёл.
Ресурсы
Настройка

Промпт DALL·E 3: иллюстрация в духе мультфильма 1950-х годов, где плата Arduino NICLA VISION с камерой и другими датчиками стоит в центре старомодного стола. На экране компьютера со скруглёнными углами открыта Arduino IDE. Код связан с настройкой светодиодов и распознаванием голосовых команд. В Serial Monitor явно выводятся слова «yes» и «no».
Обзор
Arduino Nicla Vision (иногда её называют NiclaV) — отладочная плата с двумя процессорами, которые могут выполнять задачи параллельно. Она входит в семейство плат с тем же форм-фактором, но рассчитанных на конкретные задачи, например Nicla Sense ME и Nicla Voice. Платы Nicla хорошо справляются с эффективным выполнением процессов, созданных с помощью TensorFlow Lite. Например, одно ядро NiclaV на лету выполняет алгоритм компьютерного зрения (инференс), а другое решает низкоуровневые задачи: управляет мотором, обменивается данными или работает как пользовательский интерфейс. Встроенный беспроводной модуль позволяет одновременно использовать Wi-Fi и Bluetooth Low Energy (BLE).
Аппаратная часть
Два параллельных ядра
Центральный процессор — двухъядерный STM32H747: ядро Cortex-M7 работает на частоте 480 МГц, ядро Cortex-M4 — на 240 МГц. Два ядра обмениваются данными через механизм удалённого вызова процедур (RPC), который позволяет без затруднений вызывать функции на другом процессоре. Оба процессора используют общую периферию кристалла и могут запускать:
- скетчами Arduino поверх Arm Mbed OS;
- нативными приложениями Mbed;
- MicroPython и JavaScript через интерпретатор;
- TensorFlow Lite.
Память
Память важна для проектов встраиваемого машинного обучения. На плате NiclaV можно разместить до 16 МБ флеш-памяти QSPI для хранения данных. Но для инференса ML используется именно SRAM микроконтроллера: в STM32H747 её всего 1 МБ, и её делят оба процессора. Кроме того, в микроконтроллер встроено 2 МБ флеш-памяти, в основном для хранения кода.
Датчики
- Камера: цветная CMOS-камера GC2145 с разрешением 2 Мп.
- Микрофон:
MP34DT05— сверхкомпактный маломощный всенаправленный цифровой MEMS-микрофон с ёмкостным чувствительным элементом и интерфейсом. - 6-осевой IMU: данные 3D-гироскопа и 3D-акселерометра от 6-осевого IMU
LSM6DSOX. - Датчик времени полёта (ToF):
VL53L1CBV0FYдобавляет точное измерение расстояния с малым энергопотреблением. Невидимый ближний инфракрасный лазер VCSEL (вместе с аналоговым драйвером) и приёмная оптика собраны в компактный модуль под камерой.
Установка Arduino IDE
Сначала подключите плату (micro USB) к компьютеру:
Установите ядро Mbed OS для плат Nicla в Arduino IDE. При открытой IDE перейдите в Tools > Board > Board Manager, найдите в поиске Arduino Nicla Vision и установите плату.
Затем откройте Tools > Board > Arduino Mbed OS Nicla Boards и выберите Arduino Nicla Vision. Когда плата подключена по USB, в списке портов появится Nicla — выберите её.
Откройте скетч Blink в разделе Examples/Basic и запустите его кнопкой Upload в IDE. Должен замигать встроенный светодиод (зелёный RGB). Это значит, что плата Nicla установлена правильно и работает!
Проверка микрофона
В Arduino IDE перейдите в Examples > PDM > PDMSerialPlotter, откройте скетч и запустите его. Откройте Plotter и посмотрите аудиосигнал с микрофона:
Меняйте частоту звука, который вы создаёте, и убедитесь, что микрофон работает правильно.
Проверка IMU
Перед проверкой IMU нужно установить библиотеку LSM6DSOX. Для этого откройте Library Manager и найдите LSM6DSOX. Установите библиотеку от Arduino:
Затем откройте Examples > Arduino_LSM6DSOX > SimpleAccelerometer и запустите тест акселерометра (можно также запустить тест гироскопа и температуры платы):
Проверка датчика ToF (времени полёта)
Как и в случае с IMU, нужно установить библиотеку VL53L1X ToF. Для этого откройте Library Manager и найдите VL53L1X. Установите библиотеку от Pololu:
Затем запустите скетч proximity_detection.ino:
В Serial Monitor вы увидите расстояние от камеры до объекта перед ней (максимум 4 м).
Проверка камеры
Камеру можно проверить, например, с помощью кода из раздела Examples > Camera > CameraCaptureRawBytes. Изображение мы напрямую не увидим, но сможем получить необработанные данные, которые формирует камера.
Чтобы увидеть изображение с камеры, можно воспользоваться Web Serial Camera (API). Это веб-приложение передаёт изображение с камеры по Web Serial с плат Arduino, оснащённых камерой.
Пример Web Serial Camera показывает, как передать данные изображения с платы Arduino и как распаковать их в JavaScript для отображения. Кроме того, в исходном коде веб-приложения есть примеры фильтров изображения, которые показывают, как изменять пиксельные данные для получения визуальных эффектов.
Скетч Arduino (CameraCaptureWebSerial) для передачи данных изображения находится здесь. Его также можно открыть в меню «Examples→Camera» Arduino IDE, если выбрана плата Nicla.
Веб-приложение для отображения изображения с камеры доступно здесь. Также можно изучить урок, в котором подробнее описана настройка.
Установка OpenMV IDE
OpenMV IDE — ведущая интегрированная среда разработки для камер OpenMV, в том числе похожих на Nicla Vision. В ней есть мощный текстовый редактор, терминал для отладки и просмотрщик кадров с гистограммой. Камеру мы будем программировать на MicroPython.
Откройте страницу загрузки OpenMV IDE, скачайте версию для вашей операционной системы и установите её, следуя инструкциям.
IDE должна открыться с кодом helloworld_1.py в области редактирования. Если этого не произошло, откройте его через File > Examples > HelloWorld > helloworld.py.
Все сообщения, которые отправляются по последовательному соединению (через print() или сообщения об ошибках), во время работы отображаются в Serial Terminal. Изображение с камеры выводится в области Camera Viewer (или Frame Buffer), а гистограмма — сразу под ним, в области Histogram.
Обновление загрузчика
Прежде чем подключать Nicla к OpenMV IDE, убедитесь, что установлена последняя версия загрузчика. Откройте Arduino IDE, выберите плату Nicla и откройте скетч Examples > STM_32H747_System STM32H747_manageBootloader. Загрузите код на плату. Serial Monitor подскажет дальнейшие действия.
Установка прошивки
После обновления загрузчика переведите Nicla Vision в режим загрузчика, дважды нажав кнопку reset на плате. Встроенный зелёный светодиод начнёт плавно гаснуть и загораться. Теперь вернитесь в OpenMV IDE и нажмите значок подключения (левая панель инструментов):
Появится всплывающее окно, которое сообщит, что обнаружена плата в режиме DFU, и предложит выбрать дальнейшее действие. Сначала выберите Install the latest release firmware (vX.Y.Z). Это установит на Nicla Vision последнюю версию прошивки OpenMV.
Опцию Erase internal file system можно не выбирать, затем нажмите [OK].
Зелёный светодиод Nicla начнёт мигать, пока прошивка OpenMV загружается на плату. Затем откроется окно терминала с ходом прошивки.
Дождитесь, пока зелёный светодиод перестанет мигать и плавно менять яркость. По окончании процесса появится сообщение «DFU firmware update complete!». Нажмите [OK].
Когда Nicla Vision подключается к панели инструментов, появляется зелёная кнопка «Play».
Кроме того, на компьютере появится диск с именем «NO NAME».
Каждый раз, когда вы нажимаете кнопку [RESET] на плате, автоматически запускается сохранённый на ней скрипт main.py. Вы можете загрузить код main.py в IDE (File > Open File...).
Это код «Blink», он подтверждает, что аппаратная часть исправна.
Проверка камеры
Чтобы проверить камеру, запустим helloworld_1.py. Для этого выберите скрипт в File > Examples > HelloWorld > helloworld.py.
Когда вы нажмёте зелёную кнопку «Play», скрипт MicroPython (helloworld.py) из области редактирования загрузится и запустится на Nicla Vision. В области Camera Viewer начнётся потоковая передача видео. В Serial Monitor будет выводиться FPS (кадров в секунду): примерно 27 кадров/с.
Вот скрипт helloworld.py:
import sensor, time
sensor.reset() # Сброс и инициализация
# датчика.
sensor.set_pixformat(sensor.RGB565) # Формат пикселей RGB565
# (или GRAYSCALE)
sensor.set_framesize(sensor.QVGA) # Размер кадра
# QVGA (320x240)
sensor.skip_frames(time = 2000) # Ожидание, пока
# настройки вступят в силу.
clock = time.clock() # Создание объекта часов
# для отслеживания FPS.
while(True):
clock.tick() # Обновление счётчика FPS.
img = sensor.snapshot() # Снимок и возврат
# изображения.
print(clock.fps())
Скрипты на Python, которые использовались здесь, можно найти на GitHub.
Код можно разделить на две части:
-
Настройка: подключаются и инициализируются библиотеки, определяются и задаются переменные.
-
Цикл: часть кода (цикл while), которая выполняется непрерывно. Снимается изображение (переменная img), то есть один кадр. Каждый кадр можно использовать для инференса в приложениях машинного обучения.
Чтобы остановить программу, нажмите красную кнопку [X].
Примечание: при подключении к IDE OpenMV Cam работает примерно вдвое медленнее. После отключения FPS должен вырасти.
Другие скрипты на Python можно найти в репозитории GitHub. Попробуйте проверить встроенные датчики.
Подключение Nicla Vision к Edge Impulse Studio
Edge Impulse Studio понадобится в других лабораторных работах. Edge Impulse — ведущая платформа для разработки машинного обучения на периферийных устройствах.
Edge Impulse официально поддерживает Nicla Vision. Поэтому сначала создайте новый проект в Studio и подключите Nicla к нему. Для этого выполните шаги:
- Скачайте Arduino CLI для вашей архитектуры компьютера (ОС).
- Скачайте самую свежую прошивку EI.
- Распакуйте оба архива и поместите все файлы в одну папку.
- Переведите Nicla Vision в режим загрузки, дважды нажав кнопку reset.
- Запустите загрузчик (прошивку EI), соответствующий вашей ОС:
- Пакетный скрипт для вашей ОС загрузит двоичный файл arduino-nicla-vision.bin на плату.
В
Chromeдля подключения Nicla к EI Studio можно использовать WebUSB. EI CLI не нужен.
Откройте свой проект в Studio, на вкладке Data Acquisition выберите WebUSB (1). Откроется окно; выберите вариант, в котором видно, что Nicla is paired (2), и нажмите [Connect] (3).
Какие данные датчика собирать, можно выбрать в разделе Collect Data на вкладке Data Acquisition.
Например, IMU data (inertial):
Или изображение (Camera):
Также можно проверить внешний датчик, подключённый к ADC (вывод 0 Nicla), и другие встроенные датчики: например, built-in microphone, ToF (Proximity) или комбинацию датчиков (fusion).
Расширение платы Nicla Vision (необязательно)
Иногда при прототипировании нужно поэкспериментировать с внешними датчиками и устройствами. Хорошее расширение для Nicla — Arduino MKR Connector Carrier (Grove compatible).
На плате 14 разъёмов Grove: пять одиночных аналоговых входов (A0–A5), один двойной аналоговый вход (A5/A6), пять одиночных цифровых входов/выходов (D0–D4), один двойной цифровой вход/выход (D5/D6), один I2C (TWI) и один UART (Serial). Все разъёмы совместимы с напряжением 5 В.
Учтите, что все 17 выводов Nicla Vision подключаются к разъёмам Grove расширения, но некоторые соединения Grove остаются неподключёнными.
Эта плата расширения совместима с MKR и может использоваться с Nicla Vision и Portenta.
Например, предположим, что в проекте TinyML нужно отправлять результаты инференса через устройство LoRaWAN и добавлять данные о локальной освещённости. При автономной работе часто рекомендуется локальный маломощный дисплей, например OLED. Такая схема показана ниже:
Датчик освещённости Grove подключается к одному из одиночных аналоговых выводов (A0/PC4), устройство LoRaWAN — к UART, а OLED-дисплей — к разъёму I2C.
Выводы Nicla 3 (Tx) и 4 (Rx) подключаются к разъёму Serial Shield. Обмен по UART используется для устройства LoRaWAN. Вот простой код для работы с UART:
# Тест UART — By: marcelo_rovai — Sat Sep 23 2023
import time
from pyb import UART
from pyb import LED
redLED = LED(1) # встроенный красный светодиод
# Инициализация объекта UART.
# UART Nicla Vision (выводы TX/RX) находится на «LP1»
uart = UART("LP1", 9600)
while(True):
uart.write("Hello World!\r\n")
redLED.toggle()
time.sleep_ms(1000)
Чтобы проверить работу UART, подключите другое устройство, например Arduino UNO, которое выводит «Hello World» в Serial Monitor. Вот код.
Ниже приведён код «Hello World» для OLED с интерфейсом I2C. Драйвер OLED SSD1306 для MicroPython (ssd1306.py), созданный Adafruit, тоже нужно загрузить на Nicla (скрипт ssd1306.py можно найти на GitHub).
# Nicla_OLED_Hello_World — By: marcelo_rovai — Sat Sep 30 2023
# Сохраните на устройство драйвер OLED SSD1306 для MicroPython,
# интерфейсы I2C и SPI от Adafruit
import ssd1306
from machine import I2C
i2c = I2C(1)
oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)
oled.text('Hello, World', 10, 10)
oled.show()
Наконец, простой скрипт для чтения значения ADC на выводе «PC4» (вывод A0 Nicla):
# Датчик освещённости (A0) — By: marcelo_rovai — Wed Oct 4 2023
import pyb
from time import sleep
adc = pyb.ADC(pyb.Pin("PC4")) # создание аналогового объекта
# из вывода
val = adc.read() # чтение аналогового значения
while (True):
val = adc.read()
print ("Light={}".format (val))
sleep (1)
Тот же ADC можно использовать и для других величин, например температуры.
Обратите внимание: приведённые выше скрипты (скачаны с GitHub) лишь показывают, как подключать внешние устройства к Nicla Vision с помощью MicroPython.
Итоги
Arduino Nicla Vision — отличное миниатюрное устройство для промышленного и профессионального применения! Оно мощное, надёжное, маломощное и имеет подходящие датчики для самых распространённых задач встраиваемого машинного обучения: зрения, движения, слияния данных датчиков и звука.
В репозитории GitHub вы найдёте самые свежие версии всех кодов, которые использовались или комментировались в этой практической лабораторной работе.
Ресурсы
Обзор
Эти лабораторные работы дают уникальную возможность получить практический опыт работы с системами машинного обучения (ML). В отличие от работы с большими моделями, которым нужны ресурсы уровня дата-центра, эти упражнения позволяют напрямую работать с оборудованием и программным обеспечением с помощью TinyML. Такой практический подход даёт осязаемое понимание проблем и возможностей развёртывания ИИ, пусть и в небольшом масштабе. При этом принципы в целом те же, что и при работе с более крупными системами.
Где купить
Комплект XIAOML Kit включает плату XIAO ESP32S3 Sense, плату расширения с IMU, OLED-дисплеем и набором для работы с SD-картой:
- XIAOML Kit (Seeed Studio) (~$40)
Отдельные компоненты также доступны у Seeed Studio.
Предварительные требования
-
Комплект XIAOML Kit:
- плата XIAO ESP32S3 Sense;
- плата расширения с 6-осевым IMU и 0,42-дюймовым OLED-дисплеем;
- набор для работы с SD-картой:
- SD-карта и адаптер USB для хранения данных;
- кабель USB-C для подключения платы к компьютеру.
-
Сеть: доступ в интернет для загрузки необходимого программного обеспечения.
Настройка
Упражнения
| Модальность | Задача | Описание | Ссылка |
|---|---|---|---|
| Зрение | Классификация изображений | Научитесь классифицировать изображения | Открыть |
| Зрение | Обнаружение объектов | Реализуйте обнаружение объектов | Открыть |
| Звук | Распознавание ключевых слов | Изучите системы распознавания голоса | Открыть |
| IMU | Классификация движения и обнаружение аномалий | Классифицируйте данные движения и находите аномалии | Открыть |
Классификация изображений

Промпт DALL·E: иллюстрация в стиле мультфильма 1950-х, основанная на реальном изображении Marcelo Rovai
Обзор
Мы всё чаще сталкиваемся с революцией искусственного интеллекта (ИИ), и, как утверждает Gartner, Edge AI и компьютерное зрение обладают очень высоким потенциалом влияния, и это уже сейчас!
Когда мы смотрим на машинное обучение (ML) применительно к зрению, первое понятие, с которым мы сталкиваемся, — классификация изображений, своего рода «Hello World» для ML: простая и вместе с тем глубокая задача!
XIAOML Kit от Seeed Studio — комплексное аппаратное решение на основе XIAO ESP32-S3 Sense со встроенной камерой OV3660 и поддержкой SD-карт. Эти возможности делают XIAO ESP32S3 Sense отличной отправной точкой для знакомства с компьютерным зрением на базе TinyML.
В этой лабораторной работе мы разберём классификацию изображений с помощью инструмента SenseCraft AI, не требующего написания кода, а затем более подробно рассмотрим разработку с Edge Impulse Studio и Arduino IDE.
Цели обучения
- Развёртывание предобученных моделей с помощью SenseCraft AI Studio для быстрых приложений компьютерного зрения;
- Сбор и управление наборами изображений для собственных задач классификации с правильной организацией данных;
- Обучение собственных моделей классификации изображений методом переноса обучения (transfer learning) с архитектурой MobileNet V2;
- Оптимизация моделей для развёртывания на периферии с помощью квантизации и экономной по памяти предобработки;
- Реализация конвейеров постобработки, включая управление GPIO и интеграцию инференса в реальном времени;
- Сравнение подходов к разработке без кода и с помощью продвинутых платформ ML для встраиваемых приложений.
Классификация изображений
Классификация изображений — фундаментальная задача компьютерного зрения, которая состоит в отнесении целых изображений к одному из нескольких заранее заданных классов. Для этого анализируется визуальное содержание изображения и ему присваивается метка из фиксированного набора категорий в зависимости от доминирующего объекта или сцены.
Классификация изображений важна во многих областях: от организации и поиска по большим базам изображений в цифровых библиотеках и социальных сетях до помощи автономным системам в понимании окружения. Среди архитектур, которые существенно продвинули эту область, — свёрточные нейронные сети (CNN), например AlexNet, VGGNet и ResNet. Эти модели показали впечатляющую точность на сложных наборах данных, таких как ImageNet, выучивая иерархические представления визуальных данных.
Классификация изображений — основа многих систем компьютерного зрения: она открывает путь к более сложным задачам, таким как обнаружение объектов и сегментация изображений, и помогает глубже понимать визуальные данные в разных отраслях. Начнём знакомство с моделью Person Classification («Person — No Person», то есть «человек — не человек»), готовым приложением компьютерного зрения в SenseCraft AI.
Классификация изображений в рабочем пространстве SenseCraft AI
Сначала подключите XIAOML Kit (или только XIAO ESP32S3 Sense, отсоединённую от платы расширения) к компьютеру через USB-C, затем откройте рабочее пространство SenseCraft AI и подключите устройство.
После подключения выберите опцию [Select Model...] и введите в строке поиска: «Person Classification». Из доступных вариантов выберите модель, обученную на MobileNet V2 (при наведении курсора на модели открывается всплывающее окно с их основными характеристиками).
Нажмите на выбранную модель и подтвердите развёртывание. Прошивка с моделью начнёт загружаться на устройство.
Процент загруженной модели и прошивки будет отображаться на экране. Если этого не происходит, попробуйте отключить устройство, затем подключить его снова и нажать кнопку boot.
После успешной загрузки модели в области Preview мы увидим видеопоток с камеры XIAO и результат классификации (Person или Not a Person), а в Device Logger — подробности инференса.
Также можно выбрать интервал кадров инференса — от «Real-Time» (по умолчанию) до 10 секунд, а также режим передачи данных (UART, I2C и др.) — данные передаются устройством (по умолчанию UART через USB).
В Device Logger видно, что задержка модели составляет от 52 до 78 мс на предобработку и около 532 мс на инференс. Итого выходит чуть меньше 600 мс, или примерно 1,7 кадра в секунду (FPS).
Для работы MobileNet V2 0.35 XIAO потребляла пиковый ток 160 мА при напряжении 5,23 В, что даёт энергопотребление 830 мВт.
Постобработка
Важный этап в конвейере проекта классификации изображений — решить, что делать с результатом инференса. Представим, что мы используем XIAO, чтобы автоматически включать свет в комнате, когда обнаружен человек.
В SenseCraft AI это делается в разделе Output -> GPIO. Нажмите значок «Add», чтобы запускать действие при выполнении условий события. Откроется всплывающее окно, где можно задать нужное действие. Например, если человек обнаружен с уверенностью более 60 %, внутренний светодиод LED должен включаться. В реальном сценарии для запуска реле, которое включит свет, использовали бы GPIO, например D0, D1, D2, D11 или D12.
После подтверждения появится созданное Trigger Action (триггерное действие). Нажмите Send, чтобы отправить команду на XIAO.
Теперь, если направить XIAO на человека, внутренний светодиод загорится.
Другие триггерные действия и методы постобработки мы рассмотрим дальше в этой лабораторной работе.
Проект классификации изображений
Создадим простой проект классификации изображений с помощью SenseCraft AI Studio. Ниже показан типичный конвейер машинного обучения, который мы используем в проекте.
В SenseCraft AI Studio откройте вкладку Training:
По умолчанию обучается модель Classification с веб-камерой, если она доступна. Выберем вместо этого XIAOESP32S3 Sense. Нажатие зелёной кнопки [Connect] откроет всплывающее окно. Выберите соответствующий порт и нажмите синюю кнопку [Connect].
Изображение с камеры отобразится в области предпросмотра.
Цель
Первый шаг, как видно из конвейера машинного обучения, — определить цель. Представим, что у нас есть промышленная установка, которая должна автоматически сортировать колёса и коробки.
Смоделируем эту задачу: будем классифицировать игрушечную коробку (box) и игрушечное колесо (wheel). Также добавим третий класс изображений — фон (background), на котором в кадре нет объектов.
Сбор данных
Создадим классы, например в алфавитном порядке:
- Класс 1: background (фон);
- Класс 2: box (коробка);
- Класс 3: wheel (колесо).
Выберите один из классов и удерживайте зелёную кнопку (Hold to Record) под областью предпросмотра. Собранные изображения (и их количество) появятся на экране образцов. Аккуратно и медленно перемещайте камеру, чтобы снять объект с разных ракурсов. Чтобы поменять положение объекта или помешать съёмке, отпустите зелёную кнопку, переставьте объект и снова удерживайте её, чтобы продолжить съёмку.
После съёмки просмотрите изображения и удалите неверные.
Соберите около 50 изображений из каждого класса и переходите к этапу обучения.
Собранные изображения можно скачать и использовать в другом приложении, например в Edge Impulse Studio.
Обучение
Убедитесь, что выбрано правильное устройство (XIAO ESP32S3 Sense), и нажмите [Start Training].
Тест
После обучения можно просмотреть результат инференса.
Учтите, что модель не работает на устройстве. Мы лишь снимаем изображения устройством и выполняем предварительный просмотр в реальном времени, используя обучаемую модель, которая работает в Studio.
Теперь пора по-настоящему развернуть модель на устройстве.
Развёртывание
В окне Supported Devices выберите обученную модель и XIAO ESP32S3 Sense, затем нажмите [Deploy to device].
SenseCraft AI перенаправит нас на вкладку Vision Workplace. Подтвердите развёртывание (Confirm), выберите порт и подключите устройство (Connect).
Модель будет записана в устройство. После автоматической перезагрузки модель начнёт работать на устройстве. В Device Logger видно, что задержка инференса составляет примерно 426 мс, плюс около 110 мс на предобработку, что соответствует частоте кадров 1,8 кадра в секунду (FPS).
Также в Settings можно настроить уверенность модели.
Для работы модели классификации изображений XIAO ESP32S3 потребляла пиковый ток 140 мА при напряжении 5,23 В, что даёт энергопотребление 730 мВт.
Как и раньше, в разделе Output –> GPIO можно включать GPIO или внутренний светодиод в зависимости от обнаруженного класса. Например, светодиод загорится, когда обнаружено колесо.
Сохранение модели
Модель можно сохранить в SenseCraft AI Studio. Studio сохранит все наши модели для последующего развёртывания. Для этого вернитесь на вкладку Training и нажмите кнопку [Save to SenseCraft]:
Следуйте инструкциям, чтобы ввести название модели, описание, изображение и другие детали.
Обратите внимание: обученную модель (Int8 MobileNet V2 размером 320 КБ) можно скачать для дальнейшего использования или анализа, например с помощью Netron. Модель использует изображения размером в качестве входного тензора. На следующем шаге мы применим другие гиперпараметры в Edge Impulse Studio.
Модель также можно развернуть на устройстве повторно в любой момент. Рабочее пространство Workspace откроется в SenseCraft AI автоматически.
Проект классификации изображений на основе набора данных
Основная цель нашего проекта — обучить модель и выполнить инференс на XIAO ESP32S3 Sense. Для обучения нужно найти данные (на самом деле огромное количество данных!).
Но, как мы уже знаем, прежде всего нужна цель! Что мы хотим классифицировать?
С TinyML — набором методов, связанных с инференсом моделей машинного обучения на встраиваемых устройствах, — классификацию следует ограничивать тремя-четырьмя категориями из-за ограничений, главным образом по памяти. Например, можно обучить модель на снятых изображениях «коробка против колеса»; их можно скачать из SenseCraft AI Studio.
Также можно использовать совершенно новый набор данных, например с различением яблок, бананов и картофеля или других категорий. По возможности найдите специализированный набор данных с изображениями этих категорий. Хорошим началом будет Kaggle fruit-and-vegetable-image-recognition.
Загрузим набор данных, собранный в предыдущем разделе. Откройте меню (три точки) для каждого снятого класса и выберите Export Data.
Набор данных будет загружен на компьютер в виде ZIP-файла: по одному файлу на каждый класс. Сохраните их в рабочую папку и распакуйте. У вас должны появиться три папки — по одной на каждый класс.
При желании можно добавить свежие изображения, используя, например, код из лабораторной работы по настройке.
Обучение модели в Edge Impulse Studio
Для обучения модели мы будем использовать Edge Impulse Studio. Edge Impulse — ведущая платформа разработки машинного обучения для периферийных устройств.
Войдите в аккаунт Edge Impulse (или создайте бесплатный) и создайте новый проект.
Сбор данных
Перейдите в раздел Data acquisition и выберите + Add data. Появится всплывающее окно. Выберите UPLOAD DATA.
После выбора появится новое всплывающее окно с запросом на обновление данных.
- В режиме загрузки (Upload mode) выберите
select a folderи нажмите[Choose Files]. - Перейдите в папку, которая содержит один из классов, и нажмите
[Upload].
- Вы автоматически вернётесь в окно Upload data.
- Выберите
Automatically split between training and testing. - Введите метку изображений, которые находятся в папке.
- Выберите
[Upload data]. - Файлы начнут загружаться. После этого появится другое всплывающее окно с вопросом, создаёте ли вы проект обнаружения объектов. Выберите
[no].
Повторите процедуру для всех классов. Не забудьте изменить имя метки! Если забыть и загрузить изображения, они будут перемешаны в Studio. Не беспокойтесь: данные можно вручную перенести между классами позже.
Закройте окно Upload Data и вернитесь на страницу Data acquisition. Мы видим, что весь набор загружен. На верхней панели видно 158 элементов, распределённых поровну. Также 19 % изображений оставлены для тестирования.
Проектирование Impulse
Impulse получает сырые данные (в данном случае изображения), извлекает признаки (меняет размер изображений) и затем использует блок обучения для классификации новых данных.
Классификация изображений — самое распространённое применение глубокого обучения, но для этой задачи нужен большой объём данных. У нас около 50 изображений на каждую категорию. Достаточно ли этого? Совсем нет! Чтобы «научить» модель различать каждый класс, нужны тысячи изображений. Однако эту проблему можно решить, дообучив уже обученную модель на наших данных. Этот приём называется «переносом обучения» (Transfer Learning, TL). С TL можно дообучить предобученную модель классификации изображений на наших данных, добиваясь хорошей производительности даже на относительно небольших наборах изображений, как в нашем случае.
С помощью TL можно дообучить предобученную модель классификации изображений на наших данных, сохраняя хорошую производительность даже на относительно небольших наборах изображений (как в нашем случае).
Итак, начиная с сырых изображений, изменим их размер пикселей и передадим в блок Transfer Learning. Создадим Impulse.
На этом этапе можно также задать целевое устройство, чтобы контролировать «бюджет» (память и задержку). XIAO ESP32S3 официально не поддерживается Edge Impulse, поэтому рассмотрим Espressif ESP-EYE: он похож, но медленнее.
Сохраните Impulse, как показано выше, и перейдите в раздел Image.
Предобработка (генерация признаков)
Помимо изменения размера изображений, их можно перевести в оттенки серого или сохранить исходную глубину цвета RGB. Выберем [RGB] в разделе Image. Тогда каждый образец будет иметь размерность 27 648 признаков (). Нажатие [Save Parameters] откроет новую вкладку Generate Features. Нажмите кнопку [Generate Features], чтобы создать признаки.
Проектирование модели, обучение и тестирование
В 2017 году Google представил MobileNetV1. В 2018 году вышла MobileNetV2: Inverted Residuals and Linear Bottlenecks, а в 2019-м — V3. MobileNet — семейство универсальных нейронных сетей для компьютерного зрения, специально созданных для мобильных устройств и поддерживающих классификацию, обнаружение и другие задачи. MobileNet — компактные модели с малой задержкой и низким энергопотреблением, параметры которых подобраны под ограничения ресурсов разных сценариев.
Хотя базовая архитектура MobileNet уже компактна и быстра, конкретному приложению часто нужна ещё более маленькая и быстрая модель. MobileNet вводит простой параметр α (альфа), называемый множителем ширины (width multiplier), для построения меньших и менее требовательных к вычислениям моделей. Роль множителя ширины α — равномерно «утончить» сеть на каждом слое.
В Edge Impulse Studio доступны MobileNet V1 (изображения ) и V2 (изображения и ) с несколькими значениями α (от 0,05 до 1,0). Например, наибольшая точность достигается у V2 с изображениями и α=1,0. Конечно, есть компромисс: чем выше точность, тем больше памяти (около 1,3 МБ ОЗУ и 2,6 МБ ПЗУ) нужно для работы модели и тем выше задержка. Наименьший объём — у MobileNet V1 с α=0,10 (около 53,2 КБ ОЗУ и 101 КБ ПЗУ).
Мы будем использовать MobileNet V2 0.35 как базовую модель (но можно выбрать модель с большим α). Последний слой нашей модели, предшествующий выходному, будет содержать 16 нейронов с 10 %-ным dropout для предотвращения переобучения.
Ещё один обязательный приём в глубоком обучении — аугментация данных (data augmentation). Это метод, который помогает повысить точность моделей машинного обучения за счёт создания дополнительных искусственных данных. Система аугментации вносит небольшие случайные изменения в обучающие данные в процессе обучения (например, отражает, обрезает или поворачивает изображения).
Заглянем «под капот» и посмотрим, как Edge Impulse реализует политику аугментации данных:
# Реализация политики аугментации данных
def augment_image(image, label):
# Случайно отражаем изображение по горизонтали
image = tf.image.random_flip_left_right(image)
# Увеличиваем размер изображения, затем случайно обрезаем его до
# исходных размеров
resize_factor = random.uniform(1, 1.2)
new_height = math.floor(resize_factor * INPUT_SHAPE[0])
new_width = math.floor(resize_factor * INPUT_SHAPE[1])
image = tf.image.resize_with_crop_or_pad(image, new_height,
new_width)
image = tf.image.random_crop(image, size=INPUT_SHAPE)
# Меняем яркость изображения
image = tf.image.random_brightness(image, max_delta=0.2)
return image, label
Теперь определим гиперпараметры:
- эпох: 20;
- размер батча (batch size): 32;
- скорость обучения (learning rate): 0,0005;
- размер валидационной выборки: 20 %.
Результат обучения:
Профиль модели предсказывает 233 КБ ОЗУ и 546 КБ флеш-памяти, что не создаёт проблем для XIAO ESP32S3, у которой 8 МБ PSRAM. Кроме того, Studio показывает задержку около 1160 мс, что очень много. Однако этого и следовало ожидать: мы используем ESP-EYE, у которого процессор Xtensa LX6, тогда как ESP32S3 использует более новый и мощный Xtensa LX7.
На тестовых данных мы также получили 100 % точности, даже для квантованной модели INT8. В реальных проектах такой результат нехарактерен, но наш проект относительно прост: два объекта очень хорошо отличаются друг от друга.
Развёртывание модели
Обученную модель можно развернуть:
- как
.TFLITEдля использования в SenseCraft AI; - как
Arduino Libraryв Edge Impulse Studio.
Начнём с SenseCraft, это проще и интуитивнее.
Развёртывание модели в SenseCraft AI
На Dashboard можно скачать обученную модель в нескольких форматах. Скачаем TensorFlow Lite (int8 quantized) размером 623 КБ.
В SenseCraft AI Studio откройте вкладку Workspace, выберите XIAO ESP32S3, соответствующий порт и подключите устройство.
Вы увидите последнюю модель, загруженную на устройство. Нажмите зелёную кнопку [Upload Model]. Появится всплывающее окно с запросом названия модели, файла модели и названий классов (объектов). Используем метки в алфавитном порядке: 0: background, 1: box и 2: wheel, затем нажмите [Send].
Через несколько секунд модель будет загружена («прошита») на устройство, и изображение с камеры появится в реальном времени в области Preview. Результат классификации будет показан под предпросмотром. В Settings с помощью курсора можно выбрать Confidence Threshold (порог уверенности) инференса.
В Device Logger в Serial Monitor видно задержку: около 81 мс на предобработку и 205 мс на инференс, что соответствует 3,4 кадра в секунду (FPS). Это вдвое больше, чем мы получили при обучении модели в SenseCraft, потому что здесь используются изображения меньшего размера ( против ).
Общая задержка примерно в 4 раза меньше, чем оценка в Edge Impulse Studio для процессора Xtensa LX6; теперь инференс выполняется на процессоре Xtensa LX7.
Постобработка
Результат инференса модели, включая задержку, ID класса и уверенность, можно получить, как показано в Device Logger в SenseCraft AI. Это позволяет использовать XIAO ESP32S3 Sense как ИИ-датчик. Иначе говоря, данные модели можно получать по разным протоколам связи, таким как MQTT, UART, I2C или SPI, в зависимости от требований проекта.
Идея похожа на то, что мы делали в лабораторной работе по классификации изображений для Seeed Grove Vision AI V2 (раздел «Постобработка»).
Ниже пример подключения по шине I2C.
Подробнее см. вики Seeed Studio.
Развёртывание как библиотеки Arduino в EI Studio
На вкладке Deploy в Edge Impulse Studio выберите Arduino library, TensorFlow Lite, Quantized(int8) и нажмите [Build]. Обученная модель будет скачана как ZIP-библиотека Arduino:
Откройте Arduino IDE, в меню Sketch выберите Include Library и Add .ZIP Library. Затем укажите файл, скачанный из Edge Impulse Studio, и нажмите [Open].
Перейдите в Examples в Arduino IDE и найдите проект по имени (в данном случае: «Box_versus_Wheel_…Interfering»). Откройте esp32 → esp32_camera. Скетч esp32_camera.ino появится в IDE.
Этот скетч создан для обычной платы ESP32 и не будет работать с XIAO ESP32S3 Sense. Его нужно изменить. Скачайте изменённую версию из репозитория проекта на GitHub: Image_class_XIAOML-Kit.ino.
Разбор кода классификации изображений для XIAO ESP32S3
Код снимает изображения со встроенной камеры, обрабатывает их и классифицирует (в данном случае «Box», «Wheel» или «Background») с помощью модели, обученной в EI Studio. Он работает непрерывно и выполняет инференс в реальном времени на периферийном устройстве.
Коротко:
Камера → изображение JPEG → преобразование в RGB888 → изменение размера до → нейронная сеть → результаты классификации → вывод в последовательный порт
Основные компоненты
- Подключение библиотек и зависимостей
#include <Box_versus_Wheel_-_XIAO_ESP32S3_inferencing.h>
#include "edge-impulse-sdk/dsp/image/image.hpp"
#include "esp_camera.h"
- Библиотека инференса Edge Impulse: содержит нашу обученную модель и движок инференса;
- Обработка изображений: предоставляет функции для работы с изображениями;
- ESP Camera: аппаратный интерфейс модуля камеры.
- Конфигурации выводов камеры
Плата XIAO ESP32S3 Sense может работать с разными сенсорами камеры (OV2640 или OV3660), у которых могут быть разные конфигурации выводов. Код задаёт три возможные конфигурации:
// Конфигурация 1: наиболее распространённая конфигурация OV2640
#define CONFIG_1_XCLK_GPIO_NUM 10
#define CONFIG_1_SIOD_GPIO_NUM 40
#define CONFIG_1_SIOC_GPIO_NUM 39
// ... остальные выводы
Такая гибкость позволяет коду автоматически пробовать разные отображения выводов, если первое не сработало, и делает его устойчивее к разным ревизиям аппаратуры.
- Настройки управления памятью
#define EI_CAMERA_RAW_FRAME_BUFFER_COLS 320
#define EI_CAMERA_RAW_FRAME_BUFFER_ROWS 240
#define EI_CLASSIFIER_ALLOCATION_HEAP 1
- Размер буфера кадра: задаёт размер исходного изображения ( пикселей);
- Выделение в куче (heap): используется динамическое выделение памяти для гибкости;
- Поддержка PSRAM: у ESP32S3 есть 8 МБ PSRAM для хранения больших данных, например изображений.
setup() — инициализация
void setup() {
Serial.begin(115200);
while (!Serial);
if (ei_camera_init() == false) {
ei_printf("Failed to initialize Camera!\r\n");
} else {
ei_printf("Camera initialized\r\n");
}
ei_sleep(2000); // Ждём 2 секунды перед запуском
}
Эта функция:
- Инициализирует последовательную связь для отладочного вывода;
- Инициализирует камеру с автоматическим определением конфигурации;
- Ждёт 2 секунды перед началом непрерывного инференса.
loop() — главный цикл обработки
Цикл выполняет следующие шаги непрерывно:
Шаг 1. Выделение памяти
snapshot_buf = (uint8_t*)ps_malloc(EI_CAMERA_RAW_FRAME_BUFFER_COLS *
EI_CAMERA_RAW_FRAME_BUFFER_ROWS *
EI_CAMERA_FRAME_BYTE_SIZE);
Выделяет память под буфер изображения, предпочтительно в PSRAM (более быстрой внешней ОЗУ), а при необходимости — в обычной куче.
Шаг 2. Захват изображения
if (ei_camera_capture((size_t)EI_CLASSIFIER_INPUT_WIDTH,
(size_t)EI_CLASSIFIER_INPUT_HEIGHT,
snapshot_buf) == false) {
ei_printf("Failed to capture image\r\n");
free(snapshot_buf);
return;
}
Снимает изображение с камеры и сохраняет его в буфер.
Шаг 3. Запуск инференса
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR err = run_classifier(&signal, &result, false);
Запускает модель машинного обучения на снятом изображении.
Шаг 4. Вывод результатов
for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
ei_printf(" %s: %.5f\r\n",
ei_classifier_inferencing_categories[i],
result.classification[i].value);
}
Выводит результаты классификации с оценками уверенности для каждой категории.
ei_camera_init() — интеллектуальная инициализация камеры
Эта функция реализует интеллектуальную последовательность инициализации:
bool ei_camera_init(void) {
// Пробуем конфигурацию 1 (распространённая для OV2640)
update_camera_config(1);
esp_err_t err = esp_camera_init(&camera_config);
if (err == ESP_OK) goto camera_init_success;
// Пробуем конфигурацию 2 (OV3660)
esp_camera_deinit();
update_camera_config(2);
err = esp_camera_init(&camera_config);
if (err == ESP_OK) goto camera_init_success;
// Продолжаем пробовать другие конфигурации...
}
Функция:
- Пробует несколько конфигураций выводов;
- Проверяет разные тактовые частоты (10 МГц или 16 МГц);
- Сначала пытается использовать PSRAM, затем переходит на DRAM;
- Применяет настройки сенсора в зависимости от обнаруженного оборудования.
ei_camera_capture() — конвейер обработки изображений
bool ei_camera_capture(uint32_t img_width, uint32_t img_height, uint8_t *out_buf) {
// 1. Получаем кадр с камеры
camera_fb_t *fb = esp_camera_fb_get();
// 2. Преобразуем JPEG в формат RGB888
bool converted = fmt2rgb888(fb->buf, fb->len, PIXFORMAT_JPEG, snapshot_buf);
// 3. Возвращаем буфер кадра драйверу камеры
esp_camera_fb_return(fb);
// 4. При необходимости меняем размер
if (do_resize) {
ei::image::processing::crop_and_interpolate_rgb888(...);
}
}
Функция:
- Снимает JPEG-изображение с камеры;
- Преобразует его в формат RGB888 (требуется моделью машинного обучения);
- Изменяет размер изображения под входной размер модели ( пикселей).
Инференс
- Загрузите код на XIAO ESP32S3 Sense.
⚠️ Внимание
- На XIAO ESP32S3 ОБЯЗАТЕЛЬНО должна быть включена PSRAM. Проверить это можно в верхнем меню Arduino IDE:
Tools→PSRAM:OPI PSRAM;- Пакет поддержки плат (
esp32 by Espressif Systems) должен быть версии 2.0.17. Не обновляйте его.
- Откройте Serial Monitor.
- Наведите камеру на объекты и проверьте результат в Serial Monitor.
Постобработка
В приложениях периферийного ИИ ценность результата инференса определяется тем, насколько мы можем на него отреагировать. Последовательный вывод даёт подробную информацию для отладки и разработки, однако в реальных развёртываниях нужна немедленная обратная связь, понятная человеку и не зависящая от внешних мониторов или соединений.
Маленький 0,42-дюймовый OLED-дисплей XIAOML Kit ( пикселей) становится важным компонентом постобработки: он превращает сырые результаты инференса в понятную человеку обратную связь — выводит названия классов и уровни уверенности прямо на устройстве. Это исключает необходимость во внешних мониторах и позволяет создавать по-настоящему автономные периферийные ИИ-системы в промышленности, сельском хозяйстве или розничной торговле, где мгновенное визуальное подтверждение предсказаний ИИ особенно важно.
Теперь изменим скетч так, чтобы он автоматически адаптировался к модели, обученной в Edge Impulse: он будет читать названия классов и их количество прямо из модели. На дисплее будут показываться сокращённые названия классов (по 3 буквы) крупным шрифтом для лучшей читаемости на маленьком дисплее пикселей. Скачайте код с GitHub: XIAOML-Kit-Img_Class_OLED_Gen.
Запустим код и посмотрим результат:
Итоги
XIAO ESP32S3 Sense — очень способная и гибкая платформа для приложений классификации изображений. В ходе этой лабораторной работы мы изучили два разных подхода к разработке, которые подходят разному уровню подготовки и требованиям проекта.
-
SenseCraft AI Studio даёт доступную отправную точку с интерфейсом без кода, позволяя быстро создавать прототипы и развёртывать предобученные модели, например для распознавания людей. С инференсом в реальном времени и встроенными средствами постобработки она показывает, как ИИ можно развернуть без глубоких знаний программирования или ML.
-
Для более сложных приложений Edge Impulse Studio предлагает полноценные инструменты конвейера машинного обучения: управление собственными наборами данных, перенос обучения с несколькими предобученными моделями (например, MobileNet) и оптимизацию модели.
Ключевые выводы этой лабораторной работы: важность компромисса между разрешением изображения, эффективность переноса обучения для небольших наборов данных и практические соображения развёртывания периферийного ИИ — энергопотребление и ограничения памяти.
Работа демонстрирует фундаментальные принципы TinyML, которые выходят за рамки конкретного оборудования: инференс при ограниченных ресурсах, требования к обработке в реальном времени и полный путь от сбора данных через развёртывание модели до практических применений. Благодаря встроенным средствам постобработки, включая управление GPIO и протоколы связи, XIAO становится не просто движком инференса, а полноценной платформой ИИ-датчика.
Эта основа в классификации изображений подготовит вас к более сложным задачам компьютерного зрения и покажет, как современный периферийный ИИ делает продвинутое компьютерное зрение доступным, экономичным и применимым во встраиваемых системах реального мира — от промышленной автоматизации до систем «умного дома».
Ресурсы
- Начало работы с XIAO ESP32S3
- SenseCraft AI Studio
- Рабочее пространство SenseCraft Vision
- Пример набора данных
- Проект Edge Impulse
- XIAO как ИИ-датчик
- Библиотека Seeed Arduino SSCMA
- Код XIAOML Kit
Распознавание ключевых слов (KWS)

Промпт DALL·E: иллюстрация в стиле мультфильма 1950-х на основе реального изображения Marcelo Rovai
Обзор
Распознавание ключевых слов (Keyword Spotting, KWS) — неотъемлемая часть многих систем распознавания речи: оно позволяет устройствам реагировать на определённые слова или фразы. Эта технология лежит в основе популярных устройств вроде Google Assistant или Amazon Alexa, но она в равной мере применима и достижима на небольших маломощных устройствах. Эта лабораторная работа проведёт вас через реализацию системы KWS с помощью TinyML на отладочной плате XIAO ESP32S3.
XIAO ESP32S3 с чипом ESP32-S3 от Espressif — компактный и мощный микроконтроллер с двухъядерным процессором Xtensa LX7, встроенными Wi-Fi и Bluetooth. Сочетание вычислительной мощности, энергоэффективности и гибкой связи делает его отличной платформой для приложений TinyML. Кроме того, благодаря плате расширения Sense у нас будет доступ к «sense»-части устройства, на которой есть камера, слот для SD-карты и цифровой микрофон. Встроенный микрофон и SD-карта будут ключевыми в этом проекте.
Мы будем использовать Edge Impulse Studio — мощную и удобную платформу, которая упрощает создание моделей машинного обучения и их развёртывание на периферийных устройствах. Мы шаг за шагом обучим модель KWS, оптимизируем её и развернём на XIAO ESP32S3 Sense.
Наша модель будет распознавать ключевые слова, которые могут будить устройство или запускать определённые действия (в случае «YES»), и оживлять ваши проекты голосовым управлением.
Опираясь на опыт работы с TensorFlow Lite для микроконтроллеров (движок «под капотом» EI Studio), мы создадим систему KWS, способную выполнять машинное обучение в реальном времени прямо на устройстве.
По ходу работы мы разберём каждый этап — от сбора и подготовки данных до обучения модели и развёртывания, — чтобы дать полное представление о реализации системы KWS на микроконтроллере.
Цели обучения
- Понять архитектуру голосового помощника, включая каскадные системы обнаружения и роль распознавания ключевых слов на периферии как первого этапа конвейеров обработки голоса;
- Освоить методы сбора аудиоданных: офлайн (микрофон XIAO ESP32S3 с записью на SD-карту) и онлайн (смартфон и Edge Impulse Studio);
- Реализовать цифровую обработку сигналов для аудио: основы протокола I2S, дискретизацию аудио 16 кГц/16 бит и преобразование сигналов из временной области в признаки частотной области с помощью MFCC;
- Обучить свёрточные нейронные сети для классификации аудио методом переноса обучения, с приёмами аугментации данных и оптимизацией модели для классификации по четырём классам (YES, NO, NOISE, UNKNOWN);
- Развернуть оптимизированные модели на микроконтроллерах с квантизацией (INT8), управлением памятью через PSRAM и оптимизацией инференса в реальном времени для встраиваемых систем;
- Создать полные конвейеры постобработки, включая пороги уверенности, управление GPIO для внешних устройств, интеграцию OLED-дисплея и создание автономных ИИ-датчиков;
- Сравнить способы разработки на платформах без кода (Edge Impulse Studio) и в традиционном программировании встраиваемых систем (Arduino IDE) для приложений TinyML.
Проект KWS
Как работает голосовой помощник?
Распознавание ключевых слов (KWS) критически важно для многих голосовых помощников: оно позволяет устройствам реагировать на определённые слова или фразы. Для начала важно понять, что голосовые помощники на рынке, например Google Home или Amazon Echo Dot, реагируют на человека, только когда их «будят» определённые ключевые слова: «Hey Google» в первом случае и «Alexa» во втором.
Иначе говоря, распознавание голосовых команд основано на многоступенчатой модели, или каскадном обнаружении (Cascade Detection).
Этап 1: маленький микропроцессор внутри Echo Dot или Google Home постоянно слушает звук и ждёт, когда будет распознано ключевое слово. Для такого обнаружения на периферии используется модель TinyML (приложение KWS).
Этап 2: данные отправляются в облако и обрабатываются более крупной моделью, только если приложение KWS на этапе 1 сработало.
В видео ниже показан пример: я эмулирую Google Assistant на Raspberry Pi (этап 2), а в роли устройства TinyML (этап 1) используется Arduino Nano 33 BLE.
Если хотите глубже изучить весь проект, см. моё руководство: Building an Intelligent Voice Assistant From Scratch.
В этой лабораторной работе мы сосредоточимся на этапе 1 (KWS, или Keyword Spotting), где используем XIAO ESP32S3 Sense с цифровым микрофоном для распознавания ключевого слова.
Конвейер инференса
Схема ниже показывает, как должно работать итоговое приложение KWS (во время инференса):
Наше приложение KWS будет распознавать четыре класса звуков:
- YES (ключевое слово 1);
- NO (ключевое слово 2);
- NOISE (ключевых слов нет, есть только фоновый шум);
- UNKNOWN (смесь слов, отличных от YES и NO).
Для реальных проектов всегда рекомендуется включать в набор не только ключевые слова, но и другие слова и звуки, например «Noise» (шум или фон) и «Unknown» (неизвестные слова).
Рабочий процесс машинного обучения
Главный компонент приложения KWS — его модель. Поэтому нужно обучить модель на наших ключевых словах, шуме и других словах («unknown»):
Набор данных
Критически важный компонент рабочего процесса машинного обучения — набор данных. Выбрав конкретные ключевые слова (YES и NO), мы можем воспользоваться набором данных, который разработал Пит Уорден (Pete Warden): «Speech Commands: A Dataset for Limited-Vocabulary Speech Recognition». В этом наборе 35 ключевых слов (по более чем 1000 образцов каждого), например yes, no, stop и go. То есть для yes и no можно получить по 1500 образцов.
Небольшую часть набора данных можно скачать из Edge Impulse Studio (готовый набор данных для распознавания ключевых слов). Он включает образцы четырёх классов, которые мы будем использовать в проекте: yes, no, noise и background. Для этого выполните следующие шаги:
- Скачайте набор ключевых слов.
- Распакуйте файл в удобное место.
Хотя данных из набора Пита много, рекомендуется записать и слова, которые произнесём сами. При работе с акселерометрами важно было собирать набор данных датчиком того же типа. Для звука ситуация иная, потому что на самом деле мы классифицируем аудио.
Ключевое различие между звуком и аудио — в виде энергии. Звук — это энергия механических волн (продольных звуковых волн), которые распространяются в среде и вызывают колебания давления. Аудио — это электрическая энергия (аналоговые или цифровые сигналы), которая электрически представляет звук.
Когда мы произносим ключевое слово, звуковые волны нужно преобразовать в аудиоданные. Преобразование выполняется дискретизацией сигнала с микрофона с частотой 16 кГц и разрядностью 16 бит.
Поэтому подойдёт любое устройство, которое может создавать аудиоданные с такими базовыми параметрами (16 кГц/16 бит). В качестве устройства можно использовать XIAO ESP32S3 Sense, компьютер или даже смартфон.
Онлайн-запись аудиоданных с Edge Impulse и смартфона
В лабораторной работе «Классификация движения и обнаружение аномалий» мы подключали устройство напрямую к Edge Impulse Studio для сбора данных (с частотой дискретизации 50–100 Гц). Для такой низкой частоты можно было использовать функцию CLI Data Forwarder, однако, по словам Jan Jongboom, CTO Edge Impulse, аудио (16 кГц) передаётся слишком быстро для Data Forwarder. Поэтому, получив цифровые данные с микрофона, мы можем превратить их в файл WAV и отправить в Studio через Data Uploader (так же, как мы поступим с набором Пита).
Если хотите собирать аудио непосредственно в Studio, можно использовать любой смартфон, подключённый к Studio через интернет. Этот вариант мы здесь рассматривать не будем, но вы легко найдёте его описание в документации EI.
Офлайн-запись аудиоданных с XIAO ESP32S3 Sense
Встроенный микрофон — MSM261D3526H1CPM, MEMS-микрофон с PDM-выходом и несколькими режимами работы. Внутри он подключён к ESP32S3 через шину I2S: выводы IO42 (тактовый сигнал) и IO41 (данные).
Что такое I2S?
I2S (Inter-IC Sound) — стандартный протокол для передачи цифрового аудио от одного устройства к другому. Его первоначально разработала Philips Semiconductor (ныне NXP Semiconductors). Он широко используется в аудиоустройствах, например в цифровых сигнальных процессорах, цифровых аудиопроцессорах и, всё чаще, в микроконтроллерах с поддержкой цифрового аудио (как в нашем случае).
Протокол I2S состоит как минимум из трёх линий:
1. Линия битового (или последовательного) тактового сигнала (BCLK или CLK): переключается, чтобы обозначить начало нового бита данных (вывод IO42).
2. Линия выбора слова (WS): переключается, чтобы обозначить начало нового слова (левого или правого канала). Частота сигнала выбора слова (WS) задаёт частоту дискретизации. В нашем случае вывод L/R на микрофоне подключён к земле, поэтому используется только левый канал (моно).
3. Линия данных (SD): по ней передаются аудиоданные (вывод IO41).
В потоке данных I2S данные передаются последовательностью кадров, каждый из которых содержит слово левого канала и слово правого канала. Поэтому I2S особенно удобен для передачи стереоаудио, однако он может использоваться и для моно- или многоканального звука с дополнительными линиями данных.
Начнём с того, как записывать сырые данные с микрофона. Перейдите в проект на GitHub и скачайте скетч XIAOEsp2s3_Mic_Test:
⚠️ Внимание
- На XIAO ESP32S3 ОБЯЗАТЕЛЬНО должна быть включена PSRAM. Проверить это можно в верхнем меню Arduino IDE:
Tools→PSRAM:OPI PSRAM;- Пакет поддержки плат (
esp32 by Espressif Systems) должен быть версии 2.0.17. Пожалуйста, не обновляйте его.
/*
Простой тест микрофона XIAO ESP32S3
*/
#include <I2S.h>
void setup() {
Serial.begin(115200);
while (!Serial) {
}
// Запуск I2S с частотой 16 кГц и 16 битами на отсчёт
I2S.setAllPins(-1, 42, 41, -1, -1);
if (!I2S.begin(PDM_MONO_MODE, 16000, 16)) {
Serial.println("Failed to initialize I2S!");
while (1); // ничего не делать
}
}
void loop() {
// Считываем отсчёт
int sample = I2S.read();
if (sample && sample != -1 && sample != 1) {
Serial.println(sample);
}
}
Этот код — простой тест микрофона XIAO ESP32S3 с интерфейсом I2S (Inter-IC Sound). Он настраивает I2S на захват аудио с частотой дискретизации 16 кГц и 16 битами на отсчёт, затем непрерывно читает отсчёты с микрофона и выводит их в последовательный порт.
Разберём основные части кода:
- Подключение библиотеки I2S: она предоставляет функции для настройки и использования интерфейса I2S, который является стандартом для подключения цифровых аудиоустройств.
I2S.setAllPins(-1, 42, 41, -1, -1): настраивает выводы I2S. Параметры — (-1, 42, 41, -1, -1): второй параметр (42) — вывод тактового сигнала I2S (CLK), третий (41) — вывод данных I2S (DATA). Остальные параметры равны -1, то есть эти выводы не используются.I2S.begin(PDM_MONO_MODE, 16000, 16): инициализирует I2S в режиме моно PDM (Pulse Density Modulation) с частотой дискретизации 16 кГц и 16 битами на отсчёт. Если инициализация не удалась, выводится сообщение об ошибке и программа останавливается.int sample = I2S.read(): считывает отсчёт аудио из интерфейса I2S.
Если отсчёт корректен, он выводится в Serial Monitor и Plotter.
Ниже показан тест: «шёпот» двумя разными тонами.
Сохранение записанных звуковых образцов
Для записи аудиофайлов .wav на встроенный считыватель SD-карт нужно сначала включить PSRAM на XIAO.
В ESP32-S3 на чипе микроконтроллера всего несколько сотен килобайт внутренней оперативной памяти. Этого может не хватить для некоторых задач, поэтому ESP32-S3 может использовать до 16 МБ внешней PSRAM (псевдостатического ОЗУ), подключённой параллельно к флеш-чипу SPI. Внешняя память включена в адресное пространство процессора и при определённых ограничениях может использоваться так же, как внутренняя оперативная память данных.
Для начала вставьте SD-карту в XIAO, как показано на фото ниже (SD-карта должна быть отформатирована в FAT32).
Включите функцию PSRAM микросхемы ESP-32 (Arduino IDE): Tools > PSRAM: "OPI PSRAM" > OPI PSRAM.
- Скачайте скетч Wav_Record_dataset из репозитория проекта на GitHub.
Этот код записывает аудио через интерфейс I2S платы Seeed XIAO ESP32S3 Sense, сохраняет запись в виде файла .wav на SD-карту и позволяет управлять процессом записи командами, которые отправляются через Serial Monitor. Имя аудиофайла можно задать (оно должно совпадать с метками классов для обучения), и можно делать несколько записей, каждая из которых сохраняется в новый файл. Код также включает функцию увеличения громкости записей.
Разберём самые важные части кода:
#include <I2S.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
Это необходимые библиотеки для программы. I2S.h позволяет вводить аудио, FS.h предоставляет возможности работы с файловой системой, SD.h позволяет программе работать с SD-картой, а SPI.h отвечает за связь SPI с SD-картой.
#define RECORD_TIME 10
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16
#define WAV_HEADER_SIZE 44
#define VOLUME_GAIN 2
Здесь заданы различные константы программы.
- RECORD_TIME задаёт длительность записи аудио в секундах.
- SAMPLE_RATE и SAMPLE_BITS задают качество записи.
- WAV_HEADER_SIZE задаёт размер заголовка файла .wav.
- VOLUME_GAIN используется для увеличения громкости записи.
int fileNumber = 1;
String baseFileName;
bool isRecording = false;
Эти переменные хранят номер текущего файла (для создания уникальных имён файлов), базовое имя файла и признак того, идёт ли сейчас запись.
void setup() {
Serial.begin(115200);
while (!Serial);
I2S.setAllPins(-1, 42, 41, -1, -1);
if (!I2S.begin(PDM_MONO_MODE, SAMPLE_RATE, SAMPLE_BITS)) {
Serial.println("Failed to initialize I2S!");
while (1);
}
if(!SD.begin(21)){
Serial.println("Failed to mount SD Card!");
while (1);
}
Serial.printf("Enter with the label name\n");
}
Функция setup инициализирует последовательный порт, интерфейс I2S для аудиовхода и интерфейс SD-карты. Если I2S не инициализировался или SD-карта не смонтировалась, выводится сообщение об ошибке и выполнение останавливается.
void loop() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim();
if (command == "rec") {
isRecording = true;
} else {
baseFileName = command;
fileNumber = 1; // сбрасываем номер файла при каждом
// задании нового базового имени
Serial.printf("Send rec for starting recording label \n");
}
}
if (isRecording && baseFileName != "") {
String fileName = "/" + baseFileName + "."
+ String(fileNumber) + ".wav";
fileNumber++;
record_wav(fileName);
delay(1000); // задержка, чтобы не записать несколько файлов
// одновременно
isRecording = false;
}
}
В основном цикле программа ждёт команду из Serial Monitor. Если команда — rec, программа начинает запись. В противном случае команда считается базовым именем для файлов .wav. Если сейчас идёт запись и задано базовое имя, программа записывает аудио и сохраняет его в файл .wav. Имена файлов формируются добавлением номера файла к базовому имени.
void record_wav(String fileName)
{
...
File file = SD.open(fileName.c_str(), FILE_WRITE);
...
rec_buffer = (uint8_t *)ps_malloc(record_size);
...
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0,
rec_buffer,
record_size,
&sample_size,
portMAX_DELAY);
...
}
Эта функция записывает аудио и сохраняет его в файл .wav с заданным именем. Сначала инициализируются переменные sample_size и record_size. Значение record_size вычисляется по частоте дискретизации, разрядности и желаемой длительности записи. Рассмотрим важные части:
File file = SD.open(fileName.c_str(), FILE_WRITE);
// Записываем заголовок в файл WAV
uint8_t wav_header[WAV_HEADER_SIZE];
generate_wav_header(wav_header, record_size, SAMPLE_RATE);
file.write(wav_header, WAV_HEADER_SIZE);
Этот фрагмент открывает файл на SD-карте для записи, формирует заголовок .wav с помощью функции generate_wav_header и записывает его в файл.
// Выделение памяти в PSRAM для записи
rec_buffer = (uint8_t *)ps_malloc(record_size);
if (rec_buffer == NULL) {
Serial.printf("malloc failed!\n");
while(1) ;
}
Serial.printf("Buffer: %d bytes\n", ESP.getPsramSize()
- ESP.getFreePsram());
Функция ps_malloc выделяет память в PSRAM для записи. Если выделение не удалось (rec_buffer равен NULL), выводится сообщение об ошибке и выполнение останавливается.
// Запуск записи
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0,
rec_buffer,
record_size,
&sample_size,
portMAX_DELAY);
if (sample_size == 0) {
Serial.printf("Record Failed!\n");
} else {
Serial.printf("Record %d bytes\n", sample_size);
}
Функция i2s_read считывает аудиоданные с микрофона в rec_buffer. Если данные не прочитаны (sample_size равен 0), выводится сообщение об ошибке.
// Увеличиваем громкость
for (uint32_t i = 0; i < sample_size; i += SAMPLE_BITS/8) {
(*(uint16_t *)(rec_buffer+i)) <<= VOLUME_GAIN;
}
Этот фрагмент увеличивает громкость записи, сдвигая значения отсчётов на VOLUME_GAIN.
// Записываем данные в файл WAV
Serial.printf("Writing to the file ...\n");
if (file.write(rec_buffer, record_size) != record_size)
Serial.printf("Write file Failed!\n");
free(rec_buffer);
file.close();
Serial.printf("Recording complete: \n");
Serial.printf("Send rec for a new sample or enter "
"a new label\n\n");
Наконец, аудиоданные записываются в файл .wav. Если запись не удалась, выводится сообщение об ошибке. После записи память, выделенная под rec_buffer, освобождается и файл закрывается. Функция заканчивается сообщением о завершении и предложением отправить новую команду.
void generate_wav_header(uint8_t *wav_header,
uint32_t wav_size,
uint32_t sample_rate)
{
...
memcpy(wav_header, set_wav_header, sizeof(set_wav_header));
}
Функция generate_wav_header формирует заголовок файла .wav на основе параметров (wav_size и sample_rate). Она создаёт массив байтов по формату .wav: поля размера файла, аудиоформата, количества каналов, частоты дискретизации, битовой скорости, выравнивания блоков, разрядности и размера данных. Готовый заголовок копируется в массив wav_header, переданный в функцию.
Теперь загрузите код на XIAO и запишите образцы ключевых слов (yes и no). Также можно записать шум и другие слова.
Serial Monitor предложит получить метку, которую нужно записать:
Отправьте метку (например, yes). Программа будет ждать следующую команду: rec.
Программа будет записывать новые образцы каждый раз, когда получает команду rec. Файлы сохраняются как yes.1.wav, yes.2.wav, yes.3.wav и так далее, пока не будет отправлена новая метка (например, no). В этом случае для каждого нового образца нужно отправлять команду rec; файлы будут сохраняться как no.1.wav, no.2.wav, no.3.wav и так далее.
В итоге на SD-карте появятся сохранённые файлы.
Файлы готовы к загрузке в Edge Impulse Studio.
Приложения для офлайн-записи аудиоданных
Аудиоданные можно записывать разными способами. Самый простой — использовать смартфон или ПК как подключённое устройство в Edge Impulse Studio.
ПК или смартфон должны записывать аудио с частотой дискретизации 16 кГц и разрядностью 16 бит.
Другой вариант — специализированные приложения. Хорошее приложение для этого — Voice Recorder Pro (iOS). Записи нужно сохранять как файлы .wav и отправлять на компьютер.
Обучение модели в Edge Impulse Studio
Загрузка данных
Когда сырой набор данных определён и собран (набор Пита и записанные ключевые слова), создадим новый проект в Edge Impulse Studio:
После создания проекта выберите инструмент Upload Existing Data в разделе Data Acquisition. Выберите файлы для загрузки:
Загрузите их в Studio (можно автоматически разделить данные на обучающую и тестовую выборки). Повторите для всех классов и всех сырых данных.
Образцы появятся в разделе Data acquisition.
Все данные из набора Пита имеют длительность 1 с, а образцы, записанные в предыдущем разделе, длятся 10 с. Поэтому их нужно разбить на отрезки по 1 с, чтобы они были совместимы.
Нажмите «три точки» после названия образца и выберите Split sample.
Внутри инструмента разбейте данные на записи по 1 секунде. При необходимости добавьте или удалите сегменты:
Эту процедуру нужно повторить для всех образцов.
Примечание: для более длинных аудиофайлов (минуты) сначала разбейте их на сегменты по 10 секунд, а затем снова используйте инструмент, чтобы получить окончательные отрезки по 1 секунде.
Если при загрузке данные не разделялись автоматически на обучающую и тестовую выборки, это можно сделать вручную (через меню «три точки», перемещая образцы по одному) или с помощью Perform Train / Test Split на вкладке Dashboard в разделе Danger Zone.
При желании все наборы данных можно проверить на вкладке Data Explorer.
Создание Impulse (предобработка и определение модели)
Impulse получает сырые данные, с помощью обработки сигналов извлекает признаки, а затем использует блок обучения для классификации новых данных.
Сначала мы возьмём точки данных с окном в 1 секунду, применим аугментацию и будем сдвигать окно каждые 500 мс. Обратите внимание, что включён параметр zero-pad data (дополнение нулями). Он важен: образцы короче 1 секунды дополняются нулями (в некоторых случаях я уменьшал окно 1000 мс в инструменте разбиения, чтобы избежать шумов и выбросов).
Каждый образец аудио длиной 1 секунда предобрабатывается и преобразуется в изображение (например, ). Мы будем использовать MFCC — метод, который извлекает признаки из аудиосигналов с помощью мел-частотных кепстральных коэффициентов, хорошо подходящих для человеческого голоса.
Далее мы выбираем Keras для классификации и строим модель с нуля, используя классификацию изображений с помощью свёрточной нейронной сети (CNN).
Предобработка (MFCC)
Следующий шаг — создать изображения для обучения на следующем этапе:
Можно оставить значения параметров по умолчанию или воспользоваться опцией DSP Autotune parameters, что и сделаем.
Предобработка почти не расходует памяти (всего 16 КБ), однако расчётное время обработки довольно велико: 675 мс для Espressif ESP-EYE (ближайший доступный эталон) при тактовой частоте 240 МГц (как у нашего устройства), но с более слабым процессором (Xtensa LX6 вместо LX7 у ESP32S3). Реальное время инференса должно быть меньше.
Если позже понадобится уменьшить время инференса, вернитесь к этапу предобработки и, например, уменьшите длину FFT до 256, измените количество коэффициентов или другой параметр.
Пока оставим параметры, заданные инструментом автонастройки. Сохраним параметры и сгенерируем признаки.
Если хотите подробнее изучить преобразование временных последовательных данных в изображения с помощью FFT, спектрограммы и т. п., можете поэкспериментировать с этим блокнотом Colab: Audio Raw Data Analysis.
Проектирование модели и обучение
Мы будем использовать свёрточную нейронную сеть (CNN). Базовая архитектура состоит из двух блоков Conv1D + MaxPooling (с 8 и 16 нейронами соответственно) и Dropout 0,25. На последнем слое, после Flatten, четыре нейрона — по одному на каждый класс:
Гиперпараметры: скорость обучения (Learning Rate) 0,005, модель обучается за 100 эпох. Также добавим аугментацию данных, в том числе добавление шума. Результат выглядит хорошо:
Если хотите понять, что происходит «под капотом», можно скачать набор данных и запустить блокнот Jupyter, поэкспериментировать с кодом. Например, можно проанализировать точность по каждой эпохе:
Этот блокнот Colab поможет узнать, как двигаться дальше: KWS Classifier Project — Looking «Under the hood».
Тестирование
Тестирование модели на данных, отложенных до обучения (тестовых данных), дало точность около 87 %.
Если посмотреть F1-меру, для YES получилось 0,95 — отличный результат, поскольку именно это слово мы будем использовать как «триггер» для постобработки (включения встроенного светодиода). Даже для NO получилось 0,90. Худший результат — у unknown, и это нормально.
Можно переходить к проекту, но перед развёртыванием на устройстве полезно выполнить классификацию в реальном времени с помощью смартфона. Перейдите в раздел Live Classification и нажмите Connect a Development board:
Наведите телефон на QR-код и выберите ссылку.
Телефон подключится к Studio. В приложении выберите опцию Classification и, когда она запустится, начните проверять свои ключевые слова, убедившись, что модель работает на реальных данных в реальном времени:
Развёртывание и инференс
Studio упакует все нужные библиотеки, функции предобработки и обученные модели и загрузит их на компьютер. Выберите опцию Arduino Library, затем внизу выберите Quantized (Int8) и нажмите Build.
Теперь пора провести настоящий тест. Мы будем делать инференс, полностью отключившись от Studio. Изменим один из примеров кода для ESP32, созданных при развёртывании Arduino Library.
В Arduino IDE откройте вкладку File/Examples, найдите свой проект и выберите esp32/esp32_microphone:
Этот код создан для встроенного микрофона ESP-EYE и его нужно адаптировать под наше устройство.
Начнём с замены библиотек для работы с шиной I2S:
На:
#include <I2S.h>
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16
Инициализируйте микрофон I2S в функции setup(), добавив строки:
void setup()
{
...
I2S.setAllPins(-1, 42, 41, -1, -1);
if (!I2S.begin(PDM_MONO_MODE, SAMPLE_RATE, SAMPLE_BITS)) {
Serial.println("Failed to initialize I2S!");
while (1) ;
...
}
В функции static void capture_samples(void* arg) замените строку 153, которая читает данные с микрофона I2S:
На:
/* Чтение данных сразу из i2s */
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0,
(void*)sampleBuffer,
i2s_bytes_to_read,
&bytes_read, 100);
В функции static bool microphone_inference_start(uint32_t n_samples) закомментируйте или удалите строки 198–200, где вызывается функция инициализации микрофона. Она больше не нужна, поскольку микрофон I2S уже инициализирован в setup().
Наконец, в функции static void microphone_inference_end(void) замените строку 243:
На:
static void microphone_inference_end(void)
{
free(sampleBuffer);
ei_free(inference.buffer);
}
Полный код есть в репозитории проекта на GitHub. Загрузите скетч на плату и проверьте реальный инференс:
⚠️ Внимание
- На XIAO ESP32S3 ОБЯЗАТЕЛЬНО должна быть включена PSRAM. Проверить это можно в верхнем меню Arduino IDE:
Tools→PSRAM:OPI PSRAM;- Пакет поддержки плат (
esp32 by Espressif Systems) должен быть версии 2.0.17. Пожалуйста, не обновляйте его.
Постобработка
В приложениях периферийного ИИ ценность результата инференса определяется тем, насколько мы можем на него отреагировать. Последовательный вывод даёт подробную информацию для отладки и разработки, однако реальным развёртываниям нужна немедленная обратная связь, понятная человеку и не зависящая от внешних мониторов или соединений.
Рассмотрим два подхода к постобработке: использование встроенного светодиода XIAO и OLED-дисплея в XIAOML Kit.
Светодиод
Теперь, когда мы знаем, что модель работает и распознаёт ключевые слова, изменим код так, чтобы встроенный светодиод загорался каждый раз, когда распознано YES.
Нужно инициализировать светодиод:
#define LED_BUILT_IN 21
...
void setup()
{
...
pinMode(LED_BUILT_IN, OUTPUT); // Настраиваем вывод на выход
digitalWrite(LED_BUILT_IN, HIGH); // Выключить
...
}
Затем замените фрагмент // print the predictions в предыдущем коде (в функции loop()):
int pred_index = 0; // Инициализация pred_index
float pred_value = 0; // Инициализация pred_value
// вывод предсказаний
ei_printf("Predictions ");
ei_printf("(DSP: %d ms., Classification: %d ms., Anomaly: %d ms.)",
result.timing.dsp, result.timing.classification,
result.timing.anomaly);
ei_printf(": \n");
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf(" %s: ", result.classification[ix].label);
ei_printf_float(result.classification[ix].value);
ei_printf("\n");
if (result.classification[ix].value > pred_value){
pred_index = ix;
pred_value = result.classification[ix].value;
}
}
// показываем результат инференса светодиодом
if (pred_index == 3){
digitalWrite(LED_BUILT_IN, LOW); // Включить
}
else{
digitalWrite(LED_BUILT_IN, HIGH); // Выключить
}
Полный код есть в репозитории проекта на GitHub. Загрузите скетч на плату и проверьте реальный инференс:
Идея в том, что светодиод будет гореть всякий раз, когда распознано ключевое слово YES. Также вместо светодиода можно запускать внешнее устройство, как мы видели во введении.
OLED-дисплей
Маленький 0,42-дюймовый OLED-дисплей XIAOML Kit ( пикселей) — важный компонент постобработки: он превращает сырые результаты инференса в немедленную, понятную человеку обратную связь, показывая названия классов и уровни уверенности прямо на устройстве. Это исключает необходимость во внешних мониторах и позволяет создавать по-настоящему автономные периферийные ИИ-системы в промышленности, сельском хозяйстве или розничной торговле, где мгновенное визуальное подтверждение предсказаний ИИ особенно важно.
Теперь изменим скетч так, чтобы он автоматически адаптировался к модели, обученной в Edge Impulse: он будет читать названия классов и их количество прямо из модели. Скачайте код с GitHub: xiaoml-kit_kws_oled.
Запустим код и посмотрим результат:
Итоги
Эта лабораторная работа продемонстрировала полный цикл разработки системы распознавания ключевых слов на XIAOML Kit и показала, как современные платформы TinyML делают сложный аудио-ИИ доступным на устройствах с ограниченными ресурсами. В ходе практической реализации мы соединили теоретические концепции машинного обучения с практическим развёртыванием встраиваемого ИИ.
Технические достижения. Проект реализует полный конвейер обработки аудио: от захвата сырого звука до инференса в реальном времени. Используя встроенный цифровой микрофон XIAO ESP32S3, мы записали аудио профессионального качества (16 кГц/16 бит) и обработали его с помощью мел-частотных кепстральных коэффициентов (MFCC) для извлечения признаков. Развёрнутая CNN-модель достигла высокой точности в различении целевых ключевых слов («YES», «NO») и фоновых условий («NOISE», «UNKNOWN»), а время инференса подходит для приложений реального времени.
Интеграция с платформой. Edge Impulse Studio оказалась незаменимой комплексной MLOps-платформой для встраиваемых систем: она охватывает всё — от сбора и разметки данных до обучения, оптимизации и развёртывания моделей. Бесшовная интеграция облачного обучения и развёртывания на периферии характерна для современных рабочих процессов TinyML, а Arduino IDE обеспечила гибкость, нужную для собственной постобработки.
Практические применения. Освоенные приёмы выходят далеко за рамки простого распознавания ключевых слов. Системы голосового управления, промышленный мониторинг безопасности с классификацией звуков, медицинские приложения для анализа дыхания и экологический мониторинг по звукам животных или оборудования используют похожие подходы к обработке аудио. Каскадная архитектура обнаружения, показанная здесь — когда KWS на периферии запускает более сложную обработку в облаке, — лежит в основе современных систем голосовых помощников.
Принципы встраиваемого ИИ. Проект подчеркнул важные соображения TinyML: управление энергопотреблением, оптимизацию памяти за счёт использования PSRAM и компромиссы между сложностью модели и скоростью инференса. Успешное развёртывание нейронной сети, выполняющей анализ аудио в реальном времени на микроконтроллере, показывает, как возможности ИИ, раньше требовавшие мощных настольных компьютеров, теперь работают на устройствах с батарейным питанием.
Методология разработки. Мы рассмотрели несколько путей разработки: от стратегий сбора данных (офлайн-запись на SD-карту или онлайн-потоковая передача) до вариантов развёртывания (автоматическая генерация библиотеки в Edge Impulse или собственная реализация на Arduino). Такая гибкость важна для адаптации к разным требованиям и ограничениям проектов.
Дальнейшее развитие. Основа, заложенная здесь, открывает путь к более продвинутым приложениям аудио-ИИ. Распознавание нескольких ключевых слов, идентификация говорящего, определение эмоций по голосу и классификация звуков окружающей среды опираются на те же базовые методы. Возможности интеграции, продемонстрированные с OLED-дисплеем и управлением GPIO, показывают, как KWS может служить интеллектуальным интерфейсом для более широких IoT-систем.
Классификация звуков охватывает гораздо больше, чем распознавание речи. Методы этого проекта применяются во многих областях:
- Безопасность: обнаружение битого стекла, мониторинг вторжений, распознавание выстрелов;
- Промышленный IoT: мониторинг состояния оборудования, обнаружение аномалий в производственном оборудовании;
- Здравоохранение: мониторинг нарушений сна, оценка состояния дыхательной системы, системы ухода за пожилыми людьми;
- Экологический мониторинг: отслеживание дикой природы, анализ городского шума, акустическое управление «умными» зданиями;
- Интеграция с умным домом: голосовое управление в нескольких комнатах, контроль состояния бытовой техники по звуковым признакам.
Ключевые выводы. XIAOML Kit доказывает, что разработка ИИ профессионального уровня достижима с помощью доступных инструментов и при скромном бюджете. Сочетание мощного оборудования (ESP32S3 с PSRAM и встроенными датчиками), зрелых платформ разработки (Edge Impulse Studio) и полноценных программных библиотек создаёт среду, в которой сложные концепции ИИ превращаются в осязаемые работающие системы.
Эта лабораторная работа показывает, что будущее ИИ — не только в огромных дата-центрах, но и в интеллектуальных периферийных устройствах, которые могут обрабатывать, понимать и реагировать на окружающую среду в реальном времени. Это открывает возможности для повсеместного, сохраняющего приватность и отзывчивого искусственного интеллекта.
Ресурсы
Классификация движения и обнаружение аномалий

Промпт DALL·E: мультяшная иллюстрация в стиле 1950-х в винтажной аудиолаборатории. Учёные в классических белых халатах внимательно анализируют аудиоданные на больших досках. На досках — графики БПФ (быстрого преобразования Фурье) и кривые во временной области. Вокруг разбросано старинное аудиооборудование, но графики данных ясные и подробные, что подчёркивает внимание к анализу звука.
Обзор
Транспорт — основа мировой торговли. Миллионы контейнеров ежедневно перевозятся морем, автомобилями и поездами в разные уголки мира. Безопасная и эффективная перевозка контейнеров — огромная задача, которая требует современных технологий, и TinyML, без сомнения, — одно из ключевых решений.
В этой практической работе мы будем решать реальные задачи транспортной логистики. Мы разработаем систему классификации движения и обнаружения аномалий на основе XIAOML Kit, Arduino IDE и Edge Impulse Studio. Проект поможет понять, какие силы и движения испытывает контейнер на разных этапах перевозки: при наземной и морской транспортировке, при вертикальном перемещении вилочным погрузчиком и при хранении на складе.
Цели обучения
- настройка XIAOML Kit;
- сбор и предобработка данных;
- создание модели классификации движения;
- реализация обнаружения аномалий;
- тестирование и анализ в реальных условиях.
К концу этой лабораторной работы у вас будет рабочий прототип, который умеет классифицировать разные типы движения и обнаруживать аномалии во время перевозки контейнеров. Эти знания станут ступенькой к более сложным проектам в быстро развивающейся области TinyML, особенно к тем, что связаны с вибрацией.
Установка IMU
В комплект XIAOML Kit на плате расширения уже встроен 6-осевой датчик IMU LSM6DS3TR-C, поэтому внешние датчики подключать не нужно. Такое встроенное решение даёт чистую и надёжную платформу для приложений машинного обучения на основе движения.
LSM6DS3TR-C объединяет в одном корпусе 3-осевой акселерометр и 3-осевой гироскоп. Он подключён по шине I2C к XIAO ESP32S3 по адресу 0x6A и обеспечивает:
- диапазоны акселерометра: ±2/±4/±8/±16 g (по умолчанию используем ±2 g);
- диапазоны гироскопа: ±125/±250/±500/±1000/±2000 dps (по умолчанию используем ±250 dps);
- разрешение: 16-битный АЦП;
- интерфейс связи: I2C, адрес 0x6A;
- энергопотребление: сверхнизкое.

Система координат. Датчик работает в правосторонней системе координат. Если смотреть на плату расширения снизу (там, где виден датчик IMU с меткой-точкой):
- ось X направлена вправо;
- ось Y направлена вперёд (от вас);
- ось Z направлена вверх (из платы).
Настройка аппаратной части
Поскольку XIAOML Kit поставляется собранным с платой расширения, дополнительных аппаратных соединений не требуется. Датчик LSM6DS3TR-C уже правильно подключён по I2C.
Что уже подключено:
- IMU LSM6DS3TR-C → I2C (SDA/SCL) → XIAO ESP32S3;
- адрес I2C: 0x6A;
- питание: 3,3 В от XIAO ESP32S3.
Необходимая библиотека. Библиотека должна была быть установлена во время настройки. Если это не так, установите библиотеку Seeed Arduino LSM6DS3, выполнив такие шаги:
- Откройте Library Manager в Arduino IDE.
- Найдите «LSM6DS3».
- Установите «Seeed Arduino LSM6DS3» от Seeed Studio.
- Важно: НЕ устанавливайте «Arduino_LSM6DS3 by Arduino» — она предназначена для других плат!
Проверка датчика IMU
Начнём с простого теста, чтобы убедиться, что IMU работает правильно. Загрузите этот код для проверки датчика:
#include <LSM6DS3.h>
#include <Wire.h>
// Создаём объект IMU через интерфейс I2C
LSM6DS3 myIMU(I2C_MODE, 0x6A);
float accelX, accelY, accelZ;
float gyroX, gyroY, gyroZ;
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
Serial.println("Тест IMU XIAOML Kit");
Serial.println("6-осевой IMU LSM6DS3TR-C");
Serial.println("====================");
// Инициализируем IMU
if (myIMU.begin() != 0) {
Serial.println("ОШИБКА: не удалось инициализировать IMU!");
while(1) delay(1000);
} else {
Serial.println("✓ IMU успешно инициализирован");
Serial.println("Формат данных: AccelX,AccelY,AccelZ,"
"GyroX,GyroY,GyroZ");
Serial.println("Единицы: g (ускорение свободного падения), градусы/с");
Serial.println();
}
}
void loop() {
// Читаем данные акселерометра (в g)
accelX = myIMU.readFloatAccelX();
accelY = myIMU.readFloatAccelY();
accelZ = myIMU.readFloatAccelZ();
// Читаем данные гироскопа (в градусах в секунду)
gyroX = myIMU.readFloatGyroX();
gyroY = myIMU.readFloatGyroY();
gyroZ = myIMU.readFloatGyroZ();
// Выводим в удобном для чтения формате
Serial.print("Акселерометр (g): X="); Serial.print(accelX, 3);
Serial.print(" Y="); Serial.print(accelY, 3);
Serial.print(" Z="); Serial.print(accelZ, 3);
Serial.print(" | Гироскоп (°/с): X="); Serial.print(gyroX, 2);
Serial.print(" Y="); Serial.print(gyroY, 2);
Serial.print(" Z="); Serial.println(gyroZ, 2);
delay(100); // частота обновления 10 Гц
}
Когда комплект лежит ровно на столе, вы должны увидеть:
- ускорение по оси Z около +1,0 g (гравитация);
- ускорение по осям X и Y близко к 0,0 g;
- все значения гироскопа близки к 0,0 °/с.
Пошевелите комплект, чтобы увидеть, как меняются значения.
Проект классификации движения TinyML
Мы смоделируем перевозку контейнеров (точнее, упаковок) в разных сценариях, чтобы сделать руководство более наглядным и практичным.

С помощью акселерометра XIAOML Kit мы будем записывать данные о движении, вручную имитируя условия:
- морская перевозка (паллеты на лодках) — движение по всем осям с волнообразными колебаниями;
- наземная перевозка (паллеты на грузовиках или поездах) — преимущественно горизонтальное движение;
- подъём (паллеты, которые перемещает вилочный погрузчик) — преимущественно вертикальное движение;
- простой (паллеты на складе) — минимальное движение.
По изображению выше мы определим для нашей симуляции: преимущественно горизонтальные движения (по осям или ) соответствуют классу «Наземная перевозка», вертикальные движения (ось ) — классу «Подъём», отсутствие активности — классу «Простой», а движение по всем трём осям — классу «Морская перевозка».

Сбор данных
Для сбора данных есть несколько вариантов. В реальном сценарии устройство можно подключить непосредственно к контейнеру, а собранные данные сохранять в файл (например, CSV) на SD-карте. Данные можно также передавать удалённо на ближайший накопитель, например на телефон, по Wi-Fi или Bluetooth (как в проекте Sensor DataLogger). Когда набор данных собран и сохранён как файл .CSV, его можно загрузить в Studio с помощью инструмента CSV Wizard.
В этом видео вы узнаете о других способах отправки данных в Edge Impulse Studio.
Подготовка кода для сбора данных
В этой работе мы подключим комплект напрямую к Edge Impulse Studio, которая будет также использоваться для предобработки данных, обучения модели, тестирования и развёртывания.
Для сбора данных сначала подключите комплект к Edge Impulse Studio, которая будет также использоваться для предобработки данных, обучения модели, тестирования и развёртывания.
Следуйте инструкциям, чтобы установить Node.js и Edge Impulse CLI на ваш компьютер.
Поскольку XIAOML Kit не является полностью поддерживаемой отладочной платой Edge Impulse, для захвата данных с датчика и отправки их в Studio используем CLI Data Forwarder, как показано на схеме:

Изменим тестовый код так, чтобы он выводил данные в формате, подходящем для Edge Impulse:
#include <LSM6DS3.h>
#include <Wire.h>
#define FREQUENCY_HZ 50
#define INTERVAL_MS (1000 / (FREQUENCY_HZ + 1))
LSM6DS3 myIMU(I2C_MODE, 0x6A);
static unsigned long last_interval_ms = 0;
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
Serial.println("XIAOML Kit — сбор данных о движении");
Serial.println("Датчик IMU LSM6DS3TR-C");
// Инициализируем IMU
if (myIMU.begin() != 0) {
Serial.println("ОШИБКА: не удалось инициализировать IMU!");
while(1) delay(1000);
}
delay(2000);
Serial.println("Начало сбора данных через 3 секунды...");
delay(3000);
}
void loop() {
if (millis() > last_interval_ms + INTERVAL_MS) {
last_interval_ms = millis();
// Читаем данные акселерометра
float ax = myIMU.readFloatAccelX();
float ay = myIMU.readFloatAccelY();
float az = myIMU.readFloatAccelZ();
// Переводим в м/с² (умножаем на 9,81)
float ax_ms2 = ax * 9.81;
float ay_ms2 = ay * 9.81;
float az_ms2 = az * 9.81;
// Выводим в формате Edge Impulse
Serial.print(ax_ms2);
Serial.print("\t");
Serial.print(ay_ms2);
Serial.print("\t");
Serial.println(az_ms2);
}
}
Загрузите код в Arduino IDE. В Serial Monitor должны появиться значения акселерометра (переведённые в м/с²):

Оставьте код запущенным, но выключите Serial Monitor. Данные с комплекта будут отправлены в Edge Impulse Studio по последовательному соединению.
Подключение к Edge Impulse для сбора данных
Создайте проект Edge Impulse
- Перейдите в Edge Impulse Studio и создайте новый проект.
- Выберите понятное название (не длиннее 63 символов для совместимости с библиотеками Arduino).

Настройте CLI Data Forwarder
- Установите Edge Impulse CLI на компьютер.
- Убедитесь, что XIAOML Kit подключён к компьютеру, код запущен, а Serial Monitor выключен, иначе может возникнуть ошибка.
- В терминале компьютера выполните:
edge-impulse-data-forwarder --clean. - Введите учётные данные Edge Impulse.
- Выберите проект и настройте параметры устройства.

- Откройте проект в Edge Impulse Studio. В разделе
Deviceможно проверить, правильно ли подключён комплект (точка должна быть зелёной).

Сбор данных в Studio
Как обсуждалось ранее, нужно собрать данные для всех четырёх классов перевозки. Представьте, что у вас есть контейнер со встроенным акселерометром (в нашем случае это XIAOML Kit). Теперь представьте, что контейнер стоит на корабле в бушующем океане:

Или в грузовике, который едет по дороге, или его перемещает вилочный погрузчик и т. д.
Имитация движения
Класс «Морская перевозка»:
- держите комплект и имитируйте движение лодки;
- двигайте его по всем трём осям волнообразными, колебательными движениями;
- добавьте плавные покачивания с крена на дифферент.
Класс «Наземная перевозка»:
- двигайте комплект горизонтально по прямым линиям (слева направо и обратно);
- имитируйте вибрации грузовика или поезда небольшими горизонтальными встряхиваниями;
- иногда делайте плавные толчки и повороты.
Класс «Подъём»:
- двигайте комплект преимущественно по вертикали (вверх и вниз);
- имитируйте работу погрузчика: подъём, пауза, опускание;
- добавьте несколько коротких горизонтальных перемещений для позиционирования.
Класс «Простой»:
- поставьте комплект на устойчивую поверхность;
- минимальное движение или его отсутствие;
- фиксируйте окружающие вибрации и шум датчика.
Получение данных
В разделе Data Acquisition вы должны увидеть, что ваша плата [xiaoml-kit] подключена. Датчик доступен: [sensor with 3 axes (accX, accY, accZ)], с частотой дискретизации [50 Hz]. Studio предлагает длину образца [10000] мс (10 с). Осталось задать метку образца. Начнём, например, с [terrestrial].
Нажмите [Start Sample] и двигайте комплект горизонтально (слева направо), сохраняя одно направление. Через 10 секунд данные загрузятся в Studio.
Ниже показан один образец (сырые данные) длиной 10 секунд. Видно, что волнообразное движение происходит преимущественно по оси Y (слева направо). Остальные оси почти неподвижны: ось X колеблется около нуля, а ось Z — около 9,8 м/с² из-за гравитации.

Для каждого из четырёх классов соберите, например, около 2 минут данных (десять–двенадцать образцов по 10 секунд). С помощью значка 3 dots после каждого образца выберите два и перенесите их в Test set (тестовую выборку). Либо используйте автоматический инструмент Train/Test Split в разделе Danger Zone вкладки Dashboard. Ниже видны наборы данных:

Предобработка данных
Сырые данные акселерометра представляют собой «временной ряд», который нужно преобразовать в «табличные данные». Такое преобразование выполняется с помощью скользящего окна по образцам. Например, на рисунке ниже

видно 10 секунд данных акселерометра, записанных с частотой дискретизации (SR) 50 Гц. Окно длиной 2 секунды захватывает 300 точек данных (3 оси × 2 секунды × 50 отсчётов). Мы будем сдвигать это окно каждые 200 мс, создавая набор большего размера, где каждый экземпляр содержит 300 сырых признаков.
Выбирайте оптимальную частоту дискретизации для своей задачи, учитывая теорему Найквиста: периодический сигнал нужно дискретизировать с частотой, более чем вдвое превышающей его наибольшую частотную составляющую.
Предобработка данных — непростая задача для встраиваемого машинного обучения. Тем не менее Edge Impulse помогает справиться с ней благодаря шагу цифровой обработки сигналов (DSP), а точнее — спектральным признакам.
В Studio этот набор данных поступит на вход блока спектрального анализа (Spectral Analysis), который отлично подходит для анализа повторяющихся движений, например данных акселерометров. Блок выполняет DSP (цифровую обработку сигналов) и извлекает такие признаки, как «FFT» или «вейвлеты». В самом распространённом случае, при FFT, статистические признаки временной области для каждой оси/канала таковы:
- RMS;
- асимметрия (Skewness);
- эксцесс (Kurtosis).

А спектральные признаки частотной области для каждой оси/канала:
- спектральная мощность (Spectral Power);
- асимметрия (Skewness);
- эксцесс (Kurtosis).

Например, при длине FFT 32 точки на выходе блока спектрального анализа будет 21 признак на каждую ось (всего 63 признака).
Эти 63 признака станут входным тензором для нейросетевого классификатора и для модели обнаружения аномалий (K-Means).
Подробнее можно узнать из лабораторной работы Спектральные признаки DSP.
Проектирование модели
Наш классификатор будет полносвязной нейронной сетью (Dense Neural Network, DNN). У неё будет 63 нейрона на входном слое, два скрытых слоя с 20 и 10 нейронами и выходной слой с четырьмя нейронами (по одному на каждый класс), как показано здесь:

Проектирование Impulse
Impulse принимает сырые данные, с помощью обработки сигналов извлекает признаки, а затем использует блок обучения (Dense model), чтобы классифицировать новые данные.
Мы также используем вторую модель — K-means, которая применяется для обнаружения аномалий. Представим, что известные классы — это кластеры. Любой образец, который не попадает ни в один из этих кластеров, может оказаться выбросом, аномалией (например, контейнер, сорвавшийся с корабля в океан, или лежащий вверх дном на полу).

Представьте, что наш XIAOML Kit катится или движется вверх дном, с движением, которого не было в обучающей выборке.

Итоговый дизайн Impulse:

Генерация признаков
На этом этапе проекта мы определили метод предобработки, и модель спроектирована. Теперь пора выполнить работу. Сначала преобразуем сырые данные (временной ряд) в табличные. Перейдите на вкладку Spectral Features и нажмите [Save Parameters]. Либо вместо значений по умолчанию нажмите кнопку [Autotune parameters]. В этом случае Studio сама подберёт гиперпараметры, например конструкцию фильтра и длину FFT, на основе сырых данных.

В верхнем меню выберите вкладку Generate features, отметьте опции Calculate feature importance и Normalize features и нажмите [Generate features]. Каждое двухсекундное окно данных (300 точек) будет преобразовано в одну табличную точку с 63 признаками.
Feature Explorer отобразит эти данные в 2D с помощью UMAP. Uniform Manifold Approximation and Projection (UMAP) — метод снижения размерности, который используется для визуализации, похоже на t-SNE, но также и для общего нелинейного снижения размерности.
Визуализация показывает, что классы хорошо разделяются, а значит, классификатор должен работать хорошо.

По желанию можно проанализировать относительную важность каждого признака для одного класса по сравнению с другими классами.
Обучение
Наш классификатор — полносвязная нейронная сеть (DNN) со 63 нейронами на входном слое, двумя скрытыми слоями с 20 и 10 нейронами и выходным слоем из четырёх нейронов (по одному на класс), как показано здесь:

В качестве гиперпараметров используем скорость обучения (Learning Rate) 0,005 и 20 % данных для валидации, 30 эпох. После обучения точность составляет 100 %.

Для обнаружения аномалий выберем предложенные признаки, которые как раз и являются наиболее важными в извлечении признаков. Количество кластеров будет 32, как предлагает Studio. После обучения можно выбрать часть данных для проверки, например данные морской перевозки. Полученный показатель аномалий: min: -0.1642, max: 0.0738, avg: -0.0867.
Если менять данные, можно заметить, что малые или отрицательные значения показателя аномалий означают, что данные нормальные.

Тестирование
С помощью 20 % данных, оставшихся после этапа сбора, можно проверить, как модель поведёт себя на неизвестных данных. Ожидается 100 %, но результат оказался очень хорошим — 8 % ошибок.

Также используйте свой комплект (он всё ещё подключён к Studio) и проведите живую классификацию. Например, проверим движение «наземной перевозки»:

Учтите, что здесь вы захватываете реальные данные с устройства и загружаете их в Studio, где выполняется инференс с помощью обученной модели (модель при этом не находится на вашем устройстве).
Развёртывание
Пришло время для волшебства! Studio упакует все нужные библиотеки, функции предобработки и обученные модели и загрузит их на ваш компьютер. Выберите вариант Arduino Library, затем внизу выберите Quantized (Int8) и нажмите [Build]. ZIP-файл будет создан и загружен на компьютер.

В Arduino IDE откройте вкладку Sketch, выберите пункт Add .ZIP Library и укажите ZIP-файл, который скачала Studio:

Инференс
Теперь пора провести настоящий тест. Мы будем делать инференс полностью независимо от Studio. Изменим один из примеров кода, который создала Studio при развёртывании библиотеки Arduino.
В Arduino IDE откройте вкладку File/Examples, найдите свой проект и в примерах выберите nano_ble_sense_accelerometer:
Конечно, это не ваша плата, но код можно адаптировать, внеся всего несколько изменений.
Например, в начале кода подключается библиотека для IMU Arduino Sense:
/* Подключение библиотек -------------------------------- */
#include <XIAOML_Kit_Motion_Class_-_AD_inferencing.h>
#include <Arduino_LSM9DS1.h>
Замените блок подключений на код для IMU:
#include <XIAOML_Kit_Motion_Class_-_AD_inferencing.h>
#include <LSM6DS3.h>
#include <Wire.h>
Измените константы:
// Настройка IMU
LSM6DS3 myIMU(I2C_MODE, 0x6A);
// Настройки инференса
#define CONVERT_G_TO_MS2 9.81f
#define MAX_ACCEPTED_RANGE 2.0f * CONVERT_G_TO_MS2
В функции setup инициализируйте IMU:
// Инициализация IMU
if (myIMU.begin() != 0) {
Serial.println("ОШИБКА: не удалось инициализировать IMU!");
return;
}
В функции loop буферы buffer[ix], buffer[ix + 1] и buffer[ix + 2] получают данные трёхосного акселерометра. В исходном коде есть строка:
IMU.readAcceleration(buffer[ix], buffer[ix + 1], buffer[ix + 2]);
Замените её на такой блок кода:
// Читаем данные IMU
float x = myIMU.readFloatAccelX();
float y = myIMU.readFloatAccelY();
float z = myIMU.readFloatAccelZ();
Два блока кода нужно поменять местами. Сначала выполняется перевод в «метры на секунду в квадрате» (м/с²), затем проверка максимально допустимого диапазона (здесь он задан в м/с², а в примере для Arduino — в g):
// Переводим в м/с²
buffer[i + 0] = x * CONVERT_G_TO_MS2;
buffer[i + 1] = y * CONVERT_G_TO_MS2;
buffer[i + 2] = z * CONVERT_G_TO_MS2;
// Ограничиваем диапазон
for (int j = 0; j < 3; j++) {
if (fabs(buffer[i + j]) > MAX_ACCEPTED_RANGE) {
buffer[i + j] = copysign(MAX_ACCEPTED_RANGE, buffer[i + j]);
}
}
На этом изменения заканчиваются. Вывод инференса в Serial Monitor тоже можно настроить. Теперь загрузите полный код ниже на плату и переходите к инференсу.
// Классификация движения с датчиком LSM6DS3TR-C
#include <XIAOML_Kit_Motion_Class_-_AD_inferencing.h>
#include <LSM6DS3.h>
#include <Wire.h>
// Настройка IMU
LSM6DS3 myIMU(I2C_MODE, 0x6A);
// Настройки инференса
#define CONVERT_G_TO_MS2 9.81f
#define MAX_ACCEPTED_RANGE 2.0f * CONVERT_G_TO_MS2
static bool debug_nn = false;
static float buffer[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE] = { 0 };
static float inference_buffer[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
Serial.println("XIAOML Kit — классификация движения");
Serial.println("Инференс с датчиком LSM6DS3TR-C");
// Инициализация IMU
if (myIMU.begin() != 0) {
Serial.println("ОШИБКА: не удалось инициализировать IMU!");
return;
}
Serial.println("✓ IMU инициализирован");
if (EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME != 3) {
Serial.println("ОШИБКА: EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME "
"должен быть равен 3");
return;
}
Serial.println("✓ Модель загружена");
Serial.println("Запуск классификации движения...");
}
void loop() {
ei_printf("\nЗапуск инференса через 2 секунды...\n");
delay(2000);
ei_printf("Сбор данных...\n");
// Очищаем буфер
for (size_t i = 0; i < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; i++) {
buffer[i] = 0.0f;
}
// Собираем данные акселерометра
for (int i = 0; i < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; i += 3) {
uint64_t next_tick = micros() +
(EI_CLASSIFIER_INTERVAL_MS * 1000);
// Читаем данные IMU
float x = myIMU.readFloatAccelX();
float y = myIMU.readFloatAccelY();
float z = myIMU.readFloatAccelZ();
// Переводим в м/с²
buffer[i + 0] = x * CONVERT_G_TO_MS2;
buffer[i + 1] = y * CONVERT_G_TO_MS2;
buffer[i + 2] = z * CONVERT_G_TO_MS2;
// Ограничиваем диапазон
for (int j = 0; j < 3; j++) {
if (fabs(buffer[i + j]) > MAX_ACCEPTED_RANGE) {
buffer[i + j] = copysign(MAX_ACCEPTED_RANGE,
buffer[i + j]);
}
}
delayMicroseconds(next_tick - micros());
}
// Копируем во входной буфер инференса
for (int i = 0; i < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; i++) {
inference_buffer[i] = buffer[i];
}
// Создаём сигнал из буфера
signal_t signal;
int err = numpy::signal_from_buffer(inference_buffer,
EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
if (err != 0) {
ei_printf("ОШИБКА: не удалось создать сигнал из буфера (%d)\n",
err);
return;
}
// Запускаем классификатор
ei_impulse_result_t result = { 0 };
err = run_classifier(&signal, &result, debug_nn);
if (err != EI_IMPULSE_OK) {
ei_printf("ОШИБКА: не удалось запустить классификатор (%d)\n", err);
return;
}
// Выводим предсказания
ei_printf("Предсказания (DSP: %d мс, классификация: %d мс, "
"аномалии: %d мс):\n",
result.timing.dsp, result.timing.classification, result.timing.anomaly);
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf(" %s: %.5f\n", result.classification[ix].label,
result.classification[ix].value);
}
// Выводим показатель аномалии
#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf("Показатель аномалии: %.3f\n", result.anomaly);
#endif
// Определяем предсказание
float max_confidence = 0.0;
String predicted_class = "unknown";
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
if (result.classification[ix].value > max_confidence) {
max_confidence = result.classification[ix].value;
predicted_class = String(result.classification[ix].label);
}
}
// Выводим результат с порогом уверенности
if (max_confidence > 0.6) {
ei_printf("\n🎯 ПРЕДСКАЗАНИЕ: %s (уверенность %.1f%%)\n",
predicted_class.c_str(), max_confidence * 100);
} else {
ei_printf("\n❓ НЕОПРЕДЕЛЁННО: наибольшая уверенность у %s (%.1f%%)\n",
predicted_class.c_str(), max_confidence * 100);
}
// Проверяем аномалию
#if EI_CLASSIFIER_HAS_ANOMALY == 1
if (result.anomaly > 0.5) {
ei_printf("⚠️ ОБНАРУЖЕНА АНОМАЛИЯ! Показатель: %.3f\n", result.anomaly);
}
#endif
delay(1000);
}
void ei_printf(const char *format, ...) {
static char print_buf[1024] = { 0 };
va_list args;
va_start(args, format);
int r = vsnprintf(print_buf, sizeof(print_buf), format, args);
va_end(args);
if (r > 0) {
Serial.write(print_buf);
}
}
Полный код доступен на странице лабораторной работы на GitHub.
Теперь попробуйте свои движения и посмотрите результат инференса для каждого класса на изображениях:




И, конечно, попробуйте «аномалию», например переверните XIAO вверх дном. Показатель аномалии превысит 0,5:

Постобработка
Теперь, когда мы убедились, что модель работает, предлагаем изменить код так, чтобы результат выводился полностью офлайн: без подключения к ПК и с питанием от аккумулятора, пауэрбанка или отдельного источника на 5 В.
Идея в том, что при обнаружении определённого движения на OLED-дисплее появляется соответствующее сообщение.

Изменённый код инференса с выводом на OLED-дисплей доступен на странице лабораторной работы на GitHub.
Итоги
В этой работе мы показали, как построить полную систему классификации движения на основе встроенного 6-осевого датчика IMU LSM6DS3TR-C из XIAOML Kit. Основные достижения:
Техническая реализация:
- использован встроенный 6-осевой IMU для регистрации движения;
- собраны размеченные обучающие данные для четырёх сценариев транспортировки;
- реализовано извлечение спектральных признаков для анализа временных рядов;
- развёрнут нейросетевой классификатор, оптимизированный для инференса на микроконтроллере;
- добавлено обнаружение аномалий для выявления необычных движений.
Конвейер машинного обучения:
- сбор данных напрямую со встраиваемых датчиков;
- инженерия признаков с помощью анализа в частотной области;
- обучение и оптимизация модели в Edge Impulse;
- инференс в реальном времени на аппаратуре с ограниченными ресурсами;
- контроль производительности и валидация.
Практические применения: изученные методы напрямую применяются в реальных сценариях, включая:
- отслеживание активов и мониторинг логистики;
- предиктивное обслуживание оборудования;
- распознавание человеческой активности;
- мониторинг транспортных средств и оборудования;
- сети IoT-датчиков для «умных городов».
Ключевые выводы:
- работа с системами координат IMU и слиянием данных датчиков;
- баланс между точностью модели и скоростью инференса на периферийных устройствах;
- построение надёжных конвейеров сбора и предобработки данных;
- развёртывание моделей машинного обучения во встраиваемых системах;
- объединение нескольких датчиков (IMU + дисплей) в полноценные решения.
Интеграция классификации движения с XIAOML Kit показывает, как современные встраиваемые системы могут решать сложные задачи ИИ локально и принимать решения в реальном времени без облака. Такой подход лежит в основе будущего периферийного ИИ в промышленном IoT, автономных системах и умных устройствах.
Ресурсы
- Код XIAOML KIT
- Спектральные признаки DSP
- Проект Edge Impulse
- Блокнот Colab с блоком спектральных признаков Edge Impulse
- Документация Edge Impulse
- Спектральные признаки Edge Impulse
- Библиотека LSM6DS3 от Seeed Studio
Обнаружение объектов

Промпт DALL·E: мультфильм в стиле анимации 1950-х с подробной платой, на которой видны датчики, в первую очередь камера, на столе с узорчатой скатертью. За платой стоит компьютер с большой задней панелью, на котором открыта Arduino IDE. Содержимое IDE намекает на назначение выводов светодиодов и инференс модели машинного обучения для распознавания голосовых команд. Serial Monitor в отдельном окне показывает результаты для команд «yes» и «no».
Обзор
В предыдущем разделе о компьютерном зрении (CV) и XIAO ESP32S3, посвящённом классификации изображений, мы научились настраивать эту замечательную отладочную плату и классифицировать изображения. Продолжая знакомство с компьютерным зрением, мы изучим обнаружение объектов на микроконтроллерах.
Обнаружение объектов и классификация изображений
Основная задача моделей классификации изображений — определить наиболее вероятную категорию объекта, присутствующего на изображении, например отличить кота от собаки, то есть доминирующий «объект» на изображении:
Но что будет, если на изображении нет доминирующей категории?
Модель классификации изображений совершенно неверно определяет это изображение как «ashcan» (мусорный ящик), возможно из-за цветовых оттенков.
Модель, которая использовалась на предыдущих изображениях, — MobileNet, обученная на большом наборе данных ImageNet, работающая на Raspberry Pi.
Чтобы решить эту проблему, нужен другой тип моделей, которые находят не только несколько категорий (или меток), но и где на изображении расположены объекты.
Такие модели гораздо сложнее и крупнее, например MobileNetV2 SSD FPN-Lite 320x320, обученная на наборе COCO. Эта предобученная модель обнаружения объектов умеет находить до 10 объектов на изображении и выдаёт ограничивающий прямоугольник (bounding box) для каждого обнаруженного объекта. Изображение ниже — результат работы такой модели на Raspberry Pi:
Модели для обнаружения объектов (такие как MobileNet SSD или YOLO) обычно занимают несколько мегабайт. Для Raspberry Pi это нормально, но для встраиваемых устройств, где ОЗУ обычно составляет в лучшем случае несколько мегабайт, как у XIAO ESP32S3, такие модели не подходят.
Инновационное решение для обнаружения объектов: FOMO
Edge Impulse представила в 2022 году FOMO (Faster Objects, More Objects) — новое решение для обнаружения объектов на встраиваемых устройствах, таких как Nicla Vision и Portenta (Cortex M7), процессорах Cortex M4F (Arduino Nano33 и серии OpenMV M4), а также устройствах Espressif ESP32 (ESP-CAM, ESP-EYE и XIAO ESP32S3 Sense).
В этом практическом проекте мы будем обнаруживать объекты с помощью FOMO.
Чтобы больше узнать о FOMO, прочитайте официальное объявление FOMO от Edge Impulse, где Louis Moreau и Mat Kelcey подробно объясняют принцип работы.
Цель проекта обнаружения объектов
Любой проект машинного обучения должен начинаться с подробно сформулированной цели. Допустим, мы работаем на промышленном или сельском предприятии и должны сортировать и считать апельсины (фрукты) и определённых лягушек (жуки).
Иначе говоря, нам нужна многоклассовая классификация, где каждое изображение может относиться к трём классам:
- фон (объектов нет);
- фрукт (Fruit);
- жук (Bug).
Вот несколько неразмеченных примеров изображений, по которым мы будем обнаруживать объекты (фрукты и жуков):
Нас интересует, какой объект есть на изображении, его положение (центроид) и сколько таких объектов на нём присутствует. Размер объекта FOMO не определяет — в отличие от MobileNet SSD или YOLO, где ограничивающий прямоугольник является одним из выходных данных модели.
Проект мы будем разрабатывать с использованием XIAO ESP32S3 для захвата изображений и инференса. Проект машинного обучения будет создан в Edge Impulse Studio. Но прежде чем начинать проект обнаружения объектов в Studio, создадим сырой набор данных (без разметки) с изображениями, на которых есть объекты для обнаружения.
Сбор данных
Изображения можно снимать с помощью XIAO, телефона или других устройств. Здесь мы будем использовать XIAO с кодом из библиотеки ESP32 для Arduino IDE.
Сбор набора данных с XIAO ESP32S3
Откройте Arduino IDE и выберите плату XIAO_ESP32S3 (и порт, к которому она подключена). В меню File > Examples > ESP32 > Camera выберите CameraWebServer.
На панели BOARDS MANAGER (менеджер плат) убедитесь, что установлен последний стабильный (stable) пакет.
⚠️ Внимание
Альфа-версии (например, 3.x-alpha) некорректно работают с XIAO и Edge Impulse. Используйте последнюю стабильную версию (например, 2.0.11).
Также закомментируйте все модели камер, кроме выводов модели XIAO:
#define CAMERA_MODEL_XIAO_ESP32S3 // Есть PSRAM
И включите PSRAM в меню Tools. Введите данные Wi-Fi и загрузите код на устройство:
Если код выполнился правильно, в Serial Monitor появится адрес:
Скопируйте адрес в браузер и дождитесь загрузки страницы. Выберите разрешение камеры (например, QVGA) и нажмите [START STREAM]. Подождите несколько секунд или минут в зависимости от качества соединения. Кнопкой [Save] можно сохранить изображение в папку загрузок на компьютере.
Edge Impulse рекомендует, чтобы объекты были похожего размера и не перекрывали друг друга, — так модель работает лучше. На промышленном предприятии это нормально: камера закреплена неподвижно и находится на одном расстоянии от объектов. Тем не менее мы попробуем и смешанные размеры и положения, чтобы посмотреть на результат.
Отдельные папки для изображений создавать не нужно, потому что каждое изображение содержит несколько меток.
Рекомендуем около 50 изображений, с разным сочетанием объектов и разным их количеством в кадре. Старайтесь снимать с разных углов, на разном фоне и при разном освещении.
Сохраняемые изображения имеют размер кадра QVGA и формат RGB565 (формат цветных пикселей).
Когда набор данных собран, нажмите [Stop Stream] и переместите изображения в папку.
Edge Impulse Studio
Настройка проекта
Откройте Edge Impulse Studio, войдите в систему по кнопке Login (или создайте аккаунт) и начните новый проект.
Здесь можно клонировать проект, созданный для этой практической работы: XIAO-ESP32S3-Sense-Object_Detection.
На панели проекта Dashboard прокрутите вниз до Project info, выберите Bounding boxes (object detection) и Espressif ESP-EYE (наиболее похожее на нашу плату) в качестве целевого устройства:
Загрузка неразмеченных данных
В Studio откройте вкладку Data acquisition и в разделе UPLOAD DATA загрузите снятые изображения из папки на компьютере.
Можно поручить Studio автоматически разделить данные на обучающую и тестовую выборки или сделать это вручную. Мы загрузим все изображения как обучающие.
Все неразмеченные изображения (47) загружены, но перед использованием в наборе данных их нужно правильно разметить. Для этого в Studio есть инструмент, который открывается по ссылке Labeling queue (47).
В бесплатной версии Edge Impulse Studio доступны два способа разметки с помощью ИИ:
- с использованием YOLOv5;
- отслеживанием объектов между кадрами.
Для клиентов Enterprise Edge Impulse запустила функцию автоматической разметки, которая упрощает разметку в проектах обнаружения объектов.
Обычные объекты можно быстро распознать и разметить с помощью готовой библиотеки предобученных моделей обнаружения YOLOv5 (обученных на наборе COCO). Но в нашем случае объектов нет в наборах COCO, поэтому выберем опцию tracking objects (отслеживание объектов). С ней, как только вы нарисуете рамки и разметите изображения в одном кадре, объекты будут автоматически отслеживаться от кадра к кадру, и новые объекты будут частично размечены (не все — корректно).
Если у вас уже есть размеченный набор данных с ограничивающими рамками, импортируйте его через загрузчик EI.
Разметка набора данных
Начните с первого неразмеченного изображения: с помощью мыши обведите объект рамкой, чтобы добавить метку. Затем нажмите Save labels, чтобы перейти к следующему элементу.
Продолжайте, пока очередь не опустеет. В конце все изображения должны быть размечены так же, как примеры ниже:
Затем просмотрите размеченные примеры на вкладке Data acquisition. Если какая-то метка неверна, её можно отредактировать через меню «три точки» после названия образца:
Studio подскажет, как заменить неверную метку и исправить набор данных.
Балансировка набора данных и разделение на обучающую и тестовую выборки
После разметки всех данных стало очевидно, что класса «фрукт» гораздо больше, чем «жука». Поэтому собрали 11 дополнительных изображений жуков (всего стало 58). После их разметки нужно отобрать часть изображений в тестовую выборку. Это делается через меню «три точки» после названия изображения. Я отобрал шесть изображений, что составляет 13 % от общего набора данных.
Проектирование Impulse
На этом этапе нужно определить, как:
- Предобработка: изменить размер отдельных изображений с до , сжав их к квадратной форме (без обрезки). Затем изображения переводятся из RGB в оттенки серого (Grayscale).
- Проектирование модели: в данном случае «Object Detection» (обнаружение объектов).
Предобработка всего набора данных
В этом разделе выберите Color depth — Grayscale, подходящий для моделей FOMO, и сохраните параметры (Save parameters).
Studio автоматически переходит к разделу Generate features, где обрабатываются все образцы. В результате получается набор данных из отдельных изображений , или 9 216 признаков.
Просмотр признаков показывает, что после генерации все образцы хорошо разделяются.
Некоторые образцы, судя по всему, находятся не в том месте, но щелчок по ним подтверждает правильность разметки.
Проектирование модели, обучение и тестирование
Мы будем использовать FOMO — модель обнаружения объектов на основе MobileNetV2 (альфа 0,35), которая грубо сегментирует изображение на сетку: фон и интересующие объекты (здесь фрукты и жуки).
FOMO — инновационная модель машинного обучения для обнаружения объектов, которая может использовать в 30 раз меньше энергии и памяти, чем традиционные модели вроде Mobilenet SSD и YOLOv5. FOMO способна работать на микроконтроллерах с ОЗУ менее 200 КБ. Главная причина в том, что другие модели вычисляют размер объекта, рисуя вокруг него квадрат (ограничивающий прямоугольник), а FOMO не учитывает размер и выдаёт только информацию о том, где объект находится на изображении, — в виде координат его центроида.
Как работает FOMO
FOMO берёт изображение в оттенках серого и делит его на блоки пикселей с коэффициентом 8. Для входного изображения сетка будет . Затем FOMO запускает классификатор для каждого блока пикселей, чтобы вычислить вероятность того, что в нём есть фрукт или жук, и определяет области с наибольшей вероятностью наличия объекта (если в блоке пикселей объектов нет, он классифицируется как background). По перекрытию итоговой области FOMO выдаёт координаты (относительно размеров изображения) центроида этой области.
Для обучения нужно выбрать предобученную модель. Используем FOMO (Faster Objects, More Objects) MobileNetV2 0.35. Эта модель занимает около 250 КБ ОЗУ и 80 КБ ПЗУ (флеш-памяти), что хорошо подходит для нашей платы.
Гиперпараметры обучения такие:
- эпох: 60;
- размер батча (batch size): 32;
- скорость обучения (learning rate): 0,001.
Для валидации во время обучения будут отложены 20 % набора данных (validation_dataset). Для оставшихся 80 % (train_dataset) применим аугментацию данных: случайно отразим, изменим размер и яркость изображений и обрежем их, искусственно увеличивая количество образцов для обучения.
В итоге модель получает общую F1-меру 85 %, что близко к результату на тестовых данных (83 %).
FOMO автоматически добавил третью метку background к двум заданным ранее (fruit и bug).
В задачах обнаружения объектов точность, как правило, не является основной метрикой оценки. Обнаружение объектов включает их классификацию и ограничивающие рамки вокруг них, то есть это более сложная задача, чем простая классификация. Проблема в том, что у нас нет ограничивающих рамок, только центроиды. Коротко говоря, использование точности как метрики может вводить в заблуждение и не даёт полной картины того, насколько хорошо работает модель. Поэтому мы используем F1-меру.
Проверка модели в режиме «Live Classification»
После обучения модели её можно проверить с помощью инструмента Live Classification. В соответствующем разделе нажмите значок подключения платы разработки (небольшой микроконтроллер) и отсканируйте QR-код телефоном.
После подключения телефон можно использовать для съёмки реальных изображений, которые будет тестировать обученная модель в Edge Impulse Studio.
Учтите, что модель может давать ложные срабатывания и пропуски. Это можно уменьшить, задав подходящий Confidence Threshold (порог уверенности; настройка через меню «три точки»). Попробуйте значение 0,8 и выше.
Развёртывание модели (Arduino IDE)
Выберите Arduino Library и модель Quantized (int8), включите компилятор EON на вкладке Deploy и нажмите [Build].
Откройте Arduino IDE, в меню Sketch выберите Include Library и Add .ZIP Library. Выберите файл, скачанный из Edge Impulse Studio, — и готово!
На вкладке Examples в Arduino IDE под именем вашего проекта вы найдёте скетч (esp32 > esp32_camera).
Нужно изменить строки с 32 по 75, которые задают модель камеры и выводы, используя данные нашей модели. Скопируйте приведённые ниже строки и замените ими строки 32–75:
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
Вот получившийся код:
Загрузите код на XIAO ESP32S3 Sense — и можно начинать обнаруживать фрукты и жуков. Результат можно проверить в Serial Monitor.
Фон
Фрукты
Жуки
Обратите внимание: задержка модели составляет 143 мс, а частота кадров — около 7 кадров в секунду (примерно как в проекте классификации изображений). Это объясняется тем, что FOMO искусно построена на основе CNN-модели, а не модели обнаружения вроде SSD MobileNet. Например, при запуске модели MobileNetV2 SSD FPN-Lite на Raspberry Pi 4 задержка примерно в пять раз выше (около 1,5 кадра в секунду).
Развёртывание модели (SenseCraft Web Toolkit)
Как обсуждалось в главе о классификации изображений, проверка инференса моделей для изображений в Arduino IDE очень затруднительна, потому что мы не видим, на что направлена камера. Поэтому воспользуемся SenseCraft Web Toolkit.
Чтобы запустить SenseCraft Web Toolkit, выполните следующие шаги:
- Откройте сайт SenseCraft Web Toolkit.
- Подключите XIAO к компьютеру:
- При подключённом XIAO выберите его, как показано ниже:
- Выберите устройство и порт, затем нажмите
[Connect]:
Можно попробовать несколько моделей компьютерного зрения, заранее загруженных Seeed Studio. Экспериментируйте!
В нашем случае мы используем синюю кнопку внизу страницы: [Upload Custom AI Model].
Но сначала нужно скачать из Edge Impulse Studio нашу квантованную модель .tflite.
- Перейдите в свой проект в Edge Impulse Studio или клонируйте этот:
- На вкладке
Dashboardскачайте модель («block output»):Object Detection model — TensorFlow Lite (int8 quantized).
- В SenseCraft Web Toolkit нажмите синюю кнопку внизу страницы:
[Upload Custom AI Model]. Откроется окно. Укажите файл модели, скачанный из Edge Impulse Studio, выберите имя модели и введите метки (ID: Object):
Учтите, что нужно использовать метки, заданные в EI Studio, и вводить их в алфавитном порядке (в нашем случае: background, bug, fruit).
Через несколько секунд (или минут) модель загрузится на устройство, и изображение с камеры появится в реальном времени в области предпросмотра (Preview):
Обнаруженные объекты будут отмечены (центроиды). Можно выбрать уверенность инференса курсором Confidence и IoU — меру, которая оценивает точность предсказанных ограничивающих рамок по сравнению с истинными.
Нажав верхнюю кнопку (Device Log), можно открыть Serial Monitor и следить за инференсом, как мы делали в Arduino IDE.
В Device Log вы получите такие данные:
- время предобработки (захват и обрезка изображения): 3 мс;
- время инференса (задержка модели): 115 мс;
- время постобработки (вывод изображения и отметка объектов): 1 мс;
- выходной тензор (рамки), например одна из рамок: [[30, 150, 20, 20, 97, 2]]. Здесь 30, 150, 20, 20 — координаты рамки (вокруг центроида); 97 — результат инференса, а 2 — класс (в данном случае 2: fruit, фрукт).
В приведённом примере получилось 5 рамок, поскольку ни один фрукт не получил по 3 центроида. Одно из решений — постобработка, при которой близкие центроиды объединяются в один.
Ещё несколько снимков экрана:
Итоги
FOMO — важный скачок в области обработки изображений, как сказали Louis Moreau и Mat Kelcey при её запуске в 2022 году:
FOMO — революционный алгоритм, который впервые приносит обнаружение, отслеживание и подсчёт объектов в реальном времени на микроконтроллеры.
Для исследования обнаружения объектов (и, точнее, их подсчёта) на встраиваемых устройствах есть множество возможностей.
Ресурсы
Настройка

Промпт DALL·E: рисунок в стиле мультфильма 1950-х с платой XIAO ESP32S3 с характерным модулем камеры, как на предоставленном изображении. Плата стоит на классическом лабораторном столе среди различных датчиков, включая микрофон. За платой — старинный экран компьютера с Arduino IDE в приглушённых цветах; код посвящён настройке выводов светодиодов и инференсу модели машинного обучения для голосовых команд. Serial Monitor в IDE показывает распознанные команды «yes» и «no». Сцена сочетает ретро-шарм лабораторий середины века с современной электроникой.
Обзор
XIAOML Kit предназначен для практического знакомства с приложениями TinyML. В комплект входят мощная отладочная плата XIAO ESP32S3 Sense и плата расширения, которая добавляет датчики, необходимые для проектов машинного обучения.
Состав комплекта XIAOML Kit:
- XIAO ESP32S3 Sense: основная отладочная плата со встроенной камерой, цифровым микрофоном и поддержкой SD-карт;
- Плата расширения: содержит 6-осевой IMU (LSM6DS3TR-C) и 0,42-дюймовый OLED-дисплей для регистрации движения и визуализации данных;
- Набор для работы с SD-картой: включает SD-карту и адаптер USB для хранения данных и развёртывания моделей;
- Кабель USB-C: для подключения платы к компьютеру;
- Антенна и радиаторы.
⚠️ Внимание
Не устанавливайте радиаторы на плату XIAO ESP32S3 (или снимите их аккуратно), если хотите использовать плату расширения XIAO ML Kit. Подробнее см. в приложении.
Основные особенности платы XIAO ESP32S3 Sense
XIAO ESP32S3 Sense — сердце комплекта XIAOML Kit. Она объединяет вычислительные возможности для ML с фото- и аудиофункциями и подходит для проектов TinyML в области интеллектуального голосового и визуального ИИ.
Ключевые особенности
- Мощный микроконтроллер: 32-разрядный двухъядерный процессор ESP32S3 с архитектурой Xtensa, работающий на частоте до 240 МГц, с поддержкой Arduino и MicroPython;
- Расширенные функции: съёмная камера OV2640 с разрешением , совместимая с камерой OV5640, а также встроенный цифровой микрофон;
- Продуманное питание: управление зарядом литиевой батареи, четыре режима энергопотребления, режим глубокого сна с потреблением всего 14 мкА;
- Достаточно памяти: 8 МБ PSRAM и 8 МБ флеш-памяти, слот для SD-карты до 32 ГБ (FAT);
- Отличные радиохарактеристики: двойная беспроводная связь Wi-Fi 2,4 ГГц и BLE, дальность связи более 100 м при использовании антенны с разъёмом U.FL;
- Компактный корпус: мм, классический форм-фактор XIAO, удобный для проектов с ограниченным местом.
Ниже показана общая распиновка платы:
Подробности см. на вики Seeed Studio.
Особенности платы расширения
Плата расширения дополняет XIAOML Kit возможностями для приложений машинного обучения, основанных на движении:
Компоненты:
-
6-осевой IMU (LSM6DS3TR-C):
- 3-осевой акселерометр и 3-осевой гироскоп для обнаружения и классификации движения;
- диапазон акселерометра: ±2/±4/±8/±16 g;
- диапазон гироскопа: ±125/±250/±500/±1000/±2000 dps;
- интерфейс I2C (адрес: 0x6A).
- 3-осевой акселерометр и 3-осевой гироскоп для обнаружения и классификации движения;
-
0,42-дюймовый OLED-дисплей:
- монохромный дисплей (разрешение ) для визуализации данных в реальном времени;
- контроллер: SSD1306;
- интерфейс I2C (адрес: 0x3C).
- монохромный дисплей (разрешение ) для визуализации данных в реальном времени;
-
Кнопка перезапуска (EN);
-
Разъём аккумулятора (BAT+, BAT−).
Сборка комплекта
Плата расширения легко подключается к XIAO ESP32S3 Sense, образуя универсальную платформу для мультимодальных экспериментов с машинным обучением: зрение, звук и движение.
Обратите внимание на ориентацию установки модуля:
Обратите внимание:
- соединение
EN, которое находится внизу ESP32S3 Sense, доступно на плате расширения через кнопкуRST; - соединения
BAT+иBAT-также доступны через белый разъёмBAT3.7V.
Применения XIAOML Kit:
- Зрение: классификация изображений и обнаружение объектов с помощью встроенной камеры;
- Звук: распознавание ключевых слов и голосовых команд со встроенным микрофоном;
- Движение: распознавание активности и обнаружение аномалий с помощью датчиков IMU;
- Мультимодальность: комплексное слияние данных датчиков для сложных приложений ML.
Установка XIAO ESP32S3 Sense в Arduino IDE
- Подключите XIAOML Kit к компьютеру через порт USB-C.
-
Скачайте и установите стабильную версию Arduino IDE для вашей операционной системы.
-
Откройте Arduino IDE и выберите Boards Manager (менеджер плат), который обозначен значком
UNO. -
Введите «ESP32» и выберите «esp32 by Espressif Systems». Можно
установитьилиобновитьпакеты поддержки плат.
Не выбирайте «Arduino ESP32 Boards by Arduino»: это пакет поддержки Arduino Nano ESP32, а не нашей платы.
⚠️ Внимание
Версии 3.x могут создавать проблемы при использовании XIAO ESP32S3 Sense с кодом развёртывания Edge Impulse. Если это так, используйте последнюю стабильную версию 2.0.x (например, 2.0.17).
- Нажмите
Select Board, введите xiao или esp32s3, выберитеXIAO_ESP32S3в менеджере плат и соответствующий порт PORT, к которому подключена ESP32S3.
Готово! Плата должна работать. Проведём несколько тестов.
Проверка платы: мигание светодиодом (Blink)
На плате XIAO ESP32S3 Sense есть встроенный светодиод, подключённый к GPIO21. Поэтому можно запустить скетч Blink, который находится в File/Examples/Basics/Blink. Скетч использует константу Arduino LED_BUILTIN, которая внутри соответствует светодиоду на выводе 21. Или можно изменить скетч Blink вручную:
#define LED_BUILT_IN 21 // Эта строка необязательна
void setup() {
pinMode(LED_BUILT_IN, OUTPUT); // Настраиваем вывод на выход
}
// Помните, что выводы работают с инверсной логикой:
// LOW — включить, HIGH — выключить
void loop() {
digitalWrite(LED_BUILT_IN, LOW); // Включить
delay (1000); // Ждать 1 секунду
digitalWrite(LED_BUILT_IN, HIGH); // Выключить
delay (1000); // Ждать 1 секунду
}
Обратите внимание: выводы работают с инверсной логикой — LOW включает светодиод, HIGH выключает.
Проверка микрофона
Начнём с обнаружения звука. Введите код ниже или перейдите в проект на GitHub и скачайте скетч XIAOML_Kit_Mic_Test, затем запустите его в Arduino IDE:
/*
Простой тест микрофона XIAO ESP32S3
(для библиотеки ESP32 версии 3.0.x и новее)
*/
#include <ESP_I2S.h>
I2SClass I2S;
void setup() {
Serial.begin(115200);
while (!Serial) {
}
// Настройка тактового сигнала PDM на выводе 42 и данных PDM на выводе 41
I2S.setPinsPdmRx(42, 41);
// Запуск I2S с частотой 16 кГц и 16 битами на отсчёт
if (!I2S.begin(I2S_MODE_PDM_RX,
16000,
I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_MONO)) {
Serial.println("Failed to initialize I2S!");
while (1); // ничего не делать
}
}
void loop() {
// Считываем отсчёт
int sample = I2S.read();
if (sample && sample != -1 && sample != 1) {
Serial.println(sample);
}
}
Откройте Serial Plotter — вы увидите кривую изменения громкости звука.
Когда вы издаёте звуки, изменения сигнала видны в Serial Plotter.
Сохранение записанного звука (аудиофайлы .wav) на microSD-карту.
Теперь с помощью встроенного считывателя SD-карт можно сохранять аудиофайлы .wav. Для этого сначала нужно включить PSRAM на XIAO.
У ESP32-S3 на чипе микроконтроллера всего несколько сотен килобайт внутренней оперативной памяти. Этого может не хватить для некоторых задач, поэтому к флеш-чипу SPI можно подключить до 16 МБ внешней PSRAM (псевдостатического ОЗУ). На XIAO установлено 8 МБ PSRAM. Внешняя память включена в адресное пространство процессора и при определённых ограничениях может использоваться так же, как внутренняя оперативная память данных.
- Чтобы её включить, откройте
Tools->PSRAM:"OPI PSRAM"->OPI PSRAM.
XIAO ESP32S3 Sense поддерживает microSD-карты объёмом до 32 ГБ. Если вы собираетесь купить microSD-карту для XIAO, выбирайте карту объёмом не более 32 ГБ. Перед использованием отформатируйте microSD-карту в FAT32.
Теперь вставьте отформатированную в FAT32 SD-карту в XIAO, как показано на фото:
/*
* Диктофон WAV для Seeed XIAO ESP32S3 Sense
* (для библиотеки ESP32 версии 3.0.x и новее)
*/
#include "ESP_I2S.h"
#include "FS.h"
#include "SD.h"
void setup() {
// Создаём экземпляр I2SClass
I2SClass i2s;
// Переменные для хранения аудиоданных
uint8_t *wav_buffer;
size_t wav_size;
// Инициализация последовательного порта
Serial.begin(115200);
while (!Serial) {
delay(10);
}
Serial.println("Initializing I2S bus...");
// Настройка выводов для аудиовхода
i2s.setPinsPdmRx(42, 41);
// Запуск I2S с частотой 16 кГц и 16 битами на отсчёт
if (!i2s.begin(I2S_MODE_PDM_RX,
16000,
I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_MONO)) {
Serial.println("Failed to initialize I2S!");
while (1); // ничего не делать
}
Serial.println("I2S bus initialized.");
Serial.println("Initializing SD card...");
// Настройка выводов для доступа к SD-карте
if(!SD.begin(21)){
Serial.println("Failed to mount SD Card!");
while (1) ;
}
Serial.println("SD card initialized.");
Serial.println("Recording 20 seconds of audio data...");
// Запись 20 секунд аудиоданных
wav_buffer = i2s.recordWAV(20, &wav_size);
// Создаём файл на SD-карте
File file = SD.open("/arduino_rec.wav", FILE_WRITE);
if (!file) {
Serial.println("Failed to open file for writing!");
return;
}
Serial.println("Writing audio data to file...");
// Записываем аудиоданные в файл
if (file.write(wav_buffer, wav_size) != wav_size) {
Serial.println("Failed to write audio data to file!");
return;
}
// Закрываем файл
file.close();
Serial.println("Application complete.");
}
void loop() {
delay(1000);
Serial.printf(".");
}
- Сохраните код, например, как
Wav_Record.ino, и запустите его в Arduino IDE. - Эта программа выполняется только один раз после того, как пользователь откроет Serial Monitor (или нажмёт кнопку
RESET). Она записывает 20 секунд и сохраняет файл на microSD-карту под именем «arduino_rec.wav». - Когда в Serial Monitor каждую секунду появляется «.», выполнение программы завершено, и записанный звуковой файл можно воспроизвести, подключив карту через считыватель.
Качество звука отличное!
Принцип работы кода выходит за рамки этой лабораторной работы, но подробное описание есть на вики.
Подробнее о файловой системе XIAO ESP32S3 Sense — по этой ссылке.
Проверка камеры
Для проверки и использования камеры можно применять несколько способов:
- SenseCraft AI Studio;
- приложение CameraWebServer в Arduino IDE (см. следующий раздел);
- захват изображений и сохранение их на SD-карту (аналогично тому, что мы делали со звуком).
Проверка камеры в SenseCraft AI Studio
Самый простой способ увидеть работу камеры — использовать SenseCraft AI Studio, надёжную платформу, которая предлагает широкий набор моделей ИИ для разных устройств, включая XIAO ESP32S3 Sense и Grove Vision AI V2.
Также можно воспользоваться SenseCraft Web Toolkit — упрощённой версией SenseCraft AI Studio.
Чтобы запустить SenseCraft AI, выполните следующие шаги:
- Откройте рабочее пространство SenseCraft AI Vision в веб-браузере, например Chrome, и войдите в систему (или создайте аккаунт).
- Подключите XIAOML Kit к ноутбуку и выберите его, как показано ниже:
Примечание: инструмент WebUSB может работать некорректно в некоторых браузерах, например в Safari. Используйте Chrome. Также убедитесь, что Arduino IDE или любое другое последовательное устройство не подключено к XIAO.
Чтобы увидеть работу камеры, нужно загрузить модель. Можно попробовать несколько моделей компьютерного зрения, заранее загруженных Seeed Studio. Нажмите кнопку [Select Model] и выберите одну из доступных моделей.
Наведя курсор на модели ИИ, можно увидеть их название, описание, категорию или задачу (классификация изображений, обнаружение объектов или определение позы/ключевых точек), алгоритм (например, YOLO V5 или V8, FOMO, MobileNet V2 и т. д.), а в некоторых случаях и метрики (точность или mAP).
Можно выбрать одну из готовых моделей, например «Person Classification», нажав на неё и затем кнопку [Confirm], или загрузить собственную модель.
В области Preview мы видим видеопоток с камеры.
Мы подробнее вернёмся к SenseCraft AI Studio в лабораторных работах по компьютерному зрению.
Проверка Wi-Fi
Установка антенны
Комплект XIAOML Kit приходит полностью собранным. Сначала снимите плату расширения Sense (на ней камера, микрофон и считыватель SD-карт) с XIAO.
В левом нижнем углу передней стороны XIAO ESP32S3 есть отдельный разъём «WiFi/BT Antenna Connector». Чтобы улучшить сигнал Wi-Fi и Bluetooth, достаньте антенну из упаковки и прикрепите её к разъёму.
Есть небольшой приём при установке антенны. Если давить на неё прямо и сильно, она будет поддаваться с трудом, и пальцы заболят! Правильный способ: сначала вставить один край антенного разъёма в гнездо, а затем аккуратно прижать другой край, чтобы антенна надёжно закрепилась.
Снимается антенна так же: не тяните её силой напрямую, а поднимите с одной стороны, и тогда её будет легко снять.
Установить плату расширения обратно очень просто: совместите разъём платы расширения с разъёмом B2B на XIAO ESP32S3, сильно надавите и услышите «щелчок». Установка завершена.
Одна из отличительных черт XIAO ESP32S3 — возможности Wi-Fi. Проверим радиомодуль, сканируя окружающие сети Wi-Fi. Для этого запустите один из примеров кода на плате.
Откройте Arduino IDE, выберите нашу плату и порт. Перейдите в Examples и найдите WiFi > WiFiScan в разделе «Examples for the XIAO ESP32S3». Загрузите скетч на плату.
В Serial Monitor вы должны увидеть сети Wi-Fi (SSID и RSSI) в радиусе действия вашего устройства. Вот что получилось у нас в лаборатории:
Простой Wi-Fi-сервер (включение и выключение светодиода)
Проверим способность устройства работать как Wi-Fi-сервер. Мы разместим на устройстве простую страницу, которая отправляет команды для включения и выключения встроенного светодиода XIAO.
Перейдите в Examples и найдите WiFi > SimpleWiFiServer в разделе «Examples for the XIAO ESP32S3».
Перед запуском скетча введите данные своей сети:
const char* ssid = "Your credentials here";
const char* password = "Your credentials here";
Затем измените вывод 5 на вывод 21, к которому подключён встроенный светодиод. Также измените веб-страницу (строки 85 и 86), чтобы отразить правильный вывод светодиода и то, что он включается сигналом LOW:
client.print("Click <a href=\"/H\">here</a> to turn the LED on pin 21 OFF.<br>");
client.print("Click <a href=\"/L\">here</a> to turn the LED on pin 21 ON.<br>");
Работу сервера можно отслеживать через Serial Monitor.
Возьмите IP-адрес из Serial Monitor и введите его в браузере. Откроется страница со ссылками, которые включают и выключают встроенный светодиод XIAO.
Использование CameraWebServer
В Arduino IDE откройте File > Examples > ESP32 > Camera и выберите CameraWebServer.
На вкладке board_config.h закомментируйте все модели камер, кроме выводов модели XIAO:
#define CAMERA_MODEL_XIAO_ESP32S3 // Есть PSRAM
Не забудьте проверить в меню
Tools, включена ли PSRAM.
Как и раньше, на вкладке CameraWebServer.ino введите данные Wi-Fi и загрузите код на устройство.
Если код выполнился правильно, в Serial Monitor появится адрес:
WiFi connecting....
WiFi connected
Camera Ready! Use 'http://192.168.5.60' to connect
Скопируйте адрес в браузер и дождитесь загрузки страницы. Выберите разрешение камеры (например, QVGA) и нажмите [START STREAM]. Подождите несколько секунд в зависимости от качества соединения. Кнопкой [Save] можно сохранить изображение в папку загрузок на компьютере.
Вот и всё! Изображения можно сохранить прямо на компьютер и использовать в проектах.
Проверка датчика IMU (LSM6DS3TR-C)
Инерциальный измерительный блок (IMU) — датчик, который измеряет движение и ориентацию. LSM6DS3TR-C на вашем XIAOML Kit — это 6-осевой IMU, то есть он объединяет:
- 3-осевой акселерометр: измеряет линейное ускорение (включая силу тяжести) по осям X, Y и Z;
- 3-осевой гироскоп: измеряет угловую скорость (скорость вращения) вокруг осей X, Y и Z.
Технические характеристики
- Связь: интерфейс I2C, адрес
0x6A; - Диапазон акселерометра: ±2/±4/±8/±16 g (по умолчанию используем ±2 g);
- Диапазон гироскопа: ±125/±250/±500/±1000/±2000 dps (по умолчанию используем ±250 dps);
- Разрешение: 16-битный АЦП;
- Энергопотребление: сверхнизкое.
Система координат
Датчик использует правостороннюю систему координат. Если смотреть на датчик IMU с видимой меткой (вид платы расширения снизу):
- Ось X: направлена вправо;
- Ось Y: направлена вперёд (от вас);
- Ось Z: направлена вверх (из платы).
Необходимые библиотеки
Перед загрузкой кода установите нужную библиотеку:
-
Откройте Arduino IDE и выберите Manage Libraries (менеджер библиотек), который обозначен значком
Books Icon. -
Для библиотеки IMU введите «LSM6DS3» и выберите «Seeed Arduino LSM6DS3 by Seeed». Можно
INSTALL(установить) илиUPDATE(обновить).
⚠️ Важно: НЕ устанавливайте «Arduino_LSM6DS3 by Arduino» — она предназначена для других плат!
Тестовый код
Введите приведённый ниже код в Arduino IDE и загрузите его в комплект:
#include <LSM6DS3.h>
#include <Wire.h>
// Создаём объект IMU через интерфейс I2C
// Датчик LSM6DS3TR-C находится по адресу I2C 0x6A
LSM6DS3 myIMU(I2C_MODE, 0x6A);
// Переменные для хранения показаний датчика
float accelX, accelY, accelZ; // Значения акселерометра (перегрузка в g)
float gyroX, gyroY, gyroZ; // Значения гироскопа (градусы в секунду)
void setup() {
// Инициализация последовательной связи со скоростью 115200 бод
Serial.begin(115200);
// Ждём подключения последовательного порта (полезно для отладки)
while (!Serial) {
delay(10);
}
Serial.println("XIAOML Kit IMU Test");
Serial.println("LSM6DS3TR-C 6-Axis IMU Sensor");
Serial.println("=============================");
// Инициализация датчика IMU
if (myIMU.begin() != 0) {
Serial.println("ERROR: IMU initialization failed!");
Serial.println("Check connections and I2C address");
while(1) {
delay(1000); // Останавливаем выполнение, если IMU не инициализировался
}
} else {
Serial.println("✓ IMU initialized successfully");
Serial.println();
// Вывод информации о датчике
Serial.println("Sensor Information:");
Serial.println("- Accelerometer range: ±2g");
Serial.println("- Gyroscope range: ±250 dps");
Serial.println("- Communication: I2C at address 0x6A");
Serial.println();
// Вывод описания формата данных
Serial.println("Data Format:");
Serial.println("AccelX,AccelY,AccelZ,GyroX,GyroY,GyroZ");
Serial.println("Units: g-force (m/s²), degrees/second");
Serial.println();
delay(2000); // Короткая пауза перед началом измерений
}
}
void loop() {
// Считываем данные акселерометра (в единицах g)
accelX = myIMU.readFloatAccelX();
accelY = myIMU.readFloatAccelY();
accelZ = myIMU.readFloatAccelZ();
// Считываем данные гироскопа (в градусах в секунду)
gyroX = myIMU.readFloatGyroX();
gyroY = myIMU.readFloatGyroY();
gyroZ = myIMU.readFloatGyroZ();
// Вывод в удобном формате в Serial Monitor
Serial.print("Accelerometer (g): ");
Serial.print("X="); Serial.print(accelX, 3);
Serial.print(" Y="); Serial.print(accelY, 3);
Serial.print(" Z="); Serial.print(accelZ, 3);
Serial.print(" | Gyroscope (°/s): ");
Serial.print("X="); Serial.print(gyroX, 2);
Serial.print(" Y="); Serial.print(gyroY, 2);
Serial.print(" Z="); Serial.print(gyroZ, 2);
Serial.println();
// Вывод в формате CSV для Serial Plotter
Serial.println(String(accelX) + "," + String(accelY) + "," +
String(accelZ) + "," + String(gyroX) + "," +
String(gyroY) + "," + String(gyroZ));
// Частота обновления: 10 Гц (задержка 100 мс)
delay(100);
}
Serial Monitor покажет значения, а Plotter — их изменение во времени. Например, если перемещать комплект по оси Y, будет меняться значение 2 (красная линия). Ось Z представлена значением 3 (зелёная линия), которое близко к 1,0 g. Синяя линия (значение 1) относится к оси X.
Значения с 4 по 6 можно выбрать, чтобы увидеть поведение гироскопа.
Проверка OLED-дисплея (SSD1306)
OLED-дисплеи (Organic Light-Emitting Diode) — экраны с самосвечением, где каждый пиксель излучает собственный свет. Компактный 0,42-дюймовый монохромный OLED-дисплей XIAO ML kit подходит для вывода данных датчиков, информации о состоянии и простой графики.
Технические характеристики
- Размер: 0,42 дюйма по диагонали;
- Разрешение: пикселей;
- Контроллер: SSD1306;
- Интерфейс: I2C, адрес
0x3C; - Цвет: монохромный (чёрные пиксели на белом фоне или наоборот);
- Читаемость: высокий контраст, видно даже при ярком освещении;
- Питание: низкое энергопотребление, подсветка не нужна.
Характеристики дисплея
- Точность пикселей: каждым из 2 880 пикселей () можно управлять отдельно;
- Быстрое обновление: подходит для анимации и данных в реальном времени;
- Без послесвечения: мгновенный отклик пикселей;
- Широкий угол обзора: изображение читается с разных позиций.
Необходимые библиотеки
Перед загрузкой кода установите нужную библиотеку:
-
Откройте Arduino IDE и выберите «Manage Libraries» (менеджер библиотек), который обозначен значком
Books Icon. -
Введите u8g2 и выберите U8g2 by oliver. Можно
install(установить) илиupdate(обновить) пакеты.ℹ️ Примечание: U8g2 — мощная графическая библиотека, которая поддерживает множество типов дисплеев.
Библиотека U8g2 — монохромная графическая библиотека со следующими возможностями:
- поддержка множества контроллеров дисплеев (включая SSD1306);
- вывод текста различными шрифтами;
- графические примитивы (линии, прямоугольники, окружности);
- экономичный постраничный рендеринг;
- аппаратная и программная поддержка I2C.
Тестовый код
Введите приведённый ниже код в Arduino IDE и загрузите его в комплект:
#include <U8g2lib.h>
#include <Wire.h>
// Инициализация OLED-дисплея
// Контроллер SSD1306, разрешение 72x40, интерфейс I2C
U8G2_SSD1306_72X40_ER_1_HW_I2C u8g2(U8G2_R2, U8X8_PIN_NONE);
void setup() {
Serial.begin(115200);
Serial.println("XIAOML Kit - Hello World");
Serial.println("==========================");
// Инициализация дисплея
u8g2.begin();
Serial.println("✓ Display initialized");
Serial.println("Showing Hello World message...");
// Очистка дисплея
u8g2.clearDisplay();
}
void loop() {
// Начало последовательности рисования
u8g2.firstPage();
do {
// Выбор шрифта
u8g2.setFont(u8g2_font_ncenB08_tr);
// Вывод «Hello World» по центру
u8g2.setCursor(8, 15);
u8g2.print("Hello");
u8g2.setCursor(12, 30);
u8g2.print("World!");
// Простое оформление — рамка вокруг текста
u8g2.drawFrame(2, 2, 68, 36);
} while (u8g2.nextPage());
// Задержка не нужна — дисплей будет показывать изображение постоянно
}
Если всё работает правильно, на дисплее вы увидите надпись «Hello World» внутри прямоугольника.
Размеры и позиционирование текста на OLED
-
Текст позиционируется функцией
setCursor(x, y), в данном случае по центру:u8g2.setCursor(8, 15); -
В коде используется шрифт среднего размера:
u8g2.setFont(u8g2_font_ncenB08_tr);Но доступны и другие размеры шрифта:
u8g2_font_4x6_tr: очень маленький шрифт ( пикселей);u8g2_font_6x10_tr: маленький шрифт ( пикселей);u8g2_font_ncenB08_tr: средний полужирный шрифт;u8g2_font_ncenB14_tr: крупный полужирный шрифт.
Фигуры
В коде добавлено простое оформление: рамка вокруг текста.
u8g2.drawFrame(2, 2, 68, 36);
Доступны и другие фигуры:
- Контур прямоугольника:
drawFrame(x, y, width, height); - Закрашенный прямоугольник:
drawBox(x, y, width, height); - Окружность:
drawCircle(x, y, radius); - Линия:
drawLine(x1, y1, x2, y2); - Отдельные пиксели:
drawPixel(x, y).
Координаты
Дисплей использует систему координат, где:
- начало координат (0,0): верхний левый угол;
- ось X: возрастает слева направо (от 0 до 71);
- ось Y: возрастает сверху вниз (от 0 до 39);
- позиционирование текста:
setCursor(x, y), где y — базовая линия текста.
Поворот дисплея
Параметр поворота можно изменить:
U8G2_R0: обычная ориентация;U8G2_R1: поворот на 90° по часовой стрелке;U8G2_R2: 180° (перевёрнуто);U8G2_R3: поворот на 270° по часовой стрелке.
Собственные символы
// Рисуем собственное растровое изображение
static const unsigned char myBitmap[] = {0x00, 0x3c, 0x42, 0x42, 0x3c, 0x00};
u8g2.drawBitmap(x, y, 1, 6, myBitmap);
Измерение текста
int width = u8g2.getStrWidth("Hello"); // Ширина текста
int height = u8g2.getAscent(); // Высота шрифта
OLED-дисплей готов показывать данные датчиков, состояние системы или любую графику, которую вы создадите для своих проектов ML!
Итоги
XIAOML Kit с ESP32S3 Sense — мощная и доступная отправная точка в мир TinyML и встраиваемого машинного обучения. В ходе настройки мы последовательно проверили каждый компонент XIAOML Kit и убедились, что все датчики и периферийные устройства работают правильно. Двухъядерный процессор ESP32S3 и 8 МБ PSRAM обеспечивают достаточную вычислительную мощность для инференса в реальном времени, а камера OV2640, цифровой микрофон, IMU LSM6DS3TR-C и 0,42-дюймовый OLED-дисплей составляют полноценную мультимодальную платформу для сбора данных. Возможности Wi-Fi открывают путь к рабочим процессам ML от периферии к облаку, а среда разработки Arduino IDE теперь настроена со всеми необходимыми библиотеками.
Помимо проверки функций, мы получили практические знания о системах координат, форматах данных и рабочих характеристиках каждого датчика. Эти знания пригодятся при проектировании конвейеров сбора и предобработки данных ML в предстоящих проектах.
Этот процесс настройки демонстрирует ключевые принципы, которые выходят далеко за рамки этого конкретного комплекта. Работа с ограничениями памяти и возможностями обработки ESP32S3 даёт реальный опыт работы с ресурсными ограничениями периферийного ИИ — теми же, что возникают при развёртывании моделей на смартфонах, устройствах IoT или автономных системах. Наличие нескольких модальностей (зрение, звук, движение) на одной платформе позволяет исследовать мультимодальные подходы к машинному обучению, которые всё важнее в реальных приложениях ИИ.
Главное: от сырых данных датчиков до инференса модели и обратной связи через OLED-дисплей комплект воплощает полный цикл развёртывания ML в миниатюре, повторяя трудности, с которыми сталкиваются промышленные системы ИИ.
С этой основой вы готовы решать основные задачи TinyML в следующих главах:
- Проекты по зрению: использование камеры для классификации изображений и обнаружения объектов;
- Звуковые проекты: обработка аудиопотоков для распознавания ключевых слов и голосовых команд;
- Проекты по движению: использование данных IMU для распознавания активности и обнаружения аномалий.
Каждое приложение будет опираться на аппаратное понимание и программную инфраструктуру, которые мы уже построили, и покажет, как ИИ применяется не только в дата-центрах, но и в устройствах с ограниченными ресурсами, которые напрямую взаимодействуют с физическим миром.
Принципы, с которыми мы познакомились в этом комплекте — обработка в реальном времени, слияние данных датчиков и инференс на периферии, — те же, что определяют будущее развёртывания ИИ в автономных автомобилях, умных городах, медицинских устройствах и промышленной автоматизации. Успешно завершив настройку, вы готовы исследовать этот захватывающий рубеж встраиваемого машинного обучения.
Ресурсы
- Код XIAOML Kit
- Руководство и примеры кода для XIAO ESP32S3 Sense
- Использование микрофона Seeed Studio XIAO ESP32S3
- Файловая система и XIAO ESP32S3 Sense
- Использование камеры в Seeed Studio XIAO ESP32S3 Sense
Приложение: радиаторы
Соображения о радиаторах
Если вам нужно использовать XIAO ESP32S3 Sense для задач с камерой БЕЗ платы расширения, можно установить радиатор.
Учтите: если радиатор установлен, подключить XIAO ESP32S3 Sense к плате расширения будет невозможно.
Установка радиатора
Чтобы обеспечить оптимальное охлаждение XIAO ESP32S3 Sense, во время работы с камерой следует установить входящий в комплект радиатор. Его конструкция специально рассчитана на охлаждение, особенно при интенсивных операциях, таких как работа с камерой.
В комплекте два радиатора, но можно использовать только один, чтобы сохранить доступ к контактам аккумулятора.
Установка:
- Перед началом убедитесь, что устройство выключено и отключено от любого источника питания.
- В первую очередь закройте радиатором термопрокладку (THERMAL PAD), которая находится прямо над чипом ESP32S3 — главным источником тепла. Правильное выравнивание обеспечивает оптимальный отвод тепла, и важно, чтобы контакты BAT оставались максимально свободными.
Теперь начнём установку:
Шаг 1. Подготовьте радиатор: снимите защитную плёнку с радиатора, чтобы открыть термоклей. Это подготовит радиатор к надёжному креплению на чипе ESP32S3.
Шаг 2. Соберите радиатор:
После установки убедитесь, что всё надёжно закреплено и нет риска короткого замыкания. Проверьте, что радиатор правильно выровнен и надёжно прикреплён.
Если одного радиатора недостаточно, можно установить второй, оба будут использовать одну термопрокладку. Но в этом случае учтите, что все контакты станут недоступны.
⚠️ Внимание
Аккуратно снимите радиаторы перед тем, как снова использовать плату расширения IMU.
Обзор
Эти лабораторные работы дают возможность получить практический опыт работы с системами машинного обучения (ML) на мощном, но компактном встраиваемом устройстве — Seeed Studio Grove Vision AI V2. В отличие от работы с большими моделями, которым нужны ресурсы уровня дата-центра, эти упражнения позволяют напрямую работать с оборудованием и программным обеспечением с помощью TinyML. Такой практический подход даёт осязаемое понимание проблем и возможностей развёртывания ИИ, пусть и в небольшом масштабе. При этом принципы в целом те же, что и при работе с более крупными или, наоборот, ещё более мелкими системами.
Grove Vision AI V2 занимает особое место в мире встраиваемого ИИ: он заполняет разрыв между простыми решениями на микроконтроллерах, такими как Seeed XIAO ESP32S3 Sense или Arduino Nicla Vision, и более мощными одноплатными компьютерами, например Raspberry Pi. В основе платы лежит процессор Himax WiseEye2 HX6538 с двухъядерным Arm Cortex-M55 и встроенным блоком нейронных сетей Arm Ethos-U55.
Arm Ethos-U55 — это специализированный процессор для машинного обучения, класса микро-NPU, который ускоряет инференс ML во встраиваемых и IoT-устройствах с ограниченной площадью кристалла. Сочетание Ethos-U55 с процессором Cortex-M55, рассчитанным на ИИ, даёт прирост производительности ML в 480 раз по сравнению с существующими системами на базе Cortex-M. Работая на частоте 400 МГц с настраиваемой внутренней системной памятью (SRAM) объёмом до 2,4 МБ, Grove Vision AI V2 предлагает возможности компьютерного зрения профессионального уровня и при этом сохраняет энергоэффективность и компактность, необходимые для периферийных приложений.
Такое место на рынке делает плату идеальной платформой для изучения продвинутых концепций TinyML: она сочетает простоту и низкое энергопотребление небольших систем с возможностями, которые намного превосходят традиционные решения на основе микроконтроллеров.

Grove - Vision AI Module V2. Источник: SEEED Studio.
Где купить
Grove Vision AI V2 продаётся в Seeed Studio:
Вам также понадобятся совместимый модуль камеры (Raspberry Pi OV5647) и, при желании, управляющая плата, например XIAO ESP32S3.
Предварительные требования
- Плата Grove Vision AI V2: убедитесь, что плата у вас есть.
- Модуль камеры Raspberry Pi OV5647: камера должна быть подключена к плате Grove Vision AI V2 для захвата изображений.
- Управляющий контроллер: может быть Seeed XIAO ESP32S3, XIAO ESP32C6 или другое устройство.
- Кабель USB-C: для подключения платы к компьютеру.
- Сеть: доступ в интернет для загрузки необходимого программного обеспечения.
- Базовая плата расширения XIAO: помогает подключить управляющее устройство к физическому миру (необязательно).
Настройка и приложения без кода
Упражнения
| Модальность | Задача | Описание | Ссылка |
|---|---|---|---|
| Зрение | Классификация изображений | Научитесь классифицировать изображения | Открыть |
| Зрение | Обнаружение объектов | Реализуйте обнаружение объектов | Уточняется |
Классификация изображений
С использованием Seeed Studio Grove Vision AI Module V2 (Himax WiseEye2)

В этой лабораторной работе мы исследуем классификацию изображений с помощью Grove Vision AI Module V2 от Seeed Studio — мощного, но компактного устройства, созданного специально для приложений встраиваемого машинного обучения. Модуль построен на микросхеме Himax WiseEye2 и предназначен для работы ИИ на периферийных устройствах, поэтому он идеально подходит для задач периферийного машинного обучения (Edge ML).
Введение
До сих пор мы исследовали несколько моделей компьютерного зрения, которые Seeed Studio загрузила заранее, либо использовали SenseCraft AI Studio для классификации изображений, не выбирая конкретную модель. Теперь создадим проект классификации изображений с нуля: сами выберем данные и модель.
Ниже показаны основные шаги проекта и то, где мы будем работать на каждом из них:
Цель проекта
Первый шаг любого проекта машинного обучения (ML) — определить цель. В данном случае нужно обнаружить и классифицировать два конкретных объекта, присутствующих на одном изображении. Для проекта мы возьмём две небольшие игрушки: робота и маленького бразильского попугайчика (по имени Periquito). Также соберём изображения фона, на котором этих двух объектов нет.

Сбор данных
Когда цель проекта машинного обучения определена, следующий и самый важный шаг — сбор набора данных. Если вашему проекту нужны изображения, которые уже публично доступны в наборах данных, например для проекта Person Detection (обнаружение людей), можно скачать набор Wake Vision и использовать его.
Но в нашем случае изображения не существуют в открытом доступе, поэтому их нужно создать. Фотографии можно сделать телефоном, веб-камерой компьютера или другим устройством — офлайн или с подключением к Edge Impulse Studio.
Если вы хотите использовать Grove Vision AI V2 для съёмки набора данных, можно воспользоваться SenseCraft AI Studio, как в предыдущей лабораторной работе, или скетчем camera_web_server, который мы опишем позже в разделе «Постобработка / Получение видеопотока» этой работы.

В этой лабораторной работе для сбора набора данных мы будем использовать SenseCraft AI Studio.
Сбор данных в SenseCraft AI Studio
В SenseCraft AI Studio откройте вкладку Training.
По умолчанию обучается модель Classification с веб-камерой, если она доступна. Выберем вместо неё Grove Vision AI V2. Нажмите зелёную кнопку [Connect] (1) — появится всплывающее окно. Выберите нужный порт (2) и нажмите синюю кнопку [Connect] (3).

На экране появится изображение, которое передаёт Grove Vision AI V2.
Сбор изображений
Создадим классы, например, в алфавитном порядке:
- Класс 1:
background(фон) - Класс 2:
periquito(попугайчик) - Класс 3:
robot(робот)

Выберите один из классов (вокруг окна появится зелёная рамка) и удерживайте зелёную кнопку под областью предпросмотра. Собранные изображения появятся на экране выборки (Image Samples).

После сбора изображений просмотрите их и при необходимости удалите некорректные.

Соберите примерно по 50 изображений для каждого класса. Когда все три класса готовы, откройте меню каждого из них и выберите Export Data.

В папке загрузок компьютера появятся три ZIP-архива — по одному на каждый класс. Каждый архив содержит папку с изображениями.
Загрузка набора данных в Edge Impulse Studio
Для обучения модели мы будем использовать Edge Impulse Studio. Edge Impulse — ведущая платформа для разработки моделей машинного обучения на периферийных устройствах.
- Войдите в Edge Impulse с вашими учётными данными (или создайте бесплатный аккаунт).
- Затем создайте новый проект:

Набор данных содержит примерно по 50 изображений на метку: 40 для обучения и 10 для тестирования.
Проектирование Impulse и предобработка
Проектирование Impulse
Impulse принимает сырые данные (в нашем случае изображения), извлекает признаки (уменьшает размер изображений), а затем использует блок обучения, чтобы классифицировать новые данные.
Классификация изображений — самое распространённое применение глубокого обучения, но для неё нужен большой объём данных. У нас около 50 изображений на каждую категорию. Достаточно ли этого? Совсем нет! Чтобы «научить» модель различать каждый класс, нужны тысячи изображений. Однако эту проблему можно решить, дообучив уже обученную модель на небольшом наборе данных. Такой приём называется «переносом обучения» (Transfer Learning, TL). С TL можно донастроить предобученную модель классификации изображений на наших данных и добиться хорошего качества даже на относительно небольших наборах, как в нашем случае.

Итак, начиная с исходных изображений, уменьшим их до пикселей и передадим в блок переноса обучения (Transfer Learning):

Для сравнения оставим размер изображений . Однако учтите: с Grove Vision AI Module V2 и его внутренней SRAM объёмом 2,4 МБ можно использовать и более крупные изображения (например, ).
Также в правом верхнем углу выберите устройство Target (Himax WiseEye2 (M55 400 MHz + U55)).
Предобработка (генерация признаков)
Помимо изменения размера, изображения можно перевести в оттенки серого или сохранить исходную цветовую глубину RGB. Выберем [RGB] в разделе Image. Тогда каждый образец данных будет иметь размерность 27 648 признаков (). Нажатие [Save Parameters] откроет новую вкладку Generate Features. Нажмите кнопку [Generate Features], чтобы сгенерировать признаки.
Проектирование модели, обучение и тестирование
В 2017 году Google представил MobileNetV1. В 2018 году вышла MobileNetV2: Inverted Residuals and Linear Bottlenecks, а в 2019 году — V3. MobileNet — это семейство универсальных нейронных сетей для компьютерного зрения, специально созданных для мобильных устройств и поддерживающих классификацию, обнаружение и другие задачи. MobileNet — компактные модели с малой задержкой и низким энергопотреблением, параметризованные так, чтобы соответствовать ограничениям ресурсов разных сценариев.
Хотя базовая архитектура MobileNet уже компактна и быстра, конкретному сценарию часто нужна модель ещё меньше и быстрее. Для этого в MobileNet есть простой параметр α (альфа), называемый множителем ширины (width multiplier), с помощью которого строятся меньшие и менее требовательные к вычислениям модели. Множитель ширины α равномерно «утончает» сеть на каждом слое.
В Edge Impulse Studio доступны MobileNet V1 (изображения ) и V2 (изображения и ) с несколькими значениями α (от 0,05 до 1,0). Например, наибольшая точность достигается у V2 с изображениями и α=1,0. Конечно, здесь есть компромисс: чем выше точность, тем больше памяти (примерно 1,3 МБ ОЗУ и 2,6 МБ ПЗУ) нужно для запуска модели и тем выше задержка. Самый маленький объём получится на другом полюсе — у MobileNet V1 с α=0,10 (примерно 53,2 КБ ОЗУ и 101 КБ ПЗУ).
Для сравнения мы возьмём MobileNet V2 0.1 в качестве базовой модели (но можно использовать и модель с большим α). Последний слой модели, перед выходным слоем, будет содержать 8 нейронов с dropout-регуляризацией 10 % для предотвращения переобучения.
Ещё один обязательный приём в глубоком обучении — аугментация данных. Это метод, который помогает повысить точность моделей машинного обучения за счёт создания дополнительных искусственных данных. Система аугментации вносит небольшие случайные изменения в обучающие данные в процессе обучения (например, отражает, обрезает или поворачивает изображения).
Задайте гиперпараметры:
- Эпохи (Epochs): 20;
- Размер батча (Batch Size): 32;
- Скорость обучения (Learning Rate): 0,0005;
- Размер валидационной выборки (Validation size): 20 %.
Результат обучения:

Профиль модели предсказывает 146 КБ ОЗУ и 187 КБ флеш-памяти, что не создаёт проблем для Grove Vision AI (V2), у которого почти 2,5 МБ внутренней SRAM. Кроме того, Studio показывает задержку около 4 мс.
Тем не менее, при точности 100 % на валидационной выборке и при тестировании на отложенных данных мы получили точность 81 % для квантованной (Int8) обученной модели. Однако для наших целей этого достаточно.
Развёртывание модели
На вкладке Deployment выберите Seeed Grove Vision AI Module V2 (Himax WiseEye2) и нажмите [Build]. На компьютер загрузится ZIP-файл.
ZIP-архив содержит файл model_vela.tflite — модель TensorFlow Lite (TFLite), оптимизированную для нейронных процессоров (NPU) с помощью компилятора Vela, инструмента от Arm для адаптации моделей TFLite под NPU Ethos-U.

Модель можно прошить, следуя инструкциям в README.txt, или через SenseCraft AI Studio. Мы воспользуемся вторым способом.
Развёртывание модели в SenseCraft AI Studio
В SenseCraft AI Studio перейдите на вкладку Vision Workspace и подключите устройство:

Вы увидите последнюю модель, загруженную на устройство. Нажмите зелёную кнопку [Upload Model]. Во всплывающем окне нужно указать имя модели, файл модели и ввести названия классов (объектов). Используйте метки в алфавитном порядке: 0: background, 1: periquito и 2: robot, затем нажмите [Send].

Через несколько секунд модель будет загружена («прошита») в устройство, а изображение с камеры появится в реальном времени в секции Preview. Результат классификации отображается под предпросмотром изображения. Также можно выбрать порог уверенности инференса (Confidence Threshold), передвинув ползунок в разделе Settings.
В Device Logger можно посмотреть Serial Monitor, где видна задержка: около 1–2 мс на предобработку и 4–5 мс на инференс, что совпадает с оценками в Edge Impulse Studio.

Вот другие снимки экрана:

Энергопотребление этой модели составляет около 70 мА, или 0,4 Вт.
Неофициальный бенчмарк классификации изображений
Для встраиваемого машинного обучения (TinyML) можно использовать несколько отладочных плат. Наиболее распространённые на сегодня для задач компьютерного зрения с низким энергопотреблением — ESP32 CAM, Seeed XIAO ESP32S3 Sense и Arduino Nicla Vision.
Мы воспользовались возможностью и развернули аналогичную модель, обученную похожим образом — MobileNetV2 с α=0,1 — также на ESP-CAM, XIAO и Raspberry Pi Zero W2. Вот результат:

Grove Vision AI V2 с ARM Ethos-U55 работает примерно в 14 раз быстрее устройств с ARM-M7 и более чем в 100 раз быстрее Xtensa LX6 (ESP-CAM). Даже по сравнению с Raspberry Pi, у которой гораздо более мощный процессор, U55 почти вдвое снижает задержку. Кроме того, энергопотребление ниже, чем у других устройств (сравнение энергопотребления см. в полной статье).
Постобработка
Теперь, когда модель загружена на плату и корректно классифицирует наши изображения, подключим управляющее устройство, которое получит результаты инференса, и посмотрим результат полностью офлайн — без подключения к ПК и, например, от батареи.
Учтите, что в качестве управляющего контроллера можно использовать любой микроконтроллер, например XIAO, Arduino или Raspberry Pi.
Получение видеопотока
Обработка изображений и инференс модели выполняются локально в Grove Vision AI (V2), а результат мы хотим передать на XIAO (управляющий контроллер) по шине IIC. Для этого используем библиотеку Arduino SSCMA. Её основная задача — обрабатывать поток данных Grove Vision AI, но не сам инференс модели.
Grove Vision AI (V2) передаёт результаты инференса на XIAO по IIC; адрес устройства на шине IIC — 0x62. Передача изображения осуществляется через последовательный порт USB.
Шаг 1. Скачайте библиотеку Arduino SSCMA в виде ZIP-архива с её страницы на GitHub:

Шаг 2. Установите её в Arduino IDE (sketch > Include Library > Add .Zip Library).
Шаг 3. Установите библиотеку ArduinoJSON.

Шаг 4. Установите библиотеку Eigen.

Шаг 5. Теперь подключите XIAO и Grove Vision AI (V2) через разъём (ряд контактов), расположенный на задней стороне устройства.

ВНИМАНИЕ: соблюдайте направление соединения: Type-C разъём Grove Vision AI должен быть направлен в ту же сторону, что и Type-C разъём XIAO.
Шаг 6. Подключите порт XIAO USB-C к компьютеру.

Шаг 7. В Arduino IDE выберите плату XIAO и соответствующий порт USB.
Когда мы захотим передавать видео на веб-страницу, понадобится XIAO ESP32S3: у неё есть Wi-Fi и достаточно памяти для обработки изображений. Выберите XIAO_ESP32S3 и нужный порт USB:

По умолчанию PSRAM отключена. Откройте меню Tools и в пункте PSRAM выберите "OPI PSRAM".

Шаг 8. Откройте пример в Arduino IDE:
File → Examples → Seeed_Arduino_SSCMA → camera_web_server.
Затем измените ssid и password в скетче camera_web_server.ino, чтобы они совпадали с вашей сетью Wi-Fi.
Шаг 9. Загрузите скетч на плату и откройте Serial Monitor. Когда плата подключится к сети Wi-Fi, в мониторе отобразится её IP-адрес.

Откройте этот адрес в веб-браузере. Станет доступно видеоприложение (Video App). Чтобы видеть только видеопоток с Grove Vision AI V2, нажмите [Sample Only] и [Start Stream].

Если хотите создать набор изображений, это приложение можно использовать, сохраняя кадры видео с устройства. Нажмите [Save Frame] — изображение сохранится в папку загрузок на компьютере.

Если открыть приложение без выбора [Sample Only], результат инференса должен появиться на видеоэкране. Для классификации изображений этого не происходит, а вот для обнаружения объектов или оценки позы результат встраивается в видеопоток.
Например, если модель — Person Detection на основе YOLOv8:

Получение результата инференса
-
Перейдите в
File→Examples→Seeed_Arduino_SSCMA→inference_class. -
Загрузите скетч на плату и откройте Serial Monitor.
-
Направьте камеру на один из наших объектов — результат инференса появится в последовательном терминале.

Инференс в Arduino IDE потреблял в среднем 160 мА, или 800 мВт, с пиком 330 мА (1,65 Вт) при передаче изображения в приложение.
Постобработка со светодиодом
Идея постобработки такая: когда распознан определённый объект (например, попугайчик — метка 1), пользовательский светодиод включается. Если обнаружены робот или фон, светодиод выключен.
Скопируйте код ниже и вставьте его в IDE:
#include <Seeed_Arduino_SSCMA.h>
SSCMA AI;
void setup()
{
AI.begin();
Serial.begin(115200);
while (!Serial);
Serial.println("Инференс — Grove AI V2 / XIAO ESP32S3");
// Пины встроенного светодиода
pinMode(LED_BUILTIN, OUTPUT);
// Убедитесь, что светодиод выключен по умолчанию.
// Примечание: светодиод горит, когда пин в состоянии LOW, и гаснет при HIGH.
digitalWrite(LED_BUILTIN, HIGH);
}
void loop()
{
if (!AI.invoke()){
Serial.println("\nВызов выполнен успешно");
Serial.print("Задержка [мс]: предобработка=");
Serial.print(AI.perf().prepocess);
Serial.print(", инференс=");
Serial.print(AI.perf().inference);
Serial.print(", постобработка=");
Serial.println(AI.perf().postprocess);
int pred_index = AI.classes()[0].target;
Serial.print("Результат= метка: ");
Serial.print(pred_index);
Serial.print(", оценка=");
Serial.println(AI.classes()[0].score);
turn_on_led(pred_index);
}
}
/**
* @brief Функция turn_off_led — выключает пользовательский светодиод
*/
void turn_off_led(){
digitalWrite(LED_BUILTIN, HIGH);
}
/**
* @brief Функция turn_on_led — включает пользовательский светодиод
* @param[in] pred_index
* метка 0: [0] ==> все выключены
* метка 1: [1] ==> светодиод включён
* метка 2: [2] ==> все выключены
* метка 3: [3] ==> все выключены
*/
void turn_on_led(int pred_index) {
switch (pred_index)
{
case 0:
turn_off_led();
break;
case 1:
turn_off_led();
digitalWrite(LED_BUILTIN, LOW);
break;
case 2:
turn_off_led();
break;
case 3:
turn_off_led();
break;
}
}
Этот скетч использует библиотеку Seeed_Arduino_SSCMA.h для связи с модулем Grove Vision AI Module V2. Модуль ИИ и светодиод инициализируются в функции setup(), и запускается последовательная связь.
Функция loop() многократно вызывает метод invoke(), чтобы выполнить инференс с помощью встроенных алгоритмов модуля Grove Vision AI Module V2. При успешном инференсе скетч выводит в монитор порта метрики производительности: время предобработки, инференса и постобработки.
Скетч обрабатывает и выводит подробную информацию о результатах инференса:
- (
AI.classes()[0]) определяет класс изображения (.target) и его оценку уверенности (.score); - результат инференса (класс) сохраняется в целочисленной переменной
pred_index, которая передаётся в функциюturn_on_led(). В результате светодиод включается или выключается в зависимости от результата классификации.
Вот результат:
Если распознан попугайчик (метка 1), светодиод горит:

Если распознан робот (метка 2), светодиод не горит (то же самое для фона, метка 0):

Поэтому теперь мы можем питать Grove Vision AI V2 + XIAO ESP32S3 от внешней батареи, а результат инференса будет отображаться светодиодом полностью офлайн. Потребление составляет примерно 165 мА, или 825 мВт.
Результат также можно передавать по Wi-Fi, BLE или другим протоколам связи, доступным на используемом управляющем устройстве.
Дополнительно: постобработка на внешних устройствах
Одно из важных преимуществ работы с периферийным ИИ (Edge AI) — устройства могут работать полностью без облака, обеспечивая непосредственное взаимодействие с реальным миром. В предыдущем разделе мы делали это со встроенным светодиодом XIAO. Теперь подключим внешние светодиоды (это может быть любое исполнительное устройство).
Светодиоды следует подключать к земле XIAO через резистор 220 Ом.

Идея в том, чтобы изменить предыдущий скетч так, чтобы он управлял тремя внешними светодиодами.
ЦЕЛЬ: при распознавании изображения попугайчика включается зелёный светодиод; если это робот, включается жёлтый светодиод; если это фон, включается красный светодиод.
Обработка изображений и инференс модели выполняются локально в Grove Vision AI (V2), а результат мы хотим передать на XIAO по IIC. Для этого снова используем библиотеку Arduino SSCMA.
Вот скетч, который будем использовать:
#include <Seeed_Arduino_SSCMA.h>
SSCMA AI;
// Определите пины светодиодов согласно схеме выводов
// Отрицательный вывод светодиодов следует подключить к земле XIAO
// через резистор 220 Ом.
int LEDR = D1; // Пин 1 XIAO ESP32S3
int LEDY = D2; // Пин 2 XIAO ESP32S3
int LEDG = D3; // Пин 3 XIAO ESP32S3
void setup()
{
AI.begin();
Serial.begin(115200);
while (!Serial);
Serial.println("Инференс — Grove AI V2 / XIAO ESP32S3");
// Инициализация внешних светодиодов
pinMode(LEDR, OUTPUT);
pinMode(LEDY, OUTPUT);
pinMode(LEDG, OUTPUT);
// Убедитесь, что светодиоды выключены по умолчанию.
// Примечание: светодиоды горят, когда пин в состоянии HIGH, и гаснут при LOW.
digitalWrite(LEDR, LOW);
digitalWrite(LEDY, LOW);
digitalWrite(LEDG, LOW);
}
void loop()
{
if (!AI.invoke()){
Serial.println("\nВызов выполнен успешно");
Serial.print("Задержка [мс]: предобработка=");
Serial.print(AI.perf().prepocess);
Serial.print(", инференс=");
Serial.print(AI.perf().inference);
Serial.print(", постобработка=");
Serial.println(AI.perf().postprocess);
int pred_index = AI.classes()[0].target;
Serial.print("Результат= метка: ");
Serial.print(pred_index);
Serial.print(", оценка=");
Serial.println(AI.classes()[0].score);
turn_on_leds(pred_index);
}
}
/**
* @brief turn_off_leds — выключает все светодиоды
*/
void turn_off_leds(){
digitalWrite(LEDR, LOW);
digitalWrite(LEDY, LOW);
digitalWrite(LEDG, LOW);
}
/**
* @brief turn_on_leds — включает определённый светодиод
* @param[in] pred_index
* метка 0: [0] ==> горит красный
* метка 1: [1] ==> горит зелёный
* метка 2: [2] ==> горит жёлтый
*/
void turn_on_leds(int pred_index) {
switch (pred_index)
{
case 0:
turn_off_leds();
digitalWrite(LEDR, HIGH);
break;
case 1:
turn_off_leds();
digitalWrite(LEDG, HIGH);
break;
case 2:
turn_off_leds();
digitalWrite(LEDY, HIGH);
break;
case 3:
turn_off_leds();
break;
}
}
Grove Vision AI V2 нужно подключить к XIAO через разъём I2C Grove. Для XIAO мы используем плату расширения (хотя IIC можно подключить и напрямую к контактам XIAO). Питание плат можно подать через разъём USB-C, но можно использовать и батарею.

Вот результат:

Пиковое энергопотребление составило 240 мА (зелёный светодиод), или 1,2 Вт. Питание жёлтого и красного светодиодов потребляет 140 мА, или 0,7 Вт. Вывод информации в терминал через последовательный порт не влияет на энергопотребление.
Итоги
В этой лабораторной работе мы полностью прошли процесс разработки системы классификации изображений на модуле Seeed Studio Grove Vision AI Module V2 с чипом Himax WiseEye2. Мы прошли все этапы рабочего процесса машинного обучения: от определения целей проекта до развёртывания рабочей модели с взаимодействием с реальным миром.
Grove Vision AI V2 показал впечатляющую производительность: время инференса составляет всего 4–5 мс, что намного превосходит другие распространённые платформы TinyML. По результатам нашего бенчмарка он примерно в 14 раз быстрее устройств на ARM-M7 и более чем в 100 раз быстрее Xtensa LX6 (ESP-CAM). Даже по сравнению с Raspberry Pi Zero W2 блок NPU Ethos-U55 обеспечил почти двукратный прирост скорости при меньшем энергопотреблении.
В этом проекте мы увидели, как перенос обучения позволяет получить хорошие результаты классификации на относительно небольшом наборе собственных изображений. Модель MobileNetV2 с α=0,1 обеспечила отличный баланс точности и эффективности для нашей задачи с тремя классами: ей нужно всего 146 КБ ОЗУ и 187 КБ флеш-памяти, что легко укладывается в 2,4 МБ внутренней SRAM модуля Grove Vision AI Module V2.
Мы также рассмотрели несколько вариантов развёртывания: от просмотра результатов инференса в SenseCraft AI Studio до автономной системы с визуальной индикацией на светодиодах. Возможность передавать видео в веб-браузер и обрабатывать результаты инференса локально демонстрирует универсальность систем периферийного ИИ для реальных задач.
Потребление энергии итоговой системы оставалось очень низким — примерно от 70 мА (0,4 Вт) при базовом инференсе до 240 мА (1,2 Вт) при управлении внешними компонентами. Благодаря этой эффективности Grove Vision AI Module V2 — отличный выбор для устройств с батарейным питанием, где энергопотребление критично.
Эта работа показала, что сложные задачи компьютерного зрения теперь можно выполнять полностью на периферии, без облачных сервисов и мощных компьютеров. Благодаря таким инструментам, как Edge Impulse Studio и SenseCraft AI Studio, процесс разработки стал доступен даже тем, у кого нет серьёзного опыта в машинном обучении.
По мере развития технологий периферийного ИИ компактные и энергоэффективные устройства, такие как Grove Vision AI Module V2, будут становиться всё мощнее, открывая новые возможности для умных датчиков, IoT-приложений и встроенного интеллекта в повседневных предметах.
Ресурсы
- Сбор изображений в SenseCraft AI Studio
- Проект в Edge Impulse Studio
- SenseCraft AI Studio — Vision Workplace (развёртывание моделей)
- Другие примеры Himax
- Скетчи для Arduino
Обнаружение объектов
Эта лабораторная работа находится в разработке.
Настройка и приложения без кода

В этой лабораторной работе мы исследуем приложения компьютерного зрения (CV) с помощью модуля Grove Vision AI Module V2 от Seeed Studio — мощного, но компактного устройства, созданного специально для приложений встраиваемого машинного обучения. Модуль построен на микросхеме Himax WiseEye2 и предназначен для работы ИИ на периферийных устройствах, поэтому он идеально подходит для задач периферийного машинного обучения (Edge ML).
Введение
Обзор модуля Grove Vision AI (V2)
Grove Vision AI (V2) — модуль компьютерного зрения на базе микроконтроллера. В нём используется процессор Himax WiseEye2 HX6538 с двухъядерным Arm Cortex-M55 и встроенным блоком нейронных сетей Arm Ethos-U55. Arm Ethos-U55 — это класс процессоров для машинного обучения (ML), созданный специально как микро-NPU для ускорения инференса ML во встраиваемых и IoT-устройствах с ограниченной площадью кристалла. В сочетании с процессором Cortex-M55, рассчитанным на ИИ, Ethos-U55 обеспечивает прирост производительности ML в 480 раз по сравнению с существующими системами на базе Cortex-M. Тактовая частота составляет 400 МГц, а внутренняя системная память (SRAM) конфигурируется и имеет максимальный объём 2,4 МБ.

Примечание: по данным документации Seeed Studio, помимо внутренней памяти Himax объёмом 2,5 МБ (2,4 МБ SRAM и 64 КБ ПЗУ), Grove Vision AI (V2) оснащён внешней флеш-памятью 16 МБ / 133 МГц.

Ниже приведена блок-схема системы Grove Vision AI (V2), включающей камеру и управляющий контроллер.

Благодаря интерфейсам IIC, UART, SPI и Type-C Grove Vision AI (V2) легко подключается к устройствам вроде XIAO, Raspberry Pi, BeagleBoard и плат на базе ESP для дальнейшей разработки. Например, интеграция Grove Vision AI V2 с одним из устройств семейства XIAO упрощает доступ к результатам инференса через Arduino IDE или MicroPython и подключение к облаку или выделенным серверам, например к Home Assistant.
С помощью разъёма I2C Grove Grove Vision AI V2 легко подключается к любому управляющему устройству.

Помимо производительности, стоит отметить энергопотребление. Например, в сравнительном тесте с XIAO ESP32S3 Sense при запуске модели Swift-YOLO Tiny 96×96 Grove Vision AI V2 показал более высокую производительность (30 FPS против 5,5 FPS) и при этом меньшее энергопотребление (0,35 Вт против 0,45 Вт).

Сравнение (и с другими устройствами) можно найти в статье 2024 MCU AI Vision Boards: Performance Comparison — она подтверждает высокую производительность Grove Vision AI (V2).
Установка камеры
Когда Grove Vision AI (V2) и камера готовы, можно подключить, например, модуль Raspberry Pi OV5647 Camera Module через шлейф CSI.
При подключении обратите внимание на направление ряда контактов и убедитесь, что шлейф вставлен правильно, а не в обратную сторону.

SenseCraft AI Studio
SenseCraft AI Studio — надёжная платформа, которая предлагает широкий набор моделей ИИ для разных устройств, включая XIAO ESP32S3 Sense и Grove Vision AI V2. В этой работе мы пройдём процесс использования модели ИИ с Grove Vision AI V2 и посмотрим результат её работы. Также мы разберём ключевые понятия и настройки и научимся оптимизировать производительность модели.

Модели также можно развернуть с помощью SenseCraft Web Toolkit — упрощённой версии SenseCraft AI Studio.
Для простоты можно начать с SenseCraft Web Toolkit, а можно сразу перейти в SenseCraft AI Studio, где больше ресурсов.
SenseCraft Web Toolkit
SenseCraft Web Toolkit — это визуальный инструмент развёртывания моделей, входящий в состав SSCMA (Seeed SenseCraft Model Assistant). Он позволяет просто и быстро развёртывать модели на разных платформах. У инструмента удобный интерфейс, и для работы не нужно писать код.
SenseCraft Web Toolkit основан на Himax AI Web Toolkit, который при желании (необязательно) можно скачать по ссылке. После загрузки и распаковки на локальный компьютер дважды щёлкните index.html, чтобы запустить его локально.

В нашем случае выполним следующие шаги, чтобы запустить SenseCraft-Web-Toolkit:
- Откройте сайт SenseCraft-Web-Toolkit в браузере Chrome.
- Подключите Grove Vision AI (V2) к компьютеру кабелем Type-C.
- Когда Grove Vision AI (V2) подключён, выберите его, как показано ниже:

- Выберите устройство и порт и нажмите
[Connect]:

Примечание: инструмент WebUSB может работать некорректно в некоторых браузерах, например в Safari. Используйте Chrome.
Можно попробовать несколько базовых моделей компьютерного зрения, заранее загруженных Seeed Studio. Если навести курсор на модель ИИ, станут видны её название, описание, категория (классификация изображений, обнаружение объектов или детекция позы и ключевых точек), алгоритм (например, YOLO V5 или V8, FOMO, MobileNet V2 и т. д.) и метрики (точность Accuracy или mAP).

Чтобы использовать одну из готовых моделей, нажмите на неё и затем на кнопку [Send], либо загрузите собственную модель.
Для платформы SenseCraft AI следуйте инструкциям по ссылке.
Изучение моделей компьютерного зрения
Обнаружение объектов
Обнаружение объектов — ключевая технология компьютерного зрения, которая занимается поиском и локализацией объектов на цифровых изображениях или кадрах видео. В отличие от классификации изображений, которая относит всё изображение к одной метке, обнаружение объектов находит на изображении несколько объектов и определяет их точное положение, обычно заданное ограничивающими рамками. Эта возможность важна во многих приложениях — автономных автомобилях, системах безопасности и видеонаблюдения, дополненной реальности, — где нужно понимать контекст и содержимое визуальной среды.
К архитектурам, которые задали эталон в обнаружении объектов, относятся модели YOLO (You Only Look Once), SSD (Single Shot MultiBox Detector), FOMO (Faster Objects, More Objects) и Faster R-CNN (Region-based Convolutional Neural Networks).
Выберем одну из готовых моделей ИИ, например Person Detection (обнаружение людей), которая обучена с помощью алгоритма Swift-YOLO.

После успешной загрузки модели в области предпросмотра справа появится видеопоток с камеры Grove Vision AI (V2). Подробности инференса можно вывести в Serial Monitor, нажав кнопку [Device Log] вверху.

В SenseCraft AI Studio журнал устройства (Device Logger) всегда отображается на экране.
Я направил камеру на себя, поэтому модель нашла только одного человека и выдала одну рамку (box). Если присмотреться, модуль непрерывно передаёт две строки данных:

perf (производительность) — задержки в миллисекундах:
- время предобработки (захват изображения и обрезка): 7 мс;
- время инференса (задержка модели): 76 мс (13 fps);
- время постобработки (вывод изображения и добавление данных): меньше 1 мс (в журнале — 0 мс).
boxes — объекты, найденные на изображении. В данном случае — один.
- У рамки координаты x, y, w и h равны (245, 292, 449, 392), а объект (человек, метка 0) распознан с уверенностью 0,89.
Если направить камеру на изображение с несколькими людьми, для каждого человека (объекта) будет своя рамка:

В SenseCraft AI Studio задержка инференса (48 мс) меньше, чем в SenseCraft ToolKit (76 мс), благодаря другой реализации развёртывания.

Энергопотребление
Пиковое энергопотребление при работе этой модели Swift-YOLO составило 410 милливатт.
Настройки предпросмотра
В настройках можно изменить два параметра, чтобы повысить точность распознавания модели:
-
Confidence (уверенность): уровень достоверности, который модель присваивает своим предсказаниям. Этот параметр задаёт минимальный уровень уверенности, при котором детекция считается верной. При более высоком пороге детекций будет меньше, но они будут надёжнее; при более низком — детекций будет больше, однако среди них могут оказаться ложные срабатывания.
-
IoU: метрика, которая оценивает точность предсказанных ограничивающих рамок по сравнению с эталонными (ground truth). IoU измеряет степень перекрытия предсказанной рамки и эталонной и используется для оценки точности обнаружения объектов. Порог IoU задаёт минимальное значение, при котором детекция считается истинно положительной. Изменение этого порога помогает тонко настроить точность (precision) и полноту (recall) модели.

Поэкспериментируйте со значениями порогов Confidence и IoU, чтобы найти оптимальный баланс между точным обнаружением людей и минимизацией ложных срабатываний. Лучшие настройки могут отличаться в зависимости от конкретного приложения и характеристик изображений или видеопотока.
Детекция позы и ключевых точек
Детекция позы, или ключевых точек, — продвинутая область компьютерного зрения, которая находит на изображении или кадре видео определённые точки интереса, чаще всего относящиеся к телам людей, лицам или другим объектам. Эта технология обнаруживает и отображает ключевые точки объекта, например суставы человеческого тела или черты лица, что позволяет анализировать позы, движения и жесты. Это важно для дополненной реальности, взаимодействия человека с компьютером, спортивной аналитики и мониторинга здоровья, где нужно понимать движения и активность человека.
В отличие от общего обнаружения объектов, которое находит и локализует объекты, детекция позы работает на более тонком уровне и фиксирует положение и ориентацию отдельных частей. Ведущие архитектуры в этой области — OpenPose, AlphaPose и PoseNet, каждая из которых решает задачи оценки позы со своей степенью сложности и точности. Благодаря развитию глубокого обучения и нейронных сетей детекция позы становится всё точнее и эффективнее и даёт данные о динамике объектов на видео в реальном времени.
Давайте исследуем это популярное приложение компьютерного зрения — детекцию позы и ключевых точек.

Остановите текущий инференс кнопкой [Stop] в области предпросмотра. Выберите модель и нажмите [Send]. После успешной загрузки модели вы увидите видеопоток с камеры Grove Vision AI (V2) в области предпросмотра справа и данные инференса в Serial Monitor (кнопка [Device Log] вверху).

Модель YOLOv8 Pose обучена на наборе данных COCO-Pose, который содержит 200 тысяч изображений с разметкой из 17 ключевых точек для задач оценки позы.
Рассмотрим один кадр инференса (для простоты возьмём изображение с одним человеком). Видны две строки: одна — с производительностью инференса в миллисекундах (121 мс), вторая — с ключевыми точками:
- 1 блок информации, такой же, как в примере с обнаружением объектов: координаты рамки (113, 119, 67, 208), результат инференса (90) и метка (0);
- 17 групп по 4 числа — это 17 «суставов» тела, где «0» — нос, «1» и «2» — глаза, «15» и «16» — стопы и так далее.

Чтобы глубже разобраться в проекте оценки позы, см. руководство Exploring AI at the Edge! — Pose Estimation.
Классификация изображений
Классификация изображений — базовая задача компьютерного зрения: она относит всё изображение к одному из нескольких заранее заданных классов. Для этого анализируется визуальное содержимое изображения и назначается метка из фиксированного набора категорий в зависимости от основного объекта или сцены.
Классификация изображений важна во многих приложениях — от организации и поиска в больших базах изображений в цифровых библиотеках и соцсетях до помощи автономным системам в понимании окружающего мира. Среди архитектур, которые заметно продвинули классификацию изображений, — свёрточные нейронные сети (CNN), например AlexNet, VGGNet и ResNet. Эти модели показали высокую точность на сложных наборах данных, таких как ImageNet, обучаясь иерархическим представлениям визуальных данных.
Классификация изображений лежит в основе многих систем компьютерного зрения: она закладывает фундамент для более сложных задач, например обнаружения объектов и сегментации изображений, и помогает глубже понимать визуальные данные в самых разных отраслях. Давайте исследуем и это приложение компьютерного зрения.

Этот пример доступен в SenseCraft ToolKit, но не в SenseCraft AI Studio. В последнем можно найти и другие примеры классификации изображений.
После успешной загрузки модели можно посмотреть видеопоток с камеры Grove Vision AI (V2) в области предпросмотра справа и данные инференса в Serial Monitor (кнопка [Device Log] вверху).

В результате мы получаем оценку и класс.

Например, [99, 1] означает класс 1 (человек) с оценкой 0,99. Поскольку эта модель бинарная, класс 0 будет «Нет человека» (или фон). Задержка инференса — 15 мс, то есть около 70 кадров в секунду.
Энергопотребление
Для запуска MobileNet V2 0.35 Grove Vision AI V2 потреблял пиковый ток 80 мА при напряжении 5,24 В, что соответствует энергопотреблению 420 мВт.
При запуске той же модели на XIAO ESP32S3 Sense энергопотребление составило 523 мВт при задержке 291 мс.

Другие модели в SenseCraft AI Studio
Несколько публичных моделей ИИ можно скачать на веб-странице SenseCraft AI. Например, можно запустить модель Swift-YOLO, распознающую светофоры, как показано ниже:

Задержка этой модели составляет около 86 мс, среднее энергопотребление — 420 мВт.
Проект классификации изображений
Создадим полноценный проект классификации изображений в SenseCraft AI Studio.
В SenseCraft AI Studio откройте вкладку Training:

По умолчанию обучается модель Classification с веб-камерой, если она доступна. Выберем вместо неё Grove Vision AI V2. Нажмите зелёную кнопку [Connect] — появится всплывающее окно. Выберите нужный порт и нажмите синюю кнопку [Connect].

На экране появится изображение, которое передаёт Grove Vision AI V2.
Цель
Первый шаг — всегда определить цель. Классифицируем, например, два простых предмета — игрушечную коробку и игрушечное колесо. Также нужен третий класс изображений — фон, где в кадре нет объектов.

Сбор данных
Создадим классы, например, в алфавитном порядке:
- Класс 1:
background(фон) - Класс 2:
box(коробка) - Класс 3:
wheel(колесо)

Выберите один из классов и удерживайте зелёную кнопку под областью предпросмотра. Собранные изображения появятся на экране выборки (Image Samples).

После сбора изображений просмотрите их и удалите некорректные.

Соберите примерно по 50 изображений для каждого класса и перейдите к этапу обучения.
Обучение
Убедитесь, что выбрано правильное устройство (Grove Vision AI V2), и нажмите [Start Training].

Тестирование
После обучения можно посмотреть результат инференса.
Обратите внимание: модель при этом не работает на самом устройстве. Устройство только захватывает изображения, а предпросмотр выполняет обучаемая модель, которая запущена в Studio.

Теперь пора по-настоящему развернуть модель на устройстве.
Развёртывание
Выберите обученную модель и нажмите [Deploy to device], затем выберите Grove Vision AI V2:

Studio перенаправит нас на вкладку Vision Workplace. Подтвердите развёртывание, выберите подходящий порт и подключитесь к нему:

Модель будет записана («прошита») в устройство. После автоматической перезагрузки модель начнёт работать на устройстве. В журнале устройства (Device Logger) видно, что задержка инференса составляет около 8 мс, что соответствует частоте кадров 125 кадров в секунду (FPS).
Также можно настроить уверенность модели (confidence).

Для работы модели классификации изображений Grove Vision AI V2 потреблял пиковый ток 80 мА при 5,24 В, что соответствует энергопотреблению 420 мВт.
Сохранение модели
Модель можно сохранить в SenseCraft AI Studio. Studio хранит все наши модели, и их можно развернуть позже. Для этого вернитесь на вкладку Training и нажмите кнопку [Save to SenseCraft]:

Итоги
В этой лабораторной работе мы исследовали несколько приложений компьютерного зрения (CV) с помощью модуля Seeed Studio Grove Vision AI Module V2. Модуль показал себя как мощное, но компактное устройство, созданное специально для приложений встраиваемого машинного обучения.
Производительность. Grove Vision AI V2 показал отличные результаты в нескольких задачах компьютерного зрения. Благодаря чипу Himax WiseEye2 с двухъядерным Arm Cortex-M55 и встроенным блоком нейронных сетей Arm Ethos-U55 устройство обеспечило:
- Классификация изображений: время инференса 15 мс (67 FPS);
- Обнаружение объектов (человек): время инференса от 48 до 76 мс (от 21 до 13 FPS);
- Детекция позы: 121 мс на обнаружение ключевых точек в реальном времени с отслеживанием 17 суставов (8 FPS).
Лидерство в энергоэффективности. Одно из самых убедительных преимуществ Grove Vision AI V2 — высокая энергоэффективность. Сравнительные тесты показали заметное превосходство над традиционными встраиваемыми платформами:
- Grove Vision AI V2: пиковое потребление 80 мА (410 мВт) при более чем 60 FPS;
- XIAO ESP32S3: при похожей задаче классификации изображений — 523 мВт при более чем 3 FPS.
Практическая реализация. Возможности устройства продемонстрированы в полном сквозном проекте: создание набора данных, обучение модели, развёртывание и автономный инференс.
Удобная экосистема для разработчиков. SenseCraft AI Studio с возможностями развёртывания без кода и интеграции в пользовательские приложения делает Grove Vision AI V2 доступным как новичкам, так и опытным разработчикам. Обширная библиотека предобученных моделей и поддержка собственных моделей обеспечивают гибкость для разных приложений.
Grove Vision AI V2 — заметный шаг вперёд в аппаратном обеспечении периферийного ИИ: он предлагает профессиональные возможности компьютерного зрения в компактном энергоэффективном корпусе, который делает развёртывание ИИ во встраиваемых приложениях доступным в промышленности, IoT и образовании.
Главные выводы
Эта лабораторная работа показывает, что сложные приложения компьютерного зрения не ограничены облачными решениями или энергоёмким оборудованием, таким как Raspberry Pi или Jetson Nano: сегодня их можно эффективно развернуть на периферии с впечатляющими эффективностью и производительностью.
При желании можно использовать XIAO Vision AI Camera. Это решение объединяет модуль Grove Vision AI V2, контроллер XIAO ESP32-C3 и камеру OV5647 в корпусе, напечатанном на 3D-принтере:

Ресурсы
- Инструкции SenseCraft AI Studio
- Сайт SenseCraft-Web-Toolkit
- SenseCraft AI Studio
- Himax AI Web Toolkit
- Примеры Himax
Обзор
Эти лабораторные работы дают бесценный практический опыт работы с системами машинного обучения на основе Raspberry Pi — гибкой и доступной платформы. В отличие от работы с крупномасштабными моделями, которым нужны обширные облачные ресурсы, эти упражнения позволяют напрямую работать с оборудованием и программным обеспечением в компактной, но мощной среде периферийных вычислений. Вы получите практическое представление о развёртывании ИИ на периферии, используя возможности Raspberry Pi: от экономичной Pi Zero до более мощных моделей Pi 4 и Pi 5. Такой подход даёт осязаемое понимание проблем и возможностей внедрения решений машинного обучения в условиях ограниченных ресурсов. Хотя мы работаем в меньшем масштабе, принципы и методы, которые вы освоите, принципиально похожи на те, что используются в более крупных системах. Способность Raspberry Pi запускать полноценную операционную систему и широкие возможности GPIO создают богатый учебный опыт, который соединяет теоретические знания с реальными приложениями. В ходе этих работ вы разберётесь в тонкостях EdgeML и приобретёте навыки, применимые к широкому спектру сценариев развёртывания ИИ.

Где купить
Платы Raspberry Pi доступны у официальных продавцов по всему миру:
- Продукция Raspberry Pi (официальный сайт с поиском продавцов);
- Raspberry Pi Zero 2 W: ~$15;
- Raspberry Pi 4 (4 ГБ): ~$55;
- Raspberry Pi 5 (4 ГБ): ~$60;
- Raspberry Pi 5 (8 ГБ): ~$80.
Модули камер, блоки питания и SD-карты доступны у тех же продавцов.
Предварительные требования
-
Raspberry Pi: убедитесь, что у вас есть хотя бы одна из плат: Raspberry Pi Zero 2 W, Raspberry Pi 4 или 5 для лабораторных работ по зрению и Raspberry Pi 5 для лабораторных работ по генеративному ИИ.
-
Блок питания: для питания плат.
- Raspberry Pi Zero 2 W: 5 В / 2,5 А через адаптер micro-USB;
- Raspberry Pi 4: 5 В / 3 А через адаптер USB-C;
- Raspberry Pi 5: 5 В / 5 А через адаптер USB-C.
-
Сеть: доступ в интернет для загрузки необходимого программного обеспечения и удалённого управления платами.
-
SD-карта (не менее 32 ГБ) и переходник для SD-карты: для операционной системы Raspberry Pi OS.
Настройка
Упражнения
| Модальность | Задача | Описание | Ссылка |
|---|---|---|---|
| Зрение | Классификация изображений | Научитесь классифицировать изображения | Открыть |
| Зрение | Обнаружение объектов | Реализуйте обнаружение объектов | Открыть |
| GenAI | Малые языковые модели | Развёртывание SLM на периферии | Открыть |
| GenAI | Визуально-языковые модели | Развёртывание VLM на периферии | Открыть |
Классификация изображений

Промпт DALL·E: обложка для главы «Классификация изображений» руководства по Raspberry Pi, выполненная в том же винтажном стиле лаборатории электроники 1950-х, что и предыдущие обложки. На сцене — Raspberry Pi, подключённая к модулю камеры, которая снимает фотографию маленького синего робота, предоставленного пользователем. Робот стоит на верстаке, вокруг — классические лабораторные инструменты: паяльники, резисторы и провода. На фоне — старинное оборудование: осциллографы и ламповые радиоприёмники, сохраняющие детальное и ностальгическое ощущение эпохи. Логотипов и текста быть не должно.
Обзор
Классификация изображений — фундаментальная задача компьютерного зрения, которая заключается в отнесении изображения к одному из нескольких заранее заданных классов. Это краеугольный камень искусственного интеллекта: она позволяет машинам интерпретировать и понимать визуальную информацию так, как это делает человеческое восприятие.
Классификация изображений — это присвоение метки или категории всему изображению на основе его визуального содержания. Эта задача важна в компьютерном зрении и имеет множество применений в самых разных отраслях. Её ценность в том, что она автоматизирует задачи визуального понимания, которые иначе потребовали бы участия человека.
Применение в реальных сценариях
Классификация изображений нашла применение во многих реальных областях и меняет целые сектора:
- Здравоохранение: помощь в анализе медицинских изображений, например в выявлении отклонений на рентгеновских снимках или МРТ.
- Сельское хозяйство: контроль состояния посевов и выявление болезней растений по снимкам с воздуха.
- Автомобилестроение: системы помощи водителю и беспилотные автомобили распознают дорожные знаки, пешеходов и другие автомобили.
- Розничная торговля: визуальный поиск товаров и автоматизированное управление складскими запасами.
- Безопасность и наблюдение: повышение эффективности обнаружения угроз и систем распознавания лиц.
- Мониторинг окружающей среды: анализ спутниковых снимков для изучения вырубки лесов, городского планирования и исследований изменения климата.
Преимущества классификации на периферийных устройствах, таких как Raspberry Pi
Реализация классификации изображений на периферийных устройствах, таких как Raspberry Pi, даёт несколько весомых преимуществ:
-
Низкая задержка. Локальная обработка изображений исключает передачу данных на облачные серверы и существенно сокращает время отклика.
-
Работа без сети. Классификация выполняется без подключения к интернету, что удобно в удалённых местах или там, где связь ненадёжна.
-
Приватность и безопасность. Чувствительные данные изображений остаются на локальном устройстве, что решает задачи защиты персональных данных и требования соответствия нормативам.
-
Экономичность. Не нужны дорогие облачные вычислительные ресурсы, особенно для непрерывной или большой по объёму классификации.
-
Масштабируемость. Возможны распределённые архитектуры, в которых несколько устройств работают независимо или в составе сети.
-
Энергоэффективность. Оптимизированные модели на специализированном оборудовании могут быть энергоэффективнее облачных решений, что важно для устройств с батарейным питанием и удалённых применений.
-
Настраиваемость. Развёртывать специализированные или часто обновляемые модели, подогнанные под конкретные задачи, проще.
Используя возможности периферийных устройств, таких как Raspberry Pi, для классификации изображений, мы можем создавать более отзывчивые, безопасные и эффективные решения компьютерного зрения. Такой подход открывает новые возможности для встраивания интеллектуальной визуальной обработки в самые разные приложения и среды.
В следующих разделах мы разберём, как реализовать и оптимизировать классификацию изображений на Raspberry Pi, используя эти преимущества для создания мощных и эффективных систем компьютерного зрения.
Настройка окружения
Обновление Raspberry Pi
Сначала убедитесь, что Raspberry Pi обновлён:
sudo apt update
sudo apt upgrade -y
Установка необходимых библиотек
Установите библиотеки, необходимые для обработки изображений и машинного обучения:
sudo apt install python3-pip
sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED
pip3 install --upgrade pip
Настройка виртуального окружения (необязательно, но рекомендуется)
Создайте виртуальное окружение для управления зависимостями:
python3 -m venv ~/tflite
source ~/tflite/bin/activate
Установка TensorFlow Lite
Нас интересует инференс — выполнение модели TensorFlow Lite на устройстве для получения предсказаний по входным данным. Чтобы выполнить инференс с моделью TensorFlow Lite, её нужно запустить через интерпретатор. Интерпретатор TensorFlow Lite спроектирован лёгким и быстрым. Он использует статический порядок вычислений графа и собственный (менее динамичный) распределитель памяти, чтобы минимизировать задержки загрузки, инициализации и выполнения.
Мы будем использовать среду выполнения TensorFlow Lite для Raspberry Pi — упрощённую библиотеку для запуска моделей машинного обучения на мобильных и встраиваемых устройствах без установки всех пакетов TensorFlow.
pip install tflite_runtime --no-deps
Установлено колесо (wheel):
tflite_runtime-2.14.0-cp311-cp311-manylinux_2_34_aarch64.whl
Установка дополнительных библиотек Python
Установите библиотеки Python, необходимые для классификации изображений:
Если у вас установлена другая версия NumPy, сначала удалите её.
pip3 uninstall numpy
Установите версию 1.23.2, которая совместима с tflite_runtime.
pip3 install numpy==1.23.2
pip3 install Pillow matplotlib
Создание рабочего каталога
Если вы работаете на Raspi-Zero с минимальной ОС (без рабочего стола), у вас может не быть заранее созданной структуры каталогов пользователя (проверить её можно командой ls). Поэтому создадим её:
mkdir Documents
cd Documents/
mkdir TFLITE
cd TFLITE/
mkdir IMG_CLASS
cd IMG_CLASS
mkdir models
cd models
На Raspi-5 каталог /Documents должен уже существовать.
Получите предобученную модель классификации изображений:
Подходящая предобученная модель — ключ к успешной классификации изображений на устройствах с ограниченными ресурсами, таких как Raspberry Pi. MobileNet создана для мобильного и встраиваемого зрения и хорошо балансирует точность и скорость. Существуют версии MobileNetV1, MobileNetV2 и MobileNetV3. Загрузим версию V2:
# Одна длинная строка, разбитая обратным слешем \
wget https://storage.googleapis.com/download.tensorflow.org/\
models/tflite_11_05_08/mobilenet_v2_1.0_224_quant.tgz
tar xzf mobilenet_v2_1.0_224_quant.tgz
Получите также её метки классов:
wget https://raw.githubusercontent.com/Mjrovai/EdgeML-with-Raspberry-Pi/refs/heads/\
main/IMG_CLASS/models/labels.txt
В итоге в каталоге с моделями должны появиться файлы:

Нам понадобятся только файл
mobilenet_v2_1.0_224_quant.tfliteиlabels.txt. Остальные файлы можно удалить.
Настройка Jupyter Notebook (необязательно)
Если вы предпочитаете разрабатывать в Jupyter Notebook:
pip3 install jupyter
jupyter notebook --generate-config
Чтобы запустить Jupyter Notebook, выполните команду (замените IP-адрес на свой):
jupyter notebook --ip=192.168.4.210 --no-browser
В терминале появится локальный URL-адрес, по которому можно открыть блокнот:

К нему можно обратиться с другого устройства, введя в веб-браузере IP-адрес Raspberry Pi и токен из терминала (токен можно скопировать из терминала).

Определите рабочий каталог на Raspi и создайте новый блокнот Python 3.
Проверка установки
Проверьте установку, запустив простой скрипт Python:
import tflite_runtime.interpreter as tflite
import numpy as np
from PIL import Image
print("NumPy:", np.__version__)
print("Pillow:", Image.__version__)
# Пробуем создать интерпретатор TFLite
model_path = "./models/mobilenet_v2_1.0_224_quant.tflite"
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
print("TFLite Interpreter created successfully!")
Скрипт можно создать в nano прямо в терминале и сохранить комбинацией CTRL+O, ENTER, затем CTRL+X:

И запустить командой:

Или запустите его прямо в блокноте:

Инференс с MobileNet V2
В предыдущем разделе мы настроили окружение, в том числе загрузили популярную предобученную модель MobileNet V2. Она обучена на изображениях ImageNet размером (1,2 миллиона) для 1 001 класса (1 000 категорий объектов и 1 фоновый класс). Модель преобразована в компактный формат TensorFlow Lite размером 3,5 МБ, что делает её подходящей для Raspberry Pi с её ограниченными объёмами памяти и накопителя.

Начнём новый блокнот, чтобы пройти все шаги классификации одного изображения.
Импортируйте нужные библиотеки:
import time
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import tflite_runtime.interpreter as tflite
Загрузите модель TFLite и выделите тензоры:
model_path = "./models/mobilenet_v2_1.0_224_quant.tflite"
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
Получите входные и выходные тензоры.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
Входные данные (input details) сообщают, как модели нужно подавать изображение. Форма (1, 224, 224, 3) означает, что изображение размером подаётся по одному (размерность батча: 1).

Выходные данные (output details) показывают, что инференс даст массив из 1 001 целого значения. Каждое значение — вероятность того, что соответствующая метка относится к изображению.

Проверим тип данных (dtype) входных данных модели:
input_dtype = input_details[0]["dtype"]
input_dtype
dtype('uint8')
Это означает, что на вход должны подаваться сырые пиксели (0–255).
Получим тестовое изображение. Его можно перенести с вашего компьютера или скачать для проверки. Сначала создадим папку в рабочем каталоге:
mkdir images
cd images
wget https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg
Загрузим и выведем изображение:
# Загружаем изображение
img_path = "./images/Cat03.jpg"
img = Image.open(img_path)
# Выводим изображение
plt.figure(figsize=(8, 8))
plt.imshow(img)
plt.title("Original Image")
plt.show()

Размер изображения можно узнать командой:
width, height = img.size
Видно, что это RGB-изображение шириной 1600 и высотой 1600 пикселей. Чтобы использовать модель, его нужно привести к размеру (224, 224, 3) и добавить размерность батча 1, как указано во входных данных: (1, 224, 224, 3). Результатом инференса, как показано в выходных данных, будет массив размером 1001, как видно ниже:

Изменим размер изображения, добавим размерность батча и посмотрим результат:
img = img.resize(
(input_details[0]["shape"][1], input_details[0]["shape"][2])
)
input_data = np.expand_dims(img, axis=0)
input_data.shape
Форма input_data соответствует ожидаемой: (1, 224, 224, 3).
Проверим тип данных входного массива:
input_data.dtype
dtype('uint8')
Тип данных uint8 совпадает с типом, который ожидает модель.
Используем input_data, запустим интерпретатор и получим предсказания (выход):
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
predictions = interpreter.get_tensor(output_details[0]["index"])[0]
Предсказание — это массив из 1001 элемента. Получим Top-5 индексов с наибольшими значениями:
top_k_results = 5
top_k_indices = np.argsort(predictions)[::-1][:top_k_results]
top_k_indices
top_k_indices — массив из 5 элементов: array([283, 286, 282, 288, 479])
Итак, 283, 286, 282, 288 и 479 — наиболее вероятные классы изображения. Зная индекс, нужно понять, какому классу он соответствует (например, автомобиль, кошка или собака). Текстовый файл, загруженный вместе с моделью, содержит метку для каждого индекса от 0 до 1000. Используем функцию, чтобы загрузить .txt-файл как список:
def load_labels(filename):
with open(filename, "r") as f:
return [line.strip() for line in f.readlines()]
Получим список и выведем метки, соответствующие индексам:
labels_path = "./models/labels.txt"
labels = load_labels(labels_path)
print(labels[286])
print(labels[283])
print(labels[282])
print(labels[288])
print(labels[479])
В результате получаем:
Egyptian cat
tiger cat
tabby
lynx
carton
Как минимум четыре верхних индекса относятся к кошачьим. Предсказание — это вероятность, связанная с каждой из меток. Как мы видели в выходных данных, эти значения квантованы, поэтому их нужно деквантовать и применить softmax:
scale, zero_point = output_details[0]["quantization"]
dequantized_output = (
predictions.astype(np.float32) - zero_point
) * scale
exp_output = np.exp(dequantized_output - np.max(dequantized_output))
probabilities = exp_output / np.sum(exp_output)
Выведем вероятности для топ-5:
print(probabilities[286])
print(probabilities[283])
print(probabilities[282])
print(probabilities[288])
print(probabilities[479])
0.27741462
0.3732285
0.16919471
0.10319158
0.023410844
Для наглядности создадим функцию, которая сопоставляет метки с вероятностями:
for i in range(top_k_results):
print(
"\t{:20}: {}%".format(
labels[top_k_indices[i]],
(int(probabilities[top_k_indices[i]] * 100)),
)
)
tiger cat : 37%
Egyptian cat : 27%
tabby : 16%
lynx : 10%
carton : 2%
Определение общей функции классификации изображений
Создадим общую функцию: на вход подаётся изображение, на выходе получаем Top-5 возможных классов:
def image_classification(
img_path, model_path, labels, top_k_results=5
):
# Загружаем изображение
img = Image.open(img_path)
plt.figure(figsize=(4, 4))
plt.imshow(img)
plt.axis("off")
# Загружаем модель TFLite
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
# Получаем входные и выходные тензоры
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
# Предобработка
img = img.resize(
(input_details[0]["shape"][1], input_details[0]["shape"][2])
)
input_data = np.expand_dims(img, axis=0)
# Инференс на Raspi-Zero
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
# Получаем результаты и сопоставляем их с классами
predictions = interpreter.get_tensor(output_details[0]["index"])[
0
]
# Получаем индексы top-k результатов
top_k_indices = np.argsort(predictions)[::-1][:top_k_results]
# Получаем параметры квантования
scale, zero_point = output_details[0]["quantization"]
# Деквантуем выход и применяем softmax
dequantized_output = (
predictions.astype(np.float32) - zero_point
) * scale
exp_output = np.exp(
dequantized_output - np.max(dequantized_output)
)
probabilities = exp_output / np.sum(exp_output)
print("\n\t[PREDICTION] [Prob]\n")
for i in range(top_k_results):
print(
"\t{:20}: {}%".format(
labels[top_k_indices[i]],
(int(probabilities[top_k_indices[i]] * 100)),
)
)
Загрузив несколько изображений для проверки, получим:

Тестирование модели, обученной с нуля
Получим модель TFLite, обученную с нуля. Для этого следуйте блокноту:
CNN для классификации набора данных Cifar-10
В блокноте мы обучили модель на наборе данных CIFAR-10, который содержит 60 000 изображений 10 классов CIFAR (самолёт, автомобиль, птица, кошка, олень, собака, лягушка, лошадь, корабль и грузовик). Изображения CIFAR — цветные, размером (3 цветовых канала). Объекты на них не центрированы и могут находиться на фоне: например, самолёты могут быть на фоне облачного неба! Короче говоря, это маленькие, но настоящие изображения.
Обученная CNN-модель (cifar10_model.keras) занимала 2,0 МБ. С помощью TFLite Converter модель cifar10.tflite уменьшилась до 674 КБ (около трети исходного размера).

В блокноте Cifar 10 — классификация изображений на Raspi с TFLite, который можно запустить на Raspi, мы повторим те же шаги, что и с mobilenet_v2_1.0_224_quant.tflite. Ниже приведены примеры классификации изображений с помощью общей функции классификации на Raspi-Zero, как в предыдущем разделе.

Установка Picamera2
Picamera2 — библиотека Python для работы с камерой Raspberry Pi. Она основана на стеке камер libcamera и поддерживается фондом Raspberry Pi. Picamera2 работает на всех моделях Raspberry Pi — от Pi Zero до RPi 5. На Raspi она уже установлена в системе, но её нужно сделать доступной внутри виртуального окружения.
-
Сначала активируйте виртуальное окружение, если оно ещё не активировано:
source ~/tflite/bin/activate -
Теперь создайте файл
.pthв виртуальном окружении, чтобы добавить путь к системным site-packages:echo "/usr/lib/python3/dist-packages" > \ $VIRTUAL_ENV/lib/python3.11/site-packages/system_site_packages.pthПримечание: если у вас другая версия Python, замените
python3.11на нужную версию. -
После создания файла попробуйте импортировать picamera2 в Python:
python3 >>> import picamera2 >>> print(picamera2.__file__)
Этот код покажет путь к файлу самого модуля picamera2, что подтверждает доступность библиотеки из окружения.
/home/username/tflite/lib/python3.11/site-packages/\
picamera2/__init__.py
Также можно вывести список доступных в системе камер:
>>> print(Picamera2.global_camera_info())
В моём случае, с подключённой USB-камерой, получилось:

Теперь, когда мы убедились, что picamera2 работает в окружении с index 0, попробуем простой скрипт Python, который захватывает изображение с USB-камеры:
from picamera2 import Picamera2
import time
# Инициализируем камеру
picam2 = Picamera2() # по умолчанию индекс 0
# Настраиваем камеру
config = picam2.create_still_configuration(main={"size": (640, 480)})
picam2.configure(config)
# Запускаем камеру
picam2.start()
# Даём камере прогреться
time.sleep(2)
# Делаем снимок
picam2.capture_file("usb_camera_image.jpg")
print("Image captured and saved as 'usb_camera_image.jpg'")
# Останавливаем камеру
picam2.stop()
Используйте редактор Nano, Jupyter Notebook или любой другой редактор. Сохраните код как Python-скрипт (например, capture_image.py) и запустите его. Скрипт захватит изображение с камеры и сохранит его как «usb_camera_image.jpg» в том же каталоге, где находится скрипт.

Если Jupyter открыт, захваченное изображение можно увидеть прямо на своём компьютере. Иначе перенесите файл с Raspi на компьютер.

Если вы работаете с Raspi-5 и у вас полноценный рабочий стол, можно открыть файл прямо на устройстве.
Проект классификации изображений
Теперь разработаем полноценный проект классификации изображений с использованием Edge Impulse Studio. Как и в случае с MobileNet V2, для инференса будет использоваться обученная и сконвертированная модель TFLite.
Цель
Первый шаг в любом проекте машинного обучения — определить цель. В данном случае нужно обнаружить и классифицировать два конкретных объекта на одном изображении. Для проекта мы возьмём две небольшие игрушки: робота и маленького бразильского попугая (по имени Periquito). Также соберём изображения фона, на котором этих двух объектов нет.

Сбор данных
Определив цель проекта машинного обучения, мы переходим к следующему, самому важному шагу — сбору набора данных. Снимки можно делать с телефона, но здесь мы будем использовать Raspi. Настроим простой веб-сервер на Raspberry Pi, чтобы просматривать в браузере снимки в формате QVGA (320 x 240).
-
Сначала установите Flask, лёгкий веб-фреймворк для Python:
pip3 install flask -
Создадим новый скрипт Python, который объединяет захват изображений и веб-сервер. Назовём его
get_img_data.py:
from flask import (Flask, Response, render_template_string,
request, redirect, url_for)
from picamera2 import Picamera2
import io
import threading
import time
import os
import signal
app = Flask(__name__)
# Глобальные переменные
base_dir = "dataset"
picam2 = None
frame = None
frame_lock = threading.Lock()
capture_counts = {}
current_label = None
shutdown_event = threading.Event()
def initialize_camera():
global picam2
picam2 = Picamera2()
config = picam2.create_preview_configuration(
main={"size": (320, 240)}
)
picam2.configure(config)
picam2.start()
time.sleep(2) # Ждём, пока камера прогреется
def get_frame():
global frame
while not shutdown_event.is_set():
stream = io.BytesIO()
picam2.capture_file(stream, format='jpeg')
with frame_lock:
frame = stream.getvalue()
time.sleep(0.1) # При необходимости измените для плавного предпросмотра
def generate_frames():
while not shutdown_event.is_set():
with frame_lock:
if frame is not None:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' +
frame + b'\r\n')
time.sleep(0.1) # При необходимости измените для плавной передачи видео
def shutdown_server():
shutdown_event.set()
if picam2:
picam2.stop()
# Даём время остальным потокам завершиться
time.sleep(2)
# Отправляем SIGINT основному процессу
os.kill(os.getpid(), signal.SIGINT)
@app.route('/', methods=['GET', 'POST'])
def index():
global current_label
if request.method == 'POST':
current_label = request.form['label']
if current_label not in capture_counts:
capture_counts[current_label] = 0
os.makedirs(os.path.join(base_dir, current_label),
exist_ok=True)
return redirect(url_for('capture_page'))
return render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>Dataset Capture - Label Entry</title>
</head>
<body>
<h1>Enter Label for Dataset</h1>
<form method="post">
<input type="text" name="label" required>
<input type="submit" value="Start Capture">
</form>
</body>
</html>
''')
@app.route('/capture')
def capture_page():
return render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>Dataset Capture</title>
<script>
var shutdownInitiated = false;
function checkShutdown() {
if (!shutdownInitiated) {
fetch('/check_shutdown')
.then(response => response.json())
.then(data => {
if (data.shutdown) {
shutdownInitiated = true;
document.getElementById(
'video-feed').src = '';
document.getElementById(
'shutdown-message')
.style.display = 'block';
}
});
}
}
setInterval(checkShutdown, 1000); // Проверка каждую секунду
</script>
</head>
<body>
<h1>Dataset Capture</h1>
<p>Current Label: {{ label }}</p>
<p>Images captured for this label: {{ capture_count
}}</p>
<img id="video-feed" src="{{ url_for('video_feed')
}}" width="640"
height="480" />
<div id="shutdown-message" style="display: none;
color: red;">
Capture process has been stopped.
You can close this window.
</div>
<form action="/capture_image" method="post">
<input type="submit" value="Capture Image">
</form>
<form action="/stop" method="post">
<input type="submit" value="Stop Capture"
style="background-color: #ff6666;">
</form>
<form action="/" method="get">
<input type="submit" value="Change Label"
style="background-color: #ffff66;">
</form>
</body>
</html>
''', label=current_label, capture_count=capture_counts.get(
current_label, 0))
@app.route('/video_feed')
def video_feed():
return Response(generate_frames(),
mimetype='multipart/x-mixed-replace; boundary=frame')
@app.route('/capture_image', methods=['POST'])
def capture_image():
global capture_counts
if current_label and not shutdown_event.is_set():
capture_counts[current_label] += 1
timestamp = time.strftime("%Y%m%d-%H%M%S")
filename = f"image_{timestamp}.jpg"
full_path = os.path.join(base_dir, current_label,
filename)
picam2.capture_file(full_path)
return redirect(url_for('capture_page'))
@app.route('/stop', methods=['POST'])
def stop():
summary = render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>Dataset Capture - Stopped</title>
</head>
<body>
<h1>Dataset Capture Stopped</h1>
<p>The capture process has been stopped.
You can close this window.</p>
<p>Summary of captures:</p>
<ul>
{% for label, count in capture_counts.items() %}
<li>{{ label }}: {{ count }} images</li>
{% endfor %}
</ul>
</body>
</html>
''', capture_counts=capture_counts)
# Запускаем новый поток для остановки сервера
threading.Thread(target=shutdown_server).start()
return summary
@app.route('/check_shutdown')
def check_shutdown():
return {'shutdown': shutdown_event.is_set()}
if __name__ == '__main__':
initialize_camera()
threading.Thread(target=get_frame, daemon=True).start()
app.run(host='0.0.0.0', port=5000, threaded=True)
- Запустите скрипт:
python3 get_img_data.py
-
Откройте веб-интерфейс:
- На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу
http://localhost:5000. - С другого устройства в той же сети: откройте веб-браузер и перейдите по адресу
http://<raspberry_pi_ip>:5000(замените<raspberry_pi_ip>на IP-адрес вашей Raspberry Pi). Например:http://192.168.4.210:5000/
- На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу
Этот скрипт Python создаёт веб-интерфейс для захвата и организации наборов изображений с помощью Raspberry Pi и её камеры. Он удобен для проектов машинного обучения, которым нужны размеченные изображения.
Основные возможности
- Веб-интерфейс: доступ с любого устройства в той же сети, что и Raspberry Pi.
- Живой предпросмотр с камеры: показывает видео с камеры в реальном времени.
- Система меток: позволяет вводить метки для разных категорий изображений.
- Организованное хранение: автоматически сохраняет изображения в подкаталогах по меткам.
- Счётчики по меткам: ведут учёт количества снимков для каждой метки.
- Итоговая статистика: выводит сводку по снимкам при остановке захвата.
Основные компоненты
- Веб-приложение Flask: обрабатывает маршруты и отдаёт веб-интерфейс.
- Интеграция с Picamera2: управляет камерой Raspberry Pi.
- Потоковый захват кадров: обеспечивает плавный предпросмотр.
- Управление файлами: раскладывает снимки по каталогам с метками.
Ключевые функции
initialize_camera(): настраивает экземпляр Picamera2.get_frame(): непрерывно захватывает кадры для предпросмотра.generate_frames(): отдаёт кадры для живой видеотрансляции.shutdown_server(): устанавливает событие остановки, останавливает камеру и выключает сервер Flask.index(): обрабатывает страницу ввода метки.capture_page(): выводит основной интерфейс захвата.video_feed(): показывает живой предпросмотр для наведения камеры.capture_image(): сохраняет изображение с текущей меткой.stop(): останавливает процесс захвата и выводит сводку.
Порядок работы
- Запустите скрипт на Raspberry Pi.
- Откройте веб-интерфейс в браузере.
- Введите метку для изображений, которые хотите снять, и нажмите
Start Capture.

- Используйте живой предпросмотр, чтобы навести камеру.
- Нажмите
Capture Image, чтобы сохранить изображения под текущей меткой.

- При необходимости меняйте метки для разных категорий, выбирая
Change Label. - Нажмите
Stop Capture, когда закончите, чтобы увидеть сводку.

Технические заметки
- Скрипт использует потоки (threading), чтобы одновременно захватывать кадры и обслуживать веб-страницы.
- Имена файлов содержат временную метку, чтобы каждое имя было уникальным.
- Веб-интерфейс адаптивен и доступен с мобильных устройств.
Возможности настройки
- Измените разрешение изображения в функции
initialize_camera(). Здесь мы использовали QVGA . - Измените HTML-шаблоны, чтобы поменять внешний вид.
- Добавьте дополнительные шаги обработки или анализа изображений в функции
capture_image().
Количество образцов в наборе данных
Снимите около 60 изображений для каждой категории (periquito, robot и background). Старайтесь снимать с разных ракурсов, на разных фонах и при разном освещении. На Raspi в итоге появится папка dataset, которая содержит 3 подпапки: periquito, robot и background — по одной для каждого класса изображений.
Для переноса созданного набора данных на основной компьютер можно использовать Filezilla.
Обучение модели в Edge Impulse Studio
Для обучения модели мы будем использовать Edge Impulse Studio. Перейдите на страницу Edge Impulse, войдите в свой аккаунт и создайте новый проект:

Здесь можно клонировать похожий проект: Raspi - Img Class.
Набор данных
В Studio (EI Studio) мы пройдём четыре основных шага: Dataset (набор данных), Impulse, Tests (тесты) и Deploy (развёртывание на периферийном устройстве, в нашем случае на Raspi). Эти шаги помогают подготовить модель к работе на Raspi.
Важно понимать, что исходный набор данных, снятый с помощью Raspi, будет разделён на обучающую, валидационную и тестовую выборки. Тестовая выборка отделяется с самого начала и используется только на этапе тестирования после обучения. Валидационная выборка используется во время обучения.
В Studio загрузите снятые данные, выполнив шаги:
- Откройте вкладку
Data acquisitionи в разделеUPLOAD DATAзагрузите файлы с компьютера в выбранные категории. - Оставьте разбиение исходного набора данных на обучающую и тестовую выборки за Studio и выберите метку (label) для каждой категории.
- Повторите процедуру для всех трёх классов. В итоге в Studio должны появиться ваши «сырые данные»:

Studio позволяет изучать данные, показывая полную картину всех данных проекта. Можно очищать, проверять и менять метки, нажимая на отдельные элементы данных. В нашем простом проекте данные выглядят корректными.

Проектирование Impulse
На этом этапе нужно определить, как:
- предобработать данные: изменить размер отдельных изображений и выбрать
глубину цвета(RGB или оттенки серого); - задать модель. В нашем случае это
Transfer Learning (Images): дообучение предобученной модели классификации изображений MobileNet V2 на наших данных. Этот метод хорошо работает даже с относительно небольшими наборами изображений (около 180 изображений в нашем случае).
Перенос обучения (transfer learning) с MobileNet упрощает обучение модели, что особенно полезно в условиях ограниченных ресурсов и для проектов с небольшим числом размеченных данных. MobileNet, известная своей лёгкой архитектурой, — это предобученная модель, которая уже выучила полезные признаки на большом наборе данных (ImageNet).

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

Такой подход заметно сокращает время обучения и вычислительные затраты, что делает его идеальным для быстрого прототипирования и развёртывания на встраиваемых устройствах, где эффективность важнее всего.
Перейдите на вкладку Impulse Design и создайте impulse, задав размер изображения и сжатие (квадратная форма без обрезки). Выберите блоки Image и Transfer Learning. Сохраните Impulse.

Предобработка изображений
Все входные изображения QVGA/RGB565 будут преобразованы в 76 800 признаков .

Нажмите Save parameters и на следующей вкладке выберите Generate features.
Модель
MobileNet — семейство эффективных свёрточных нейронных сетей, созданных для мобильного и встраиваемого зрения. Ключевые особенности MobileNet:
- Лёгкость: оптимизирована для мобильных устройств и встраиваемых систем с ограниченными вычислительными ресурсами.
- Скорость: быстрый инференс, подходящий для приложений реального времени.
- Точность: сохраняет хорошую точность, несмотря на компактный размер.
MobileNetV2, представленная в 2018 году, улучшает исходную архитектуру MobileNet. Её ключевые особенности:
- Инвертированные остаточные блоки (Inverted Residuals): используются инвертированные остаточные структуры, в которых соединения-шорткаты связывают тонкие «бутылочные» слои (bottleneck).
- Линейные бутылочные слои (Linear Bottlenecks): в узких слоях удаляются нелинейности, чтобы не разрушать информацию.
- Разделяемые по глубине свёртки (depthwise separable convolutions): сохраняется эта эффективная операция из MobileNetV1.
В нашем проекте мы выполним Transfer Learning с моделью MobileNetV2 160x160 1.0. Это значит, что изображения для обучения (и для будущего инференса) должны иметь входной размер пикселей и множитель ширины (Width Multiplier) 1,0 (полная ширина, без уменьшения). Эта конфигурация сбалансирована по размеру модели, скорости и точности.
Обучение модели
Ещё одна полезная методика глубокого обучения — аугментация данных (Data Augmentation). Она повышает точность моделей машинного обучения за счёт создания дополнительных искусственных данных: система аугментации вносит небольшие случайные изменения в обучающие данные во время обучения (например, отражает, обрезает или поворачивает изображения).
Заглянем «под капот» и посмотрим, как Edge Impulse реализует политику аугментации данных:
# Реализует политику аугментации данных
def augment_image(image, label):
# Случайно отражаем изображение
image = tf.image.random_flip_left_right(image)
# Увеличиваем изображение, затем случайно обрезаем его до
# исходных размеров
resize_factor = random.uniform(1, 1.2)
new_height = math.floor(resize_factor * INPUT_SHAPE[0])
new_width = math.floor(resize_factor * INPUT_SHAPE[1])
image = tf.image.resize_with_crop_or_pad(
image, new_height, new_width
)
image = tf.image.random_crop(image, size=INPUT_SHAPE)
# Меняем яркость изображения
image = tf.image.random_brightness(image, max_delta=0.2)
return image, label
Такие вариации во время обучения помогают модели не срезать углы, «запоминая» поверхностные признаки в обучающих данных. Благодаря этому модель лучше отражает глубинные закономерности набора данных.
Последний полносвязный слой нашей модели будет содержать 0 нейронов с dropout 10 % для предотвращения переобучения. Вот результат обучения:

Результат отличный: задержка около 35 мс (для Raspi-4), что должно дать около 30 кадров в секунду (fps) при инференсе. Raspi-Zero будет медленнее, а Raspi-5 — быстрее.
Компромисс между точностью и скоростью
Если нужен более быстрый инференс, модель следует обучать с меньшими значениями альфа (0,35; 0,5 и 0,75) или даже уменьшить размер входного изображения, пожертвовав частью точности. Однако уменьшение размера входного изображения и снижение альфа (множителя ширины) могут ускорить инференс MobileNet V2, и у них разные компромиссы. Сравним:
- Уменьшение размера входного изображения:
Плюсы:
- Существенно снижает вычислительные затраты во всех слоях.
- Уменьшает использование памяти.
- Часто даёт заметный прирост скорости.
Минусы:
- Может ухудшить способность модели обнаруживать мелкие признаки и детали.
- Может заметно повлиять на точность, особенно в задачах, требующих тонкого распознавания.
- Уменьшение альфа (множителя ширины):
Плюсы:
- Уменьшает количество параметров и вычислений в модели.
- Сохраняет исходное разрешение входа, что потенциально позволяет сохранить больше деталей.
- Может обеспечить хороший баланс между скоростью и точностью.
Минусы:
- Может не ускорить инференс так существенно, как уменьшение входного размера.
- Может снизить способность модели выучивать сложные признаки.
Сравнение:
-
Влияние на скорость:
- Уменьшение входного размера часто даёт более заметный прирост скорости, поскольку снижает вычисления квадратично (уменьшение ширины и высоты вдвое сокращает вычисления примерно на 75 %).
- Уменьшение альфа даёт более линейное снижение вычислений.
-
Влияние на точность:
- Уменьшение входного размера может сильно повлиять на точность, особенно при обнаружении мелких объектов или деталей.
- Уменьшение альфа обычно влияет на точность более плавно.
-
Архитектура модели:
- Изменение входного размера не меняет архитектуру модели.
- Изменение альфа меняет структуру модели, уменьшая количество каналов в каждом слое.
Рекомендации:
- Если приложению не нужно обнаруживать мелкие детали и оно может мириться с небольшой потерей точности, уменьшение входного размера — часто самый эффективный способ ускорить инференс.
- Уменьшение альфа предпочтительнее, если важно сохранить способность обнаруживать мелкие детали или нужен более сбалансированный компромисс между скоростью и точностью.
- Для лучшего результата поэкспериментируйте с обоими способами:
- Попробуйте MobileNet V2 с входными размерами, например или ;
- Поэкспериментируйте со значениями альфа 1,0; 0,75; 0,5 или 0,35.
- Всегда проверяйте разные конфигурации на своём оборудовании и на своём наборе данных, чтобы найти оптимальный баланс для вашей задачи.
Помните: лучший выбор зависит от ваших требований к точности и скорости и от характера изображений, с которыми вы работаете. Часто стоит поэкспериментировать с комбинациями, чтобы найти оптимальную конфигурацию для своего случая.
Тестирование модели
Теперь возьмите набор данных, отложенный в начале проекта, и запустите на нём обученную модель. Результат снова отличный: 92,22 %.
Развёртывание модели
Как и в предыдущем разделе, модель можно развернуть в формате .tflite и запустить на Raspi с помощью Python.
На вкладке Dashboard перейдите к модели Transfer learning (int8 quantized) и нажмите значок загрузки:

Для сравнения скачайте также версию float32.
Перенесите модель с компьютера на Raspi (в ./models), например с помощью FileZilla. Также сделайте несколько снимков для инференса (в ./images).
Импортируйте нужные библиотеки:
import time
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import tflite_runtime.interpreter as tflite
Укажите пути и метки:
img_path = "./images/robot.jpg"
model_path = "./models/ei-raspi-img-class-int8-quantized-model.tflite"
labels = ["background", "periquito", "robot"]
Обратите внимание: модели, обученные в Edge Impulse Studio, выдают значения с индексами 0, 1, 2 и т. д., а метки следуют в алфавитном порядке.
Загрузите модель, выделите тензоры и получите информацию о входных и выходных тензорах:
# Загружаем модель TFLite
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
# Получаем входные и выходные тензоры
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
Важное отличие: dtype входных данных модели теперь int8, то есть входные значения лежат в диапазоне от –128 до +127, тогда как каждый пиксель нашего изображения — от 0 до 255. Поэтому изображение нужно предобработать. Проверим:
input_dtype = input_details[0]["dtype"]
input_dtype
numpy.int8
Откроем изображение и выведем его:
img = Image.open(img_path)
plt.figure(figsize=(4, 4))
plt.imshow(img)
plt.axis("off")
plt.show()

Выполним предобработку:
scale, zero_point = input_details[0]["quantization"]
img = img.resize(
(input_details[0]["shape"][1], input_details[0]["shape"][2])
)
img_array = np.array(img, dtype=np.float32) / 255.0
img_array = (
(img_array / scale + zero_point).clip(-128, 127).astype(np.int8)
)
input_data = np.expand_dims(img_array, axis=0)
Проверим входные данные, чтобы убедиться, что входной тензор соответствует ожиданиям модели:
input_data.shape, input_data.dtype
((1, 160, 160, 3), dtype('int8'))
Теперь выполним инференс. Также вычислим задержку модели:
# Инференс на Raspi-Zero
start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (end_time - start_time) * 1000 # Переводим в миллисекунды
print("Inference time: {:.1f}ms".format(inference_time))
На инференс модель затратит около 125 мс на Raspi-Zero, то есть в 3–4 раза дольше, чем на Raspi-5.
Теперь получим метки и вероятности на выходе. Важно отметить, что модель, обученная в Edge Impulse Studio, имеет softmax на выходе (в отличие от исходной MobileNet V2), поэтому в качестве «вероятностей» нужно использовать сырой выход модели.
# Получаем результаты и сопоставляем их с классами
predictions = interpreter.get_tensor(output_details[0]["index"])[0]
# Получаем индексы top-k результатов
top_k_results = 3
top_k_indices = np.argsort(predictions)[::-1][:top_k_results]
# Получаем параметры квантования
scale, zero_point = output_details[0]["quantization"]
# Деквантуем выход
dequantized_output = (
predictions.astype(np.float32) - zero_point
) * scale
probabilities = dequantized_output
print("\n\t[PREDICTION] [Prob]\n")
for i in range(top_k_results):
print(
"\t{:20}: {:.2f}%".format(
labels[top_k_indices[i]],
probabilities[top_k_indices[i]] * 100,
)
)

Изменим ранее созданную функцию так, чтобы она работала с моделями разных типов:
def image_classification(
img_path, model_path, labels, top_k_results=3, apply_softmax=False
):
# Загружаем изображение
img = Image.open(img_path)
plt.figure(figsize=(4, 4))
plt.imshow(img)
plt.axis("off")
# Загружаем модель TFLite
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
# Получаем входные и выходные тензоры
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
# Предобработка
img = img.resize(
(input_details[0]["shape"][1], input_details[0]["shape"][2])
)
input_dtype = input_details[0]["dtype"]
if input_dtype == np.uint8:
input_data = np.expand_dims(np.array(img), axis=0)
elif input_dtype == np.int8:
scale, zero_point = input_details[0]["quantization"]
img_array = np.array(img, dtype=np.float32) / 255.0
img_array = (
(img_array / scale + zero_point)
.clip(-128, 127)
.astype(np.int8)
)
input_data = np.expand_dims(img_array, axis=0)
else: # float32
input_data = (
np.expand_dims(np.array(img, dtype=np.float32), axis=0)
/ 255.0
)
# Инференс на Raspi-Zero
start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (end_time - start_time) * 1000 # Переводим в миллисекунды
# Получаем результаты
predictions = interpreter.get_tensor(output_details[0]["index"])[0]
# Получаем индексы top-k результатов
top_k_indices = np.argsort(predictions)[::-1][:top_k_results]
# Обрабатываем выход в зависимости от его типа
output_dtype = output_details[0]["dtype"]
if output_dtype in [np.int8, np.uint8]:
# Деквантуем выход
scale, zero_point = output_details[0]["quantization"]
predictions = (
predictions.astype(np.float32) - zero_point
) * scale
if apply_softmax:
# Применяем softmax
exp_preds = np.exp(predictions - np.max(predictions))
probabilities = exp_preds / np.sum(exp_preds)
else:
probabilities = predictions
print("\n\t[PREDICTION] [Prob]\n")
for i in range(top_k_results):
print(
"\t{:20}: {:.1f}%".format(
labels[top_k_indices[i]],
probabilities[top_k_indices[i]] * 100,
)
)
print("\n\tInference time: {:.1f}ms".format(inference_time))
Протестируем её на разных изображениях с int8-квантованной моделью (160x160, alpha = 1,0):

Для проверки скачаем модель меньшего размера, например обученную для Nicla Vision Lab (int8-квантованная модель, , alpha = 0,1). Можно использовать ту же функцию:

Точность модели немного снизилась, но результат всё ещё приемлем, поскольку наша модель не ищет много деталей. По задержке на Raspi-Zero модель работает примерно в десять раз быстрее.
Классификация изображений в реальном времени
Создадим приложение, которое в реальном времени снимает изображения USB-камерой и показывает результат классификации.
Через nano в терминале сохраните приведённый ниже код, например как img_class_live_infer.py.
from flask import (Flask, Response, render_template_string,
request, jsonify)
from picamera2 import Picamera2
import io
import threading
import time
import numpy as np
from PIL import Image
import tflite_runtime.interpreter as tflite
from queue import Queue, Empty
app = Flask(__name__)
# Глобальные переменные
picam2 = None
frame = None
frame_lock = threading.Lock()
is_classifying = False
confidence_threshold = 0.8
model_path = "./models/ei-raspi-img-class-int8-quantized-model.tflite"
labels = ['background', 'periquito', 'robot']
interpreter = None
classification_queue = Queue(maxsize=1)
def initialize_camera():
global picam2
picam2 = Picamera2()
config = picam2.create_preview_configuration(
main={"size": (320, 240)}
)
picam2.configure(config)
picam2.start()
time.sleep(2) # Ждём, пока камера прогреется
def get_frame():
global frame
while True:
stream = io.BytesIO()
picam2.capture_file(stream, format='jpeg')
with frame_lock:
frame = stream.getvalue()
time.sleep(0.1) # Захватываем кадры чаще
def generate_frames():
while True:
with frame_lock:
if frame is not None:
yield (
b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n'
+ frame + b'\r\n'
)
time.sleep(0.1)
def load_model():
global interpreter
if interpreter is None:
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
return interpreter
def classify_image(img, interpreter):
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
img = img.resize((input_details[0]['shape'][1],
input_details[0]['shape'][2]))
input_data = np.expand_dims(np.array(img), axis=0).astype(input_details[0]['dtype'])
interpreter.set_tensor(input_details[0]['index'], input_data)
interpreter.invoke()
predictions = interpreter.get_tensor(output_details[0]
['index'])[0]
# Обрабатываем выход в зависимости от его типа
output_dtype = output_details[0]['dtype']
if output_dtype in [np.int8, np.uint8]:
# Деквантуем выход
scale, zero_point = output_details[0]['quantization']
predictions = (predictions.astype(np.float32) -
zero_point) * scale
return predictions
def classification_worker():
interpreter = load_model()
while True:
if is_classifying:
with frame_lock:
if frame is not None:
img = Image.open(io.BytesIO(frame))
predictions = classify_image(img, interpreter)
max_prob = np.max(predictions)
if max_prob >= confidence_threshold:
label = labels[np.argmax(predictions)]
else:
label = 'Uncertain'
classification_queue.put({
'label': label,
'probability': float(max_prob)
})
time.sleep(0.1) # При необходимости настройте
@app.route('/')
def index():
return render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>Image Classification</title>
<script
src="https://code.jquery.com/jquery-3.6.0.min.js">
</script>
<script>
function startClassification() {
$.post('/start');
$('#startBtn').prop('disabled', true);
$('#stopBtn').prop('disabled', false);
}
function stopClassification() {
$.post('/stop');
$('#startBtn').prop('disabled', false);
$('#stopBtn').prop('disabled', true);
}
function updateConfidence() {
var confidence = $('#confidence').val();
$.post('/update_confidence',
{confidence: confidence}
);
}
function updateClassification() {
$.get('/get_classification', function(data) {
$('#classification').text(data.label + ': '
+ data.probability.toFixed(2));
});
}
$(document).ready(function() {
setInterval(updateClassification, 100);
// Обновляем каждые 100 мс
});
</script>
</head>
<body>
<h1>Image Classification</h1>
<img src="{{ url_for('video_feed') }}"
width="640"
height="480" />
<br>
<button id="startBtn"
onclick="startClassification()">
Start Classification
</button>
<button id="stopBtn"
onclick="stopClassification()"
disabled>
Stop Classification
</button>
<br>
<label for="confidence">Confidence Threshold:</label>
<input type="number"
id="confidence"
name="confidence"
min="0" max="1"
step="0.1"
value="0.8"
onchange="updateConfidence()" />
<br>
<div id="classification">
Waiting for classification...
</div>
</body>
</html>
''')
@app.route('/video_feed')
def video_feed():
return Response(
generate_frames(),
mimetype='multipart/x-mixed-replace; boundary=frame'
)
@app.route('/start', methods=['POST'])
def start_classification():
global is_classifying
is_classifying = True
return '', 204
@app.route('/stop', methods=['POST'])
def stop_classification():
global is_classifying
is_classifying = False
return '', 204
@app.route('/update_confidence', methods=['POST'])
def update_confidence():
global confidence_threshold
confidence_threshold = float(request.form['confidence'])
return '', 204
@app.route('/get_classification')
def get_classification():
if not is_classifying:
return jsonify({'label': 'Not classifying',
'probability': 0})
try:
result = classification_queue.get_nowait()
except Empty:
result = {'label': 'Processing', 'probability': 0}
return jsonify(result)
if __name__ == '__main__':
initialize_camera()
threading.Thread(target=get_frame, daemon=True).start()
threading.Thread(target=classification_worker,
daemon=True).start()
app.run(host='0.0.0.0', port=5000, threaded=True)
В терминале выполните:
python3 img_class_live_infer.py
И откройте веб-интерфейс:
- На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу
http://localhost:5000. - С другого устройства в той же сети: откройте веб-браузер и перейдите по адресу
http://<raspberry_pi_ip>:5000(замените<raspberry_pi_ip>на IP-адрес вашей Raspberry Pi). Например:http://192.168.4.210:5000/
Вот несколько скриншотов приложения, запущенного на внешнем компьютере:

Видео работы приложения можно посмотреть на YouTube: ссылка на видео.
Код создаёт веб-приложение для классификации изображений в реальном времени на Raspberry Pi с модулем камеры и моделью TensorFlow Lite. Приложение использует Flask для веб-интерфейса, где можно видеть изображение с камеры и результаты классификации в реальном времени.
Ключевые компоненты
- Веб-приложение Flask: отдаёт пользовательский интерфейс и обрабатывает запросы.
- PiCamera2: захватывает изображения с модуля камеры Raspberry Pi.
- TensorFlow Lite: запускает модель классификации изображений.
- Потоки (Threading): управляют параллельными операциями для плавной работы.
Основные возможности
- Вывод изображения с камеры в реальном времени
- Классификация изображений в реальном времени
- Настраиваемый порог уверенности
- Запуск и остановка классификации по требованию
Структура кода
-
Импорты и настройка:
- Flask для веб-приложения
- PiCamera2 для управления камерой
- TensorFlow Lite для инференса
- Threading и Queue для параллельных операций
-
Глобальные переменные:
- Управление камерой и кадрами
- Управление классификацией
- Информация о модели и метках
-
Функции камеры:
initialize_camera(): настраивает PiCamera2get_frame(): непрерывно захватывает кадрыgenerate_frames(): отдаёт кадры для веб-трансляции
-
Функции модели:
load_model(): загружает модель TFLiteclassify_image(): выполняет инференс для одного изображения
-
Рабочий поток классификации:
- Работает в отдельном потоке
- Непрерывно классифицирует кадры, пока включена классификация
- Обновляет очередь последними результатами
-
Маршруты Flask:
/: отдаёт главную HTML-страницу/video_feed: передаёт видеопоток с камеры/startи/stop: управляют классификацией/update_confidence: меняет порог уверенности/get_classification: возвращает последний результат классификации
-
HTML-шаблон:
- Показывает изображение с камеры и результаты классификации
- Содержит элементы управления запуском, остановкой и настройками
-
Главный блок выполнения:
- Инициализирует камеру и запускает нужные потоки
- Запускает приложение Flask
Ключевые понятия
- Параллельные операции: потоки обрабатывают захват с камеры и классификацию отдельно от веб-сервера.
- Обновления в реальном времени: результаты классификации обновляются часто, без перезагрузки страницы.
- Повторное использование модели: модель TFLite загружается один раз и используется повторно, что эффективно.
- Гибкая настройка: пользователь может менять порог уверенности на лету.
Использование
- Убедитесь, что все зависимости установлены.
- Запустите скрипт на Raspberry Pi с модулем камеры.
- Откройте веб-интерфейс в браузере, используя IP-адрес Raspberry Pi.
- Запустите классификацию и при необходимости настройте параметры.
Итоги
Классификация изображений стала мощным и универсальным применением машинного обучения с важными последствиями для самых разных областей — от здравоохранения до мониторинга окружающей среды. В этой главе показано, как реализовать надёжную систему классификации изображений на периферийных устройствах, таких как Raspi-Zero и Raspi-5. Это демонстрирует потенциал интеллекта в реальном времени прямо на устройстве.
Мы прошли весь конвейер проекта классификации изображений: от сбора данных и обучения модели в Edge Impulse Studio до развёртывания и запуска инференса на Raspi. В процессе выделилось несколько ключевых моментов:
- Важность правильного сбора данных и предобработки для обучения эффективных моделей.
- Сила переноса обучения: можно использовать предобученные модели, такие как MobileNet V2, для эффективного обучения на ограниченных данных.
- Компромиссы между точностью модели и скоростью инференса, особенно важные для периферийных устройств.
- Реализация классификации в реальном времени через веб-интерфейс, что демонстрирует практическое применение.
Возможность запускать такие модели на периферийных устройствах, таких как Raspi, открывает множество возможностей для IoT-приложений, автономных систем и решений для мониторинга в реальном времени. Это снижает задержки, повышает приватность и позволяет работать в условиях ограниченной связи.
Как мы видели, даже при вычислительных ограничениях периферийных устройств можно добиться впечатляющих результатов и по точности, и по скорости. Возможность менять параметры модели, например размер входа и значения альфа, позволяет тонко настраивать систему под требования конкретного проекта.
Область периферийного ИИ и классификации изображений быстро развивается. Улучшения в сжатии моделей, аппаратном ускорении и более эффективных архитектурах нейронных сетей обещают дальше расширить возможности периферийных устройств в задачах компьютерного зрения.
Этот проект служит основой для более сложных приложений компьютерного зрения и побуждает к дальнейшему изучению периферийного ИИ и IoT. Будь то промышленная автоматизация, «умный дом» или мониторинг окружающей среды, навыки и концепции, рассмотренные здесь, дают надёжную отправную точку для широкого круга проектов.
Ресурсы
- Набор данных (пример)
- Тестовый блокнот настройки на Raspi
- Блокнот классификации изображений на Raspi
- CNN для классификации набора данных Cifar-10 в Colab
- Cifar 10 — классификация изображений на Raspi
- Python-скрипты
- Проект Edge Impulse
Малые языковые модели (SLM)

Промпт DALL·E: иллюстрация в стиле мультфильма 1950-х, на которой Raspberry Pi запускает малую языковую модель на периферийном устройстве. Raspberry Pi оформлена в ретрофутуристическом стиле, с закруглёнными краями и хромированными деталями, и подключена к игривым мультяшным датчикам и устройствам. Вокруг плывут речевые пузыри, символизирующие обработку языка, а фон представляет собой причудливый пейзаж из связанных между собой устройств, проводов и маленьких гаджетов, нарисованных в винтажном мультяшном стиле. Цветовая палитра использует мягкие пастельные тона и жирные контуры, типичные для мультфильмов 1950-х, что придаёт сцене весёлую и ностальгическую атмосферу.
Обзор
В быстро развивающейся области искусственного интеллекта периферийные вычисления открывают возможность перенести функции, которые традиционно доступны только мощным централизованным серверам, ближе к источнику данных. Эта лабораторная работа посвящена практической интеграции небольших версий традиционных больших языковых моделей (LLM) в Raspberry Pi 5. Так это периферийное устройство превращается в ИИ-узел, способный обрабатывать данные на месте в реальном времени.
По мере того как большие языковые модели растут в размере и сложности, малые языковые модели (SLM) становятся привлекательной альтернативой для периферийных устройств: они обеспечивают баланс между производительностью и эффективным использованием ресурсов. Запуская такие модели непосредственно на Raspberry Pi, мы можем создавать отзывчивые приложения с защитой приватности, которые работают даже там, где интернет ограничен или отсутствует.
Эта лабораторная работа проведёт вас через установку, оптимизацию и использование SLM на Raspberry Pi. Мы изучим установку и применение Ollama — открытой платформы, которая позволяет запускать LLM локально на наших машинах: настольных компьютерах или периферийных устройствах, таких как Raspberry Pi или NVIDIA Jetson. Ollama спроектирована эффективной, масштабируемой и простой в использовании, поэтому подходит для развёртывания моделей ИИ, например Microsoft Phi, Google Gemma, Meta Llama и LLaVA (мультимодальная модель). Мы интегрируем некоторые из этих моделей в проекты с помощью экосистемы Python и изучим их потенциал в реальных сценариях (или хотя бы наметим такое направление).

Настройка
В предыдущих лабораторных работах можно было использовать любую модель Raspi, но здесь выбор — Raspberry Pi 5 (Raspi-5). Это надёжная платформа, которая заметно превосходит предыдущую версию 4. Она оснащена процессором Broadcom BCM2712 — 64-битным четырёхъядерным Arm Cortex-A76 с частотой 2,4 ГГц, с криптографическим расширением и улучшенным кэшированием. В ней есть GPU VideoCore VII, два выхода HDMI® с разрешением 4Kp60 с поддержкой HDR и декодер HEVC 4Kp60. Доступны варианты памяти 4 ГБ и 8 ГБ высокоскоростной LPDDR4X SDRAM; для запуска SLM мы выбираем 8 ГБ. Также есть расширяемое хранилище через слот microSD и интерфейс PCIe 2.0 для быстрой периферии, например твердотельных накопителей M.2 SSD.
Для реальных приложений SLM SSD — лучший выбор, чем SD-карты.
Кстати, как рассказал Alasdair Allan, инференс непосредственно на CPU Raspberry Pi 5 — без ускорения на GPU — сейчас сопоставим по производительности с Coral TPU.

Подробнее см. полную статью: Benchmarking TensorFlow and TensorFlow Lite on Raspberry Pi 5.
Активный кулер Raspberry Pi
Для этой лабораторной работы мы рекомендуем установить Active Cooler — навесное охлаждающее решение для Raspberry Pi 5 (Raspi-5). Оно сочетает алюминиевый радиатор с вентилятором, управляемым температурой, чтобы Raspi-5 комфортно работал под высокой нагрузкой, например при запуске SLM.

Active Cooler имеет предустановленные термопрокладки для теплопередачи и крепится непосредственно к плате Raspberry Pi 5 пружинными фиксаторами. Прошивка Raspberry Pi управляет им автоматически: при 60 °C включается вентилятор, при 67,5 °C его скорость увеличивается, а при 75 °C он работает на полной скорости. Когда температура опускается ниже этих порогов, вентилятор автоматически замедляется.

Чтобы предотвратить перегрев, все платы Raspberry Pi начинают снижать частоту процессора (троттлинг) при температуре 80 °C, а при достижении максимальной температуры 85 °C троттлинг усиливается (подробнее здесь).
Генеративный ИИ (GenAI)
Генеративный ИИ — это система искусственного интеллекта, способная создавать новый, оригинальный контент в разных форматах: текст, изображения, аудио и видео. Такие системы изучают закономерности в существующих данных и используют их, чтобы генерировать новые результаты, которых раньше не было. Большие языковые модели (LLM), малые языковые модели (SLM) и мультимодальные модели можно считать видами GenAI, если они используются для генеративных задач.
GenAI задаёт концептуальную основу для создания контента с помощью ИИ: LLM служат мощными универсальными генераторами текста. SLM адаптируют эту технологию для периферийных вычислений, а мультимодальные модели расширяют возможности GenAI на разные типы данных. Вместе они образуют спектр технологий генеративного ИИ, каждая из которых имеет свои сильные стороны и области применения, а вместе они обеспечивают создание и понимание контента на основе ИИ.
Большие языковые модели (LLM)
Большие языковые модели (LLM) — это продвинутые системы искусственного интеллекта, которые понимают, обрабатывают и генерируют человекоподобный текст. Такие модели характеризуются огромным масштабом: объёмом данных, на которых они обучаются, и количеством параметров. Ключевые аспекты LLM:
-
Размер: LLM обычно содержат миллиарды параметров. Например, у GPT-3 175 миллиардов параметров, а некоторые новые модели превышают триллион.
-
Обучающие данные: их обучают на огромных объёмах текста, часто включающих книги, веб-сайты и другие разнообразные источники; это сотни гигабайт или даже терабайты текста.
-
Архитектура: большинство LLM используют архитектуры на основе трансформеров, которые позволяют обрабатывать и генерировать текст, одновременно «обращая внимание» на разные части входных данных.
-
Возможности: LLM могут выполнять широкий круг языковых задач без специального дообучения, включая:
- генерацию текста;
- перевод;
- суммаризацию;
- ответы на вопросы;
- генерацию кода;
- логические рассуждения.
-
Обучение на нескольких примерах (few-shot learning): они часто могут понять и выполнить новую задачу по нескольким примерам или инструкциям.
-
Ресурсоёмкость: из-за своего размера LLM обычно требуют значительных вычислительных ресурсов и часто нуждаются в мощных GPU или TPU.
-
Непрерывное развитие: область LLM быстро развивается, постоянно появляются новые модели и методы.
-
Этические аспекты: использование LLM поднимает важные вопросы о предвзятости, дезинформации и экологическом воздействии обучения таких больших моделей.
-
Применения: LLM используются во многих областях, включая создание контента, обслуживание клиентов, помощь в исследованиях и разработку программного обеспечения.
-
Ограничения: несмотря на свою мощь, LLM могут выдавать неверную или предвзятую информацию и не обладают подлинным пониманием или способностью к рассуждению.
Стоит отметить, что крупные модели мы используем не только для текста: их называют мультимодальными моделями. Такие модели одновременно интегрируют и обрабатывают информацию из нескольких типов входных данных. Они спроектированы для понимания и генерации контента в разных формах: текст, изображения, аудио и видео.
Закрытые и открытые модели
Закрытые модели, также называемые проприетарными, — это модели ИИ, внутренняя работа, код и обучающие данные которых не раскрываются публично. Примеры: GPT-4 (OpenAI), Claude (Anthropic), Gemini (Google).
Открытые модели, также известные как модели с открытым исходным кодом, — это модели ИИ, базовый код, архитектура и часто обучающие данные которых публично доступны. Примеры: Gemma (Google), LLaMA (Meta) и Phi (Microsoft).
Открытые модели особенно важны для запуска на периферийных устройствах, таких как Raspberry Pi, поскольку их легче адаптировать, оптимизировать и развернуть в условиях ограниченных ресурсов. Тем не менее важно проверять их лицензии: открытые модели распространяются по разным лицензиям с открытым исходным кодом, которые могут влиять на использование в коммерческих приложениях. У закрытых моделей условия использования чёткие, хотя и ограничительные.

Адаптировано по [arXiv 2304.13712](https://arxiv.org/abs/2304.13712)
Малые языковые модели (SLM)
В контексте периферийных вычислений на таких устройствах, как Raspberry Pi, полномасштабные LLM обычно слишком велики и ресурсоёмки, чтобы работать на них напрямую. Это ограничение стимулировало разработку более компактных и эффективных моделей — малых языковых моделей (SLM).
SLM — это компактные версии LLM, созданные для эффективной работы на устройствах с ограниченными ресурсами: смартфонах, IoT-устройствах и одноплатных компьютерах вроде Raspberry Pi. Эти модели значительно меньше по размеру и требованиям к вычислениям, чем их крупные аналоги, и при этом сохраняют впечатляющие возможности понимания и генерации языка.
Ключевые характеристики SLM:
-
Меньшее число параметров: обычно от нескольких сотен миллионов до нескольких миллиардов параметров, против двузначного числа миллиардов у крупных моделей.
-
Меньший объём памяти: требуется не больше нескольких гигабайт памяти, а не десятки или сотни гигабайт.
-
Более быстрый инференс: могут генерировать ответы за миллисекунды или секунды на периферийных устройствах.
-
Энергоэффективность: потребляют меньше энергии, что делает их подходящими для устройств с батарейным питанием.
-
Сохранение приватности: обработка выполняется на устройстве, без отправки данных на облачные серверы.
-
Работа без подключения: могут работать без подключения к интернету.
SLM достигают компактности благодаря разным методам: дистилляции знаний (knowledge distillation), обрезке (pruning) и квантованию. Хотя SLM могут не сравниться с крупными моделями по широте возможностей, они превосходно справляются с конкретными задачами и предметными областями, что делает их идеальными для целевых приложений на периферийных устройствах.
Далее мы будем называть SLM языковые модели с числом параметров менее 5 миллиардов, квантованные до 4 бит.
Примеры SLM включают сжатые версии моделей вроде Meta Llama, Microsoft PHI и Google Gemma. Эти модели позволяют решать широкий круг задач обработки естественного языка прямо на периферийных устройствах: от классификации текста и анализа тональности до ответов на вопросы и ограниченной генерации текста.
Подробнее о SLM: статья LLM Pruning and Distillation in Practice: The Minitron Approach описывает подход, который применяет обрезку и дистилляцию, чтобы получить SLM из LLM. А работа SMALL LANGUAGE MODELS: SURVEY, MEASUREMENTS, AND INSIGHTS представляет всесторонний обзор и анализ малых языковых моделей (SLM) — языковых моделей со 100 миллионами — 5 миллиардами параметров, предназначенных для устройств с ограниченными ресурсами.
Ollama

Ollama — это открытая платформа, которая позволяет запускать языковые модели (LM), большие или малые, локально на наших машинах. Вот ключевые моменты об Ollama:
-
Локальный запуск моделей: Ollama позволяет запускать LM на персональных компьютерах или периферийных устройствах, таких как Raspi-5, без облачных API-вызовов.
-
Простота использования: предоставляет простой интерфейс командной строки для загрузки, запуска и управления разными языковыми моделями.
-
Разнообразие моделей: Ollama поддерживает различные LLM, включая Phi, Gemma, Llama, Mistral и другие открытые модели.
-
Настройка: пользователи могут создавать и делиться собственными моделями, адаптированными под конкретные задачи или области.
-
Лёгкость: спроектирована эффективно и работает на доступном аппаратном обеспечении.
-
Интеграция через API: предлагает API для интеграции с другими приложениями и сервисами.
-
Фокус на приватности: запуск моделей локально решает проблемы конфиденциальности, связанные с отправкой данных на внешние серверы.
-
Кроссплатформенность: доступна для macOS, Windows и Linux (наш случай).
-
Активная разработка: регулярно обновляется новыми функциями и поддержкой моделей.
-
Развитие сообществом: выигрывает от вкладов сообщества и обмена моделями.
Чтобы узнать больше о том, что такое Ollama и как она работает, посмотрите короткое видео от Matt Williams, одного из основателей Ollama: смотреть на YouTube.
Matt также ведёт полностью бесплатный курс об Ollama, который мы рекомендуем: смотреть на YouTube.
Установка Ollama
Создадим и активируем виртуальное окружение для работы с Ollama:
python3 -m venv ~/ollama
source ~/ollama/bin/activate
Теперь выполним команду установки Ollama:
curl -fsSL https://ollama.com/install.sh | sh
В результате API будет работать в фоновом режиме по адресу 127.0.0.1:11434. Теперь мы можем запускать Ollama из терминала. Для начала проверим версию Ollama — это также подтвердит, что установка прошла успешно:
ollama -v

На странице библиотеки Ollama можно найти модели, которые поддерживает Ollama. Например, если отфильтровать по Most popular (самые популярные), увидим Meta Llama, Google Gemma, Microsoft Phi, LLaVa и другие модели.
Meta Llama 3.2 1B/3B

Установим и запустим нашу первую малую языковую модель, Llama 3.2 1B (и 3B). Серия Meta Llama 3.2 — это набор многоязычных генеративных языковых моделей размером 1 и 3 миллиарда параметров. Эти модели предназначены для обработки текстового ввода и генерации текстового вывода. Инструктивные варианты этой серии специально оптимизированы для многоязычных диалоговых приложений, включая задачи поиска информации и суммаризации с агентным подходом. По сравнению со многими существующими чат-моделями с открытым исходным кодом и проприетарными моделями инструктивные версии Llama 3.2 демонстрируют превосходные результаты в широко используемых отраслевых тестах.
Модели 1B и 3B были обрезаны (pruned) из Llama 8B, а затем в качестве целевых значений на уровне токенов использовались логиты моделей 8B и 70B (дистилляция на уровне токенов). Для восстановления качества применялась дистилляция знаний: обучение проводилось на 9 триллионах токенов. Модель 1B содержит 1,24 млрд параметров с квантованием в целые числа (Q8_0), а модель 3B — 3,12 млрд параметров с квантованием Q4_0. Их размеры составляют 1,3 ГБ и 2 ГБ соответственно. Окно контекста — 131 072 токена.

Установка и запуск модели
ollama run llama3.2:1b
После запуска модели этой командой появится приглашение Ollama, в котором можно ввести вопрос и начать диалог с LLM, например:
>>> What is the capital of France?
Почти мгновенно мы получаем правильный ответ:
The capital of France is Paris.
Если при вызове модели использовать параметр --verbose, будет выведена статистика о её работе (модель загружается только при первом запуске команды).

Каждый показатель даёт представление о том, как модель обрабатывает входные данные и генерирует ответы. Вот расшифровка метрик:
- Total Duration (2,620170326 с): полное время от начала выполнения команды до завершения ответа. Оно включает загрузку модели, обработку входного запроса и генерацию ответа.
- Load Duration (39,947908 мс): время загрузки модели или необходимых компонентов в память. Если значение мало, возможно, модель была предзагружена или требовалась лишь минимальная настройка.
- Prompt Eval Count (32 токена): количество токенов во входном запросе. В NLP токены обычно являются словами или их частями, поэтому здесь учитываются все токены, которые модель обработала, чтобы понять запрос и ответить на него.
- Prompt Eval Duration (1,644773 с): время, которое модель тратит на обработку входного запроса. Оно составляет основную часть общего времени, то есть понимание запроса и подготовка ответа — самая трудоёмкая часть процесса.
- Prompt Eval Rate (19,46 токенов/с): скорость обработки токенов входного запроса. Она отражает быстродействие модели при понимании естественного языка.
- Eval Count (8 токенов): количество токенов в ответе модели; в данном случае это фраза «The capital of France is Paris.»
- Eval Duration (889,941 мс): время генерации ответа на основе обработанного входа. Оно заметно короче времени обработки запроса, а значит, генерация ответа менее сложна и вычислительно менее затратна, чем понимание запроса.
- Eval Rate (8,99 токена/с): аналогично скорости обработки запроса, показывает скорость генерации выходных токенов. Это важный показатель эффективности модели при генерации ответа.
Такой подробный разбор помогает понять вычислительные требования и характеристики производительности SLM, например Llama, на периферийных устройствах вроде Raspberry Pi 5. Видно, что обработка запроса занимает больше времени, тогда как сама генерация ответа проходит относительно быстрее. Этот анализ важен для оптимизации производительности и поиска узких мест в приложениях реального времени.
Загрузим и запустим модель 3B и сравним производительность на том же запросе:

Скорость генерации ниже: 5,3 токена/с против 9 токенов/с у меньшей модели.
На вопрос
>>> What is the distance between Paris and Santiago, Chile?
модель 1B ответила 9,841 kilometers (6,093 miles) — это неточно, а модель 3B ответила 7,300 miles (11,700 km) — это близко к правильному значению (11 642 км).
Спросим координаты Парижа:
>>> what is the latitude and longitude of Paris?
The latitude and longitude of Paris are 48.8567° N (48°55'
42" N) and 2.3510° E (2°22' 8" E), respectively.

Обе модели — 1B и 3B — дали правильные ответы.
Google Gemma 2 2B
Установим Gemma 2 — высокопроизводительную и эффективную модель, доступную в трёх размерах: 2B, 9B и 27B. Мы установим Gemma 2 2B — лёгкую модель, обученную на 2 триллионах токенов, которая даёт выдающиеся результаты, перенимая знания у более крупных моделей через дистилляцию. В модели 2,6 млрд параметров и квантование Q4_0, её размер — 1,6 ГБ. Окно контекста — 8192 токена.

Установка и запуск модели
ollama run gemma2:2b --verbose
После запуска команды появится приглашение Ollama, и можно задать вопрос, например:
>>> What is the capital of France?
Почти сразу получаем правильный ответ:
The capital of France is **Paris**. 🗼
И её статистика:

Видим, что Gemma 2:2B работает примерно так же быстро, как Llama 3.2:3B, но при меньшем числе параметров.
Другие примеры:
>>> What is the distance between Paris and Santiago, Chile?
The distance between Paris, France and Santiago, Chile is
approximately **7,000 miles (11,267 kilometers)**.
Keep in mind that this is a straight-line distance, and actual
travel distance can vary depending on the chosen routes and any
stops along the way. ✈️
Ответ тоже хороший, но менее точный, чем у Llama3.2:3B.
>>> what is the latitude and longitude of Paris?
You got it! Here are the latitudes and longitudes of Paris,
France:
* **Latitude**: 48.8566° N (north)
* **Longitude**: 2.3522° E (east)
Let me know if you'd like to explore more about Paris or its
location! 🗼🇫🇷
Хороший и точный ответ (немного многословнее, чем ответы Llama).
Microsoft Phi3.5 3.8B
Загрузим модель побольше, но всё ещё компактную: PHI3.5 — лёгкую современную открытую модель с 3,8 млрд параметров от Microsoft. Модель относится к семейству Phi-3, поддерживает контекст 128K токенов и языки: арабский, китайский, чешский, датский, нидерландский, английский, финский, французский, немецкий, иврит, венгерский, итальянский, японский, корейский, норвежский, польский, португальский, русский, испанский, шведский, тайский, турецкий и украинский.
Размер модели в байтах зависит от выбранного формата квантования. Он может варьироваться от квантования 2 бита (q2_k) размером 1,4 ГБ (выше производительность, ниже качество) до 16-битного (fp-16) размером 7,6 ГБ (ниже производительность, выше качество).
Запустим 4-битное квантование (Q4_0), которому нужно 2,2 ГБ оперативной памяти. Это компромисс между качеством ответов и производительностью.
ollama run phi3.5:3.8b --verbose
Загрузить модель можно и командой
run, и командойpull. Ollama запоминает загруженные модели: если Phi3 ещё нет на устройстве, Ollama скачает её перед запуском.
Введём тот же запрос, что и раньше:
>>> What is the capital of France?
The capital of France is Paris. It' extradites significant
historical, cultural, and political importance to the country as
well as being a major European city known for its art, fashion,
gastronomy, and culture. Its influence extends beyond national
borders, with millions of tourists visiting each year from around
the globe. The Seine River flows through Paris before it reaches
the broader English Channel at Le Havre. Moreover, France is one
of Europe's leading economies with its capital playing a key role
...
Ответ получился очень «многословным», поэтому зададим более удачный запрос:

В этом случае ответ всё равно был длиннее, чем мы ожидали, со скоростью генерации 2,25 токена/с, то есть более чем вдвое медленнее, чем у Gemma и Llama.
Выбор наиболее подходящего запроса (prompt) — один из самых важных навыков при работе с LLM, независимо от их размера.
Когда мы задали те же вопросы о расстоянии и координатах, ответ о расстоянии 13,507 kilometers (8,429 miles) оказался неверным, а координаты — приемлемыми. Ответы снова могли бы быть менее многословными (более 200 токенов на каждый ответ).
Любую модель можно использовать как ассистента, поскольку её скорость вполне приемлема. Однако по состоянию на 24 сентября 2023 года для такой задачи лучше подходит Llama2:3B. Стоит попробовать и другие модели в зависимости от ваших потребностей. 🤗 Open LLM Leaderboard поможет оценить лучшие модели по размеру, результатам тестов, лицензии и т. д.
Лучшая модель — та, которая подходит для вашей конкретной задачи. Учтите также, что эта область развивается и новые модели появляются каждый день.
Мультимодальные модели
Мультимодальные модели — это системы искусственного интеллекта (ИИ), которые могут обрабатывать и понимать информацию из нескольких источников: изображений, текста, аудио и видео. В нашем контексте мультимодальные LLM могут принимать в качестве запросов разные входные данные, включая текст, изображения и аудио, и преобразовывать их в выходные данные разных типов, а не только того же типа.
Мы будем работать с LLaVA-Phi-3 — доработанной моделью LLaVA на основе Phi 3 Mini 4k. По результатам тестов она сопоставима с исходной моделью LLaVA (Large Language and Vision Assistant).
LLaVA-Phi-3 — это крупная мультимодальная модель, обученная сквозным образом, которая понимает визуальные данные (изображения) и текстовые инструкции и генерирует контент на их основе. Она сочетает возможности визуального энкодера и языковой модели, чтобы обрабатывать мультимодальные входные данные и отвечать на них.
Установим модель:
ollama run llava-phi3:3.8b --verbose
Начнём с текстового ввода:
>>> You are a helpful AI assistant.
What is the capital of France?
As an AI language model, I can tell you that the capital of
France is Paris. It's not only the largest city in the country
but also serves as its political and administrative center.
Paris is known for its iconic landmarks such as the Eiffel
Tower, Notre-Dame Cathedral, and the Louvre Museum. The city
has a rich history, beautiful architecture, and is widely
considered to be one of the most romantic cities in the world.
Ответ занял около 30 секунд, скорость генерации — 3,93 токена/с. Неплохо!
Теперь передадим модели изображение. Для этого создадим рабочую папку:
cd Documents/
mkdir OLLAMA
cd OLLAMA
Скачаем изображение размером , например из Википедии: Paris, France:

С помощью FileZilla, например, загрузим изображение в папку OLLAMA на Raspi-5 и назовём его image_test_1.jpg. Нам понадобится полный путь к изображению (его можно узнать командой pwd):
/home/username/Documents/OLLAMA/image_test_1.jpg
Если вы используете рабочий стол, путь к изображению можно скопировать, щёлкнув по изображению правой кнопкой мыши.

Введём такой запрос:
>>> Describe the image /home/username/Documents/OLLAMA/\
image_test_1.jpg
Результат получился хорошим, но общая задержка оказалась значительной: почти 4 минуты на инференс.

Проверка локальных ресурсов
С помощью htop можно отслеживать ресурсы, используемые устройством:
htop
Пока модель работает, можно проверять ресурсы:

Все четыре ядра CPU работают почти на 100 % своей мощности, а объём памяти с загруженной моделью составляет 3.24 GB. После выхода из Ollama память снижается примерно до 377 MB (без рабочего стола).
Также важно следить за температурой. При работе Raspberry с рабочим столом температуру можно видеть на панели задач:

Если Raspi работает без монитора (headless), температуру можно проверить командой:
vcgencmd measure_temp
Если устройство простаивает, температура составляет около 50 °C при загрузке CPU 1 %. Во время инференса, когда CPU загружены на 100 %, температура может подняться почти до 70 °C. Это нормально: значит, активный кулер работает и удерживает температуру ниже предела 80/85 °C.
Библиотека Ollama для Python
До сих пор мы изучали возможности диалога с SLM через командную строку в терминале. Однако мы хотим интегрировать эти модели в наши проекты, поэтому логичнее использовать Python. Хорошая новость: у Ollama есть такая библиотека.
Библиотека Ollama для Python упрощает взаимодействие с продвинутыми LLM, позволяя получать более сложные ответы и реализовывать дополнительные возможности. Она также даёт самый простой способ интегрировать проекты на Python 3.8+ с Ollama.
Чтобы лучше понять, как создавать приложения с Ollama на Python, можно посмотреть видео Matt Williams, например это: смотреть на YouTube.
Установка:
В терминале выполните команду:
pip install ollama
Для создания Python-скрипта понадобится текстовый редактор или IDE. Если вы работаете с Raspberry Pi OS на рабочем столе, по умолчанию уже установлено несколько вариантов, например Thonny и Geany (открываются через [Menu][Programming]). Другие IDE, например Visual Studio Code, можно загрузить через [Menu][Recommended Software]. Когда откроется окно, перейдите в [Programming], выберите нужный вариант и нажмите [Apply].

Если вы предпочитаете разрабатывать в Jupyter Notebook:
pip install jupyter
jupyter notebook --generate-config
Чтобы запустить Jupyter Notebook, выполните команду (замените IP-адрес на свой):
jupyter notebook --ip=192.168.4.209 --no-browser
В терминале появится локальный URL-адрес, по которому можно открыть блокнот:

Получить доступ к блокноту можно с другого компьютера: введите в веб-браузере IP-адрес Raspberry Pi и токен из терминала (скопируйте его оттуда).
В рабочем каталоге Raspi создадим новый блокнот Python 3.
Запустим очень простой скрипт, чтобы проверить установленные модели:
import ollama
ollama.list()
Все модели будут выведены в виде словаря, например:
{'name': 'gemma2:2b',
'model': 'gemma2:2b',
'modified_at': '2024-09-24T19:30:40.053898094+01:00',
'size': 1629518495,
'digest': (
'8ccf136fdd5298f3ffe2d69862750ea7fb56555fa4d5b18c0'
'4e3fa4d82ee09d7'
),
'details': {'parent_model': '',
'format': 'gguf',
'family': 'gemma2',
'families': ['gemma2'],
'parameter_size': '2.6B',
'quantization_level': 'Q4_0'}}]}
Повторим один из вопросов, которые мы задавали раньше, но теперь с помощью ollama.generate() из библиотеки Ollama для Python. Этот API генерирует ответ на заданный запрос с помощью указанной модели. Это потоковая конечная точка, поэтому ответ приходит серией фрагментов. Итоговый объект ответа содержит статистику и дополнительные данные о запросе.
MODEL = "gemma2:2b"
PROMPT = "What is the capital of France?"
res = ollama.generate(model=MODEL, prompt=PROMPT)
print(res)
Если запускаете код как Python-скрипт, сохраните его, например, как test_ollama.py. Запустить его можно в IDE или прямо в терминале. Помните, что при запуске отдельного скрипта модель всегда нужно указывать явно.
python test_ollama.py
В результате мы получим ответ модели в формате JSON:
{
'model': 'gemma2:2b',
'created_at': '2024-09-25T14:43:31.869633807Z',
'response': 'The capital of France is **Paris**.\n',
'done': True,
'done_reason': 'stop',
'context': [
106, 1645, 108, 1841, 603, 573, 6037, 576, 6081, 235336,
107, 108, 106, 2516, 108, 651, 6037, 576, 6081, 603, 5231,
29437, 168428, 235248, 244304, 241035, 235248, 108
],
'total_duration': 24259469458,
'load_duration': 19830013859,
'prompt_eval_count': 16,
'prompt_eval_duration': 1908757000,
'eval_count': 14,
'eval_duration': 2475410000
}
Как видно, генерируется несколько видов информации, например:
-
response: основной текст, который модель сгенерировала в ответ на наш запрос:
The capital of France is **Paris**. 🇫🇷
-
context: идентификаторы токенов, представляющие входные данные и контекст, которые использовала модель. Токены — это числовые представления текста, которые языковая модель использует для обработки:
[106, 1645, 108, 1841, 603, 573, 6037, 576, 6081, 235336, 107, 108, 106, 2516, 108, 651, 6037, 576, 6081, 603, 5231, 29437, 168428, 235248, 244304, 241035, 235248, 108]
Показатели производительности:
- total_duration: общее время выполнения операции в наносекундах. В данном случае примерно 24,26 секунды.
- load_duration: время загрузки модели или её компонентов в наносекундах. Около 19,83 секунды.
- prompt_eval_duration: время обработки запроса в наносекундах. Около 1,9 секунды.
- eval_count: количество токенов, обработанных при генерации. Здесь 14 токенов.
- eval_duration: время, за которое модель сгенерировала ответ, в наносекундах. Примерно 2,5 секунды.
Но нам нужен только текст ответа (response) и, возможно, для анализа общее время инференса. Поэтому изменим код так, чтобы извлекать их из словаря:
print(f"\n{res['response']}")
print(
f"\n [INFO] Total Duration: "
f"{res['total_duration']/1e9:.2f} seconds"
)
Теперь получаем:
The capital of France is **Paris**. 🇫🇷
[INFO] Total Duration: 24.26 seconds
Использование Ollama.chat()
Другой способ получить ответ — ollama.chat(), который генерирует следующее сообщение в диалоге с указанной моделью. Это тоже потоковая конечная точка, поэтому ответ приходит серией фрагментов. Потоковую передачу можно отключить, указав "stream": false. Итоговый объект ответа также содержит статистику и дополнительные данные о запросе.
PROMPT_1 = "What is the capital of France?"
response = ollama.chat(
model=MODEL,
messages=[
{
"role": "user",
"content": PROMPT_1,
},
],
)
resp_1 = response["message"]["content"]
print(f"\n{resp_1}")
print(
f"\n [INFO] Total Duration: "
f"{(response['total_duration']/1e9):.2f} seconds"
)
Ответ такой же, как и раньше.
Важный момент: при использовании ollama.generate() ответ «забывается» моделью после завершения инференса (запрос используется только один раз). Если же мы хотим сохранить диалог, нужно использовать ollama.chat(). Посмотрим, как это работает:
PROMPT_1 = "What is the capital of France?"
response = ollama.chat(
model=MODEL,
messages=[
{
"role": "user",
"content": PROMPT_1,
},
],
)
resp_1 = response["message"]["content"]
print(f"\n{resp_1}")
print(
f"\n [INFO] Total Duration: "
f"{(response['total_duration']/1e9):.2f} seconds"
)
PROMPT_2 = "and of Italy?"
response = ollama.chat(
model=MODEL,
messages=[
{
"role": "user",
"content": PROMPT_1,
},
{
"role": "assistant",
"content": resp_1,
},
{
"role": "user",
"content": PROMPT_2,
},
],
)
resp_2 = response["message"]["content"]
print(f"\n{resp_2}")
print(
f"\n [INFO] Total Duration: "
f"{(response['total_duration']/1e9):.2f} seconds"
)
В приведённом коде мы выполняем два запроса, и второй учитывает результат первого.
Вот как ответила модель:
The capital of France is **Paris**. 🇫🇷
[INFO] Total Duration: 2.82 seconds
The capital of Italy is **Rome**. 🇮🇹
[INFO] Total Duration: 4.46 seconds
Получение описания изображения:
Так же, как мы использовали модель LlaVa-PHI-3 в командной строке для анализа изображения, это можно сделать и в Python. Используем то же изображение Парижа, но теперь с ollama.generate():
MODEL = "llava-phi3:3.8b"
PROMPT = "Describe this picture"
with open("image_test_1.jpg", "rb") as image_file:
img = image_file.read()
response = ollama.generate(model=MODEL, prompt=PROMPT, images=[img])
print(f"\n{response['response']}")
print(
f"\n [INFO] Total Duration: "
f"{(response['total_duration']/1e9):.2f} seconds"
)
Вот результат:
This image captures the iconic cityscape of Paris, France. The
vantage point is high, providing a panoramic view of the Seine
River that meanders through the heart of the city. Several
bridges arch gracefully over the river, connecting different
parts of the city. The Eiffel Tower, an iron lattice structure
with a pointed top and two antennas on its summit, stands
tall in the background, piercing the sky. It is painted in a
light gray color, contrasting against the blue sky speckled
with white clouds.
The buildings that line the river are predominantly white or
beige, their uniform color palette broken occasionally by red
roofs peeking through. The Seine River itself appears calm
and wide, reflecting the city's architectural beauty in its
surface. On either side of the river, trees add a touch of
green to the urban landscape.
The image is taken from an elevated perspective, looking down
on the city. This viewpoint allows for a comprehensive view of
Paris's beautiful architecture and layout. The relative
positions of the buildings, bridges, and other structures
create a harmonious composition that showcases the city's charm.
In summary, this image presents a serene day in Paris, with its
architectural marvels - from the Eiffel Tower to the river-side
buildings - all bathed in soft colors under a clear sky.
[INFO] Total Duration: 256.45 seconds
Модели потребовалось около 4 минут (256,45 с), чтобы вернуть подробное описание изображения.
В блокноте 10-Ollama_Python_Library можно найти эксперименты с библиотекой Ollama для Python.
Вызов функций
До сих пор мы видели, что, помещая ответ модели в переменную, его можно эффективно использовать в реальных проектах. Однако возникает серьёзная проблема: модель может давать разные ответы на один и тот же вход. Например, предположим, что в предыдущих примерах нам нужны только название столицы страны и её координаты, без дополнительной информации, даже при использовании многословных моделей вроде Microsoft Phi. Чтобы обеспечить стабильные ответы, можно использовать «вызов функций Ollama» (Ollama function call), который полностью совместим с API OpenAI.
Что такое «вызов функций»?
В современном искусственном интеллекте вызов функций с большими языковыми моделями (LLM) позволяет этим моделям выполнять действия, выходящие за рамки генерации текста. Интегрируясь с внешними функциями или API, LLM могут получать данные в реальном времени, автоматизировать задачи и взаимодействовать с различными системами.
Например, вместо простого ответа на вопрос о погоде LLM может вызвать API погоды, чтобы получить текущие условия и дать точную и актуальную информацию. Эта возможность повышает релевантность и точность ответов модели и делает её мощным инструментом для управления рабочими процессами и автоматизации, превращая модель в активного участника реальных приложений.
Подробнее о вызове функций см. видео, сделанное Mervin Praison: смотреть на YouTube.
Создадим проект
Мы хотим создать приложение, в котором пользователь вводит название страны и получает на выходе расстояние в километрах от столицы этой страны до места расположения приложения (для простоты местоположение приложения — Сантьяго, Чили).
Когда пользователь вводит название страны, модель возвращает название её столицы (строка), а также широту и долготу этого города (числа с плавающей точкой). Используя эти координаты, мы можем с помощью простой библиотеки Python (haversine) вычислить расстояние между двумя точками.
Идея проекта — продемонстрировать сочетание взаимодействия с языковой моделью, обработки структурированных данных с помощью Pydantic и геопространственных вычислений по формуле гаверсинусов (традиционные вычисления).
Сначала установим несколько библиотек. Помимо Haversine, главная из них — библиотека OpenAI для Python, которая обеспечивает удобный доступ к REST API OpenAI из любого приложения на Python 3.7+. Вторая — Pydantic (и instructor), надёжная библиотека проверки данных и управления настройками, созданная для повышения устойчивости и надёжности кода. Коротко говоря, Pydantic поможет гарантировать, что ответ модели всегда будет согласованным.
pip install haversine
pip install openai
pip install pydantic
pip install instructor
Теперь создадим Python-скрипт, который взаимодействует с нашей моделью (LLM), чтобы определить координаты столицы страны и вычислить расстояние от Сантьяго-де-Чили до этой столицы.
Разберём код по шагам.
1. Импорт библиотек
import sys
from haversine import haversine
from openai import OpenAI
from pydantic import BaseModel, Field
import instructor
- sys: предоставляет доступ к системным параметрам и функциям. Используется для получения аргументов командной строки.
- haversine: функция из библиотеки haversine, которая вычисляет расстояние между двумя географическими точками по формуле гаверсинусов.
- openAI: модуль для взаимодействия с API OpenAI (здесь он используется вместе с локальной установкой Ollama). Всё работает офлайн.
- pydantic: обеспечивает проверку данных и управление настройками с помощью аннотаций типов Python. Используется для описания структуры ожидаемых данных ответа.
- instructor: модуль, который «патчит» клиента OpenAI, чтобы он работал в определённом режиме (вероятно, связанном с обработкой структурированных данных).
2. Определение входных данных и модели
country = sys.argv[1] # Получаем страну из
# аргументов командной строки
MODEL = "phi3.5:3.8b" # Название модели для использования
mylat = -33.33 # Широта Сантьяго-де-Чили
mylon = -70.51 # Долгота Сантьяго-де-Чили
- country: в Python-скрипте название страны можно получить из аргументов командной строки. В Jupyter Notebook его можно задать вручную, например:
country = "France"
- MODEL: указывает используемую модель, в данном примере phi3.5.
- mylat и mylon: координаты Сантьяго-де-Чили, которые служат отправной точкой для расчёта расстояния.
3. Определение структуры данных ответа
class CityCoord(BaseModel):
city: str = Field(..., description="Name of the city")
lat: float = Field(
..., description="Decimal Latitude of the city"
)
lon: float = Field(
..., description="Decimal Longitude of the city"
)
- CityCoord: модель Pydantic, которая определяет ожидаемую структуру ответа LLM. Она ожидает три поля: city (название города), lat (широта) и lon (долгота).
4. Настройка клиента OpenAI
client = instructor.patch(
OpenAI(
base_url="http://localhost:11434/v1", # Базовый URL локального API
# (Ollama)
api_key="ollama", # Ключ API
# (не используется)
),
mode=instructor.Mode.JSON, # Режим для
# структурированного вывода JSON
)
- OpenAI: эта настройка создаёт клиента OpenAI с локальным базовым URL и ключом API (ollama). Используется локальный сервер.
- instructor.patch: «патчит» клиента OpenAI, переводя его в режим JSON, что позволяет получать структурированный вывод, соответствующий модели Pydantic.
5. Генерация ответа
resp = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "user",
"content": f"return the decimal latitude and \
decimal longitude of the capital of the {country}.",
}
],
response_model=CityCoord,
max_retries=10,
)
- client.chat.completions.create: вызывает LLM для генерации ответа.
- model: указывает модель, которую нужно использовать (в нашем коде это phi3.5).
- messages: содержит запрос к LLM с просьбой вернуть широту и долготу столицы заданной страны.
- response_model: указывает, что ответ должен соответствовать модели
CityCoord. - max_retries: максимальное число повторных попыток, если запрос не удался.
6. Вычисление расстояния
distance = haversine((mylat, mylon), (resp.lat, resp.lon), unit="km")
print(
f"Santiago de Chile is about {int(round(distance, -1))} "
f"kilometers away from {resp.city}."
)
- haversine: вычисляет расстояние между Сантьяго-де-Чили и столицей, которую вернула LLM, по их координатам.
- (mylat, mylon): координаты Сантьяго-де-Чили.
- resp.city: название столицы страны.
- (resp.lat, resp.lon): координаты столицы, которые предоставил ответ LLM.
- unit = ‘km’: расстояние вычисляется в километрах.
- print: выводит расстояние, округлённое до ближайших 10 километров, с разделителями тысяч для удобства чтения.
Запуск кода
Если указать разные страны, например Францию, Колумбию и США, мы видим, что каждый раз получаем одну и ту же структурированную информацию:
Santiago de Chile is about 8,060 kilometers away from
Washington, D.C..
Santiago de Chile is about 4,250 kilometers away from Bogotá.
Santiago de Chile is about 11,630 kilometers away from Paris.
Если запустить код как скрипт, результат будет выведен в терминале:

И расчёты довольно точны!

В блокноте 20-Ollama_Function_Calling можно найти эксперименты со всеми установленными моделями.
Добавление изображений
Пора собрать всё вместе! Изменим скрипт так, чтобы вместо названия страны (текстом) пользователь вводил изображение, а приложение на основе SLM возвращало город, изображённый на нём, и его географическое положение. Имея эти данные, мы можем вычислить расстояние, как и раньше.
Для простоты реализуем новый код в два шага. Сначала LLM проанализирует изображение и создаст его описание (текст). Этот текст передаётся другому экземпляру модели, которая извлечёт из него нужную информацию.
Начнём с импорта библиотек:
import sys
import time
from haversine import haversine
import ollama
from openai import OpenAI
from pydantic import BaseModel, Field
import instructor
Чтобы видеть изображение при запуске кода в Jupyter Notebook, также импортируем:
import matplotlib.pyplot as plt
from PIL import Image
Эти библиотеки не нужны, если код запускается как скрипт.
Теперь определим модель и локальные координаты:
MODEL = "llava-phi3:3.8b"
mylat = -33.33
mylon = -70.51
Можно скачать новое изображение, например Мачу-Пикчу из Википедии, и посмотреть его в блокноте:
# Загружаем изображение
img_path = "image_test_3.jpg"
img = Image.open(img_path)
# Выводим изображение
plt.figure(figsize=(8, 8))
plt.imshow(img)
plt.axis("off")
# plt.title("Image")
plt.show()

Теперь определим функцию, которая получает изображение и возвращает широту и долготу города на изображении, его название и страну, в которой он находится:
def image_description(img_path):
with open(img_path, "rb") as file:
response = ollama.chat(
model=MODEL,
messages=[
{
"role": "user",
"content": """return the decimal latitude and \
decimal longitude of the city in the image, \
its name, and what country it is located""",
"images": [file.read()],
},
],
options={
"temperature": 0,
},
)
# print(response['message']['content'])
return response["message"]["content"]
Для отладки можно вывести весь ответ целиком.
Описание изображения, сгенерированное функцией, будет передано модели снова в качестве запроса.
start_time = time.perf_counter() # Запускаем таймер
class CityCoord(BaseModel):
city: str = Field(
..., description="Name of the city in the image"
)
country: str = Field(
...,
description=(
"Name of the country where "
"the city in the image is located"
),
)
lat: float = Field(
...,
description=("Decimal latitude of the city in " "the image"),
)
lon: float = Field(
...,
description=("Decimal longitude of the city in " "the image"),
)
# Включает `response_model` в вызове create
client = instructor.patch(
OpenAI(base_url="http://localhost:11434/v1", api_key="ollama"),
mode=instructor.Mode.JSON,
)
image_description = image_description(img_path)
# Отправляем это описание модели
resp = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "user",
"content": image_description,
}
],
response_model=CityCoord,
max_retries=10,
temperature=0,
)
Если вывести описание изображения, получим:
The image shows the ancient city of Machu Picchu, located in
Peru. The city is perched on a steep hillside and consists of
various structures made of stone. It is surrounded by lush
greenery and towering mountains. The sky above is blue with
scattered clouds.
Machu Picchu's latitude is approximately 13.5086° S, and its
longitude is around 72.5494° W.
А вторым ответом модели (resp) будет:
CityCoord(city='Machu Picchu', country='Peru', lat=-13.5086,
lon=-72.5494)
Теперь можно выполнить «постобработку»: вычислить расстояние и подготовить итоговый ответ:
distance = haversine((mylat, mylon), (resp.lat, resp.lon), unit="km")
print(
(
f"\nThe image shows {resp.city}, with lat: "
f"{round(resp.lat, 2)} and long: "
f"{round(resp.lon, 2)}, located in "
f"{resp.country} and about "
f"{int(round(distance, -1)):,} kilometers "
f"away from Santiago, Chile.\n"
)
)
end_time = time.perf_counter() # Останавливаем таймер
elapsed_time = end_time - start_time # Вычисляем затраченное время
print(
f"[INFO] ==> The code (running {MODEL}), "
f"took {elapsed_time:.1f} seconds to execute.\n"
)
И получим:
The image shows Machu Picchu, with lat:-13.16 and long:
-72.54, located in Peru and about 2,250 kilometers away
from Santiago, Chile.
В блокноте 30-Function_Calling_with_images можно найти эксперименты с несколькими изображениями.
Теперь скачайте скрипт calc_distance_image.py из репозитория EdgeML-with-Raspberry-Pi и запустите его в терминале командой:
python calc_distance_image.py \
/home/username/Documents/OLLAMA/image_test_3.jpg
Укажите полный путь к изображению Мачу-Пикчу в качестве аргумента. Мы получим тот же результат, что и раньше.

А как насчёт Парижа?

Конечно, код можно оптимизировать многими способами. Однако идея в том, чтобы исследовать большой потенциал вызова функций с SLM на периферии: это позволяет моделям интегрироваться с внешними функциями и API. Выходя за рамки генерации текста, SLM могут получать данные в реальном времени, автоматизировать задачи и взаимодействовать с различными системами.
Методы оптимизации SLM
Большие языковые модели (LLM) произвели революцию в обработке естественного языка, но их развёртывание и оптимизация связаны с особыми сложностями. Одна из существенных проблем — склонность LLM (и тем более SLM) генерировать правдоподобно звучащую, но фактически неверную информацию. Это явление известно как галлюцинации: модель выдаёт содержимое, которое выглядит связным, но не опирается на истину или реальные факты.
Другие сложности включают огромные вычислительные ресурсы, необходимые для обучения и запуска таких моделей, трудность поддержания актуальности знаний внутри модели и необходимость адаптации к предметной области. Вопросы приватности возникают и при работе с чувствительными данными во время обучения или инференса. Кроме того, постоянными вызовами остаются стабильное качество в разных задачах и этичное использование таких мощных инструментов. Их решение критически важно для эффективного и ответственного развёртывания LLM в реальных приложениях.
Основные методы повышения производительности и эффективности LLM (и SLM): дообучение (fine-tuning), инженерия запросов (prompt engineering) и генерация с дополнением поиском (Retrieval-Augmented Generation, RAG).
-
Дообучение (fine-tuning) требует больше ресурсов, но позволяет специализировать LLM для конкретных областей или задач. Процесс заключается в дополнительном обучении модели на тщательно отобранных наборах данных, что позволяет адаптировать её обширные общие знания к конкретным приложениям. Дообучение может существенно улучшить производительность, особенно в узких областях или для особых сценариев.
-
Инженерия запросов (prompt engineering) стоит на переднем крае оптимизации LLM. Тщательно составляя входные запросы, мы можем направить модель на более точные и релевантные ответы. Этот метод предполагает формулировку запросов, которые используют предобученные знания и возможности модели, часто включая примеры или конкретные инструкции, задающие нужный ответ.
-
Генерация с дополнением поиском (Retrieval-Augmented Generation, RAG) — ещё один мощный подход к повышению производительности LLM. Метод сочетает обширные знания, заложенные в предобученной модели, со способностью получать и использовать внешнюю, актуальную информацию. Извлекая релевантные данные для поддержки процесса принятия решений моделью, RAG может существенно повысить точность и снизить вероятность генерации устаревших или ложных сведений.
Для периферийных приложений полезнее сосредоточиться на таких методах, как RAG, которые повышают качество модели без дообучения на самом устройстве. Разберёмся с этим подробнее.
Реализация RAG
В базовом взаимодействии пользователя с языковой моделью пользователь задаёт вопрос, который отправляется модели в виде запроса. Модель генерирует ответ, опираясь исключительно на предобученные знания. В процессе RAG между вопросом пользователя и ответом модели появляется дополнительный шаг: вопрос запускает процесс поиска в базе знаний.
Простой RAG-проект
Вот шаги для реализации базовой генерации с дополнением поиском (Retrieval Augmented Generation, RAG):
-
Определите тип документов, которые будете использовать: лучше всего подходят документы, из которых можно извлечь чистый и понятный текст. PDF-файлы могут создавать проблемы, поскольку они предназначены для печати, а не для извлечения осмысленного текста. Для работы с PDF лучше получить исходный документ или использовать специальные инструменты.
-
Разбейте текст на фрагменты (chunks): нельзя хранить текст как один длинный поток из-за ограничений размера контекста и риска путаницы. Разбиение (чанкинг) означает деление текста на более мелкие части. Текст можно разбивать разными способами: по количеству символов, токенов, слов, абзацев или разделов. Фрагменты также можно перекрывать.
-
Создайте эмбеддинги: эмбеддинги — это числовые представления текста, которые отражают его смысл. Чтобы создать эмбеддинг, каждый фрагмент текста пропускается через определённую модель эмбеддингов. Модель выдаёт вектор, длина которого зависит от используемой модели. Для этой задачи нужно загрузить из Ollama одну (или несколько) моделей эмбеддингов. Ниже приведены примеры моделей эмбеддингов, доступных в Ollama:
| Модель | Размер параметров | Размер эмбеддинга |
|---|---|---|
| mxbai-embed-large | 334M | 1024 |
| nomic-embed-text | 137M | 768 |
| all-minilm | 23M | 384 |
> Как правило, большие размеры эмбеддингов лучше передают тонкие нюансы входных данных. Однако их обработка требует больше вычислительных ресурсов, а большее число параметров должно увеличивать задержку (но и качество ответа).
-
Сохраните фрагменты и эмбеддинги в векторной базе данных: нам нужен способ эффективно находить наиболее релевантные фрагменты текста для данного запроса, и здесь на помощь приходит векторная база данных. Мы будем использовать Chromadb — открытую векторную базу данных для задач ИИ, которая упрощает создание RAG, подключая знания, факты и навыки к LLM. Для каждого фрагмента хранятся и эмбеддинг, и исходный текст.
-
Соберите запрос (prompt): когда появляется вопрос, мы создаём для него эмбеддинг и запрашиваем у векторной базы данных наиболее похожие фрагменты. Затем выбираем несколько лучших результатов и включаем их текст в запрос.
Цель RAG — предоставить модели наиболее релевантную информацию из наших документов, чтобы она могла генерировать более точные и содержательные ответы. Поэтому реализуем простой пример SLM, который опирается на набор фактов о пчёлах («Bee Facts»).
В окружении ollama выполните в терминале команду установки Chromadb:
pip install ollama chromadb
Загрузим модель эмбеддингов среднего размера nomic-embed-text:
ollama pull nomic-embed-text
Создадим рабочий каталог:
cd Documents/OLLAMA/
mkdir RAG-simple-bee
cd RAG-simple-bee/
Создадим новый блокнот Jupyter 40-RAG-simple-bee для экспериментов.
Импортируем нужные библиотеки:
import ollama
import chromadb
import time
Определим модели:
EMB_MODEL = "nomic-embed-text"
MODEL = "llama3.2:3B"
Сначала нужно создать базу знаний о пчёлах. Для этого собираем релевантные документы и преобразуем их в векторные эмбеддинги. Эти эмбеддинги сохраняются в векторной базе данных, что позволяет быстро выполнять поиск по сходству в дальнейшем. Введём «документы» — базу «фактов о пчёлах» в виде списка:
documents = [
"Пчеловодство, также известное как апикультура, подразумевает \
уход за пчелиными семьями, обычно в ульях, которым занимается человек.",
"Самый распространённый вид пчёл, которых содержат люди, — \
европейская медоносная пчела (Apis mellifera).",
...
"В мире существует ещё около 20 000 видов пчёл.",
"Только в Бразилии более 300 видов пчёл, и подавляющее большинство \
из них, в отличие от западных медоносных пчёл, не жалит.",
"В отчётах, написанных в 1577 году Гансом Стаденом, упоминаются три \
местных вида пчёл, которых использовали коренные жители Бразилии.",
"Коренные жители Бразилии использовали пчёл в медицинских и пищевых целях.",
"Из отчёта Ганса Стадена: вероятные виды: mandaçaia (Melipona quadrifasciata), \
mandaguari (Scaptotrigona postica) и jataí-amarela (Tetragonisca angustula)."
]
Здесь не нужно «разбивать» документ на фрагменты, поскольку мы будем использовать каждый элемент списка как отдельный фрагмент.
Теперь создадим векторную базу эмбеддингов bee_facts и сохраним в ней документы:
client = chromadb.Client()
collection = client.create_collection(name="bee_facts")
# Сохраняем каждый документ в векторной базе эмбеддингов
for i, d in enumerate(documents):
response = ollama.embeddings(model=EMB_MODEL, prompt=d)
embedding = response["embedding"]
collection.add(
ids=[str(i)], embeddings=[embedding], documents=[d]
)
Теперь, когда «база знаний» создана, можно начинать запросы и извлекать из неё данные:
Запрос пользователя: процесс начинается, когда пользователь задаёт вопрос, например: «Сколько пчёл в семье? Кто откладывает яйца и в каком количестве? А как насчёт распространённых вредителей и болезней?»
prompt = "Сколько пчёл в семье? Кто откладывает яйца и в каком количестве? \
А как насчёт распространённых вредителей и болезней?"
Эмбеддинг запроса: вопрос пользователя преобразуется в векторный эмбеддинг с помощью той же модели эмбеддингов, что использовалась для базы знаний.
response = ollama.embeddings(prompt=prompt, model=EMB_MODEL)
Поиск релевантных документов: система ищет в базе знаний по эмбеддингу запроса наиболее релевантные документы (в данном случае 5 наиболее вероятных). Это делается поиском по сходству, который сравнивает эмбеддинг запроса с эмбеддингами документов в базе.
results = collection.query(
query_embeddings=[response["embedding"]], n_results=5
)
data = results["documents"]
Дополнение запроса: найденная релевантная информация объединяется с исходным запросом пользователя, и получается дополненный запрос. Теперь он содержит вопрос пользователя и важные факты из базы знаний.
prompt = (
f"Using this data: {data}. " f"Respond to this prompt: {prompt}"
)
Генерация ответа: дополненный запрос передаётся языковой модели, в данном случае llama3.2:3b. Модель использует этот обогащённый контекст, чтобы сформировать развёрнутый ответ. Параметры temperature, top_k и top_p задают степень случайности и качество генерируемого ответа.
output = ollama.generate(
model=MODEL,
prompt = (
f"Using this data: {data}. "
f"Respond to this prompt: {prompt}"
),
options={
"temperature": 0.0,
"top_k":10,
"top_p":0.5
}
)
Передача ответа: наконец, система возвращает пользователю сгенерированный ответ.
print(output["response"])
Based on the provided data, here are the answers to your \
questions:
1. How many bees are in a colony?
A typical bee colony can contain between 20,000 and 80,000 bees.
2. Who lays eggs and how much?
The queen bee lays up to 2,000 eggs per day during peak seasons.
3. What about common pests and diseases?
Common pests and diseases that affect bees include varroa \
mites, hive beetles, and foulbrood.
Создадим функцию, которая поможет отвечать на новые вопросы:
def rag_bees(prompt, n_results=5, temp=0.0, top_k=10, top_p=0.5):
start_time = time.perf_counter() # Запускаем таймер
# Создаём эмбеддинг запроса и получаем релевантные данные
response = ollama.embeddings(
prompt=prompt,
model=EMB_MODEL
)
results = collection.query(
query_embeddings=[response["embedding"]],
n_results=n_results
)
data = results['documents']
# Генерируем ответ, объединяя запрос и полученные данные
output = ollama.generate(
model=MODEL,
prompt = (
f"Using this data: {data}. "
f"Respond to this prompt: {prompt}"
),
options={
"temperature": temp,
"top_k": top_k,
"top_p": top_p
}
)
print(output['response'])
end_time = time.perf_counter() # Останавливаем таймер
elapsed_time = round(
(end_time - start_time), 1
) # Вычисляем затраченное время
print(
f"\n[INFO] ==> The code for model: {MODEL}, "
f"took {elapsed_time}s to generate the answer.\n"
)
Теперь можно формулировать запросы и вызывать функцию:
prompt = "Есть ли в Бразилии пчёлы?"
rag_bees(prompt)
Yes, bees are found in Brazil. According to the data, Brazil \
has more than 300 different bee species, and indigenous people \
in Brazil used bees for medicine and food purposes. \
Additionally, reports from 1577 mention three native bees \
used by indigenous people in Brazil.
[INFO] ==> The code for model: llama3.2:3b, took 22.7s to \
generate the answer.
Кстати, если модель поддерживает несколько языков, можно задавать вопросы и на другом языке (например, на португальском), даже если набор данных составлен на русском:
prompt = "Existem abelhas no Brazil?"
rag_bees(prompt)
Sim, existem abelhas no Brasil! De acordo com o relato de Hans \
Staden, há três espécies de abelhas nativas do Brasil que \
foram mencionadas: mandaçaia (Melipona quadrifasciata), \
mandaguari (Scaptotrigona postica) e jataí-amarela \
(Tetragonisca angustula). Além disso, o Brasil é conhecido \
por ter mais de 300 espécies diferentes de abelhas, a \
maioria das quais não é agressiva e não põe veneno.
[INFO] ==> The code for model: llama3.2:3b, took 54.6s to \
generate the answer.
Дальнейшие шаги
Протестированные небольшие языковые модели хорошо работают на периферии и с текстом, и с изображениями, однако при работе с изображениями задержка оказалась высокой. Сочетание специализированных моделей может дать лучшие результаты. Например, в реальных задачах модель обнаружения объектов (такая как YOLO) может получить общее описание и количество объектов на изображении. Затем это описание передаётся LLM, которая помогает извлечь ключевые выводы и определить действия.
Avi Baum, технический директор (CTO) Hailo, говорит:
В обширном ландшафте искусственного интеллекта (ИИ) одним из самых интригующих путей стала эволюция ИИ на периферии. Этот путь привёл нас от классического машинного зрения к дискриминативному ИИ, усиливающему ИИ и теперь — к революционному рубежу генеративного ИИ. Каждый шаг приближал нас к будущему, в котором интеллектуальные системы органично встраиваются в нашу повседневную жизнь и предлагают опыт не только восприятия, но и создания — прямо на ладони.

Итоги
Эта лабораторная работа показала, как Raspberry Pi 5 можно превратить в мощный ИИ-узел, способный запускать большие языковые модели (LLM) для анализа данных на месте и в реальном времени с помощью Ollama и Python. Универсальность и мощность Raspberry Pi в сочетании с возможностями лёгких LLM, таких как Llama 3.2 и LLaVa-Phi-3-mini, делают её отличной платформой для периферийных вычислений.
Потенциал запуска LLM на периферии выходит далеко за рамки простой обработки данных, как в примерах этой лабораторной работы. Вот несколько идей по использованию этого проекта:
1. Умный дом:
- Интегрируйте SLM, чтобы интерпретировать голосовые команды или анализировать данные датчиков для интеллектуальной автоматизации дома. Это может включать мониторинг и управление бытовыми устройствами в реальном времени, системами безопасности и управлением энергопотреблением, всё с локальной обработкой без облачных сервисов.
2. Сбор и анализ данных в полевых условиях:
- Развёртывайте SLM на Raspberry Pi в удалённых или мобильных установках для сбора и анализа данных в реальном времени. Это может применяться в сельском хозяйстве для мониторинга состояния посевов, в экологических исследованиях для отслеживания диких животных или при ликвидации последствий катастроф для оценки обстановки и управления ресурсами.
3. Образовательные инструменты:
- Создавайте интерактивные обучающие инструменты, которые используют SLM для мгновенной обратной связи, перевода и репетиторства. Это особенно полезно в регионах с ограниченным доступом к современным технологиям и интернету.
4. Медицинские приложения:
- Используйте SLM для медицинской диагностики и мониторинга пациентов. Они могут анализировать симптомы в реальном времени и предлагать возможные варианты лечения. Это можно интегрировать в платформы телемедицины или портативные медицинские устройства.
5. Локальная бизнес-аналитика:
- Внедряйте SLM в розничной торговле или малом бизнесе, чтобы анализировать поведение клиентов, управлять запасами и оптимизировать работу. Возможность обрабатывать данные локально обеспечивает конфиденциальность и снижает зависимость от внешних сервисов.
6. Промышленный интернет вещей (IIoT):
- Интегрируйте SLM в промышленные системы IoT для предиктивного обслуживания, контроля качества и оптимизации процессов. Raspberry Pi может служить локальным блоком обработки данных, снижая задержки и повышая надёжность автоматизированных систем.
7. Автономные транспортные средства:
- Используйте SLM для обработки сенсорных данных автономных транспортных средств, обеспечивая принятие решений и навигацию в реальном времени. Это применимо к дронам, роботам и беспилотным автомобилям для повышения автономности и безопасности.
8. Культурное наследие и туризм:
- Внедряйте SLM для интерактивных и информативных экскурсий по объектам культурного наследия и музейным экспозициям. Посетители могут получать информацию в реальном времени, не имея доступа к интернету, что улучшает их впечатления.
9. Художественные и творческие проекты:
- Используйте SLM для анализа и генерации творческого контента: музыки, искусства и литературы. Это может стимулировать инновационные проекты в креативных индустриях и создавать уникальный интерактивный опыт на выставках и представлениях.
10. Персонализированные вспомогательные технологии:
- Разрабатывайте вспомогательные технологии для людей с инвалидностью, предоставляя персонализированную и адаптивную поддержку с помощью синтеза речи в реальном времени, перевода и других доступных инструментов.
Ресурсы
- Блокнот 10-Ollama_Python_Library
- Блокнот 20-Ollama_Function_Calling
- Блокнот 30-Function_Calling_with_images
- Блокнот 40-RAG-simple-bee
- Скрипт calc_distance_image на Python
Обнаружение объектов

Промпт DALL·E: обложка для главы «Обнаружение объектов» руководства по Raspberry Pi, выполненная в том же винтажном стиле лаборатории электроники 1950-х, что и предыдущие обложки. На переднем плане на верстаке — колёса и кубики, похожие на предоставленные пользователем. Raspberry Pi с подключённым модулем камеры снимает эти объекты. Сцену окружают классические лабораторные инструменты: паяльники, резисторы и провода. На фоне лаборатории — старинное оборудование: осциллографы и ламповые радиоприёмники, сохраняющие детальное и ностальгическое ощущение эпохи. Логотипов и текста быть не должно.
Обзор
Продолжая разговор о классификации изображений, перейдём к более сложной задаче компьютерного зрения — обнаружению объектов. Классификация изображений присваивает одну метку всему изображению, а обнаружение объектов идёт дальше: оно находит и определяет положение нескольких объектов на одном изображении. Эта возможность открывает много новых применений и задач, особенно в периферийных вычислениях и IoT-устройствах вроде Raspberry Pi.
Обнаружение объектов объединяет задачи классификации и локализации. Оно не только определяет, какие объекты присутствуют на изображении, но и указывает их положение, например рисуя вокруг них ограничивающие рамки (bounding boxes). Эта дополнительная сложность делает обнаружение объектов более мощным инструментом для понимания визуальных сцен, но требует более сложных моделей и методов обучения.
В периферийном ИИ (edge AI), где мы работаем с ограниченными вычислительными ресурсами, эффективные модели обнаружения объектов становятся критически важными. Проблемы, с которыми мы столкнулись при классификации изображений — баланс между размером модели, скоростью инференса и точностью, — при обнаружении объектов усиливаются. Однако и отдача выше, поскольку обнаружение объектов позволяет гораздо более тонко и подробно анализировать визуальные данные.
Некоторые применения обнаружения объектов на периферийных устройствах:
- Системы наблюдения и безопасности
- Автономные автомобили и дроны
- Контроль качества в промышленности
- Мониторинг дикой природы
- Приложения дополненной реальности
Приступая к обнаружению объектов, мы опираемся на концепции и методы, изученные в классификации изображений. Рассмотрим популярные архитектуры обнаружения, созданные для эффективности, например:
- одностадийные детекторы (Single Stage Detectors), такие как MobileNet и EfficientDet,
- FOMO (Faster Objects, More Objects) и
- YOLO (You Only Look Once — «ты смотришь только один раз»).
Чтобы узнать больше о моделях обнаружения объектов, пройдите учебник «A Gentle Introduction to Object Recognition With Deep Learning» (на английском языке): machinelearningmastery.com.
Мы будем исследовать эти модели обнаружения объектов с помощью:
- TensorFlow Lite Runtime (теперь переименован в LiteRT),
- Edge Impulse Linux Python SDK и
- Ultralytics
На протяжении всей лабораторной работы мы разберём основы обнаружения объектов и то, чем оно отличается от классификации изображений. Также научимся обучать, дообучать, тестировать, оптимизировать и развёртывать популярные архитектуры обнаружения объектов на наборе данных, созданном с нуля.
Основы обнаружения объектов
Обнаружение объектов опирается на основы классификации изображений, но значительно расширяет её возможности. Чтобы понять обнаружение объектов, сначала важно осознать ключевые различия с классификацией изображений:
Классификация изображений и обнаружение объектов
Классификация изображений:
- Присваивает одну метку всему изображению
- Отвечает на вопрос: «Какой основной объект или сцена на этом изображении?»
- Выдаёт одно предсказание класса для всего изображения
Обнаружение объектов:
- Находит и определяет положение нескольких объектов на изображении
- Отвечает на вопросы: «Какие объекты есть на этом изображении и где они находятся?»
- Выдаёт несколько предсказаний, каждое из которых состоит из метки класса и ограничивающей рамки
Чтобы наглядно показать эту разницу, рассмотрим пример:

Эта схема иллюстрирует ключевое различие: классификация изображений даёт одну метку для всего изображения, а обнаружение объектов определяет несколько объектов, их классы и их положение на изображении.
Ключевые компоненты обнаружения объектов
Системы обнаружения объектов обычно состоят из двух основных компонентов:
-
Локализация объектов: определяет, где на изображении находятся объекты. Обычно выдаёт ограничивающие рамки — прямоугольные области, охватывающие каждый найденный объект.
-
Классификация объектов: определяет класс или категорию каждого найденного объекта. Это похоже на классификацию изображений, но применяется к каждой локализованной области.
Проблемы обнаружения объектов
Обнаружение объектов сталкивается с рядом проблем, помимо тех, что есть у классификации изображений:
- Несколько объектов: на изображении может быть много объектов разных классов, размеров и положений.
- Разные масштабы: объекты могут выглядеть по-разному в зависимости от размера на изображении.
- Окклюзия: объекты могут быть частично скрыты или перекрывать друг друга.
- Загромождённый фон: отличить объекты от сложного фона может быть трудно.
- Работа в реальном времени: многие приложения требуют быстрого инференса, особенно на периферийных устройствах.
Подходы к обнаружению объектов
Есть два основных подхода к обнаружению объектов:
-
Двухстадийные детекторы: сначала предлагают интересующие области, а затем классифицируют каждую из них. Примеры: R-CNN и его варианты (Fast R-CNN, Faster R-CNN).
-
Одностадийные детекторы: предсказывают ограничивающие рамки (или центроиды) и вероятности классов за один проход сети. Примеры: YOLO (You Only Look Once), EfficientDet, SSD (Single Shot Detector) и FOMO (Faster Objects, More Objects). Они часто быстрее и лучше подходят для периферийных устройств вроде Raspberry Pi.
Метрики оценки
Обнаружение объектов использует метрики, отличные от метрик классификации изображений:
- Intersection over Union (IoU): измеряет перекрытие предсказанной и истинной ограничивающих рамок.
- Mean Average Precision (mAP): объединяет точность (precision) и полноту (recall) по всем классам и порогам IoU.
- Кадры в секунду (FPS): измеряют скорость обнаружения, что важно для приложений реального времени на периферийных устройствах.
Обзор предобученных моделей обнаружения объектов
Как мы видели во введении, по изображению или видеопотоку модель обнаружения объектов может определить, какие объекты из известного набора могут присутствовать, и указать их положение на изображении.
Некоторые распространённые модели можно протестировать онлайн на странице Object Detection — MediaPipe Studio.
На Kaggle можно найти наиболее распространённые предобученные модели TFLite для Raspi: ssd_mobilenet_v1 и EfficientDet. Эти модели обучены на наборе данных COCO (Common Objects in Context), содержащем более 200 000 размеченных изображений в 91 категории. Скачайте модели и загрузите их в каталог ./models на Raspi.
Модели и метки COCO также можно найти на GitHub.
В первой части лабораторной работы мы сосредоточимся на предобученной модели SSD-Mobilenet V1 размером и сравним её с EfficientDet-lite0 размером , также обученной на наборе COCO 2017. Обе модели переведены в формат TensorFlow Lite (4,2 МБ для SSD Mobilenet и 4,6 МБ для EfficientDet).
SSD-Mobilenet V2 или V3 рекомендуется для проектов с переносом обучения, но поскольку модель V1 в формате TFLite общедоступна, для этого обзора мы используем её.
Настройка окружения TFLite
Повторим шаги, выполненные в предыдущей практической работе «Классификация изображений»:
-
Обновление Raspberry Pi
-
Установка необходимых библиотек
-
Настройка виртуального окружения (необязательно, но рекомендуется)
source ~/tflite/bin/activate
-
Установка среды выполнения TensorFlow Lite
-
Установка дополнительных библиотек Python (внутри окружения)
Создание рабочего каталога
Поскольку в прошлой лабораторной работе мы создали папку Documents/TFLITE, теперь создадим папки для этой лабораторной работы по обнаружению объектов:
cd Documents/TFLITE/
mkdir OBJ_DETECT
cd OBJ_DETECT
mkdir images
mkdir models
cd models
Инференс и постобработка
Начнём новый блокнот, чтобы пройти все шаги обнаружения объектов на изображении.
Импортируйте нужные библиотеки:
import time
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import tflite_runtime.interpreter as tflite
Загрузите модель TFLite и выделите тензоры:
model_path = "./models/ssd-mobilenet-v1-tflite-default-v1.tflite"
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
Получите входные и выходные тензоры.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
Входные данные (input details) сообщают, как модели нужно подавать изображение. Форма (1, 300, 300, 3) с dtype uint8 означает, что ненормализованное изображение (значения пикселей от 0 до 255) размером подаётся по одному (размерность батча: 1).
Выходные данные (output details) включают не только метки («classes») и вероятности («scores»), но и относительное положение ограничивающих рамок («boxes») — где на изображении находится объект, — а также количество найденных объектов («num_detections»). Выходные данные также показывают, что модель может обнаружить не более 10 объектов на изображении.

Итак, для приведённого примера — того же изображения кота, что и в лабораторной по классификации изображений, — мы получаем вероятность 76 % обнаружения объекта с ID класса 16 в области, ограниченной ограничивающей рамкой [0.028011084, 0.020121813, 0.9886069, 0.802299]. Четыре числа соответствуют ymin, xmin, ymax и xmax — координатам рамки.
Учитывая, что y идёт сверху вниз (от ymin до ymax), а x — слева направо (от xmin до xmax), мы фактически получаем координаты левого верхнего и правого нижнего углов. Зная эти два угла и размер изображения, можно нарисовать прямоугольник вокруг объекта, как показано на рисунке ниже:

Далее нужно определить, что означает ID класса 16. Открыв файл coco_labels.txt как список, видим, что каждый элемент имеет свой индекс; для индекса 16 получаем, как и ожидалось, cat. Вероятность — это значение, возвращаемое из score.
Теперь загрузим несколько изображений с несколькими объектами для проверки.
img_path = "./images/cat_dog.jpeg"
orig_img = Image.open(img_path)
# Выводим изображение
plt.figure(figsize=(8, 8))
plt.imshow(orig_img)
plt.title("Original Image")
plt.show()

На основе входных данных предобработаем изображение, изменив его форму и добавив размерность:
img = orig_img.resize(
(input_details[0]["shape"][1], input_details[0]["shape"][2])
)
input_data = np.expand_dims(img, axis=0)
input_data.shape, input_data.dtype
Новая форма input_data — (1, 300, 300, 3) с dtype uint8, что соответствует ожиданиям модели.
Используя input_data, запустим интерпретатор, измерим задержку и получим выход:
start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (
end_time - start_time
) * 1000 # Переводим в миллисекунды
print("Inference time: {:.1f}ms".format(inference_time))
Задержка составляет около 800 мс, и мы получаем 4 отдельных выхода:
boxes = interpreter.get_tensor(output_details[0]["index"])[0]
classes = interpreter.get_tensor(output_details[1]["index"])[0]
scores = interpreter.get_tensor(output_details[2]["index"])[0]
num_detections = int(
interpreter.get_tensor(output_details[3]["index"])[0]
)
При беглом осмотре видно, что модель обнаружила 2 объекта с оценкой выше 0,5:
for i in range(num_detections):
if scores[i] > 0.5: # Порог уверенности
print(f"Object {i}:")
print(f" Bounding Box: {boxes[i]}")
print(f" Confidence: {scores[i]}")
print(f" Class: {classes[i]}")

И можно также визуализировать результаты:
plt.figure(figsize=(12, 8))
plt.imshow(orig_img)
for i in range(num_detections):
if scores[i] > 0.5: # При необходимости настройте порог
ymin, xmin, ymax, xmax = boxes[i]
(left, right, top, bottom) = (
xmin * orig_img.width,
xmax * orig_img.width,
ymin * orig_img.height,
ymax * orig_img.height,
)
rect = plt.Rectangle(
(left, top),
right - left,
bottom - top,
fill=False,
color="red",
linewidth=2,
)
plt.gca().add_patch(rect)
class_id = int(classes[i])
class_name = labels[class_id]
plt.text(
left,
top - 10,
f"{class_name}: {scores[i]:.2f}",
color="red",
fontsize=12,
backgroundcolor="white",
)

EfficientDet
EfficientDet технически не является моделью SSD (Single Shot Detector), однако имеет общие черты и опирается на те же идеи, что SSD и другие архитектуры обнаружения объектов:
-
EfficientDet:
- Разработана исследователями Google в 2019 году
- В качестве магистральной сети (backbone) использует EfficientNet
- Применяет новую двунаправленную пирамиду признаков (BiFPN)
- Использует составное масштабирование (compound scaling), чтобы эффективно масштабировать магистральную сеть и компоненты обнаружения
-
Сходство с SSD:
- Оба являются одностадийными детекторами: локализация и классификация объектов выполняются за один проход сети
- Оба используют карты признаков разного масштаба, чтобы обнаруживать объекты разных размеров
-
Ключевые различия:
- Магистраль: SSD обычно использует VGG или MobileNet, а EfficientDet — EfficientNet
- Слияние признаков: SSD использует простую пирамиду признаков, а EfficientDet — более продвинутую BiFPN
- Метод масштабирования: EfficientDet вводит составное масштабирование для всех компонентов сети
-
Преимущества EfficientDet:
- Как правило, обеспечивает лучший компромисс между точностью и эффективностью, чем SSD и многие другие модели обнаружения объектов
- Более гибкое масштабирование позволяет получить семейство моделей с разными компромиссами между размером и производительностью
Хотя EfficientDet не является моделью SSD, её можно рассматривать как эволюцию одностадийных архитектур обнаружения: она использует более продвинутые методы для повышения эффективности и точности. При работе с EfficientDet можно ожидать выходную структуру, похожую на SSD (например, ограничивающие рамки и оценки классов).
На GitHub можно найти ещё один блокнот, в котором исследуется модель EfficientDet — по аналогии с SSD MobileNet.
Проект обнаружения объектов
Теперь разработаем полноценный проект обнаружения объектов — от сбора данных до обучения и развёртывания. Как и в проекте классификации изображений, обученная и сконвертированная модель будет использоваться для инференса.
Мы будем использовать один и тот же набор данных для обучения трёх моделей: SSD-MobileNet V2, FOMO и YOLO.
Цель
Любой проект машинного обучения начинается с цели. Представим, что мы работаем на промышленном предприятии и должны сортировать и подсчитывать колёса и специальные коробки.

Иными словами, нам нужна многометочная классификация (multi-label classification), где на каждом изображении может присутствовать три класса:
- Фон (нет объектов)
- Коробка
- Колесо
Сбор исходных данных
Определив цель проекта машинного обучения, переходим к следующему, самому важному шагу — сбору набора данных. Исходный набор без меток можно создать с помощью телефона, Raspi или их комбинации. Воспользуемся простым веб-приложением на нашей Raspberry Pi, чтобы просматривать в браузере снимки в формате QVGA (320 x 240).
Из репозитория на GitHub возьмите Python-скрипт get_img_data.py и запустите его в терминале:
python3 get_img_data.py
Откройте веб-интерфейс:
- На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу
http://localhost:5000 - С другого устройства в той же сети: откройте веб-браузер и перейдите по адресу
http://<raspberry_pi_ip>:5000(замените<raspberry_pi_ip>на IP-адрес вашей Raspberry Pi). Например:http://192.168.4.210:5000/

Python-скрипт создаёт веб-интерфейс для захвата и организации наборов изображений с помощью Raspberry Pi и её камеры. Он удобен для проектов машинного обучения, которым нужны размеченные изображения, или, как в нашем случае, изображения без разметки.
Откройте веб-интерфейс в браузере, введите общую метку для изображений, которые хотите снять, и нажмите Start Capture.

Обратите внимание: снятые изображения будут иметь несколько меток, которые определяются позже.
Используйте живой предпросмотр, чтобы навести камеру, и нажмите Capture Image, чтобы сохранить изображения под текущей меткой (в данном случае box-wheel).

Когда изображений станет достаточно, нажмите Stop Capture. Снятые изображения сохраняются в папку dataset/box-wheel:

Снимите около 60 изображений. Старайтесь снимать с разных ракурсов, на разных фонах и при разном освещении. Для переноса созданного исходного набора на основной компьютер можно использовать Filezilla.
Разметка данных
Следующий шаг проекта обнаружения объектов — создание размеченного набора данных. Нужно разметить исходные изображения, обведя рамками объекты на каждом снимке (коробку и колесо). Для разметки можно использовать инструменты вроде LabelImg, CVAT, Roboflow или даже Edge Impulse Studio. Инструмент Edge Impulse мы уже изучили в других лабораторных, а здесь воспользуемся Roboflow.
Мы используем бесплатную версию Roboflow по двум основным причинам. Во-первых, там есть автоматическая разметка (auto-labeler). Во-вторых, размеченный набор доступен во многих форматах и может использоваться как в Edge Impulse Studio (его мы применим для обучения MobileNet V2 и FOMO), так и в Colab (например, для обучения YOLOv8). Размеченный набор в Edge Impulse (с бесплатным аккаунтом) нельзя использовать для обучения на других платформах.
Загрузите исходный набор данных в Roboflow. Создайте там бесплатный аккаунт и новый проект, например «box-versus-wheel».

Подробно процесс работы в Roboflow описывать не будем, так как для него существует много обучающих материалов.
Разметка изображений
Когда проект создан и набор данных загружен, выполните разметку с помощью инструмента «Auto-Label». Можно также загружать изображения только с фоном: их нужно сохранить без разметки.

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

Предобработка данных
Последний шаг с набором данных — предобработка для получения финальной версии для обучения. Изменим размер всех изображений до и создадим аугментированные версии каждого изображения (аугментация), чтобы получить новые обучающие примеры, на которых модель сможет учиться.
Для аугментации мы будем поворачивать изображения (на ±15°), обрезать их, а также менять яркость и экспозицию.

В конце процесса у нас будет 153 изображения.

Теперь экспортируем размеченный набор данных в формате, который понимают Edge Impulse, Ultralytics и другие фреймворки и инструменты, например YOLOv8. Скачаем архивную версию набора данных на рабочий стол.

Здесь можно посмотреть, как устроен набор данных:

Есть 3 отдельные папки — по одной на каждую выборку (train, test, valid). В каждой из них есть по 2 подпапки: images и labels. Изображения хранятся как image_id.jpg, а файлы разметки — как image_id.txt, где «image_id» уникален для каждого снимка.
Формат файла разметки: class_id и координаты ограничивающей рамки, где в нашем случае class_id равен 0 для box и 1 для wheel. Числовые идентификаторы (0, 1, 2…) следуют алфавитному порядку названий классов.
Файл data.yaml содержит информацию о наборе данных, в том числе названия классов (names: ['box', 'wheel']) в формате YOLO.
Вот и всё! Теперь можно начинать обучение в Edge Impulse Studio (как мы сделаем на следующем шаге), в Ultralytics (как будем делать, когда будем рассматривать YOLO) или даже обучить модель с нуля в Colab (как мы делали с набором Cifar-10 в лабораторной по классификации изображений).
Предобработанный набор данных можно найти на сайте Roboflow.
Обучение модели SSD MobileNet в Edge Impulse Studio
Перейдите на сайт Edge Impulse Studio, войдите в систему (Login) или создайте аккаунт и начните новый проект.
Можно клонировать проект, разработанный для этой лабораторной работы: Raspi - Object Detection.
На вкладке проекта Dashboard прокрутите вниз до блока Project info и в поле Labeling method выберите Bounding boxes (object detection).
Загрузка размеченных данных
В Studio откройте вкладку Data acquisition и в разделе UPLOAD DATA загрузите исходный набор данных с компьютера.
Можно использовать опцию Select a folder, выбрав, например, папку train на компьютере, которая содержит две подпапки: images и labels. Выберите формат меток «YOLO TXT», загрузите данные в категорию Training и нажмите Upload data.

Повторите процедуру для тестовых данных (загрузите обе папки: test и validation). По окончании загрузки у вас будет размеченный набор из 153 изображений, разделённый на обучающую и тестовую выборки (84 % и 16 %).
Обратите внимание: метки будут храниться в файлах меток как
0и1, которые соответствуютboxиwheel.

Проектирование Impulse
Первое, что нужно задать на шаге Create impulse, — целевое устройство для развёртывания. Появится всплывающее окно. Мы выберем Raspberry Pi 4 — промежуточное устройство между Raspi-Zero и Raspi-5.
Этот выбор не влияет на обучение: он лишь даёт представление о задержке модели на этом конкретном устройстве.

На этом этапе нужно определить:
- Предобработку: изменение размера отдельных изображений. В нашем случае изображения уже были предобработаны в Roboflow до , поэтому оставим как есть. Изменение размера здесь не важно, поскольку изображения квадратные. Если вы загрузите прямоугольное изображение, его нужно сжать до квадрата (без обрезки). Затем можно определить, преобразовывать ли изображения из RGB в оттенки серого.
- Проектирование модели: в нашем случае «Object Detection» (обнаружение объектов).

Предобработка всего набора данных
В разделе Image выберите Color depth как RGB и нажмите Save parameters.

Studio автоматически переходит к следующему разделу Generate features, где обрабатываются все образцы. В итоге получаем 480 объектов: 207 коробок и 273 колеса.

Исследователь признаков (feature explorer) показывает, что после генерации признаков все образцы хорошо разделяются.
Проектирование модели, обучение и тестирование
Для обучения нужно выбрать предобученную модель. Используем MobileNetV2 SSD FPN-Lite (только 320x320). Это предобученная модель обнаружения объектов, которая находит до 10 объектов на изображении и выдаёт ограничивающую рамку для каждого из них. Размер модели — около 3,7 МБ. Она принимает RGB-вход размером пикселей.
Гиперпараметры обучения:
- Эпохи: 25
- Размер батча: 32
- Скорость обучения (Learning Rate): 0,15
Для валидации во время обучения будет отложено 20 % набора данных (validation_dataset).

В итоге модель получает общую точность (precision) по метрике COCO mAP 88,8 %, что выше результата на тестовых данных (83,3 %).
Развёртывание модели
Есть два способа развернуть модель:
- Модель TFLite: позволяет развернуть обученную модель в формате
.tflite, чтобы Raspi запускал её с помощью Python. - Linux (AARCH64): бинарный файл для Linux (AARCH64) реализует протокол Edge Impulse Linux, который позволяет запускать модели на любой плате разработки на базе Linux, с SDK, например для Python. Подробнее см. в документации и инструкциях по установке.
Развернём модель TFLite. На вкладке Dashboard перейдите к модели Transfer learning (int8 quantized) и нажмите значок загрузки:

Перенесите модель с компьютера в папку ./models на Raspi, а также сделайте снимки или получите изображения для инференса и сохраните их в папку ./images.
Инференс и постобработка
Инференс можно выполнить так, как описано в разделе «Обзор предобученных моделей обнаружения объектов». Начнём новый блокнот, чтобы пройти все шаги по обнаружению кубиков и колёс на изображении.
Импортируйте нужные библиотеки:
import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
import tflite_runtime.interpreter as tflite
Укажите путь к модели и метки:
model_path = "./models/ei-raspi-object-detection-SSD-MobileNetv2-320x0320-int8.lite"
labels = ["box", "wheel"]
Помните, что модель будет выводить ID классов как значения (0 и 1), следуя алфавитному порядку названий классов.
Загрузите модель, выделите тензоры и получите информацию о входных и выходных тензорах:
# Загружаем модель TFLite
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
# Получаем входные и выходные тензоры
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
Важное отличие: dtype входных данных модели теперь int8, то есть входные значения лежат в диапазоне от –128 до +127, тогда как каждый пиксель нашего исходного изображения — от 0 до 255. Поэтому изображение нужно предобработать. Проверим:
input_dtype = input_details[0]["dtype"]
input_dtype
numpy.int8
Теперь откроем изображение и выведем его:
# Загружаем изображение
img_path = "./images/box_2_wheel_2.jpg"
orig_img = Image.open(img_path)
# Выводим изображение
plt.figure(figsize=(6, 6))
plt.imshow(orig_img)
plt.title("Original Image")
plt.show()

И выполним предобработку:
scale, zero_point = input_details[0]["quantization"]
img = orig_img.resize(
(input_details[0]["shape"][1], input_details[0]["shape"][2])
)
img_array = np.array(img, dtype=np.float32) / 255.0
img_array = (
(img_array / scale + zero_point).clip(-128, 127).astype(np.int8)
)
input_data = np.expand_dims(img_array, axis=0)
Проверим входные данные, чтобы убедиться, что входной тензор соответствует ожиданиям модели:
input_data.shape, input_data.dtype
((1, 320, 320, 3), dtype('int8'))
Теперь выполним инференс. Также вычислим задержку модели:
# Инференс на Raspi-Zero
start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (
end_time - start_time
) * 1000 # Переводим в миллисекунды
print("Inference time: {:.1f}ms".format(inference_time))
На инференс модель затратит около 600 мс на Raspi-Zero, то есть примерно в 5 раз дольше, чем на Raspi-5.
Теперь получим классы обнаруженных объектов, координаты их ограничивающих рамок и вероятности:
boxes = interpreter.get_tensor(output_details[1]["index"])[0]
classes = interpreter.get_tensor(output_details[3]["index"])[0]
scores = interpreter.get_tensor(output_details[0]["index"])[0]
num_detections = int(
interpreter.get_tensor(output_details[2]["index"])[0]
)
for i in range(num_detections):
if scores[i] > 0.5: # Порог уверенности
print(f"Object {i}:")
print(f" Bounding Box: {boxes[i]}")
print(f" Confidence: {scores[i]}")
print(f" Class: {classes[i]}")

По результатам видно, что обнаружено 4 объекта: два с ID класса 0 (box) и два с ID класса 1 (wheel). Это верно!
Визуализируем результат при пороге (threshold) 0,5:
threshold = 0.5
plt.figure(figsize=(6, 6))
plt.imshow(orig_img)
for i in range(num_detections):
if scores[i] > threshold:
ymin, xmin, ymax, xmax = boxes[i]
(left, right, top, bottom) = (
xmin * orig_img.width,
xmax * orig_img.width,
ymin * orig_img.height,
ymax * orig_img.height,
)
rect = plt.Rectangle(
(left, top),
right - left,
bottom - top,
fill=False,
color="red",
linewidth=2,
)
plt.gca().add_patch(rect)
class_id = int(classes[i])
class_name = labels[class_id]
plt.text(
left,
top - 10,
f"{class_name}: {scores[i]:.2f}",
color="red",
fontsize=12,
backgroundcolor="white",
)

Но что будет, если уменьшить порог, например до 0,3?

Начинают появляться ложные срабатывания и множественные обнаружения: модель находит один и тот же объект несколько раз, с разными уровнями уверенности и немного разными рамками.
Порог иногда приходится снижать, чтобы поймать все объекты и избежать ложноотрицательных результатов, однако это приводит к множественным обнаружениям.
Чтобы улучшить результаты обнаружения, нужно применить подавление не-максимумов (Non-Maximum Suppression, NMS). Оно убирает перекрывающиеся ограничивающие рамки и оставляет только самое уверенное обнаружение.
Для этого создадим общую функцию non_max_suppression(), которая уточняет результаты обнаружения, отбрасывая избыточные и перекрывающиеся рамки. Она итеративно выбирает обнаружение с наибольшей оценкой уверенности и удаляет другие существенно перекрывающиеся обнаружения по порогу Intersection over Union (IoU).
def non_max_suppression(boxes, scores, threshold):
# Переводим в координаты углов
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= threshold)[0]
order = order[inds + 1]
return keep
Как это работает:
- Сортировка: сначала все обнаружения сортируются по оценке уверенности от высокой к низкой.
- Выбор: выбирается рамка с наибольшей оценкой и добавляется в итоговый список обнаружений.
- Сравнение: выбранная рамка сравнивается со всеми оставшимися рамками с меньшими оценками.
- Удаление: любая рамка, которая существенно перекрывается с выбранной (выше порога IoU), удаляется.
- Итерация: процесс повторяется со следующей по оценке рамкой, пока не будут обработаны все рамки.
Теперь определим более точную функцию визуализации, которая учитывает порог IoU и выводит только объекты, отобранные функцией non_max_suppression:
def visualize_detections(
image, boxes, classes, scores, labels, threshold, iou_threshold
):
if isinstance(image, Image.Image):
image_np = np.array(image)
else:
image_np = image
height, width = image_np.shape[:2]
# Переводим нормализованные координаты в координаты пикселей
boxes_pixel = boxes * np.array([height, width, height, width])
# Применяем NMS
keep = non_max_suppression(boxes_pixel, scores, iou_threshold)
# Задаём размер фигуры 12x8 дюймов
fig, ax = plt.subplots(1, figsize=(12, 8))
ax.imshow(image_np)
for i in keep:
if scores[i] > threshold:
ymin, xmin, ymax, xmax = boxes[i]
rect = patches.Rectangle(
(xmin * width, ymin * height),
(xmax - xmin) * width,
(ymax - ymin) * height,
linewidth=2,
edgecolor="r",
facecolor="none",
)
ax.add_patch(rect)
class_name = labels[int(classes[i])]
ax.text(
xmin * width,
ymin * height - 10,
f"{class_name}: {scores[i]:.2f}",
color="red",
fontsize=12,
backgroundcolor="white",
)
plt.show()
Теперь создадим функцию, которая вызывает остальные и выполняет инференс для любого изображения:
def detect_objects(img_path, conf=0.5, iou=0.5):
orig_img = Image.open(img_path)
scale, zero_point = input_details[0]["quantization"]
img = orig_img.resize(
(input_details[0]["shape"][1], input_details[0]["shape"][2])
)
img_array = np.array(img, dtype=np.float32) / 255.0
img_array = (
(img_array / scale + zero_point)
.clip(-128, 127)
.astype(np.int8)
)
input_data = np.expand_dims(img_array, axis=0)
# Инференс на Raspi-Zero
start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (
end_time - start_time
) * 1000 # Переводим в миллисекунды
print("Inference time: {:.1f}ms".format(inference_time))
# Извлекаем выходные данные
boxes = interpreter.get_tensor(output_details[1]["index"])[0]
classes = interpreter.get_tensor(output_details[3]["index"])[0]
scores = interpreter.get_tensor(output_details[0]["index"])[0]
num_detections = int(
interpreter.get_tensor(output_details[2]["index"])[0]
)
visualize_detections(
orig_img,
boxes,
classes,
scores,
labels,
threshold=conf,
iou_threshold=iou,
)
Теперь запустим код с тем же изображением, с порогом уверенности 0,3, но с небольшим IoU:
img_path = "./images/box_2_wheel_2.jpg"
detect_objects(img_path, conf=0.3, iou=0.05)

Обучение модели FOMO в Edge Impulse Studio
Инференс с моделью SSD MobileNet работает хорошо, но задержка оказалась существенной. Инференс занимал от 0,5 до 1,3 секунды на Raspi-Zero, то есть около 1 кадра в секунду (FPS) или меньше. Один из способов ускорить процесс — использовать FOMO (Faster Objects, More Objects).
Этот алгоритм машинного обучения позволяет подсчитывать несколько объектов и находить их положение на изображении в реальном времени, используя в 30 раз меньше вычислительной мощности и памяти, чем MobileNet SSD или YOLO. Главная причина в том, что другие модели вычисляют размер объекта, рисуя вокруг него квадрат (ограничивающую рамку), тогда как FOMO не учитывает размер и сообщает только, где находится объект, — через координаты его центроида.
Как работает FOMO
В типичном конвейере обнаружения объектов первый этап — извлечение признаков из входного изображения. FOMO использует MobileNetV2 для выполнения этой задачи. MobileNetV2 обрабатывает входное изображение и создаёт карту признаков, которая вычислительно эффективно отражает ключевые характеристики: текстуры, формы и границы объектов.

Когда признаки извлечены, более простая архитектура FOMO, сфокусированная на обнаружении центральных точек, интерпретирует карту признаков, чтобы определить, где на изображении находятся объекты. На выходе получается сетка ячеек, где каждая ячейка показывает, обнаружен ли центр объекта. Для каждой ячейки модель выдаёт одну или несколько оценок уверенности, указывающих вероятность присутствия объекта.
Посмотрим, как это работает на изображении.
FOMO делит изображение на блоки пикселей с коэффициентом 8. Для входа сетка будет , для — и так далее. Затем FOMO пропускает через каждый блок пикселей классификатор, чтобы вычислить вероятность того, что в нём есть коробка или колесо, и определить области с наибольшей вероятностью содержать объект (если блок пикселей не содержит объектов, он классифицируется как background, фон). По итоговой области FOMO определяет координаты центроида этой области (относительно размеров изображения).

Компромисс между скоростью и точностью:
- Разрешение сетки: FOMO использует сетку фиксированного разрешения, поэтому каждая ячейка определяет, есть ли объект в своей части изображения. Точность локализации при этом невысока, зато модель быстрая и вычислительно лёгкая, что критично для периферийных устройств.
- Обнаружение нескольких объектов: поскольку каждая ячейка независима, FOMO может одновременно обнаруживать несколько объектов, находя несколько центров.
Новое проектирование Impulse, обучение и тестирование
Вернитесь в Edge Impulse Studio, на вкладке Experiments создайте новый impulse. Теперь входные изображения должны быть (это ожидаемый входной размер для MobileNetV2).

На вкладке Image сгенерируйте признаки и перейдите на вкладку Object detection.
Для обучения нужно выбрать предобученную модель. Используем FOMO (Faster Objects, More Objects) MobileNetV2 0.35.

Гиперпараметры обучения:
- Эпохи: 30
- Размер батча: 32
- Скорость обучения (Learning Rate): 0,001
Для валидации во время обучения будет отложено 20 % набора данных (validation_dataset). Аугментацию для оставшихся 80 % (train_dataset) мы не применяем, поскольку набор уже был аугментирован на этапе разметки в Roboflow.
В итоге модель получает общую F1-меру 93,3 % при впечатляющей задержке 8 мс (Raspi-4), что примерно в 60 раз меньше, чем у SSD MobileNetV2.

Обратите внимание: FOMO автоматически добавил третью метку background к двум ранее заданным классам box (0) и wheel (1).
На вкладке Model testing точность составила 94 %. Вот один из результатов на тестовом образце:

В задачах обнаружения объектов точность, как правило, не является основной метрикой оценки. Обнаружение объектов предполагает не только классификацию, но и построение ограничивающих рамок вокруг объектов, поэтому это более сложная задача, чем простая классификация. Здесь проблема в том, что у нас нет ограничивающих рамок, а только центроиды. Короче говоря, использование точности как метрики может вводить в заблуждение и не даёт полного представления о качестве модели.
Развёртывание модели
Как и в предыдущем разделе, обученную модель можно развернуть как TFLite или как Linux (AARCH64). Сейчас развернём её как Linux (AARCH64) — бинарный файл, реализующий протокол Edge Impulse Linux.
Модели Edge Impulse for Linux поставляются в формате .eim. Этот исполняемый файл содержит наш «полный impulse», созданный в Edge Impulse Studio. Impulse состоит из блоков обработки сигнала и блоков обучения и аномалий, которые мы добавили и обучили. Он скомпилирован с оптимизациями под наш процессор или GPU (например, инструкции NEON для ядер ARM) и содержит простой слой межпроцессного взаимодействия (IPC) поверх Unix-сокета.
На вкладке Deploy выберите опцию Linux (AARCH64), модель int8 и нажмите Build.

Модель будет автоматически загружена на ваш компьютер.
На Raspi создайте новое рабочее место:
cd ~
cd Documents
mkdir EI_Linux
cd EI_Linux
mkdir models
mkdir images
Переименуйте модель для удобства, например в raspi-object-detection-linux-aarch64-FOMO-int8.eim, и перенесите её в новую папку ./models на Raspi. Также сделайте снимки или получите изображения для инференса и сохраните их в папку ./images.
Инференс и постобработка
Инференс будет выполняться с помощью Linux Python SDK. Эта библиотека позволяет запускать модели машинного обучения и собирать данные с датчиков на Linux-машинах с помощью Python. SDK имеет открытый исходный код и размещён на GitHub: edgeimpulse/linux-sdk-python.
Создадим виртуальное окружение для работы с Linux Python SDK:
python3 -m venv ~/eilinux
source ~/eilinux/bin/activate
Установим все необходимые библиотеки:
sudo apt-get update
sudo apt-get install libatlas-base-dev \
libportaudio0 libportaudio2
sudo apt-get install libportaudiocpp0 portaudio19-dev
pip3 install edge_impulse_linux -i https://pypi.python.org/simple
pip3 install Pillow matplotlib pyaudio opencv-contrib-python
sudo apt-get install portaudio19-dev
pip3 install pyaudio
pip3 install opencv-contrib-python
Сделайте модель исполняемой:
chmod +x raspi-object-detection-linux-aarch64-FOMO-int8.eim
Установите Jupyter Notebook в новом окружении:
pip3 install jupyter
Запустите блокнот локально (на Raspi-4 или 5 с рабочим столом):
jupyter notebook
или в браузере на вашем компьютере:
jupyter notebook --ip=192.168.4.210 --no-browser
Начнём новый блокнот, выполнив все шаги по обнаружению кубиков и колёс на изображении с помощью модели FOMO и Edge Impulse Linux Python SDK.
Импортируйте нужные библиотеки:
import sys, time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
import cv2
from edge_impulse_linux.image import ImageImpulseRunner
Укажите путь к модели и метки:
model_file = "raspi-object-detection-linux-aarch64-FOMO-int8.eim"
model_path = "models/" + model_file # Обученная ML-модель из Edge Impulse
labels = ["box", "wheel"]
Помните, что модель будет выдавать ID классов как значения (0 и 1), следуя алфавитному порядку названий классов.
Загрузите и инициализируйте модель:
# Загружаем файл модели
runner = ImageImpulseRunner(model_path)
# Инициализируем модель
model_info = runner.init()
Переменная model_info содержит важную информацию о модели. Однако в отличие от интерпретатора TFLite библиотека EI Linux Python SDK сама подготовит модель к инференсу.
Теперь откроем изображение и выведем его. Для совместимости используем OpenCV — библиотеку компьютерного зрения, которую EI применяет внутри. OpenCV читает изображения в формате BGR, поэтому его нужно преобразовать в RGB:
# Загружаем изображение
img_path = "./images/1_box_1_wheel.jpg"
orig_img = cv2.imread(img_path)
img_rgb = cv2.cvtColor(orig_img, cv2.COLOR_BGR2RGB)
# Выводим изображение
plt.imshow(img_rgb)
plt.title("Original Image")
plt.show()

Теперь получим признаки и предобработанное изображение (cropped) с помощью runner:
features, cropped = (
runner.get_features_from_image_auto_studio_settings(img_rgb)
)
Выполним инференс. Также вычислим задержку модели:
res = runner.classify(features)
Получим выходные классы обнаруженных объектов, центроиды их ограничивающих рамок и вероятности:
print(
"Found %d bounding boxes (%d ms.)"
% (
len(res["result"]["bounding_boxes"]),
res["timing"]["dsp"] + res["timing"]["classification"],
)
)
for bb in res["result"]["bounding_boxes"]:
print(
"\t%s (%.2f): x=%d y=%d w=%d h=%d"
% (
bb["label"],
bb["value"],
bb["x"],
bb["y"],
bb["width"],
bb["height"],
)
)
Found 2 bounding boxes (29 ms.)
1 (0.91): x=112 y=40 w=16 h=16
0 (0.75): x=48 y=56 w=8 h=8
Результаты показывают, что обнаружено два объекта: один с ID класса 0 (box) и один с ID класса 1 (wheel). Это верно!
Визуализируем результат (threshold равен 0,5 — это значение по умолчанию, заданное при тестировании модели в Edge Impulse Studio).
print(
"\tFound %d bounding boxes (latency: %d ms)"
% (
len(res["result"]["bounding_boxes"]),
res["timing"]["dsp"] + res["timing"]["classification"],
)
)
plt.figure(figsize=(5, 5))
plt.imshow(cropped)
# Перебираем каждую из возвращённых ограничивающих рамок
bboxes = res["result"]["bounding_boxes"]
for bbox in bboxes:
# Получаем углы ограничивающей рамки
left = bbox["x"]
top = bbox["y"]
width = bbox["width"]
height = bbox["height"]
# Рисуем кружок в центре обнаружения
circ = plt.Circle(
(left + width // 2, top + height // 2),
5,
fill=False,
color="red",
linewidth=3,
)
plt.gca().add_patch(circ)
class_id = int(bbox["label"])
class_name = labels[class_id]
plt.text(
left,
top - 10,
f'{class_name}: {bbox["value"]:.2f}',
color="red",
fontsize=12,
backgroundcolor="white",
)
plt.show()

Исследуем модель YOLO с помощью Ultralytics
В этой лабораторной работе мы исследуем YOLOv8. Ultralytics YOLOv8 — версия известной модели для обнаружения объектов и сегментации изображений в реальном времени, YOLO. YOLOv8 построена на передовых достижениях глубокого обучения и компьютерного зрения и обеспечивает выдающуюся скорость и точность. Её упрощённая архитектура подходит для разных приложений и легко адаптируется к разным аппаратным платформам — от периферийных устройств до облачных API.
Введение в модель YOLO
Модель YOLO (You Only Look Once) — высокоэффективный и широко используемый алгоритм обнаружения объектов, известный возможностями обработки в реальном времени. В отличие от традиционных систем обнаружения, которые адаптируют классификаторы или локализаторы для выполнения обнаружения, YOLO формулирует задачу обнаружения как единую задачу регрессии. Такой подход позволяет YOLO одновременно предсказывать несколько ограничивающих рамок и вероятности их классов по всему изображению за один проход, что заметно повышает скорость.
Ключевые особенности
-
Единая архитектура сети:
- YOLO использует одну нейронную сеть для обработки всего изображения. Сеть делит изображение на сетку и для каждой ячейки сетки напрямую предсказывает ограничивающие рамки и связанные с ними вероятности классов. Такое сквозное обучение повышает скорость и упрощает архитектуру модели.
-
Обработка в реальном времени:
- Одна из сильных сторон YOLO — возможность обнаруживать объекты в реальном времени. В зависимости от версии и оборудования YOLO может обрабатывать изображения с высокой частотой кадров (FPS). Это делает её идеальной для приложений, которым нужно быстрое и точное обнаружение объектов, например видеонаблюдения, автономного вождения и анализа спортивных трансляций.
-
Эволюция версий:
- За годы YOLO значительно улучшилась: от YOLOv1 до новейшей YOLOv10. Каждая итерация приносила улучшения точности, скорости и эффективности. YOLOv8, например, включает улучшения архитектуры сети, усовершенствованные методики обучения и лучшую поддержку различного оборудования, что обеспечивает более надёжную работу.
- Хотя YOLOv10 — новейший член семейства с многообещающими результатами, согласно её статье, она была выпущена только в мае 2024 года и пока не полностью интегрирована в библиотеку Ultralytics. При этом анализ кривых «точность — полнота» показывает, что YOLOv8 в целом превосходит YOLOv9: она захватывает большую долю истинно положительных результатов и эффективнее минимизирует ложноположительные (подробнее см. в этой статье). Поэтому эта лабораторная работа основана на YOLOv8n.

-
Точность и эффективность:
- Ранние версии YOLO жертвовали точностью ради скорости, но новые версии значительно продвинулись в балансе между ними. Новые модели быстрее и точнее: они обнаруживают мелкие объекты (например, пчёл) и хорошо справляются со сложными наборами данных.
-
Широкий спектр применений:
- Универсальность YOLO привела к её использованию во многих областях. Её применяют в системах мониторинга дорожного движения для обнаружения и подсчёта автомобилей, в системах безопасности для выявления потенциальных угроз и в агротехнологиях для мониторинга посевов и скота. Она подходит для любой области, где нужно эффективное и точное обнаружение объектов.
-
Сообщество и развитие:
- YOLO продолжает развиваться и поддерживается сильным сообществом разработчиков и исследователей (особенно YOLOv8). Открытые реализации и обширная документация сделали её доступной для настройки и интеграции в разные проекты. Популярные фреймворки глубокого обучения, включая Darknet, TensorFlow и PyTorch, поддерживают YOLO, что ещё больше расширяет её применимость.
- Ultralytics YOLOv8 умеет не только обнаруживать объекты (наш случай), но и сегментировать и определять позы с моделями, предобученными на наборе COCO, а также выполнять классификацию с моделями YOLOv8, предобученными на наборе ImageNet. Режим отслеживания доступен для всех моделей обнаружения, сегментации и определения позы.

Установка
На Raspi деактивируем текущее окружение, чтобы создать новое рабочее место:
deactivate
cd ~
cd Documents/
mkdir YOLO
cd YOLO
mkdir models
mkdir images
Создадим виртуальное окружение для работы с Ultralytics YOLOv8:
python3 -m venv ~/yolo
source ~/yolo/bin/activate
И установим пакеты Ultralytics для локального инференса на Raspi:
- Обновите список пакетов, установите pip и обновите его до последней версии:
sudo apt update
sudo apt install python3-pip -y
pip install -U pip
- Установите pip-пакет
ultralyticsс дополнительными зависимостями:
pip install ultralytics[export]
- Перезагрузите устройство:
sudo reboot
Проверка YOLO
После загрузки Raspi-Zero активируйте окружение yolo, перейдите в рабочий каталог
source ~/yolo/bin/activate
cd ~/Documents/YOLO
и запустите инференс на изображении, которое будет загружено с сайта Ultralytics, с моделью YOLOv8n (самой маленькой в семействе) в терминале (CLI):
yolo predict model='yolov8n' \
source='https://ultralytics.com/images/bus.jpg'
Семейство моделей YOLO предобучено на наборе данных COCO.
Результат инференса появится в терминале. На изображении (bus.jpg) обнаружены 4 persons (человека), 1 bus (автобус) и 1 stop signal (знак «стоп»).

Также мы получили сообщение Results saved to runs/detect/predict. В этой папке появилось новое сохранённое изображение (bus.jpg). Скачаем его с Raspi-Zero на рабочий стол для просмотра:

Итак, Ultralytics YOLO корректно установлен на Raspi. Однако на Raspi-Zero проблема — высокая задержка инференса, около 18 секунд, даже для самой маленькой модели семейства (YOLOv8n).
Экспорт модели в формат NCNN
Развёртывание моделей компьютерного зрения на периферийных устройствах с ограниченной вычислительной мощностью, таких как Raspi-Zero, может приводить к проблемам с задержкой. Один из вариантов — использовать формат, оптимизированный для производительности. Это гарантирует, что даже устройства с ограниченной вычислительной мощностью смогут справляться с продвинутыми задачами компьютерного зрения.
Среди всех форматов экспорта, поддерживаемых Ultralytics, NCNN — высокопроизводительный фреймворк для вычислений нейронных сетей, оптимизированный для мобильных платформ. С самого начала NCNN проектировался с учётом развёртывания на мобильных телефонах и не имеет сторонних зависимостей. Он кроссплатформенный и работает быстрее, чем известные открытые фреймворки (например, TFLite).
NCNN обеспечивает лучшую производительность инференса на устройствах Raspberry Pi. Он хорошо оптимизирован для мобильных встраиваемых платформ (например, на архитектуре ARM).
Итак, конвертируем нашу модель и повторим инференс:
- Экспортируйте модель YOLOv8n PyTorch в формат NCNN. В результате будет создана папка
/yolov8n_ncnn_model:
yolo export model=yolov8n.pt format=ncnn
- Запустите инференс с экспортированной моделью (источником может быть изображение bus.jpg, которое было загружено с сайта в текущий каталог при последнем инференсе):
yolo predict model='./yolov8n_ncnn_model' source='bus.jpg'
При первом инференсе, когда модель загружается, задержка обычно высокая (около 17 с), но начиная со второго инференса она снижается примерно до 2 с.
Исследование YOLO с помощью Python
Для начала запустим интерпретатор Python, чтобы построчно разобрать, как работает модель YOLO:
python3
Теперь подключим библиотеку YOLO из Ultralytics и загрузим модель:
from ultralytics import YOLO
model = YOLO("yolov8n_ncnn_model")
Далее выполним инференс на изображении (снова используем bus.jpg):
img = "bus.jpg"
result = model.predict(img, save=True, imgsz=640, conf=0.5, iou=0.3)

Результат почти идентичен тому, что мы получили при инференсе в терминале (CLI). Исключение: знак остановки не был обнаружен уменьшенной моделью NCNN. Заметим, что задержка снизилась.
Разберём содержимое переменной result.
Например, result[0].boxes.data показывает основной результат инференса — тензор формы (4, 6). Каждая строка соответствует одному обнаруженному объекту: первые 4 столбца — координаты ограничивающей рамки, 5-й — уверенность, 6-й — класс (в данном случае 0: person и 5: bus):

Можно получить отдельные результаты инференса, например время инференса, и вывести его в удобном формате:
inference_time = int(result[0].speed["inference"])
print(f"Inference Time: {inference_time} ms")
Или общее количество обнаруженных объектов:
print(f"Number of objects: {len(result[0].boxes.cls)}")

С помощью Python можно создать подробный вывод, отвечающий нашим потребностям (подробнее см. Model Prediction with Ultralytics YOLO). Запустим не строку за строкой в интерпретаторе, а Python-скрипт. Для редактирования используем nano. Сначала создадим пустой Python-скрипт, например yolov8_tests.py:
nano yolov8_tests.py
Введите строки кода:
from ultralytics import YOLO
# Загружаем модель YOLOv8
model = YOLO("yolov8n_ncnn_model")
# Выполняем инференс
img = "bus.jpg"
result = model.predict(img, save=False, imgsz=640, conf=0.5, iou=0.3)
# Выводим результаты
inference_time = int(result[0].speed["inference"])
print(f"Inference Time: {inference_time} ms")
print(f"Number of objects: {len(result[0].boxes.cls)}")

Чтобы сохранить Python-скрипт, нажмите [CTRL+O], затем [ENTER], после чего [CTRL+X].
Запустите скрипт:
python yolov8_tests.py
Результат будет таким же, как при инференсе в терминале (CLI) и во встроенном интерпретаторе Python.
Первый вызов библиотеки YOLO и загрузка модели для инференса занимают много времени, но последующие инференсы будут гораздо быстрее. Например, первый инференс может занять несколько секунд, а после этого время инференса должно снизиться до менее чем 1 секунды.
Обучение YOLOv8 на собственном наборе данных
Вернёмся к нашему набору данных «Box versus Wheel», размеченному на Roboflow. На вкладке Download Dataset, вместо варианта Download a zip to computer, который мы использовали для обучения в Edge Impulse Studio, выберем Show download code. Этот вариант откроет всплывающее окно с фрагментом кода, который нужно вставить в наш блокнот для обучения.

Для обучения адаптируем один из публичных примеров Ultralytics и запустим его в Google Colab. Ниже — мой блокнот, который можно адаптировать для собственного проекта:
- Обучение YOLOv8 на наборе данных Box versus Wheel [Open In Colab]
Ключевые моменты блокнота
-
Запускайте его с GPU (NVIDIA T4 доступна бесплатно).
-
Установите Ultralytics через PIP.

-
Теперь можно импортировать YOLO и загрузить набор данных в Colab, вставив код загрузки, полученный из Roboflow. Обратите внимание: наш набор будет смонтирован в
/content/datasets/:

- Обязательно проверьте и исправьте файл
data.yaml, указав правильные пути к изображениям (скопируйте путь к каждой папкеimages).
names:
- box
- wheel
nc: 2
roboflow:
license: CC BY 4.0
project: box-versus-wheel-auto-dataset
url: https://universe.roboflow.com/marcelo-rovai-riila/ \
box-versus-wheel-auto-dataset/dataset/5
version: 5
workspace: marcelo-rovai-riila
test: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
test/images
train: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
train/images
val: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
valid/images
-
Задайте основные гиперпараметры, которые хотите изменить по сравнению со значениями по умолчанию, например:
MODEL = 'yolov8n.pt' IMG_SIZE = 640 EPOCHS = 25 # Для итогового проекта стоит рассмотреть # не менее 100 эпох -
Запустите обучение (через CLI):
!yolo task=detect mode=train model={MODEL} \ data={dataset.location}/data.yaml \ epochs={EPOCHS} \ imgsz={IMG_SIZE} plots=True

Обучение заняло несколько минут и дало отличный результат (mAP50 = 0,995). По окончании все результаты сохраняются в указанной папке, например /runs/detect/train/. Там можно найти, например, матрицу ошибок (confusion matrix).

- Обученная модель (
best.pt) сохраняется в папке/runs/detect/train/weights/. Теперь проверьте обученную модель на изображениях изvalid/images:
!yolo task=detect mode=val model={HOME}/runs/detect/train/\
weights/best.pt data={dataset.location}/data.yaml
Результаты оказались похожими на результаты обучения.
- Теперь выполните инференс на изображениях, отложенных для тестирования:
!yolo task=detect mode=predict model={HOME}/runs/detect/train/\
weights/best.pt conf=0.25 source={dataset.location}/test/\
images save=True
Результаты инференса сохраняются в папке runs/detect/predict. Посмотрим некоторые из них:

-
Рекомендуется экспортировать результаты обучения, валидации и тестирования на Google Drive. Для этого нужно подключить диск:
from google.colab import drive drive.mount('/content/gdrive')и скопировать содержимое папки
/runsв папку на вашем Drive, например:!scp -r /content/runs '/content/gdrive/MyDrive/\ 10_UNIFEI/Box_vs_Wheel_Project'
Инференс обученной моделью на Raspi
Скачайте обученную модель /runs/detect/train/weights/best.pt на свой компьютер. С помощью FTP-клиента FileZilla перенесите best.pt в папку моделей на Raspi (перед переносом модель можно переименовать, например в box_wheel_320_yolo.pt).
С помощью FileZilla перенесите несколько изображений из тестового набора в папку ./YOLO/images:
Вернитесь в папку YOLO и запустите интерпретатор Python:
cd ..
python
Как и раньше, импортируем библиотеку YOLO и подключим нашу преобразованную модель для обнаружения коробок и колёс:
from ultralytics import YOLO
model = YOLO("./models/box_wheel_320_yolo.pt")
Теперь определим изображение и вызовем инференс (на этот раз сохраним результат в виде изображения для внешней проверки):
img = "./images/1_box_1_wheel.jpg"
result = model.predict(img, save=True, imgsz=320, conf=0.5, iou=0.3)
Повторите для нескольких изображений. Результат инференса сохраняется в переменной result, а обработанное изображение — в папке runs/detect/predict8.

С помощью FileZilla можно отправить результаты инференса на рабочий стол для проверки:

Результат инференса отличный! Модель обучена на базе меньшей модели семейства YOLOv8 (YOLOv8n). Проблема — задержка: около 1 секунды (или 1 FPS на Raspi-Zero). Конечно, эту задержку можно уменьшить, если конвертировать модель в TFLite или NCNN.
Обнаружение объектов в видеопотоке
Все модели, рассмотренные в этой лабораторной работе, могут обнаруживать объекты в реальном времени с камеры. Захваченное изображение должно подаваться на вход обученной и сконвертированной модели. Для Raspi-4 или 5 с рабочим столом OpenCV может захватывать кадры и показывать результат инференса.
Однако создать видеопоток с веб-камеры для обнаружения объектов в реальном времени тоже возможно. Например, возьмём скрипт, разработанный для приложения классификации изображений, и адаптируем его под веб-приложение обнаружения объектов в реальном времени на основе TensorFlow Lite и Flask.
Эта версия приложения работает со всеми моделями TFLite. Убедитесь, что модель лежит в правильной папке, например:
model_path = "./models/ssd-mobilenet-v1-tflite-default-v1.tflite"
Скачайте Python-скрипт object_detection_app.py с GitHub.
В терминале выполните:
python3 object_detection_app.py
И откройте веб-интерфейс:
- На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу
http://localhost:5000. - С другого устройства в той же сети: откройте веб-браузер и перейдите по адресу
http://<raspberry_pi_ip>:5000(замените<raspberry_pi_ip>на IP-адрес вашей Raspberry Pi). Например:http://192.168.4.210:5000/
Вот несколько скриншотов приложения, запущенного на внешнем компьютере:

Рассмотрим техническое описание ключевых модулей, используемых в приложении обнаружения объектов:
-
TensorFlow Lite (tflite_runtime):
- Назначение: эффективный инференс моделей машинного обучения на периферийных устройствах.
- Почему: TFLite даёт меньший размер модели и оптимизированную производительность по сравнению с полным TensorFlow, что критично для устройств с ограниченными ресурсами, таких как Raspberry Pi. Он поддерживает аппаратное ускорение и квантование, которые дополнительно повышают эффективность.
- Ключевые функции:
Interpreterдля загрузки и запуска модели,get_input_details()иget_output_details()для взаимодействия с моделью.
-
Flask:
- Назначение: лёгкий веб-фреймворк для создания серверной части.
- Почему: простота и гибкость Flask делают его идеальным для быстрой разработки и развёртывания веб-приложений. Он менее требователен к ресурсам, чем крупные фреймворки, что важно для периферийных устройств.
- Ключевые компоненты: декораторы маршрутов для определения конечных точек API, объекты
Responseдля потоковой передачи видео,render_template_stringдля отдачи динамического HTML.
-
Picamera2:
- Назначение: интерфейс к модулю камеры Raspberry Pi.
- Почему: Picamera2 — современная библиотека для управления камерами Raspberry Pi, которая даёт лучшую производительность и возможности по сравнению с исходной библиотекой Picamera.
- Ключевые функции:
create_preview_configuration()для настройки камеры,capture_file()для захвата кадров.
-
PIL (Python Imaging Library):
- Назначение: обработка и редактирование изображений.
- Почему: PIL даёт широкие возможности обработки изображений. Здесь он используется для изменения размера изображений, рисования ограничивающих рамок и преобразования между форматами.
- Ключевые классы:
Imageдля загрузки и обработки изображений,ImageDrawдля рисования фигур и текста на изображениях.
-
NumPy:
- Назначение: эффективные операции с массивами и численные вычисления.
- Почему: операции с массивами NumPy намного быстрее обычных списков Python, что критично для обработки данных изображений и входов и выходов модели.
- Ключевые функции:
array()для создания массивов,expand_dims()для добавления размерностей.
-
Threading (потоки):
- Назначение: параллельное выполнение задач.
- Почему: потоки позволяют одновременно захватывать кадры, выполнять обнаружение объектов и работать веб-серверу, что критично для работы в реальном времени.
- Ключевые компоненты: класс
Threadсоздаёт отдельные потоки выполнения, аLockиспользуется для синхронизации потоков.
-
io.BytesIO:
- Назначение: двоичные потоки в памяти.
- Почему: позволяет эффективно работать с данными изображений в памяти без временных файлов, что ускоряет работу и снижает число операций ввода-вывода.
-
time:
- Назначение: функции, связанные со временем.
- Почему: используются для задержек (
time.sleep()), чтобы управлять частотой кадров, и для измерения производительности.
-
jQuery (на стороне клиента):
- Назначение: упрощённая работа с DOM и AJAX-запросы.
- Почему: позволяет динамически обновлять веб-интерфейс и обмениваться данными с сервером без перезагрузки страницы.
- Ключевые функции:
.get()и.post()для AJAX-запросов, методы работы с DOM для обновления интерфейса.
Архитектура системы приложения:
- Главный поток: запускает сервер Flask, обрабатывает HTTP-запросы и отдаёт веб-интерфейс.
- Поток камеры: непрерывно захватывает кадры с камеры.
- Поток обнаружения: пропускает кадры через модель TFLite для обнаружения объектов.
- Буфер кадров: общая область памяти (защищённая блокировками), в которой хранятся последний кадр и результаты обнаружения.
Поток данных приложения кратко:
- Камера захватывает кадр → буфер кадров.
- Поток обнаружения читает кадр из буфера → обрабатывает его моделью TFLite → обновляет результаты обнаружения в буфере кадров.
- Маршруты Flask обращаются к буферу кадров, чтобы отдать последний кадр и результаты обнаружения.
- Веб-клиент получает обновления через AJAX и обновляет интерфейс.
Такая архитектура обеспечивает эффективное обнаружение объектов в реальном времени и сохраняет отзывчивый веб-интерфейс на периферийном устройстве с ограниченными ресурсами, таком как Raspberry Pi. Потоки и эффективные библиотеки вроде TFLite и PIL позволяют обрабатывать видеокадры в реальном времени, а Flask и jQuery дают удобный способ взаимодействия с ними.
Вы можете протестировать приложение с другой предобработанной моделью, например EfficientDet, изменив строку в приложении:
model_path = "./models/lite-model_efficientdet_lite0_detection_metadata_1.tflite"
Если нужно использовать приложение с моделью SSD-MobileNetV2, обученной в Edge Impulse Studio на наборе «Box versus Wheel», код также нужно адаптировать под входные данные модели, как мы разобрали в её блокноте.
Итоги
В этой лабораторной работе мы разобрали реализацию обнаружения объектов на периферийных устройствах, таких как Raspberry Pi, и показали, какие возможности даёт запуск продвинутых задач компьютерного зрения на оборудовании с ограниченными ресурсами. Мы рассмотрели несколько важных аспектов:
- Сравнение моделей: мы изучили разные модели обнаружения объектов, включая SSD-MobileNet, EfficientDet, FOMO и YOLO, и сравнили их производительность и компромиссы на периферийных устройствах.
- Обучение и развёртывание: на собственном наборе данных с коробками и колёсами (размеченном в Roboflow) мы прошли процесс обучения моделей в Edge Impulse Studio и Ultralytics и развернули их на Raspberry Pi.
- Методы оптимизации: для повышения скорости инференса на периферийных устройствах мы рассмотрели такие методы, как квантование модели (TFLite int8) и конвертация формата (например, в NCNN).
- Приложения реального времени: лабораторная работа показала веб-приложение для обнаружения объектов в реальном времени, демонстрируя, как такие модели можно интегрировать в практические интерактивные системы.
- Соображения о производительности: на протяжении работы мы обсуждали баланс между точностью модели и скоростью инференса — критически важный аспект для приложений периферийного ИИ.
Возможность выполнять обнаружение объектов на периферийных устройствах открывает множество перспектив в самых разных областях: от точного земледелия, промышленной автоматизации и контроля качества до «умного дома» и мониторинга окружающей среды. Обрабатывая данные локально, такие системы могут обеспечивать меньшую задержку, лучшую приватность и работу в условиях ограниченной связи.
В дальнейшем перспективными направлениями могут стать:
- реализация конвейеров из нескольких моделей для более сложных задач;
- изучение возможностей аппаратного ускорения для Raspberry Pi;
- интеграция обнаружения объектов с другими датчиками для более полных систем периферийного ИИ;
- разработка решений «периферия — облако», которые используют и локальную обработку, и облачные ресурсы.
Обнаружение объектов на периферийных устройствах позволяет создавать интеллектуальные и отзывчивые системы, которые приносят возможности ИИ прямо в физический мир и открывают новые горизонты в том, как мы взаимодействуем с окружающей средой и понимаем её.
Ресурсы
- Набор данных («Box versus Wheel»)
- Блокнот SSD-MobileNet на Raspi
- Блокнот EfficientDet на Raspi
- Блокнот FOMO (EI Linux) на Raspi
- Обучение YOLOv8 на наборе Box versus Wheel в Colab
- Проект Edge Impulse: SSD MobileNet и FOMO
- Python-скрипты
- Модели
Настройка

Промпт DALL·E: лаборатория электроники в мультяшном стиле 1950-х. Винтажное оборудование, большие осциллографы, старые ламповые радиоприёмники и громоздкие компьютеры. Плата Raspberry Pi 5 крупным планом, в реальном размере (примерно как банковская карта), лежит на верстаке. Вокруг — классические инструменты: паяльник, резисторы и провода. Сцена яркая, с утрированными цветами и игривыми деталями, характерными для мультфильма. Логотипов и текста нет.
В этой главе описана настройка моделей Raspberry Pi Zero 2 W (Raspi-Zero) и Raspberry Pi 5 (Raspi-5). Мы рассмотрим настройку оборудования, установку операционной системы, первичную конфигурацию и проверки.
Общие инструкции для Raspi-5 применимы и к более старым версиям Raspberry Pi, например Raspi-3 и Raspi-4.
Обзор
Raspberry Pi — мощный и универсальный одноплатный компьютер, который стал незаменимым инструментом инженеров самых разных специальностей. Эти компактные устройства, разработанные Raspberry Pi Foundation, сочетают доступность, вычислительную мощность и широкие возможности GPIO (General Purpose Input/Output), что делает их идеальными для прототипирования, разработки встраиваемых систем и сложных инженерных проектов.
Ключевые особенности
-
Вычислительная мощность. Несмотря на небольшой размер, Raspberry Pi обладает значительными возможностями: в новейших моделях — многоядерные процессоры ARM и до 8 ГБ оперативной памяти.
-
Интерфейс GPIO. 40-контактный разъём GPIO позволяет напрямую взаимодействовать с датчиками, исполнительными устройствами и другими электронными компонентами, что облегчает проекты интеграции аппаратной и программной частей.
-
Широкие возможности подключения. Встроенные Wi-Fi, Bluetooth, Ethernet и несколько портов USB позволяют реализовать разные коммуникационные и сетевые проекты.
-
Низкоуровневый доступ к аппаратуре. Raspberry Pi предоставляет доступ к интерфейсам I2C, SPI и UART, что позволяет детально управлять внешними устройствами и обмениваться с ними данными.
-
Возможности работы в реальном времени. При правильной настройке Raspberry Pi можно использовать для приложений мягкого реального времени, что делает его пригодным для систем управления и задач обработки сигналов.
-
Энергоэффективность. Низкое энергопотребление позволяет создавать устройства с батарейным питанием и энергоэффективные решения, особенно на моделях Pi Zero.
Модели Raspberry Pi (рассматриваемые в этой книге)
-
Raspberry Pi Zero 2 W (Raspi-Zero):
- подходит для: компактных встраиваемых систем;
- ключевые характеристики: четырёхъядерный процессор ARM Cortex-A53 с частотой 1 ГГц, 512 МБ ОЗУ, минимальное энергопотребление.
-
Raspberry Pi 5 (Raspi-5):
- подходит для: более требовательных задач, таких как периферийные вычисления, компьютерное зрение и периферийный ИИ, включая LLM;
- ключевые характеристики: четырёхъядерный процессор ARM Cortex-A76 с частотой 2,4 ГГц, до 8 ГБ ОЗУ, интерфейс PCIe для расширений.
Инженерные применения
-
Проектирование встраиваемых систем: разработка и прототипирование встраиваемых систем для реальных задач.
-
IoT и сетевые устройства: создание взаимосвязанных устройств и изучение протоколов MQTT, CoAP и HTTP/HTTPS.
-
Системы управления: реализация контуров обратной связи, ПИД-регуляторов и взаимодействие с исполнительными устройствами.
-
Компьютерное зрение и ИИ: использование библиотек OpenCV и TensorFlow Lite для обработки изображений и машинного обучения на периферии.
-
Сбор и анализ данных: сбор данных с датчиков, анализ в реальном времени и создание систем журналирования данных.
-
Робототехника: создание контроллеров роботов, реализация алгоритмов планирования движения и взаимодействие с драйверами моторов.
-
Обработка сигналов: анализ сигналов в реальном времени, фильтрация и приложения DSP.
-
Сетевая безопасность: настройка VPN, межсетевых экранов и изучение тестирования на проникновение в сеть.
Это руководство поможет настроить самые распространённые модели Raspberry Pi, чтобы быстро приступить к проекту машинного обучения. Мы рассмотрим настройку оборудования, установку операционной системы и первичную конфигурацию, уделяя внимание подготовке Pi к задачам машинного обучения.
Обзор оборудования
Raspberry Pi Zero 2W

- Процессор: 64-битный четырёхъядерный Arm Cortex-A53 с частотой 1 ГГц;
- ОЗУ: 512 МБ SDRAM;
- Беспроводная связь: 2,4 ГГц 802.11 b/g/n (Wi-Fi), Bluetooth 4.2, BLE;
- Порты: mini HDMI, micro USB OTG, разъём камеры CSI-2;
- Питание: 5 В через порт micro USB.
Raspberry Pi 5

- Процессор:
- Pi 5: 64-битный четырёхъядерный Arm Cortex-A76 с частотой 2,4 ГГц;
- Pi 4: 64-битная SoC с четырьмя ядрами Cortex-A72 (ARM v8) с частотой 1,5 ГГц;
- ОЗУ: варианты 2, 4 или 8 ГБ (для задач ИИ рекомендуется 8 ГБ);
- Беспроводная связь: двухдиапазонный 802.11ac, Bluetooth 5.0;
- Порты: 2 × micro HDMI, 2 × USB 3.0, 2 × USB 2.0, разъём камеры CSI, разъём дисплея DSI;
- Питание: 5 В постоянного тока через разъём USB-C (3 А).
В лабораторных работах мы будем использовать разные имена для Raspberry Pi:
Raspi,Raspi-5,Raspi-Zeroи т. д. ОбычноRaspiиспользуется, когда инструкция или комментарий относится ко всем моделям.
Установка операционной системы
Операционная система (ОС)
Операционная система (ОС) — это базовое программное обеспечение, которое управляет аппаратными и программными ресурсами компьютера и предоставляет стандартные сервисы для программ. Это основной программный слой, который работает на компьютере и выступает посредником между аппаратурой и прикладным программным обеспечением. ОС управляет памятью компьютера, процессами, драйверами устройств, файлами и протоколами безопасности.
- Основные функции:
- управление процессами: выделение процессорного времени разным программам;
- управление памятью: выделение и освобождение памяти по мере необходимости;
- управление файловой системой: организация файлов и каталогов и учёт их расположения;
- управление устройствами: взаимодействие с подключёнными аппаратными устройствами;
- пользовательский интерфейс: предоставление способа взаимодействия пользователя с компьютером.
- Компоненты:
- ядро (kernel): центральная часть ОС, которая управляет аппаратными ресурсами;
- оболочка (shell): пользовательский интерфейс для работы с ОС;
- файловая система: организует и управляет хранением данных;
- драйверы устройств: программное обеспечение, которое позволяет ОС взаимодействовать с аппаратурой.
Raspberry Pi работает под специальной версией Linux, созданной для встраиваемых систем. Эта операционная система, обычно представляющая собой вариант Debian под названием Raspberry Pi OS (раньше — Raspbian), оптимизирована для архитектуры ARM и ограниченных ресурсов Pi.
Последняя версия Raspberry Pi OS основана на Debian Bookworm.
Ключевые особенности:
- Легковесность: адаптирована для эффективной работы на аппаратуре Pi.
- Универсальность: поддерживает широкий набор приложений и языков программирования.
- Открытый исходный код: позволяет настраивать систему и получать улучшения от сообщества.
- Поддержка GPIO: обеспечивает взаимодействие с датчиками и другой аппаратурой через контакты Pi.
- Регулярные обновления: система постоянно совершенствуется в части производительности и безопасности.
Встраиваемый Linux на Raspberry Pi предоставляет полноценную операционную систему в компактном корпусе, что делает его идеальным для проектов — от простых IoT-устройств до более сложных приложений периферийного машинного обучения. Совместимость со стандартными инструментами и библиотеками Linux делает его мощной платформой для разработки и экспериментов.
Установка
Чтобы использовать Raspberry Pi, нам понадобится операционная система. По умолчанию Raspberry Pi ищет операционную систему на любой SD-карте, вставленной в слот, поэтому установить систему мы будем с помощью Raspberry Pi Imager.
Raspberry Pi Imager — инструмент для загрузки и записи образов в macOS, Windows и Linux. В нём есть множество популярных образов операционных систем для Raspberry Pi. Imager мы также используем для предварительной настройки учётных данных и параметров удалённого доступа.
Чтобы установить ОС на Raspi, выполните следующие шаги:
-
Скачайте и установите Raspberry Pi Imager на свой компьютер.
-
Вставьте microSD-карту в компьютер (рекомендуется SD-карта объёмом 32 ГБ).
-
Откройте Raspberry Pi Imager и выберите модель вашей Raspberry Pi.
-
Выберите подходящую операционную систему:
- Для Raspi-Zero: например, можно выбрать
Raspberry Pi OS Lite (64-bit).

Из-за небольшого объёма SDRAM (512 МБ) для Raspi-Zero рекомендуется 32-битная версия системы. Однако для запуска некоторых моделей машинного обучения, например YOLOv8 от Ultralytics, нужна 64-битная версия. Хотя на Raspi-Zero можно запустить рабочий стол, мы выберем версию LITE (без рабочего стола), чтобы уменьшить расход ОЗУ при обычной работе.
- Для Raspi-5: можно выбрать полную 64-битную версию с рабочим столом:
Raspberry Pi OS (64-bit).
- Для Raspi-Zero: например, можно выбрать

- Выберите microSD-карту в качестве устройства хранения.
- Нажмите
Next, затем значок «шестерёнки», чтобы открыть расширенные настройки. - Задайте имя хоста (hostname), имя пользователя и пароль Raspi, настройте Wi-Fi и включите SSH (очень важно!).

- Запишите образ на microSD-карту.
В примерах здесь мы используем разные имена хостов в зависимости от устройства: raspi, raspi-5, raspi-zero и т. д. Замените их на те, которые используете вы.
Первичная настройка
- Вставьте microSD-карту в Raspberry Pi.
- Подключите питание, чтобы загрузить Raspberry Pi.
- Дождитесь завершения первоначальной загрузки (это может занять несколько минут).
Самые распространённые команды Linux для работы с Raspi можно найти здесь или здесь.
Удалённый доступ
Доступ по SSH
Проще всего работать с Raspi-Zero через SSH («headless», то есть без монитора и клавиатуры). Для этого можно использовать Terminal (macOS/Linux), PuTTY (Windows) или любой другой клиент.
-
Узнайте IP-адрес вашей Raspberry Pi (например, в настройках роутера).
-
На компьютере откройте терминал и подключитесь по SSH:
ssh username@<raspberry_pi_ip_address>Если IP-адрес неизвестен, можно попробовать так:
ssh username@hostname.localнапример,
ssh username@rpi-5.localилиssh username@raspi.local.
Когда вы увидите приглашение:
username@rpi-5:~ $значит, вы работаете с Raspi удалённо. Полезно регулярно обновлять систему. Для этого выполните:
sudo apt-get update sudo apt upgradeТакже проверьте IP-адрес Raspi. В терминале это можно сделать так:
hostname -I

Выключение Raspi через терминал
Когда нужно выключить Raspberry Pi, лучше не просто выдёргивать шнур питания. Raspi может ещё записывать данные на SD-карту, и тогда простое отключение питания может привести к потере данных или, что хуже, к повреждению карты.
Для безопасного выключения используйте командную строку:
sudo shutdown -h now
Чтобы избежать потери данных и повреждения SD-карты, перед отключением питания подождите несколько секунд после выключения, пока светодиод Raspberry Pi перестанет мигать и погаснет. Когда светодиод погаснет, питание можно безопасно отключить.
Передача файлов между Raspi и компьютером
Передавать файлы между Raspi и основным компьютером можно с помощью флешки, напрямую в терминале (через scp) или по сети с помощью FTP-клиента.
Использование Secure Copy Protocol (scp)
Копирование файлов на Raspberry Pi
Создадим на компьютере текстовый файл, например test.txt.

Можно использовать любой текстовый редактор. В том же терминале удобен
nano.
Чтобы скопировать файл test.txt с личного компьютера в домашний каталог пользователя на Raspberry Pi, выполните следующую команду из каталога, где лежит test.txt, заменив <username> на имя пользователя, под которым вы входите в Raspberry Pi, и <pi_ip_address> на IP-адрес вашей Raspberry Pi:
$ scp test.txt <username>@<pi_ip_address>:~/
Обратите внимание:
~/означает, что файл будет перемещён в корневой домашний каталог Raspi. Можно выбрать любую папку на Raspi, но её нужно создать до запускаscp, так какscpне создаёт папки автоматически.
Например, перенесём файл test.txt в домашний каталог моей Raspi-Zero с IP-адресом 192.168.4.210:
scp test.txt username@192.168.4.210:~/

Для различения терминалов я использую другой профиль. Описанное выше действие выполняется на вашем компьютере. Теперь подключимся к Raspi по SSH и проверим, что файл на месте:

Копирование файлов с Raspberry Pi
Чтобы скопировать файл myfile.txt из домашнего каталога пользователя на Raspberry Pi в текущий каталог другого компьютера, выполните следующую команду на вашем компьютере-хосте:
$ scp <username>@<pi_ip_address>:myfile.txt .
Например:
На Raspi создадим копию файла с другим именем:
cp test.txt test_2.txt
А на компьютере-хосте (в моём примере — Mac):
scp username@192.168.4.210:test_2.txt .

Передача файлов по FTP
Передавать файлы можно и по FTP, например с помощью клиента FileZilla. Установите программу для операционной системы вашего компьютера и укажите IP-адрес Raspi в поле Host. Например:
sftp://192.168.4.210
Введите имя пользователя и пароль Raspi. Нажатие Quickconnect откроет два окна: одно для рабочего стола вашего компьютера (справа), другое для Raspi (слева).

Увеличение памяти SWAP
С помощью htop — интерактивного монитора процессов, работающего на разных платформах, — можно удобно отслеживать ресурсы Raspi: список процессов, загрузку процессоров и используемую память в реальном времени. Чтобы запустить htop, введите в терминале:
htop

Что касается памяти, среди устройств семейства Raspberry Pi у Raspi-Zero меньше всего ОЗУ (500 МБ), тогда как у Raspi 4 или 5 — от 2 до 8 ГБ. Для любой Raspi можно увеличить объём памяти, доступной системе, с помощью «swap». Swap-память, или область подкачки, — это приём, при котором операционная система временно сохраняет данные из ОЗУ (оперативной памяти) на SD-карту, когда физическая память полностью занята. Это позволяет ОС продолжать работу, когда ОЗУ заполнено, и предотвращает зависания и замедление системы.
Swap полезен устройствам с ограниченным объёмом ОЗУ, таким как Raspi-Zero. Увеличение swap помогает запускать более требовательные приложения и процессы, но нужно учитывать возможное снижение производительности из-за частого обращения к диску.
По умолчанию swap-память Raspi-Zero составляет всего 100 МБ, что очень мало для более сложных и требовательных приложений машинного обучения (например, YOLO). Увеличим её до 2 ГБ.
Сначала отключите файл подкачки:
sudo dphys-swapfile swapoff
Затем откройте и измените файл /etc/dphys-swapfile. Для этого используем nano:
sudo nano /etc/dphys-swapfile
Найдите переменную CONF_SWAPSIZE (по умолчанию 200) и измените её значение на 2000:
CONF_SWAPSIZE=2000
Сохраните файл.
Далее снова включите файл подкачки и перезагрузите Raspi-Zero:
sudo dphys-swapfile setup
sudo dphys-swapfile swapon
sudo reboot
После перезагрузки (снова подключитесь по SSH) вы увидите, что максимальный объём swap в верхней части htop теперь составляет около 2 ГБ (в моём случае — 1,95 ГБ).
Чтобы
htopпродолжал работать, откройте второе окно терминала и продолжайте взаимодействовать с Raspi в нём.
Установка камеры
Raspi — отличное устройство для приложений компьютерного зрения, и для этого понадобится камера. Можно подключить стандартную USB-веб-камеру к micro-USB-порту через OTG-адаптер (Raspi-Zero и Raspi-5) или модуль камеры, подключённый к порту CSI (Camera Serial Interface) на Raspi.
USB-веб-камеры обычно уступают по качеству модулям камер, которые подключаются к порту CSI. Кроме того, ими нельзя управлять командами
raspistillиraspividв терминале или пакетом записиpicameraдля Python. Тем не менее есть причины подключить USB-камеру к Raspberry Pi: например, так проще подключить несколько камер к одной плате, можно использовать длинные кабели или просто под рукой уже есть такая камера.
Установка USB-веб-камеры
- Выключите Raspi:
sudo shutdown -h now
- Подключите USB-веб-камеру (USB Camera Module, 30 кадров/с, ) к Raspi. В этом примере используется Raspi-Zero, но инструкции подходят для всех моделей Raspi.

- Включите питание снова и подключитесь по SSH.
- Чтобы проверить, распознана ли USB-камера, выполните:
lsusb
Камера должна появиться в выводе команды.

- Чтобы сделать тестовый снимок с USB-камеры, используйте:
fswebcam test_image.jpg
Эта команда сохранит изображение с именем «test_image.jpg» в текущем каталоге.

- Поскольку мы подключаемся к Raspi по SSH, снимок нужно перенести на основной компьютер, чтобы его просмотреть. Для этого можно использовать FileZilla или SCP:
Откройте терминал на основном компьютере и выполните:
scp username@raspi-zero.local:~/test_image.jpg .
Замените «username» на своё имя пользователя, а «raspi-zero» — на имя хоста Raspi.

- Если качество изображения не устраивает, можно изменить настройки. Например, задайте разрешение, подходящее для YOLO ():
fswebcam -r 640x640 --no-banner test_image_yolo.jpg
Эта команда создаёт снимок с более высоким разрешением без стандартной подписи (баннера).

Обычную USB-веб-камеру тоже можно использовать:

И проверить её командой lsusb:

Потоковое видео
Для потокового видео (это требует больше ресурсов) можно установить и использовать mjpg-streamer.
Сначала установите Git:
sudo apt install git
Теперь установите зависимости для mjpg-streamer, клонируйте репозиторий и выполните установку:
sudo apt install cmake libjpeg62-turbo-dev
git clone https://github.com/jacksonliam/mjpg-streamer.git
cd mjpg-streamer/mjpg-streamer-experimental
make
sudo make install
Затем запустите потоковую передачу:
mjpg_streamer -i "input_uvc.so" -o "output_http.so -w ./www"
Открыть поток можно в веб-браузере по адресу http://<IP-адрес_вашего_Pi>:8080. В моём случае это http://192.168.4.210:8080
Вы увидите страницу с вариантами просмотра. Нажмите на ссылку «Stream» или откройте адрес:
http://<raspberry_pi_ip_address>:8080/?action=stream

Установка модуля камеры в порт CSI
Существует несколько модулей камер Raspberry Pi. Первая камера, 5-мегапиксельная, была выпущена в 2013 году, затем в 2016 году появилась 8-мегапиксельная Camera Module 2. Самая новая модель — 12-мегапиксельная Camera Module 3, выпущенная в 2023 году.
Оригинальная 5-мегапиксельная камера (Arducam OV5647) больше не продаётся Raspberry Pi, но её можно найти у других поставщиков. Ниже показан пример такой камеры на Raspi-Zero.

Вот ещё один пример модуля камеры второй версии, в котором установлен 8-мегапиксельный сенсор Sony IMX219:

Любой модуль камеры будет работать на Raspberry Pi, но для этого нужно обновить файл config.txt:
sudo nano /boot/firmware/config.txt
В конец файла, например для камеры 5 МП Arducam OV5647, добавьте строку:
dtoverlay=ov5647,cam0
Для модуля второй версии с 8-мегапиксельной камерой Sony IMX219 добавьте:
dtoverlay=imx219,cam0
Сохраните файл (CTRL+O, [ENTER], CTRL+X) и перезагрузите Raspi:
sudo reboot
После загрузки можно проверить, видна ли камера в системе:
libcamera-hello --list-cameras


libcamera — это библиотека с открытым исходным кодом, которая напрямую поддерживает камерные системы в Linux на процессорах Arm. Она сводит к минимуму проприетарный код, работающий на GPU Broadcom.
Сделаем тестовый JPEG-снимок с разрешением и сохраним его в файл test_cli_camera.jpg:
rpicam-jpeg --output test_cli_camera.jpg --width 640 --height 480
Чтобы убедиться, что файл сохранён, используйте ls -l: команда выводит содержимое текущего каталога в длинном формате. Как и раньше, для просмотра изображения можно использовать scp:

Удалённый запуск рабочего стола Raspi
Мы в основном работали с Raspberry Pi через терминал по SSH, но можно получить удалённый доступ ко всей графической среде рабочего стола, если установлена полная версия Raspberry Pi OS (например, Raspberry Pi OS (64-bit)). Это особенно полезно для задач, где нужен визуальный интерфейс. Чтобы включить такую возможность, нужно настроить сервер VNC (Virtual Network Computing) на Raspberry Pi. Вот как это сделать:
-
Включите сервер VNC:
- Подключитесь к Raspberry Pi по SSH.
- Запустите инструмент настройки Raspberry Pi, введя команду:
sudo raspi-config - Перейдите к пункту
Interface Options, используя стрелки.

- Выберите
VNCиYes, чтобы включить сервер VNC.

- Выйдите из инструмента настройки, сохранив изменения при запросе.

-
Установите VNC Viewer на свой компьютер:
- Скачайте и установите приложение VNC-клиента на основной компьютер. Популярные варианты: RealVNC Viewer, TightVNC или VNC Viewer от RealVNC. Мы будем использовать VNC Viewer от RealVNC.
-
Убедившись, что приложение установлено, уточните IP-адрес Raspi. Например, в терминале можно выполнить:
hostname -I
-
Подключитесь к Raspberry Pi:
- Откройте приложение VNC Viewer.

- Введите IP-адрес и имя хоста Raspberry Pi.
- По запросу введите имя пользователя и пароль Raspberry Pi.

-
Рабочий стол Raspberry Pi 5 должен появиться на мониторе вашего компьютера.

-
При необходимости настройте экран:
- После подключения настройте разрешение экрана для комфортной работы. Это можно сделать в настройках рабочего стола Raspberry Pi или изменив файл config.txt.
- Сделаем это через настройки рабочего стола. Откройте меню (значок Raspberry в левом верхнем углу) и выберите наилучшее разрешение для вашего монитора:

Обновление системы и установка программ
-
Обновите систему:
sudo apt update && sudo apt upgrade -y -
Установите необходимые программы:
sudo apt install python3-pip -y -
Включите pip для Python-проектов:
sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED
Особенности разных моделей
Raspberry Pi Zero (Raspi-Zero)
- Ограниченная вычислительная мощность, лучше подходит для лёгких проектов.
- Лучше работать без монитора, через SSH, чтобы экономить ресурсы.
- Для задач с большим расходом памяти имеет смысл увеличить объём swap-памяти.
- Подходит для лабораторных работ по классификации изображений и обнаружению объектов, но не для LLM (SLM).
Raspberry Pi 4 или 5 (Raspi-4 или Raspi-5)
- Подходят для более требовательных проектов, включая ИИ и машинное обучение.
- Могут плавно работать со всей графической средой рабочего стола.
- Raspi-4 подходит для лабораторных работ по классификации изображений и обнаружению объектов, но плохо работает с LLM (SLM).
- Для Raspi-5 при интенсивной нагрузке, например в лабораторной работе по LLM (SLM), стоит использовать активный кулер для контроля температуры.
Подбирайте требования проекта под конкретную модель Raspberry Pi. Raspi-Zero хорошо подходит для проектов с низким энергопотреблением и ограниченным местом, а Raspi-4 или 5 лучше подходят для более ресурсоёмких задач.
Визуально-языковые модели (VLM)

Промпт DALL·E: установка Raspberry Pi для задач компьютерного зрения. На изображении Raspberry Pi подключена к камере, а вокруг неё визуально представлены различные задачи: обнаружение объектов, описание изображений, сегментация и визуальная привязка (visual grounding). Raspberry Pi стоит на столе, на дисплее видны ограничивающие рамки и аннотации, связанные с этими задачами. Фон — домашняя мастерская с инструментами и устройствами, которыми обычно пользуются разработчики и энтузиасты.
Введение
В этой практической работе мы будем последовательно исследовать прикладной ИИ на периферии: от базовой настройки Florence-2, передовой базовой модели компьютерного зрения от Microsoft, до продвинутых реализаций на таких устройствах, как Raspberry Pi. Мы научимся использовать визуально-языковые модели (Vision-Language Models, VLM) для таких задач, как описание изображений (captioning), обнаружение объектов, визуальная привязка (grounding), сегментация и OCR на Raspberry Pi.
Почему Florence-2 на периферии?
Florence-2 — визуально-языковая модель, которую Microsoft выпустила с открытым исходным кодом по лицензии MIT. Она заметно продвигает такие модели, сочетая лёгкую архитектуру с надёжными возможностями. Благодаря обучению на огромном наборе данных FLD-5B, который содержит 126 миллионов изображений и 5,4 миллиарда визуальных аннотаций, модель достигает производительности, сопоставимой с более крупными моделями. Это делает Florence-2 идеальной для развёртывания на периферии, где питание и вычислительные ресурсы ограничены.
В этом руководстве мы разберём, как использовать Florence-2 для приложений компьютерного зрения в реальном времени, таких как:
- описание изображений (Image captioning);
- обнаружение объектов;
- сегментация;
- визуальная привязка (Visual grounding).
Визуальная привязка (visual grounding) связывает текстовые описания с конкретными областями изображения. Это позволяет модели понять, где на изображении находятся объекты или сущности, описанные в запросе. Например, если запрос — «красная машина», модель найдёт и выделит область, где на изображении находится красная машина. Визуальная привязка полезна там, где нужно точное соответствие между текстом и визуальным содержимым, например во взаимодействии человека с компьютером, в аннотировании изображений и интерактивных системах ИИ.
В руководстве мы пройдём следующие этапы:
- настройка Florence-2 на Raspberry Pi;
- запуск задач инференса, таких как обнаружение объектов и описание изображений;
- оптимизация модели для максимальной производительности на периферийном устройстве;
- изучение практических приложений в реальных условиях с дообучением (fine-tuning).
Архитектура модели Florence-2
Florence-2 использует унифицированное представление на основе запросов для решения различных визуально-языковых задач. Архитектура модели состоит из двух основных компонентов: кодировщика изображений и мультимодального кодировщика-декодера на основе трансформера.
-
Кодировщик изображений: основан на архитектуре DaViT (Dual Attention Vision Transformers). Он преобразует входные изображения в последовательность эмбеддингов визуальных токенов. Эти эмбеддинги служат базовым представлением визуального содержимого и отражают как пространственную, так и контекстную информацию об изображении.
-
Мультимодальный кодировщик-декодер на основе трансформера: ядро Florence-2 объединяет эмбеддинги визуальных токенов из кодировщика изображений с текстовыми эмбеддингами, которые создаёт модель, похожая на BERT. Такое объединение позволяет модели одновременно обрабатывать визуальные и текстовые входные данные, обеспечивая единый подход к таким задачам, как описание изображений, обнаружение объектов и сегментация.
Обучение на обширном наборе данных FLD-5B гарантирует, что модель эффективно справляется с разнообразными задачами зрения без модификаций под каждую задачу. Florence-2 использует текстовые запросы, чтобы активировать конкретные задачи, что делает её очень гибкой и способной к обобщению без дополнительного обучения (zero-shot). Для таких задач, как обнаружение объектов или визуальная привязка, модель использует дополнительные токены местоположения, чтобы обозначать области изображения, что обеспечивает точное понимание пространственных отношений.
Компактная архитектура Florence-2 и инновационный подход к обучению позволяют ей точно выполнять задачи компьютерного зрения даже на устройствах с ограниченными ресурсами, таких как Raspberry Pi.
Обзор технологии
Florence-2 содержит несколько инновационных особенностей, которые выделяют её среди других моделей:
Архитектура

- Лёгкий дизайн: доступны два варианта:
- Florence-2-Base: 232 миллиона параметров;
- Florence-2-Large: 771 миллион параметров.
- Унифицированное представление: решает несколько задач компьютерного зрения одной архитектурой.
- Визуальный кодировщик DaViT: преобразует изображения в эмбеддинги визуальных токенов.
- Мультимодальный кодировщик-декодер на основе трансформера: обрабатывает объединённые визуальные и текстовые эмбеддинги.
Обучающий набор данных (FLD-5B)

- 126 миллионов уникальных изображений;
- 5,4 миллиарда комплексных аннотаций, включая:
- 500 млн текстовых аннотаций;
- 1,3 млрд аннотаций «регион — текст»;
- 3,6 млрд аннотаций «текстовая фраза — регион»;
- автоматический конвейер аннотирования с использованием специализированных моделей;
- итеративный процесс уточнения для получения качественных меток.
Ключевые возможности
Florence-2 показывает отличные результаты во множестве задач компьютерного зрения:
Работа без дообучения (zero-shot)
- Описание изображений: показатель CIDEr 135,6 на COCO;
- Визуальная привязка: 84,4 % recall@1 на Flickr30k;
- Обнаружение объектов: 37,5 mAP на COCO val2017;
- Выражения-ссылки (referring expression): 67,0 % точности на RefCOCO.
Работа после дообучения (fine-tuned)
- Конкурентоспособна со специализированными моделями, несмотря на меньший размер;
- Превосходит более крупные модели в отдельных тестах;
- Эффективно адаптируется к новым задачам.
Практические применения
Florence-2 можно применять во многих областях:
- Работа с контентом
- Автоматическое описание изображений для доступности;
- Модерация визуального контента;
- Управление медиаактивами.
- Электронная коммерция
- Анализ изображений товаров;
- Визуальный поиск;
- Автоматическая разметка товаров тегами.
- Здравоохранение
- Анализ медицинских изображений;
- Помощь в диагностике;
- Обработка данных исследований.
- Безопасность и наблюдение
- Обнаружение объектов и отслеживание;
- Обнаружение аномалий;
- Понимание сцены.
Сравнение Florence-2 с другими VLM
Florence-2 выделяется среди других визуально-языковых моделей впечатляющими возможностями работы без дообучения (zero-shot). В отличие от моделей вроде Google PaliGemma, которые полагаются на обширное дообучение, чтобы адаптироваться к разным задачам, Florence-2 работает сразу после установки, как мы увидим в этой лабораторной работе. Она может конкурировать и с более крупными моделями, такими как GPT-4V и Flamingo, у которых гораздо больше параметров, но которые лишь иногда показывают результаты лучше, чем Florence-2. Например, Florence-2 достигает лучших результатов без дообучения, чем Kosmos-2, хотя у той более чем вдвое больше параметров.
В тестах Florence-2 показала выдающиеся результаты в таких задачах, как описание изображений COCO и понимание выражений-ссылок (referring expression comprehension). Она превзошла модели PolyFormer и UNINEXT в задачах обнаружения объектов и сегментации на наборе данных COCO. Это высококонкурентный выбор для реальных приложений, где важны и производительность, и эффективность использования ресурсов.
Настройка и установка
Наш выбор периферийного устройства — Raspberry Pi 5 (Raspi-5). Эта надёжная платформа оснащена процессором Broadcom BCM2712 — 64-битным четырёхъядерным Arm Cortex-A76 с частотой 2,4 ГГц, с криптографическим расширением и улучшенным кэшированием. В ней есть GPU VideoCore VII, два выхода HDMI® 4Kp60 с HDR и декодер HEVC 4Kp60. Варианты памяти — 4 ГБ и 8 ГБ высокоскоростной LPDDR4X SDRAM; для запуска Florence-2 мы выбираем 8 ГБ. Также есть расширяемое хранилище через слот microSD и интерфейс PCIe 2.0 для быстрой периферии, например твердотельных накопителей M.2 SSD.
Для реальных приложений SSD — лучший выбор, чем SD-карты.
Для этой лабораторной работы мы рекомендуем установить Active Cooler — специальное навесное охлаждение для Raspberry Pi 5 (Raspi-5). Оно сочетает алюминиевый радиатор с вентилятором, управляемым температурой, чтобы Raspi-5 комфортно работал под высокой нагрузкой, например при запуске Florence-2.

Настройка окружения
Чтобы запустить Microsoft Florence-2 на Raspberry Pi 5, понадобится несколько библиотек:
-
- Florence-2 использует библиотеку
transformersиз Hugging Face для загрузки модели и инференса. Она предоставляет архитектуру для работы с предобученными визуально-языковыми моделями, что упрощает такие задачи, как описание изображений, обнаружение объектов и другие. По сути,transformersпомогает взаимодействовать с моделью, обрабатывать входные запросы и получать результаты.
- Florence-2 использует библиотеку
-
PyTorch:
- PyTorch — фреймворк глубокого обучения, который предоставляет инфраструктуру для работы Florence-2: тензорные операции, ускорение на GPU (если он доступен), функции обучения и инференса моделей. Florence-2 обучена в PyTorch, и нам нужны её функции, слои и возможности вычислений, чтобы выполнять инференс на Raspberry Pi.
-
Timm (модели компьютерного зрения для PyTorch):
- Florence-2 использует
timmдля доступа к эффективным реализациям моделей зрения и предобученным весам. В частности, библиотекаtimmприменяется для кодировщика изображений Florence-2, в первую очередь для работы с архитектурой DaViT. Она предоставляет определения моделей и оптимизированный код для распространённых задач зрения и позволяет легко подключать различные лёгкие магистрали (backbone), подходящие для периферийных устройств.
- Florence-2 использует
-
Einops:
Einops— библиотека для гибких и мощных тензорных операций. Она упрощает изменение формы тензоров и работу с их размерностями, что особенно важно для мультимодальной обработки в Florence-2. Визуально-языковые модели, такие как Florence-2, часто должны переупорядочивать данные изображений, текстовые и визуальные эмбеддинги, чтобы корректно выровнять их для блоков трансформера, иeinopsупрощает эти сложные операции, делая код более читаемым и лаконичным.
Коротко, эти библиотеки обеспечивают различные ключевые компоненты Florence-2:
- Transformers и PyTorch нужны для загрузки модели и запуска инференса.
- Timm используется для доступа к кодировщику изображений и его эффективной реализации.
- Einops помогает изменять форму данных, упрощая объединение визуальных и текстовых признаков.
Все эти компоненты работают вместе, чтобы Florence-2 работала на нашем Raspberry Pi без сбоев и выполняла сложные визуально-языковые задачи достаточно быстро.
Поскольку на Raspberry Pi уже установлена ОС, подключимся к ней с другого компьютера по SSH:
ssh username@raspi-5.local
И проверим назначенный ей IP-адрес:
hostname -I
192.168.4.209

Обновление Raspberry Pi
Сначала убедитесь, что Raspberry Pi обновлён:
sudo apt update
sudo apt upgrade -y
Первоначальная настройка для работы с PIP:
sudo apt install python3-pip
sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED
pip3 install --upgrade pip
Установка зависимостей
sudo apt-get install libjpeg-dev libopenblas-dev libopenmpi-dev \
libomp-dev
Настроим и активируем виртуальное окружение для работы с Florence-2:
python3 -m venv ~/florence
source ~/florence/bin/activate
Установка PyTorch
pip3 install setuptools numpy Cython
pip3 install requests
pip3 install torch torchvision \
--index-url https://download.pytorch.org/whl/cpu
pip3 install torchaudio \
--index-url https://download.pytorch.org/whl/cpu
Проверим, что PyTorch установлен корректно:

Установка Transformers, Timm и Einops:
pip3 install transformers
pip3 install timm einops
Установка модели:
pip3 install autodistill-florence-2
Jupyter Notebook и библиотеки Python
Для запуска и тестирования наших Python-скриптов можно установить Jupyter Notebook.
pip3 install jupyter
pip3 install numpy Pillow matplotlib
jupyter notebook --generate-config
Тестирование установки
Запустим Jupyter Notebook на удалённом компьютере:
jupyter notebook --ip=192.168.4.209 --no-browser
Выполнив эту команду в терминале SSH, мы увидим локальный URL-адрес, по которому можно открыть блокнот:

Блокнот с кодом для этого первого теста можно найти в репозитории лабораторных работ на GitHub:
Получить доступ к блокноту с удалённого компьютера можно, введя в веб-браузере IP-адрес Raspberry Pi и токен из терминала (скопируйте весь URL из терминала).
На домашней странице создайте новый блокнот Python 3 (ipykernel) и скопируйте и вставьте пример кода с Hugging Face Hub.
Код предназначен для запуска Florence-2 на заданном изображении для обнаружения объектов. Он загружает модель, обрабатывает изображение и запрос, а затем генерирует ответ, который находит и описывает объекты на изображении.
- Процессор помогает подготовить текстовые и графические входные данные.
- Модель принимает обработанные входные данные и генерирует осмысленный ответ.
- Постобработка приводит результат к более понятному виду, например к ограничивающим рамкам найденных объектов.
Этот процесс использует универсальность Florence-2 для решения визуально-языковых задач и эффективно реализован с помощью PyTorch, Transformers и связанных инструментов обработки изображений.
import requests
from PIL import Image
import torch
from transformers import AutoProcessor, AutoModelForCausalLM
device = "cuda:0" if torch.cuda.is_available() else "cpu"
torch_dtype = (
torch.float16 if torch.cuda.is_available() else torch.float32
)
model = AutoModelForCausalLM.from_pretrained(
"microsoft/Florence-2-base",
torch_dtype=torch_dtype,
trust_remote_code=True,
).to(device)
processor = AutoProcessor.from_pretrained(
"microsoft/Florence-2-base", trust_remote_code=True
)
prompt = "<OD>"
url = (
"https://huggingface.co/datasets/huggingface/"
"documentation-images/resolve/main/transformers/"
"tasks/car.jpg?download=true"
)
image = Image.open(requests.get(url, stream=True).raw)
inputs = processor(text=prompt, images=image, return_tensors="pt").to(
device, torch_dtype
)
generated_ids = model.generate(
input_ids=inputs["input_ids"],
pixel_values=inputs["pixel_values"],
max_new_tokens=1024,
do_sample=False,
num_beams=3,
)
generated_text = processor.batch_decode(
generated_ids, skip_special_tokens=False
)[0]
parsed_answer = processor.post_process_generation(
generated_text,
task="<OD>",
image_size=(image.width, image.height),
)
print(parsed_answer)
Разберём приведённый код по шагам:
Импорт необходимых библиотек
import requests
from PIL import Image
import torch
from transformers import AutoProcessor, AutoModelForCausalLM
- requests: используется для HTTP-запросов. В данном случае библиотека загружает изображение по URL.
- PIL (Pillow): предоставляет инструменты для работы с изображениями. Здесь она используется для открытия загруженного изображения.
- torch: PyTorch импортируется для работы с тензорами и определения доступности оборудования (CPU или GPU).
- transformers: модуль даёт удобный доступ к Florence-2 через
AutoProcessorиAutoModelForCausalLM, которые загружают предобученные модели и обрабатывают входные данные.
Определение устройства и типа данных
device = "cuda:0" if torch.cuda.is_available() else "cpu"
torch_dtype = (
torch.float16 if torch.cuda.is_available() else torch.float32
)
- Настройка устройства: код проверяет, доступен ли GPU с поддержкой CUDA (
torch.cuda.is_available()). Если GPU есть, устройство задаётся как «cuda:0», иначе используется"cpu"(наш случай). - Тип данных: если GPU доступен, выбирается
torch.float16— половинная точность, которая ускоряет обработку и снижает расход памяти. На CPU по умолчанию используетсяtorch.float32для совместимости.
Загрузка модели и процессора
model = AutoModelForCausalLM.from_pretrained(
"microsoft/Florence-2-base",
torch_dtype=torch_dtype,
trust_remote_code=True,
).to(device)
processor = AutoProcessor.from_pretrained(
"microsoft/Florence-2-base", trust_remote_code=True
)
-
Инициализация модели:
AutoModelForCausalLM.from_pretrained()загружает предобученную модель Florence-2 из репозитория Microsoft на Hugging Face.torch_dtypeзадаётся в соответствии с доступным оборудованием (GPU или CPU), аtrust_remote_code=Trueразрешает использовать пользовательский код, который может поставляться вместе с моделью..to(device)переносит модель на нужное устройство (CPU или GPU). В нашем случае будет выбранCPU.
-
Инициализация процессора:
AutoProcessor.from_pretrained()загружает процессор Florence-2. Процессор преобразует текстовые и графические входные данные в формат, с которым может работать модель (например, кодирует текст и нормализует изображения).
Определение запроса
prompt = "<OD>"
- Запрос: строка
"<OD>"используется как запрос. Это обозначение задачи «Object Detection» (обнаружение объектов), которое указывает модели найти объекты на изображении.
Загрузка изображения
url = (
"https://huggingface.co/datasets/huggingface/"
"documentation-images/resolve/main/transformers/"
"tasks/car.jpg?download=true"
)
image = Image.open(requests.get(url, stream=True).raw)
- Загрузка изображения: функция
requests.get()получает изображение по заданному URL. Параметрstream=Trueозначает, что изображение загружается потоком, а не целиком сразу. - Открытие изображения:
Image.open()открывает изображение, чтобы модель могла его обработать.
Обработка входных данных
inputs = processor(text=prompt, images=image, return_tensors="pt").to(
device, torch_dtype
)
- Обработка входных данных: функция
processor()обрабатывает текст (prompt) и изображение (image). Аргументreturn_tensors="pt"преобразует результат в тензоры PyTorch, которые необходимы для подачи данных в модель. - Перенос входных данных на устройство:
.to(device, torch_dtype)переносит входные данные на нужное устройство (CPU или GPU) и задаёт соответствующий тип данных.
Генерация результата
generated_ids = model.generate(
input_ids=inputs["input_ids"],
pixel_values=inputs["pixel_values"],
max_new_tokens=1024,
do_sample=False,
num_beams=3,
)
- Генерация моделью:
model.generate()создаёт результат на основе входных данных.input_ids: токенизированная форма запроса.pixel_values: обработанные данные изображения.max_new_tokens=1024: задаёт максимальное число новых токенов в ответе. Это ограничивает длину ответа.do_sample=False: отключает семплирование; вместо него используются детерминированные методы (поиск лучом, beam search).num_beams=3: включает поиск лучом с тремя лучами, что улучшает качество результата, рассматривая несколько вариантов во время генерации.
Декодирование сгенерированного текста
generated_text = processor.batch_decode(
generated_ids, skip_special_tokens=False
)[0]
- Пакетное декодирование:
processor.batch_decode()декодирует сгенерированные ID (токены) в читаемый текст. Параметрskip_special_tokens=Falseозначает, что результат будет включать специальные токены, если они являются частью ответа.
Постобработка результата
parsed_answer = processor.post_process_generation(
generated_text,
task="<OD>",
image_size=(image.width, image.height),
)
- Постобработка:
processor.post_process_generation()дополнительно обрабатывает сгенерированный текст, интерпретируя его в соответствии с задачей ("<OD>"для обнаружения объектов) и размером изображения. - Функция извлекает из текста конкретную информацию, например ограничивающие рамки обнаруженных объектов, что делает результат полезнее для задач зрения.
Вывод результата
print(parsed_answer)
- Наконец,
print(parsed_answer)выводит результат, который может включать данные обнаружения объектов: координаты ограничивающих рамок и метки найденных на изображении объектов.
Результат
Запустив код, получаем в качестве Parsed Answer:
[{'<OD>': {
'bboxes': [
[34.23999786376953, 160.0800018310547, 597.4400024414062],
[371.7599792480469, 272.32000732421875, 241.67999267578125],
[303.67999267578125, 247.4399871826172, 454.0799865722656],
[276.7200012207031, 553.9199829101562, 370.79998779296875],
[96.31999969482422, 280.55999755859375, 198.0800018310547],
[371.2799987792969]
],
'labels': ['car', 'door handle', 'wheel', 'wheel']
}}]
Сначала посмотрим на изображение:
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 8))
plt.imshow(image)
plt.axis("off")
plt.show()

По результату обнаружения объектов видим, что:
'labels': ['car', 'door handle', 'wheel', 'wheel']
Похоже, что обнаружено как минимум несколько объектов. Мы также можем написать код, который рисует ограничивающие рамки найденных объектов:
def plot_bbox(image, data):
# Создаём фигуру и оси
fig, ax = plt.subplots()
# Отображаем изображение
ax.imshow(image)
# Рисуем каждую ограничивающую рамку
for bbox, label in zip(data["bboxes"], data["labels"]):
# Распаковываем координаты рамки
x1, y1, x2, y2 = bbox
# Создаём прямоугольник
rect = patches.Rectangle(
(x1, y1),
x2 - x1,
y2 - y1,
linewidth=1,
edgecolor="r",
facecolor="none",
)
# Добавляем прямоугольник на оси
ax.add_patch(rect)
# Подписываем метку
plt.text(
x1,
y1,
label,
color="white",
fontsize=8,
bbox=dict(facecolor="red", alpha=0.5),
)
# Убираем деления и подписи осей
ax.axis("off")
# Показываем график
plt.show()
Box (x0, y0, x1, y1): токены местоположения соответствуют левому верхнему и правому нижнему углам рамки.
Запустим:
plot_bbox(image, parsed_answer['<OD>'])
Получаем:

Задачи Florence-2
Florence-2 создана для выполнения множества задач компьютерного зрения и визуально-языковых задач с помощью запросов (prompts). Эти задачи активируются заданием конкретного текстового запроса, как мы видели в случае <OD> (обнаружение объектов).
Универсальность Florence-2 основана на комбинировании таких запросов: меняя запрос, можно направлять поведение модели на выполнение конкретной задачи зрения. Смена запроса позволяет адаптировать Florence-2 к разным задачам без модификаций архитектуры. Эта возможность напрямую следует из унифицированной архитектуры модели и масштабного мультизадачного обучения на наборе данных FLD-5B.
Вот некоторые ключевые задачи, которые может решать Florence-2, с примерами запросов:
Обнаружение объектов (OD)
- Запрос:
"<OD>" - Описание: находит объекты на изображении и выдаёт ограничивающую рамку для каждого из них. Эта задача полезна для визуального контроля, систем наблюдения и общего распознавания объектов.
Описание изображения (Image Captioning)
- Запрос:
"<CAPTION>" - Описание: создаёт текстовое описание входного изображения. Эта задача помогает модели описать происходящее на изображении, давая понятную человеку подпись для понимания содержимого.
Подробное описание (Detailed Captioning)
- Запрос:
"<DETAILED_CAPTION>" - Описание: создаёт более подробную подпись с более тонкой информацией о сцене, например о присутствующих объектах и их взаимосвязях.
Визуальная привязка (Visual Grounding)
- Запрос:
"<CAPTION_TO_PHRASE_GROUNDING>" - Описание: связывает текстовое описание с конкретными областями изображения. Например, по запросу «a green car» (зелёная машина) модель выделяет, где на изображении находится зелёная машина. Это полезно для взаимодействия человека с компьютером, когда нужно найти конкретные объекты по тексту.
Сегментация (Segmentation)
- Запрос:
"<REFERRING_EXPRESSION_SEGMENTATION>" - Описание: выполняет сегментацию по выражению-ссылке, например «the blue cup» (синяя чашка). Модель находит и выделяет область, содержащую объект, упомянутый в запросе (все относящиеся к нему пиксели).
Плотное описание областей (Dense Region Captioning)
- Запрос:
"<DENSE_REGION_CAPTION>" - Описание: создаёт подписи для нескольких областей изображения и даёт подробный разбор всех видимых участков, включая разные объекты и их взаимосвязи.
OCR с областями (OCR with Region)
- Запрос:
"<OCR_WITH_REGION>" - Описание: выполняет оптическое распознавание символов (OCR) на изображении и выдаёт ограничивающие рамки для найденного текста. Это полезно для извлечения текстовой информации и определения её положения на изображении, например для чтения надписей, этикеток или других текстовых элементов.
Привязка фраз для конкретных выражений (Phrase Grounding for Specific Expressions)
- Запрос:
"<CAPTION_TO_PHRASE_GROUNDING>"вместе с конкретным выражением, например"a wine glass"(бокал вина). - Описание: находит на изображении область, соответствующую конкретной текстовой фразе. Эта задача позволяет выделять определённые объекты или элементы по слову или ключевому слову в запросе.
Обнаружение объектов с открытым словарём (Open Vocabulary Object Detection)
- Запрос:
"<OPEN_VOCABULARY_OD>" - Описание: модель может обнаруживать объекты без ограничения заранее заданным списком классов, поэтому она помогает распознавать более широкий круг предметов на основе общего визуального понимания.
Исследование задач компьютерного зрения и визуально-языковых задач
Для исследования все примеры кода можно найти на GitHub:
Загрузим несколько изображений, созданных в DALL·E, на Raspi-5 (для этого можно использовать FileZilla). Изображения сохраним в подкаталог images:
dogs_cats = Image.open("./images/dogs-cats.jpg")
table = Image.open("./images/table.jpg")

Создадим функцию, которая облегчит исследование и позволит отслеживать задержку модели для разных задач:
def run_example(task_prompt, text_input=None, image=None):
start_time = time.perf_counter() # Запускаем таймер
if text_input is None:
prompt = task_prompt
else:
prompt = task_prompt + text_input
inputs = processor(
text=prompt, images=image, return_tensors="pt"
).to(device)
generated_ids = model.generate(
input_ids=inputs["input_ids"],
pixel_values=inputs["pixel_values"],
max_new_tokens=1024,
early_stopping=False,
do_sample=False,
num_beams=3,
)
generated_text = processor.batch_decode(
generated_ids, skip_special_tokens=False
)[0]
parsed_answer = processor.post_process_generation(
generated_text,
task=task_prompt,
image_size=(image.width, image.height),
)
end_time = time.perf_counter() # Останавливаем таймер
elapsed_time = end_time - start_time # Вычисляем затраченное время
print(
f" \n[INFO] ==> Florence-2-base ({task_prompt}), \
took {elapsed_time:.1f} seconds to execute.\n"
)
return parsed_answer
Подпись (Caption)
1. Собаки и кошки
run_example(task_prompt="<CAPTION>", image=dogs_cats)
[INFO] ==> Florence-2-base (<CAPTION>), \
took 16.1 seconds to execute.
{'<CAPTION>': 'A group of dogs and cats sitting in a garden.'}
2. Стол
run_example(task_prompt="<CAPTION>", image=table)
[INFO] ==> Florence-2-base (<CAPTION>), \
took 16.5 seconds to execute.
{'<CAPTION>': 'A wooden table topped with a plate of fruit \
and a glass of wine.'}
Подробная подпись (Detailed Caption)
1. Собаки и кошки
run_example(task_prompt="<DETAILED_CAPTION>", image=dogs_cats)
[INFO] ==> Florence-2-base (<DETAILED_CAPTION>), \
took 25.5 seconds to execute.
{'<DETAILED_CAPTION>': 'The image shows a group of cats and \
dogs sitting on top of a lush green field, surrounded by plants \
with flowers, trees, and a house in the background. The sky is \
visible above them, creating a peaceful atmosphere.'}
2. Стол
run_example(task_prompt="<DETAILED_CAPTION>", image=table)
[INFO] ==> Florence-2-base (<DETAILED_CAPTION>), \
took 26.8 seconds to execute.
{'<DETAILED_CAPTION>': 'The image shows a wooden table with \
a bottle of wine and a glass of wine on it, surrounded by \
a variety of fruits such as apples, oranges, and grapes. \
In the background, there are chairs, plants, trees, and \
a house, all slightly blurred.'}
Более подробная подпись (More Detailed Caption)
1. Собаки и кошки
run_example(task_prompt="<MORE_DETAILED_CAPTION>", image=dogs_cats)
[INFO] ==> Florence-2-base (<MORE_DETAILED_CAPTION>), \
took 49.8 seconds to execute.
{'<MORE_DETAILED_CAPTION>': 'The image shows a group of four \
cats and a dog in a garden. The garden is filled with colorful \
flowers and plants, and there is a pathway leading up to \
a house in the background. The main focus of the image is \
a large German Shepherd dog standing on the left side of \
the garden, with its tongue hanging out and its mouth open, \
as if it is panting. On the right side, there are \
two smaller cats, one orange and one gray, sitting on the \
grass. In the background, there is another golden retriever \
dog sitting and looking at the camera. The sky is blue and \
the sun is shining, creating a warm and inviting atmosphere.'}
2. Стол
run_example(task_prompt="<MORE_DETAILED_CAPTION>", image=table)
[INFO] ==> Florence-2-base (<MORE_DETAILED_CAPTION>), \
took 32.4 seconds to execute.
{'<MORE_DETAILED_CAPTION>': 'The image shows a wooden table \
with a wooden tray on it. On the tray, there are various \
fruits such as grapes, oranges, apples, and grapes. There \
is also a bottle of red wine on the table. The background \
shows a garden with trees and a house. The overall mood \
of the image is peaceful and serene.'}
Заметим, что чем подробнее задача описания, тем выше задержка и вероятность ошибок (например, в ответе «The image shows a group of four cats and a dog in a garden» модель называет четырёх кошек и одну собаку, хотя на изображении было иначе).
Обнаружение объектов (Object Detection)
Ту же функцию можно использовать для обнаружения объектов с запросом <OD>.
task_prompt = "<OD>"
results = run_example(task_prompt, image=dogs_cats)
print(results)
Посмотрим результат:
[INFO] ==> Florence-2-base (<OD>), took 20.9 seconds to execute.
{'<OD>': {'bboxes': [
[737.79, 571.90, 1022.46, 980.48],
[0.51, 593.40, 211.45, 991.74],
[445.95, 721.40, 680.44, 850.43],
[39.42, 91.64, 491.00, 933.37],
[570.88, 184.83, 974.33, 782.84]
],
'labels': ['cat', 'cat', 'cat', 'dog', 'dog']
}}
По меткам ['cat', 'cat', 'cat', 'dog', 'dog'] видно, что основные объекты на изображении распознаны. Применим функцию, которую мы использовали ранее, чтобы нарисовать ограничивающие рамки:
plot_bbox(dogs_cats, results["<OD>"])

Сделаем то же самое для изображения стола:
task_prompt = "<OD>"
results = run_example(task_prompt, image=table)
plot_bbox(table, results["<OD>"])
[INFO] ==> Florence-2-base (<OD>), took 40.8 seconds to execute.

Плотное описание областей (Dense Region Caption)
Классическое обнаружение объектов можно сочетать с подписями для конкретных подобластей изображения:
task_prompt = "<DENSE_REGION_CAPTION>"
results = run_example(task_prompt, image=dogs_cats)
plot_bbox(dogs_cats, results["<DENSE_REGION_CAPTION>"])
results = run_example(task_prompt, image=table)
plot_bbox(table, results["<DENSE_REGION_CAPTION>"])

Привязка подписи к фразе (Caption to Phrase Grounding)
В этой задаче можно передать подпись, например «a wine glass» (бокал вина), «a wine bottle» (бутылка вина) или «a half orange» (половина апельсина), и Florence-2 найдёт объект на изображении:
task_prompt = "<CAPTION_TO_PHRASE_GROUNDING>"
results = run_example(
task_prompt, text_input="a wine bottle", image=table
)
plot_bbox(table, results["<CAPTION_TO_PHRASE_GROUNDING>"])
results = run_example(
task_prompt, text_input="a wine glass", image=table
)
plot_bbox(table, results["<CAPTION_TO_PHRASE_GROUNDING>"])
results = run_example(
task_prompt, text_input="a half orange", image=table
)
plot_bbox(table, results["<CAPTION_TO_PHRASE_GROUNDING>"])

[INFO] ==> Florence-2-base (<CAPTION_TO_PHRASE_GROUNDING>), \
took 15.7 seconds to execute
each task.
Каскадные задачи (Cascade Tasks)
Также можно передать подпись изображения как входной текст, чтобы заставить Florence-2 найти больше объектов:
task_prompt = "<CAPTION>"
results = run_example(task_prompt, image=dogs_cats)
text_input = results[task_prompt]
task_prompt = "<CAPTION_TO_PHRASE_GROUNDING>"
results = run_example(task_prompt, text_input, image=dogs_cats)
plot_bbox(dogs_cats, results["<CAPTION_TO_PHRASE_GROUNDING>"])
Меняя задачу (task_prompt) между <CAPTION>, <DETAILED_CAPTION> и <MORE_DETAILED_CAPTION>, мы получим больше объектов на изображении.

Обнаружение с открытым словарём (Open Vocabulary Detection)
<OPEN_VOCABULARY_DETECTION> позволяет Florence-2 находить распознаваемые объекты на изображении без опоры на заранее заданный список категорий. Это универсальный инструмент для поиска предметов, которые могли не быть явно размечены во время обучения. В отличие от <CAPTION_TO_PHRASE_GROUNDING>, которому нужна конкретная текстовая фраза, чтобы найти и выделить определённый объект, <OPEN_VOCABULARY_DETECTION> выполняет широкий просмотр и находит и классифицирует все присутствующие объекты.
Поэтому <OPEN_VOCABULARY_DETECTION> особенно полезен там, где нужен общий обзор всего, что есть на изображении, без предварительных знаний о том, что там может быть. Передайте текст с описанием конкретных объектов, которые ещё не были обнаружены, и модель их найдёт. Например:
task_prompt = "<OPEN_VOCABULARY_DETECTION>"
# Описания оставлены на английском: модель обучена на английских запросах
text = [
"a house",
"a tree",
"a standing cat at the left",
"a sleeping cat on the ground",
"a standing cat at the right",
"a yellow cat",
]
for txt in text:
results = run_example(
task_prompt, text_input=txt, image=dogs_cats
)
bbox_results = convert_to_od_format(
results["<OPEN_VOCABULARY_DETECTION>"]
)
plot_bbox(dogs_cats, bbox_results)

[INFO] ==> Florence-2-base (<OPEN_VOCABULARY_DETECTION>), \
took 15.1 seconds to execute
each task.
Примечание: попытка использовать Florence-2 для поиска объектов, которых на изображении нет, может приводить к ошибкам (примеры см. в блокноте).
Сегментация по выражению-ссылке (Referring expression segmentation)
Также можно сегментировать конкретный объект на изображении и задать его описание (подпись), например «a wine bottle» (бутылка вина) на изображении со столом или «a German Shepherd» (немецкая овчарка) на изображении собак и кошек.
Результат сегментации по выражению-ссылке имеет формат: {'<REFERRING_EXPRESSION_SEGMENTATION>': {'Polygons': [[[polygon]], ...], 'labels': ['', '', ...]}}. Один объект представлен списком полигонов, каждый полигон — [x1, y1, x2, y2, ..., xn, yn].
Полигон (x1, y1, …, xn, yn): токены местоположения представляют вершины полигона по часовой стрелке.
Сначала создадим функцию для отображения сегментации:
from PIL import Image, ImageDraw, ImageFont
import copy
import random
import numpy as np
colormap = [
"blue",
"orange",
"green",
"purple",
"brown",
"pink",
"gray",
"olive",
"cyan",
"red",
"lime",
"indigo",
"violet",
"aqua",
"magenta",
"coral",
"gold",
"tan",
"skyblue",
]
def draw_polygons(image, prediction, fill_mask=False):
"""
Рисует маски сегментации с полигонами на изображении.
Параметры:
- image_path: путь к файлу изображения.
- prediction: словарь с ключами 'polygons' и 'labels'.
'polygons' — список списков, каждый из которых
содержит вершины полигона. 'labels' — список меток,
соответствующих каждому полигону.
- fill_mask: логическое значение, которое указывает, заливать ли
полигоны цветом.
"""
# Загружаем изображение
draw = ImageDraw.Draw(image)
# Задаём коэффициент масштабирования при необходимости (1, если без масштабирования)
scale = 1
# Перебираем полигоны и метки
for polygons, label in zip(
prediction["polygons"], prediction["labels"]
):
color = random.choice(colormap)
fill_color = random.choice(colormap) if fill_mask else None
for _polygon in polygons:
_polygon = np.array(_polygon).reshape(-1, 2)
if len(_polygon) < 3:
print("Invalid polygon:", _polygon)
continue
_polygon = (_polygon * scale).reshape(-1).tolist()
# Рисуем полигон
if fill_mask:
draw.polygon(_polygon, outline=color, fill=fill_color)
else:
draw.polygon(_polygon, outline=color)
# Рисуем текст метки
draw.text(
(_polygon[0] + 8, _polygon[1] + 2), label, fill=color
)
# Сохраняем или выводим изображение
# image.show() # Показать изображение
display(image)
Теперь можно запустить функции:
task_prompt = "<REFERRING_EXPRESSION_SEGMENTATION>"
results = run_example(
task_prompt, text_input="a wine bottle", image=table
)
output_image = copy.deepcopy(table)
draw_polygons(
output_image,
results["<REFERRING_EXPRESSION_SEGMENTATION>"],
fill_mask=True,
)
results = run_example(
task_prompt, text_input="a german shepherd", image=dogs_cats
)
output_image = copy.deepcopy(dogs_cats)
draw_polygons(
output_image,
results["<REFERRING_EXPRESSION_SEGMENTATION>"],
fill_mask=True,
)

[INFO] ==> Florence-2-base
(<REFERRING_EXPRESSION_SEGMENTATION>), took 207.0 seconds
to execute each task.
Сегментация по области (Region to Segmentation)
С помощью этой задачи также можно задать координаты объекта на изображении, чтобы его сегментировать. Формат входных данных: '<loc_x1><loc_y1><loc_x2><loc_y2>', [x1, y1, x2, y2] — квантованные координаты в диапазоне [0, 999].
Например, при запуске кода:
task_prompt = "<CAPTION_TO_PHRASE_GROUNDING>"
results = run_example(
task_prompt, text_input="a half orange", image=table
)
results
Результат был таким:
{'<CAPTION_TO_PHRASE_GROUNDING>': {'bboxes': [[343.552001953125,
689.6640625,
530.9440307617188,
873.9840698242188]],
'labels': ['a half']}}
Используем округлённые координаты из bboxes:
task_prompt = "<REGION_TO_SEGMENTATION>"
results = run_example(
task_prompt,
text_input=("<loc_343><loc_690>" "<loc_531><loc_874>"),
image=table,
)
output_image = copy.deepcopy(table)
draw_polygons(
output_image, results["<REGION_TO_SEGMENTATION>"], fill_mask=True
)
Мы получили сегментацию объекта по этим координатам (задержка: 83 секунды):

Регион в текст (Region to Texts)
Также можно передать регион (координаты) и запросить для него подпись:
task_prompt = "<REGION_TO_CATEGORY>"
results = run_example(
task_prompt,
text_input=("<loc_343><loc_690>" "<loc_531><loc_874>"),
image=table,
)
results
[INFO] ==> Florence-2-base (<REGION_TO_CATEGORY>), \
took 14.3 seconds to execute.
{
'<REGION_TO_CATEGORY>':
'orange<loc_343><loc_690>'
'<loc_531><loc_874>'
}
Модель распознала в этом регионе апельсин. Запросим описание:
task_prompt = "<REGION_TO_DESCRIPTION>"
results = run_example(
task_prompt,
text_input=("<loc_343><loc_690>" "<loc_531><loc_874>"),
image=table,
)
results
[INFO] ==> Florence-2-base (<REGION_TO_DESCRIPTION>), \
took 14.6 seconds to execute.
{
'<REGION_TO_DESCRIPTION>':
'orange<loc_343><loc_690>'
'<loc_531><loc_874>'
}
В данном случае описание не дало больше подробностей, но в другой ситуации могло бы. Попробуйте другой пример.
OCR
С помощью Florence-2 можно выполнить оптическое распознавание символов (OCR) на изображении и получить написанный на нём текст (task_prompt = '<OCR>'), а также ограничивающие рамки (положение) распознанного текста (task_prompt = '<OCR_WITH_REGION>'). Эти задачи помогают извлекать текстовую информацию и находить её на изображениях, например читать вывески, этикетки или другие текстовые элементы.
Загрузим на Raspi афишу доклада из Бразилии. Так мы проверим работу модели на другом языке, здесь на португальском:
flayer = Image.open("./images/embarcados.jpg")
# Выводим изображение
plt.figure(figsize=(8, 8))
plt.imshow(flayer)
plt.axis("off")
# plt.title("Image")
plt.show()

Посмотрим, что модель видит на изображении, с задачей '<MORE_DETAILED_CAPTION>':
[INFO] ==> Florence-2-base (<MORE_DETAILED_CAPTION>), \
took 85.2 seconds to execute.
{'<MORE_DETAILED_CAPTION>': 'The image is a promotional poster \
for an event called "Machine Learning Embarcados" hosted by \
Marcelo Roval. The poster has a black background with white \
text. On the left side of the poster, there is a logo of a \
coffee cup with the text "Café Com Embarcados" above it. \
Below the logo, it says "25 de Setembro as 17th" which \
translates to "25th of September as 17" in English. \n\nOn \
the right side, there are two smaller text boxes with the names \
of the participants and their names. The first text box reads \
"Democratizando a Inteligência Artificial para Paises em \
Desenvolvimento" and the second text box says "Toda \
quarta-feira" which is Portuguese for "Transmissão via in \
Portuguese".\n\nIn the center of the image, there has a photo \
of Marcelo, a man with a beard and glasses, smiling at the \
camera. He is wearing a white hard hat and a white shirt. \
The text boxes are in orange and yellow colors.'}
Описание очень точное. Перейдём к более важным словам с помощью задачи OCR:
task_prompt = "<OCR>"
run_example(task_prompt, image=flayer)
[INFO] ==> Florence-2-base (<OCR>), took 37.7 seconds to execute.
{'<OCR>':
'Machine Learning Café com Embarcado Embarcados '
'Democratizando a Inteligência Artificial para Paises em '
'25 de Setembro às 17h Desenvolvimento Toda quarta-feira '
'Marcelo Roval Professor na UNIFIEI e Transmissão via in '
'Co-Director do TinyML4D'}
Найдём слова на афише:
task_prompt = "<OCR_WITH_REGION>"
results = run_example(task_prompt, image=flayer)
Создадим также функцию, которая рисует ограничивающие рамки вокруг найденных слов:
def draw_ocr_bboxes(image, prediction):
scale = 1
draw = ImageDraw.Draw(image)
bboxes = prediction["quad_boxes"]
labels = prediction["labels"]
for box, label in zip(bboxes, labels):
color = random.choice(colormap)
new_box = (np.array(box) * scale).tolist()
draw.polygon(new_box, width=3, outline=color)
draw.text(
(new_box[0] + 8, new_box[1] + 2),
"{}".format(label),
align="right",
fill=color,
)
display(image)
output_image = copy.deepcopy(flayer)
draw_ocr_bboxes(output_image, results["<OCR_WITH_REGION>"])

Посмотрим найденные слова:
results["<OCR_WITH_REGION>"]["labels"]
'</s>Machine Learning',
'Café',
'com',
'Embarcado',
'Embarcados',
'Democratizando a Inteligência',
'Artificial para Paises em',
'25 de Setembro ás 17h',
'Desenvolvimento',
'Toda quarta-feira',
'Marcelo Roval',
'Professor na UNIFIEI e',
'Transmissão via',
'in',
'Co-Director do TinyML4D']
Сводка по задержкам
Задержка для разных задач Florence-2 на Raspberry Pi (Raspi-5) зависит от сложности задачи:
- Описание изображения (Image Captioning): примерно 16–17 секунд на создание подписи.
- Подробное описание (Detailed Captioning): задержка выросла до 25–27 секунд, так как модель генерирует более тонкие описания сцены.
- Более подробное описание (More Detailed Captioning): около 32–50 секунд; задержка растёт по мере усложнения описания.
- Обнаружение объектов (Object Detection): примерно 20–41 секунда в зависимости от сложности изображения и количества найденных объектов.
- Визуальная привязка (Visual Grounding): около 15–16 секунд на определение местоположения конкретных объектов по текстовым запросам.
- OCR (оптическое распознавание символов): извлечение текста с изображения заняло около 37–38 секунд.
- Сегментация и сегментация по области (Region to Segmentation): эти задачи заметно дольше, около 83–207 секунд в зависимости от сложности и количества областей.
Эти задержки подчёркивают ограничения ресурсов периферийных устройств, таких как Raspberry Pi, и показывают, что модель и окружение нужно оптимизировать для работы в реальном времени.

Сложные задачи могут использовать всю память Raspi-5 объёмом 8 ГБ. Например, на скриншоте выше, снятом во время задачи Florence OD, видно, что четыре ядра CPU работают на полной мощности, а используется более 5 ГБ памяти. Имеет смысл увеличить объём SWAP-памяти до 2 ГБ.
Проверка температуры процессора командой vcgencmd measure_temp показала, что температура может подниматься до +80 °C.
Дообучение (Fine-Tuning)
Как мы видели в этой лабораторной работе, Florence поддерживает множество задач «из коробки», включая описание изображений, обнаружение объектов, OCR и другие. Однако, как и другие предобученные базовые модели, Florence-2 может потребовать знаний конкретной предметной области. Например, ей может понадобиться улучшение на медицинских или спутниковых снимках. В таких случаях необходимо дообучение (fine-tuning) на собственном наборе данных. Руководство Roboflow How to Fine-tune Florence-2 for Object Detection Tasks показывает, как дообучить Florence-2 на наборах данных для обнаружения объектов, чтобы повысить качество модели для вашей задачи.
Следуя этому руководству, можно дообучить модель Florence-2 обнаруживать коробки и колёса, которые использовались в предыдущих лабораторных работах:

Важно отметить, что после дообучения модель всё ещё может обнаруживать классы, которых нет в нашем наборе данных, например кошек, собак, винограда и т. д., как мы видели раньше.
Полный проект дообучения с набором данных, размеченным в Roboflow и выполненным в Colab, можно найти в блокноте:
В другом примере, в статье Fine-tuning Florence-2 - Microsoft’s Cutting-edge Vision Language Models, авторы показывают пример дообучения Florence на DocVQA. Авторы сообщают, что Florence-2 может отвечать на вопросы по изображениям (VQA), но опубликованные модели не включают эту возможность.
Итоги
Florence-2 предлагает универсальный и мощный подход к визуально-языковым задачам на периферии: по производительности она сопоставима с более крупными моделями, заточенными под конкретные задачи, такими как YOLO для обнаружения объектов, BERT/RoBERTa для анализа текста и специализированные модели OCR.
Благодаря мультимодальной архитектуре трансформера Florence-2 гибче YOLO с точки зрения решаемых задач. Сюда входят обнаружение объектов, описание изображений и визуальная привязка.
В отличие от BERT, который сосредоточен только на языке, Florence-2 объединяет зрение и язык, что позволяет ей хорошо справляться с приложениями, которым нужны обе модальности, например описание изображений и визуальная привязка.
Кроме того, традиционные модели OCR, такие как Tesseract и EasyOCR, предназначены только для распознавания и извлечения текста из изображений, тогда как возможности OCR у Florence-2 входят в более широкую систему, которая включает понимание контекста и согласование визуального и текстового содержимого. Это делает её особенно полезной в сценариях, где нужно и читать текст, и понимать его контекст на изображении.
В целом Florence-2 выделяется тем, что объединяет различные визуально-языковые задачи в единой модели, которая достаточно эффективна, чтобы работать на периферийных устройствах вроде Raspberry Pi. Это делает её привлекательным выбором для разработчиков и исследователей, которые изучают приложения ИИ на периферии.
Ключевые преимущества Florence-2
-
Унифицированная архитектура
- Одна модель решает несколько задач зрения вместо специализированных моделей (YOLO, BERT, Tesseract);
- Исключает необходимость развёртывания и интеграции нескольких моделей;
- Единый API и интерфейс для всех задач.
-
Сравнение производительности
- Обнаружение объектов: сопоставимо с YOLOv8 (~37,5 mAP на COCO против ~39,7 mAP у YOLOv8), несмотря на универсальность модели;
- Распознавание текста: эффективно работает с несколькими языками, как специализированные модели OCR (Tesseract, EasyOCR);
- Понимание языка: включает возможности, похожие на BERT, для обработки текста, добавляя визуальный контекст.
-
Эффективность использования ресурсов
- Базовая модель (232 млн параметров) показывает хорошие результаты, несмотря на небольшой размер;
- Эффективно работает на периферийных устройствах (Raspberry Pi);
- Развёртывание одной модели вместо нескольких специализированных.
Компромиссы
-
Производительность и специализированные модели
- Серия YOLO может обеспечивать более быстрый инференс для чистого обнаружения объектов;
- Специализированные модели OCR могут лучше справляться со сложной структурой документов;
- BERT/RoBERTa обеспечивают более глубокое понимание языка для задач только с текстом.
-
Требования к ресурсам
- Более высокая задержка на периферийных устройствах (15–200 с в зависимости от задачи);
- Требуется аккуратное управление памятью на Raspberry Pi;
- Может потребоваться оптимизация для приложений реального времени.
-
Соображения по развёртыванию
- Первоначальная настройка сложнее, чем у моделей одного назначения;
- Нужно понимать множество типов задач и запросов;
- Освоение оптимальной инженерии запросов требует времени.
Лучшие сценарии применения
-
Среды с ограниченными ресурсами
- Периферийные устройства, которым нужны несколько возможностей зрения;
- Системы с ограниченным объёмом хранения и развёртывания;
- Приложения, которым нужна гибкая обработка изображений.
-
Мультимодальные приложения
- Системы модерации контента;
- Инструменты доступности;
- Рабочие процессы анализа документов.
-
Быстрое прототипирование
- Быстрое развёртывание возможностей зрения;
- Проверка множества задач зрения без отдельных моделей;
- Разработка концепции (proof of concept).
Перспективы
Florence-2 знаменует переход к унифицированным моделям зрения, которые в перспективе могут заменить архитектуры, заточенные под конкретные задачи, во многих приложениях. Специализированные модели сохраняют преимущества в отдельных сценариях, но удобство и эффективность унифицированных моделей вроде Florence-2 делают их всё более привлекательными для реальных развёртываний.
Лабораторная работа демонстрирует, что Florence-2 пригодна для периферийных устройств, что открывает перспективы для IoT, мобильных вычислений и встраиваемых систем, где развёртывание нескольких специализированных моделей было бы непрактичным.
Ресурсы
- 10-florence2_test.ipynb
- 20-florence_2.ipynb
- 30-Finetune_florence_2_on_detection_dataset_box_vs_wheel.ipynb
Обзор
Лабораторные работы этого раздела посвящены темам и методам, которые применимы на разных аппаратных платформах. Эти работы не привязаны к конкретной плате, что позволяет сосредоточиться на фундаментальных концепциях и алгоритмах, используемых в приложениях TinyML (сверхмалого машинного обучения).
Изучая эти общие лабораторные работы, вы глубже поймёте типичные задачи и решения во встраиваемом машинном обучении. Знания и навыки, которые вы получите здесь, пригодятся независимо от того, с каким оборудованием вы будете работать в будущем.
| Упражнение | Nicla Vision | XIAO ESP32S3 |
|---|---|---|
| Инженерия признаков для KWS | ✔ Ссылка | ✔ Ссылка |
| Блок спектральных признаков DSP | ✔ Ссылка | ✔ Ссылка |
Спектральные признаки DSP

Промпт DALL·E 3: мультяшная иллюстрация в стиле 1950-х: учёные (мужчина и женщина) в лаборатории исследования вибраций. Мужчина изучает старую электронику с помощью линейки. Женщина работает за компьютером со сложными графиками вибрации. На деревянном столе лежат платы с датчиками, среди них — акселерометр. На классическом компьютере с округлым корпусом открыта Arduino IDE с кодом назначения пинов светодиодов и алгоритмами машинного обучения для обнаружения движения. Монитор порта показывает БПФ, классификацию, вейвлеты и DSP. Сцену дополняют винтажные лампы, инструменты и графики БПФ и вейвлетов.
Обзор
Проекты TinyML, связанные с движением (или вибрацией), работают с данными IMU — обычно акселерометров и гироскопов. Такие временные ряды нужно предварительно обработать перед обучением модели машинного обучения, и для встраиваемого машинного обучения это непростая задача. Тем не менее Edge Impulse упрощает её с помощью шага цифровой обработки сигналов (DSP) и, точнее, блока спектральных признаков для инерциальных датчиков.
Но как это устроено внутри? Давайте разберёмся.
Обзор извлечения признаков
Извлечение признаков из набора данных, записанных инерциальными датчиками, такими как акселерометры, включает обработку и анализ сырых данных. Акселерометры измеряют ускорение объекта по одной или нескольким осям (обычно трём: X, Y и Z). Эти измерения помогают понять различные аспекты движения объекта, например характер перемещений и вибрации. Ниже — общая схема процесса:
Сбор данных. Сначала нужно получить данные с акселерометров. В зависимости от задачи данные могут записываться с разной частотой дискретизации. Важно, чтобы частота была достаточно высокой для того, чтобы зафиксировать значимую динамику исследуемого движения (частота дискретизации должна как минимум вдвое превышать максимальную значимую частоту в сигнале).
Предобработка данных. Сырые данные акселерометров могут быть зашумлёнными и содержать ошибки или нерелевантную информацию. Этапы предобработки, такие как фильтрация и нормализация, помогают очистить и стандартизировать данные, делая их более пригодными для извлечения признаков.
Studio не выполняет нормализацию и стандартизацию, поэтому при работе со слиянием данных датчиков (Sensor Fusion) этот шаг иногда нужно выполнить до загрузки данных в Studio. Это особенно важно в проектах со слиянием датчиков, как в этом руководстве: Sensor Data Fusion with Spresense and CommonSense.
Сегментация. В зависимости от природы данных и задачи может понадобиться разделить данные на более короткие сегменты, или окна. Это помогает сосредоточиться на конкретных событиях или активности в наборе данных и делает извлечение признаков более управляемым и содержательным. Выбор размера окна и перекрытия (шага окна, window span) зависит от задачи и частоты интересующих событий. Как эмпирическое правило, стоит стараться захватить несколько «циклов данных».
Извлечение признаков. Когда данные предобработаны и разбиты на сегменты, можно извлечь признаки, которые описывают характеристики движения. Типичные признаки, извлекаемые из данных акселерометра:
- Признаки временной области описывают статистические свойства данных внутри каждого сегмента: среднее, медиану, стандартное отклонение, асимметрию (skewness), эксцесс (kurtosis) и частоту пересечения нуля.
- Признаки частотной области получают, преобразуя данные в частотную область с помощью таких методов, как быстрое преобразование Фурье (FFT). Типичные признаки этой группы — спектр мощности, спектральная энергия, доминантные частоты (амплитуда и частота) и спектральная энтропия.
- Признаки временно-частотной области объединяют информацию о времени и частоте, например кратковременное преобразование Фурье (STFT) или дискретное вейвлет-преобразование (DWT). Они дают более подробное представление о том, как частотный состав сигнала меняется во времени.
Во многих случаях число извлечённых признаков может быть большим, что ведёт к переобучению или росту вычислительной сложности. Методы отбора признаков, такие как взаимная информация, корреляционные методы или метод главных компонент (PCA), помогают выявить наиболее значимые признаки для конкретного приложения и снизить размерность набора данных. Studio умеет выполнять такие вычисления для признаков.
Теперь подробнее рассмотрим типичный проект классификации движения TinyML из этой серии практических работ.
Проект классификации движения TinyML

В практическом проекте классификация движения и обнаружение аномалий мы моделировали механические нагрузки при транспортировке. Задача состояла в том, чтобы классифицировать четыре класса движения:
- Морская перевозка (паллеты на кораблях);
- Наземная перевозка (паллеты в грузовике или поезде);
- Подъём (паллеты, которые перемещает погрузчик);
- Простой (паллеты на складе).
Акселерометры передавали данные о паллете (или контейнере).

Ниже показан один образец (сырые данные) длиной 10 секунд, записанный с частотой дискретизации 50 Гц:

Результат похож, если тот же анализ провести для другого набора данных с тем же принципом, но с другой частотой дискретизации — 62,5 Гц вместо 50 Гц.
Предобработка данных
Сырые данные, полученные акселерометром (данные «временного ряда»), нужно преобразовать в «табличные данные» одним из типичных методов извлечения признаков, описанных в предыдущем разделе.
Для извлечения признаков данные нужно разбить на сегменты с помощью скользящего окна по образцам. В проекте акселерометр записывал данные каждые 10 секунд с частотой дискретизации 62,5 Гц. Окно длиной 2 секунды захватывает 375 точек данных (3 оси × 2 секунды × 62,5 отсчёта). Окно сдвигается каждые 80 мс, что создаёт набор данных большего размера, где каждый экземпляр содержит 375 «сырых признаков».

В Studio предыдущая версия (V1) блока спектрального анализа извлекала из временной области только RMS, а из частотной области — пики и частоты (с помощью FFT) и мощностные характеристики (PSD) сигнала во времени. В результате получался фиксированный табличный набор из 33 признаков (по 11 на каждую ось).

Эти 33 признака были входным тензором нейросетевого классификатора.
В 2022 году Edge Impulse выпустила вторую версию блока спектрального анализа, которую мы и рассмотрим.
Блок спектрального анализа Edge Impulse версии 2 изнутри
Во второй версии статистические признаки временной области для каждой оси/канала таковы:
- RMS;
- асимметрия (Skewness);
- эксцесс (Kurtosis).
А спектральные признаки частотной области для каждой оси/канала:
- спектральная мощность (Spectral Power);
- асимметрия (Skewness; в следующей версии);
- эксцесс (Kurtosis; в следующей версии).
Подробнее о извлечении признаков можно прочитать по этой ссылке.
Склонируйте публичный проект. Также можно следовать объяснению и поэкспериментировать с кодом в моём блокноте Google Colab: Edge Impulse Spectral Analysis Block Notebook.
Начнём с импорта библиотек:
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math
from scipy.stats import skew, kurtosis
from scipy import signal
from scipy.signal import welch
from scipy.stats import entropy
from sklearn import preprocessing
import pywt
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['lines.linewidth'] = 3
Из изучаемого проекта выберем образец данных акселерометра:
- Размер окна 2 секунды:
[2,000]мс; - Частота дискретизации:
[62.5]Гц; - Фильтр
[None](для простоты) и - Длина FFT:
[16].
f = 62.5 # Гц
wind_sec = 2 # секунды
FFT_Length = 16
axis = ['accX', 'accY', 'accZ']
n_sensors = len(axis)

Выбрав Raw Features на вкладке Spectral Analysis в Studio, можно скопировать в буфер обмена все 375 точек данных для конкретного двухсекундного окна.

Вставьте точки данных в новую переменную data:
data = [
-5.6330, 0.2376, 9.8701,
-5.9442, 0.4830, 9.8701,
-5.4217, ...
]
No_raw_features = len(data)
N = int(No_raw_features/n_sensors)
Всего сырых признаков 375, но мы будем работать с каждой осью отдельно, где (число отсчётов на ось).
Наша цель — понять, как Edge Impulse получает обработанные признаки.

Также вставьте обработанные признаки в переменную, чтобы сравнить рассчитанные в Python признаки с теми, что выдаёт Studio:
features = [
2.7322, -0.0978, -0.3813,
2.3980, 3.8924, 24.6841,
9.6303, ...
]
N_feat = len(features)
N_feat_axis = int(N_feat/n_sensors)
Всего обработанных признаков 39, то есть 13 признаков на ось.
Если внимательно посмотреть на эти 13 признаков, в них 3 признака временной области (RMS, Skewness и Kurtosis):
[rms] [skew] [kurtosis]
и 10 признаков частотной области (вернёмся к ним позже):
[spectral skew][spectral kurtosis][Spectral Power 1] ... [Spectral Power 8]
Разделение сырых данных по датчикам
Данные содержат отсчёты всех осей; разделим их и построим графики отдельно:
def plot_data(sensors, axis, title):
[plt.plot(x, label=y) for x,y in zip(sensors, axis)]
plt.legend(loc='lower right')
plt.title(title)
plt.xlabel('Отсчёт')
plt.ylabel('Значение')
plt.box(False)
plt.grid()
plt.show()
accX = data[0::3]
accY = data[1::3]
accZ = data[2::3]
sensors = [accX, accY, accZ]
plot_data(sensors, axis, 'Сырые признаки')

Вычитание среднего
Далее вычтем среднее из data. Вычитание среднего — распространённый шаг предобработки в статистике и машинном обучении. Его цель — центрировать данные около нуля. Это важно, потому что центрирование может выявить закономерности и связи, которые скрыты, если данные не центрированы.
Вот несколько конкретных причин, почему вычитание среднего полезно:
- Упрощает анализ: после центрирования среднее становится нулём, что упрощает некоторые вычисления и их интерпретацию.
- Убирает смещение: если данные смещены, вычитание среднего устраняет смещение и позволяет провести более точный анализ.
- Помогает выявить закономерности: центрирование может помочь обнаружить закономерности, скрытые в нецентрированных данных. Например, при анализе временного ряда оно помогает увидеть тренды во времени.
- Может улучшить качество: в некоторых алгоритмах машинного обучения центрирование повышает качество, так как снижает влияние выбросов и делает данные лучше сопоставимыми. В целом вычитание среднего — простой, но мощный приём для улучшения анализа и интерпретации данных.
dtmean = [
(sum(x) / len(x))
for x in sensors
]
[
print('mean_' + x + ' =', round(y, 4))
for x, y in zip(axis, dtmean)
][0]
accX = [(x - dtmean[0]) for x in accX]
accY = [(x - dtmean[1]) for x in accY]
accZ = [(x - dtmean[2]) for x in accZ]
sensors = [accX, accY, accZ]
plot_data(sensors, axis, 'Сырые признаки — вычитание среднего')

Статистические признаки временной области
Вычисление RMS
Среднеквадратичное значение (RMS) набора значений (или непрерывного сигнала) — это квадратный корень из среднего арифметического квадратов этих значений или квадрата функции, которая задаёт непрерывную форму сигнала. В физике RMS значения электрического тока определяется как «значение постоянного тока, который рассеивает ту же мощность в резисторе».
Для набора из значений RMS вычисляется так:
Обратите внимание: значение RMS различается для исходных сырых данных и для данных после вычитания среднего.
# Используем numpy и стандартизованные данные (после вычитания среднего)
rms = [np.sqrt(np.mean(np.square(x))) for x in sensors]
Сравним рассчитанные значения RMS с теми, что показывает Edge Impulse:
[print('rms_'+x+'= ', round(y, 4)) for x,y in zip(axis, rms)][0]
print("\nСравнение с признаками результата Edge Impulse")
print(features[0:N_feat:N_feat_axis])
rms_accX= 2.7322
rms_accY= 0.7833
rms_accZ= 0.1383
Сравнение с признаками из Edge Impulse:
[2.7322, 0.7833, 0.1383]
Вычисление асимметрии и эксцесса
В статистике асимметрия и эксцесс — два способа описать форму распределения.
Посмотрим на распределение значений датчиков:
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(13, 4))
sns.kdeplot(accX, fill=True, ax=axes[0])
sns.kdeplot(accY, fill=True, ax=axes[1])
sns.kdeplot(accZ, fill=True, ax=axes[2])
axes[0].set_title('accX')
axes[1].set_title('accY')
axes[2].set_title('accZ')
plt.suptitle('Распределение значений IMU-датчиков', fontsize=16, y=1.02)
plt.show()

Асимметрия (skewness) — мера асимметрии распределения. Её значение может быть положительным или отрицательным.

- Отрицательная асимметрия означает, что «хвост» распределения находится слева и тянется в сторону более отрицательных значений.
- Положительная асимметрия означает, что «хвост» находится справа и тянется в сторону более положительных значений.
- Нулевое значение означает отсутствие асимметрии: распределение полностью симметрично.
skew = [skew(x, bias=False) for x in sensors]
[print('skew_'+x+'= ', round(y, 4))
for x,y in zip(axis, skew)][0]
print("\nСравнение с признаками результата Edge Impulse")
features[1:N_feat:N_feat_axis]
skew_accX= -0.099
skew_accY= 0.1756
skew_accZ= 6.9463
Сравнение с признаками из Edge Impulse:
[-0.0978, 0.1735, 6.8629]
Эксцесс (kurtosis) — мера того, является ли распределение «тяжёлохвостым» или «легкохвостым» по сравнению с нормальным распределением.

- Эксцесс нормального распределения равен нулю.
- Если эксцесс распределения отрицательный, его называют платикуртическим: такое распределение даёт меньше и менее экстремальных выбросов, чем нормальное.
- Если эксцесс положительный, распределение называют лептокуртическим: оно даёт больше выбросов, чем нормальное.
kurt = [kurtosis(x, bias=False) for x in sensors]
[print('kurt_'+x+'= ', round(y, 4))
for x,y in zip(axis, kurt)][0]
print("\nСравнение с признаками результата Edge Impulse")
features[2:N_feat:N_feat_axis]
kurt_accX= -0.3475
kurt_accY= 1.2673
kurt_accZ= 68.1123
Сравнение с признаками из Edge Impulse:
[-0.3813, 1.1696, 65.3726]
Спектральные признаки
Отфильтрованный сигнал передаётся в раздел спектральной мощности (Spectral power), который вычисляет FFT для получения спектральных признаков.
Поскольку размер окна обычно больше размера FFT, окно разбивается на кадры («подокна»), и FFT вычисляется для каждого кадра.
Длина FFT — размер FFT. Он определяет количество бинов FFT и разрешение, с которым можно разделить пики частот. Малое значение означает, что в одном бине FFT усредняется больше сигналов, но при этом уменьшается количество признаков и размер модели. Большое значение разделяет сигналы по большему числу бинов и даёт более крупную модель.
- Общее количество признаков спектральной мощности зависит от настроек фильтра и параметров FFT. Без фильтрации количество признаков равно половине длины FFT.
Спектральная мощность — метод Уэлча
Для разбиения сигнала в частотной области на бины и вычисления спектра мощности для каждого бина используем метод Уэлча. Он делит сигнал на перекрывающиеся сегменты, применяет к каждому сегменту оконную функцию, вычисляет периодограмму каждого сегмента через DFT и усредняет их, чтобы получить более сглаженную оценку спектра мощности.
# Функция, которую использует Edge Impulse вместо scipy.signal.welch().
def welch_max_hold(fx, sampling_freq, nfft, n_overlap):
n_overlap = int(n_overlap)
spec_powers = [0 for _ in range(nfft//2+1)]
ix = 0
while ix <= len(fx):
# Срез обрезается, если end_idx больше длины,
# а rfft автоматически дополняет нулями
fft_out = np.abs(np.fft.rfft(fx[ix:ix+nfft], nfft))
spec_powers = np.maximum(spec_powers, fft_out**2/nfft)
ix = ix + (nfft-n_overlap)
return np.fft.rfftfreq(nfft, 1/sampling_freq), spec_powers
Применим эту функцию к трём сигналам:
fax,Pax = welch_max_hold(accX, fs, FFT_Length, 0)
fay,Pay = welch_max_hold(accY, fs, FFT_Length, 0)
faz,Paz = welch_max_hold(accZ, fs, FFT_Length, 0)
specs = [Pax, Pay, Paz ]
Построим график спектра мощности P(f):
plt.plot(fax,Pax, label='accX')
plt.plot(fay,Pay, label='accY')
plt.plot(faz,Paz, label='accZ')
plt.legend(loc='upper right')
plt.xlabel('Частота (Гц)')
#plt.ylabel('PSD [V**2/Hz]')
plt.ylabel('Мощность')
plt.title('Спектр мощности P(f) по методу Уэлча')
plt.grid()
plt.box(False)
plt.show()

Помимо спектра мощности, в частотной области можно также учесть асимметрию и эксцесс признаков (в новой версии, которая будет доступна позже):
spec_skew = [skew(x, bias=False) for x in specs]
spec_kurtosis = [kurtosis(x, bias=False) for x in specs]
Теперь выведем все спектральные признаки для каждой оси и сравним их с Edge Impulse:
print("Обработанные спектральные признаки EI (accX): ")
print(features[3:N_feat_axis][0:])
print("\nРассчитанные признаки:")
print (round(spec_skew[0],4))
print (round(spec_kurtosis[0],4))
[print(round(x, 4)) for x in Pax[1:]][0]
Обработанные спектральные признаки EI (accX):
2.398, 3.8924, 24.6841, 9.6303, 8.4867, 7.7793, 2.9963, 5.6242, 3.4198, 4.2735
Рассчитанные признаки:
2.9069 8.5569 24.6844 9.6304 8.4865 7.7794 2.9964 5.6242 3.4198 4.2736
print("Обработанные спектральные признаки EI (accY): ")
print(features[16:26][0:]) # 13: 3+N_feat_axis;
# 26 = 2x N_feat_axis
print("\nРассчитанные признаки:")
print (round(spec_skew[1],4))
print (round(spec_kurtosis[1],4))
[print(round(x, 4)) for x in Pay[1:]][0]
Обработанные спектральные признаки EI (accY):
0.9426, -0.8039, 5.429, 0.999, 1.0315, 0.9459, 1.8117, 0.9088, 1.3302, 3.112
Рассчитанные признаки:
1.1426 -0.3886 5.4289 0.999 1.0315 0.9458 1.8116 0.9088 1.3301 3.1121
print("Обработанные спектральные признаки EI (accZ): ")
print(features[29:][0:]) #29: 3+(2*N_feat_axis);
print("\nРассчитанные признаки:")
print (round(spec_skew[2],4))
print (round(spec_kurtosis[2],4))
[print(round(x, 4)) for x in Paz[1:]][0]
Обработанные спектральные признаки EI (accZ):
0.3117, -1.3812, 0.0606, 0.057, 0.0567, 0.0976, 0.194, 0.2574, 0.2083, 0.166
Рассчитанные признаки:
0.3781 -1.4874 0.0606 0.057 0.0567 0.0976 0.194 0.2574 0.2083 0.166
Временно-частотная область
Вейвлеты
Вейвлет — мощный инструмент для анализа сигналов с кратковременными особенностями или резкими изменениями, такими как всплески или фронты, которые трудно интерпретировать традиционными методами на основе преобразования Фурье.
Вейвлет-преобразования работают, разлагая сигнал на различные частотные компоненты и анализируя каждую из них отдельно. Преобразование выполняется свёрткой сигнала с вейвлет-функцией — небольшой волной, сосредоточенной в определённый момент времени и на определённой частоте. Так сигнал раскладывается на разные частотные полосы, каждую из которых можно анализировать отдельно.
Одно из главных преимуществ вейвлет-преобразований в том, что они позволяют проводить временно-частотный анализ: видно, как частотный состав сигнала меняется во времени. Поэтому они особенно полезны для нестационарных сигналов, которые меняются со временем.
Вейвлеты имеют много практических применений: сжатие сигналов и изображений, подавление шума, извлечение признаков и обработка изображений.
Выберем вейвлет на блоке спектральных признаков в том же проекте:
- Тип: Wavelet;
- Уровень декомпозиции вейвлетом: 1;
- Вейвлет: bior1.3.

Вейвлет-функция
wavelet_name='bior1.3'
num_layer = 1
wavelet = pywt.Wavelet(wavelet_name)
[phi_d,psi_d,phi_r,psi_r,x] = wavelet.wavefun(level=5)
plt.plot(x, psi_d, color='red')
plt.title('Вейвлет-функция')
plt.ylabel('Значение')
plt.xlabel('Время')
plt.grid()
plt.box(False)
plt.show()

Как и раньше, скопируем и вставим обработанные признаки:

features = [
3.6251, 0.0615, 0.0615,
-7.3517, -2.7641, 2.8462,
5.0924, ...
]
N_feat = len(features)
N_feat_axis = int(N_feat/n_sensors)
Edge Impulse вычисляет дискретное вейвлет-преобразование (DWT) для каждого выбранного уровня декомпозиции вейвлетом. После этого извлекаются признаки.
Для вейвлетов извлекаются базовые статистические значения, значения пересечений и энтропия. Всего на каждый уровень приходится 14 признаков:
- [11] статистических признаков: n5, n25, n75, n95, mean, median, стандартное отклонение (std), дисперсия (var), среднеквадратичное значение (rms), kurtosis (эксцесс) и асимметрия (skew);
- [2] признака пересечений: частота пересечения нуля (zcross) и частота пересечения среднего уровня (mcross) — то есть количество раз, когда сигнал пересекает базовую линию и средний уровень (y = u), за единицу времени;
- [1] признак сложности: энтропия (Entropy) — характеристическая мера сложности сигнала.
Все 14 значений рассчитываются для каждого уровня (включая L0 — исходный сигнал).
- Общее количество признаков зависит от настроек фильтра и числа уровней. Например, при фильтрации [None] и уровне [1] количество признаков на ось составит (L0 и L1) = 28. Для трёх осей получится 84 признака.
Анализ вейвлетами
Вейвлет-анализ раскладывает сигнал (accX, accY и accZ) на различные частотные компоненты с помощью набора фильтров. Эти фильтры разделяют компоненты на низкочастотные (медленно меняющиеся части сигнала, содержащие долгосрочные закономерности), такие как accX_l1, accY_l1, accZ_l1, и высокочастотные (быстро меняющиеся части, содержащие краткосрочные закономерности), такие как accX_d1, accY_d1, accZ_d1. Это позволяет извлекать признаки для дальнейшего анализа или классификации.
Используются только низкочастотные компоненты (коэффициенты аппроксимации, или cA). В этом примере предполагается один уровень (одноуровневое дискретное вейвлет-преобразование), когда функция возвращает кортеж. При многоуровневой декомпозиции («Multilevel 1D Discrete Wavelet Transform») результатом будет список (подробности см. в описании дискретного вейвлет-преобразования (DWT)).
(accX_l1, accX_d1) = pywt.dwt(accX, wavelet_name)
(accY_l1, accY_d1) = pywt.dwt(accY, wavelet_name)
(accZ_l1, accZ_d1) = pywt.dwt(accZ, wavelet_name)
sensors_l1 = [accX_l1, accY_l1, accZ_l1]
# Строим график мощности спектра в зависимости от частоты
plt.plot(accX_l1, label='accX')
plt.plot(accY_l1, label='accY')
plt.plot(accZ_l1, label='accZ')
plt.legend(loc='lower right')
plt.xlabel('Время')
plt.ylabel('Значение')
plt.title('Аппроксимация вейвлетом')
plt.grid()
plt.box(False)
plt.show()

Извлечение признаков
Начнём с базовых статистических признаков. Обратите внимание: функцию применяем и к исходным сигналам, и к коэффициентам аппроксимации cA, полученным из DWT:
def calculate_statistics(signal):
n5 = np.percentile(signal, 5)
n25 = np.percentile(signal, 25)
n75 = np.percentile(signal, 75)
n95 = np.percentile(signal, 95)
median = np.percentile(signal, 50)
mean = np.mean(signal)
std = np.std(signal)
var = np.var(signal)
rms = np.sqrt(np.mean(np.square(signal)))
return [n5, n25, n75, n95, median, mean, std, var, rms]
stat_feat_l0 = [calculate_statistics(x) for x in sensors]
stat_feat_l1 = [calculate_statistics(x) for x in sensors_l1]
Асимметрия и эксцесс:
skew_l0 = [skew(x, bias=False) for x in sensors]
skew_l1 = [skew(x, bias=False) for x in sensors_l1]
kurtosis_l0 = [kurtosis(x, bias=False) for x in sensors]
kurtosis_l1 = [kurtosis(x, bias=False) for x in sensors_l1]
Пересечение нуля (zcross) — это количество раз, когда коэффициент вейвлета пересекает нулевую ось. Оно помогает оценить частотное содержание сигнала, так как высокочастотные сигналы обычно имеют больше пересечений нуля, чем низкочастотные.
Пересечение среднего (mcross), в свою очередь, — это количество раз, когда коэффициент вейвлета пересекает среднее значение сигнала. Оно помогает оценить амплитуду, так как сигналы с большой амплитудой обычно имеют больше пересечений среднего, чем сигналы с малой амплитудой.
def getZeroCrossingRate(arr):
my_array = np.array(arr)
zcross = float(
"{:.2f}".format(
(((my_array[:-1] * my_array[1:]) < 0).sum()) / len(arr)
)
)
return zcross
def getMeanCrossingRate(arr):
mcross = getZeroCrossingRate(np.array(arr) - np.mean(arr))
return mcross
def calculate_crossings(list):
zcross=[]
mcross=[]
for i in range(len(list)):
zcross_i = getZeroCrossingRate(list[i])
zcross.append(zcross_i)
mcross_i = getMeanCrossingRate(list[i])
mcross.append(mcross_i)
return zcross, mcross
cross_l0 = calculate_crossings(sensors)
cross_l1 = calculate_crossings(sensors_l1)
В вейвлет-анализе энтропия означает степень беспорядка, или случайности, в распределении коэффициентов вейвлета. Здесь мы используем энтропию Шеннона, которая измеряет неопределённость, или случайность, сигнала. Она вычисляется как взятая с обратным знаком сумма вероятностей различных возможных исходов сигнала, умноженных на их логарифм по основанию 2. В контексте вейвлет-анализа энтропия Шеннона может использоваться для оценки сложности сигнала: чем выше значение, тем сложнее сигнал.
def calculate_entropy(signal, base=None):
value, counts = np.unique(signal, return_counts=True)
return entropy(counts, base=base)
entropy_l0 = [calculate_entropy(x) for x in sensors]
entropy_l1 = [calculate_entropy(x) for x in sensors_l1]
Теперь перечислим все вейвлет-признаки и сгруппируем их по уровням.
L1_features_names = [
"L1-n5", "L1-n25", "L1-n75", "L1-n95", "L1-median",
"L1-mean", "L1-std", "L1-var", "L1-rms", "L1-skew",
"L1-Kurtosis", "L1-zcross", "L1-mcross", "L1-entropy"
]
L0_features_names = [
"L0-n5", "L0-n25", "L0-n75", "L0-n95", "L0-median",
"L0-mean", "L0-std", "L0-var", "L0-rms", "L0-skew",
"L0-Kurtosis", "L0-zcross", "L0-mcross", "L0-entropy"
]
all_feat_l0 = []
for i in range(len(axis)):
feat_l0 = (
stat_feat_l0[i]
+ [skew_l0[i]]
+ [kurtosis_l0[i]]
+ [cross_l0[0][i]]
+ [cross_l0[1][i]]
+ [entropy_l0[i]]
)
[print(axis[i] + ' +x+= ', round(y, 4))
for x, y in zip(L0_features_names, feat_l0)][0]
all_feat_l0.append(feat_l0)
all_feat_l0 = [
item
for sublist in all_feat_l0
for item in sublist
]
print(f"\nВсего признаков L0 = {len(all_feat_l0)}")
all_feat_l1 = []
for i in range(len(axis)):
feat_l1 = (
stat_feat_l1[i]
+ [skew_l1[i]]
+ [kurtosis_l1[i]]
+ [cross_l1[0][i]]
+ [cross_l1[1][i]]
+ [entropy_l1[i]]
)
[print(axis[i]+' '+x+'= ', round(y, 4))
for x,y in zip(L1_features_names, feat_l1)][0]
all_feat_l1.append(feat_l1)
all_feat_l1 = [
item
for sublist in all_feat_l1
for item in sublist
]
print(f"\nВсего признаков L1 = {len(all_feat_l1)}")

Итоги
Edge Impulse Studio — мощная онлайн-платформа, которая может взять на себя задачи предобработки. Тем не менее с инженерной точки зрения важно понимать, что происходит «под капотом». Эти знания помогут подобрать лучшие опции и гиперпараметры для настройки проектов.
Daniel Situnayake написал в своём блоге: «Сырые данные датчиков многомерны и зашумлены. Алгоритмы цифровой обработки сигналов помогают отделить сигнал от шума. DSP — важная часть встраиваемой инженерии, и многие периферийные процессоры имеют аппаратное ускорение DSP. Как инженер по ML, освоив основы DSP, вы получаете суперсилу для работы с высокочастотными временными рядами в своих моделях». Рекомендую прочитать его замечательный пост целиком: nn to cpp: What you need to know about porting deep learning models to the edge.
Инженерия признаков для KWS

Промпт DALL·E 3: мультяшная сцена в стиле 1950-х в аудиолаборатории. Два учёных, один с лупой, другой с записями, изучают большие графики, прикреплённые к стене. На графиках — спектры БПФ и временные кривые аудиоданных. Комната в ретро-стиле: деревянные столы, винтажные лампы и классические инструменты анализа звука.
Обзор
В этом практическом руководстве основное внимание уделяется той критической роли, которую инженерия признаков играет в повышении качества моделей машинного обучения для задач классификации звука, например для распознавания речи. Важно понимать, что качество любой модели машинного обучения во многом зависит от качества используемых признаков. Мы разберём «внутреннюю механику» извлечения признаков, в первую очередь кепстральные коэффициенты на мел-шкале (Mel-frequency Cepstral Coefficients, MFCC) — основу обработки аудиосигналов.
Модели машинного обучения, особенно классические алгоритмы, не понимают звуковых волн. Они работают с числами, организованными определённым образом, то есть с признаками. Признаки описывают характеристики аудиосигнала и помогают модели отличать одни звуки от других.
В этом руководстве речь пойдёт о генерации признаков именно для классификации звука. Это может быть интересно для самых разных аудиоданных: распознавания речи, категоризации музыки, классификации насекомых по звукам крыльев или других задач анализа звука.
KWS
Самое распространённое применение TinyML — распознавание ключевых слов (Keyword Spotting, KWS), которое является частью более широкой области распознавания речи. Общее распознавание речи переводит всю произнесённую речь в текст, а KWS сосредоточено на обнаружении определённых «ключевых слов» или «слов пробуждения» в непрерывном аудиопотоке. Система обучена распознавать эти ключевые слова как заранее заданные фразы или слова, например yes или no. Короче говоря, KWS — это специализированная форма распознавания речи со своими задачами и требованиями.
Типичный процесс KWS с преобразователем признаков MFCC выглядит так:

Применения KWS
- Голосовые помощники: в устройствах вроде Amazon Alexa или Google Home KWS используется для обнаружения слова пробуждения («Alexa» или «Hey Google») и активации устройства.
- Голосовое управление: в автомобилях или на промышленных объектах KWS может запускать конкретные команды, например «Запустить двигатель» или «Выключить свет».
- Системы безопасности: системы безопасности с голосовым доступом могут использовать KWS для аутентификации пользователей по произнесённой парольной фразе.
- Телекоммуникационные услуги: линии службы поддержки могут использовать KWS для маршрутизации звонков по произнесённым ключевым словам.
Отличия от общего распознавания речи
- Вычислительная эффективность: KWS обычно требует меньше вычислений, чем полное распознавание речи, поскольку нужно распознавать лишь небольшой набор фраз.
- Обработка в реальном времени: KWS часто работает в реальном времени и оптимизировано для обнаружения ключевых слов с малой задержкой.
- Ограничения ресурсов: модели KWS часто делают лёгкими, чтобы они могли работать на устройствах с ограниченными вычислительными ресурсами, например на микроконтроллерах или мобильных телефонах.
- Узкая задача: модели общего распознавания речи обучаются работать с широким словарём и разными акцентами, а модели KWS настраиваются на распознавание конкретных ключевых слов, часто точно и в шумной обстановке.
Обзор аудиосигналов
Понимание основных свойств аудиосигналов необходимо для эффективного извлечения признаков и, в итоге, для успешного применения алгоритмов машинного обучения в задачах классификации звука. Аудиосигналы — это сложные волны, которые отражают колебания давления воздуха во времени. Их можно описать несколькими основными характеристиками: частотой дискретизации, частотой и амплитудой.
-
Частота и амплитуда: частота — это количество колебаний волны за единицу времени, измеряется в герцах (Гц). Для аудиосигналов разные частоты соответствуют разной высоте тона. Амплитуда, в свою очередь, измеряет величину колебаний и связана с громкостью звука. И частота, и амплитуда — важные признаки, которые отражают тональные и ритмические свойства звука.
-
Частота дискретизации: частота дискретизации, которая обычно обозначается в герцах (Гц), определяет количество отсчётов, взятых за секунду при оцифровке аналогового сигнала. Чем выше частота дискретизации, тем точнее цифровое представление сигнала, но тем больше вычислительных ресурсов требуется для обработки. Типичные частоты дискретизации — 44,1 кГц для аудио в качестве CD и 16 кГц или 8 кГц для задач распознавания речи. Понимание компромиссов при выборе подходящей частоты дискретизации важно для баланса точности и вычислительной эффективности. В целом в проектах TinyML мы работаем с частотой 16 кГц. Хотя тоны музыки слышны на частотах до 20 кГц, голос ограничен 8 кГц. Традиционные телефонные системы используют частоту дискретизации 8 кГц.
Для точного представления сигнала частота дискретизации должна быть как минимум вдвое больше самой высокой частоты, присутствующей в сигнале.
- Временная и частотная области: аудиосигналы можно анализировать во временной и в частотной областях. Во временной области сигнал представлен как волна, где амплитуда отложена по времени. Такое представление помогает увидеть временные признаки, например начало и длительность звука, но тональные характеристики в нём выражены слабо. Напротив, представление в частотной области показывает составляющие сигнала и их амплитуды, обычно получаемые с помощью преобразования Фурье. Это незаменимо для задач, где нужно понимать спектральное содержание сигнала, например для определения музыкальных нот или речевых фонем (как в нашем случае).
На изображении ниже показаны слова YES и NO с типичными представлениями во временной (исходное аудио) и частотной областях:

Почему не сырое аудио?
Использование сырых аудиоданных напрямую в задачах машинного обучения может показаться заманчивым, но этот подход создаёт несколько проблем, из-за которых он хуже подходит для надёжных и эффективных моделей.
Применение сырого аудио для KWS, например на устройствах TinyML, сопряжено с трудностями: высокая размерность (при частоте дискретизации 16 кГц), вычислительная сложность при выделении временных признаков, чувствительность к шуму и отсутствие семантически осмысленных признаков. Поэтому для приложений с ограниченными ресурсами более практичным выбором становятся методы извлечения признаков, такие как MFCC.
Вот некоторые дополнительные подробности о ключевых проблемах, связанных с сырым аудио:
-
Высокая размерность: аудиосигналы, особенно при высокой частоте дискретизации, дают большие объёмы данных. Например, односекундный фрагмент, записанный с частотой 16 кГц, содержит 16 000 отдельных точек данных. Высокая размерность увеличивает вычислительную сложность, время обучения и затраты на вычисления, что делает подход непрактичным в условиях ограниченных ресурсов. Кроме того, широкий динамический диапазон аудиосигналов требует много бит на отсчёт, при этом полезной информации передаётся мало.
-
Временные зависимости: у сырых аудиосигналов есть временная структура, которую простым моделям машинного обучения бывает трудно уловить. Рекуррентные нейронные сети, например LSTM, способны моделировать такие зависимости, но они требовательны к вычислениям и трудны в обучении на крошечных устройствах.
-
Шум и вариативность: сырые аудиосигналы часто содержат фоновый шум и другие второстепенные элементы, которые ухудшают работу модели. Кроме того, один и тот же звук может звучать по-разному в зависимости от расстояния до микрофона, ориентации источника звука и акустических свойств окружения, что усложняет данные.
-
Отсутствие семантического смысла: сырое аудио по своей природе не содержит семантически осмысленных признаков для задач классификации. Такие признаки, как высота тона, темп и спектральные характеристики, которые важны для распознавания речи, нельзя напрямую получить из формы сигнала.
-
Избыточность сигнала: аудиосигналы часто содержат избыточную информацию: отдельные участки сигнала почти не вносят вклад в решение задачи. Эта избыточность делает обучение неэффективным и может привести к переобучению.
Поэтому для преобразования сырых аудиоданных в более удобный и информативный формат обычно используют методы извлечения признаков: кепстральные коэффициенты на мел-шкале (MFCC), мел-энергии (Mel-Frequency Energies, MFE) и простые спектрограммы. Эти признаки сохраняют главные характеристики аудиосигнала, уменьшая размерность и шум, и делают машинное обучение эффективнее.
Обзор MFCC
Что такое MFCC?
Кепстральные коэффициенты на мел-шкале (MFCC) — набор признаков, которые вычисляются по спектральному содержанию аудиосигнала. Они основаны на особенностях человеческого слуха и часто используются для описания фонетических характеристик звука. MFCC вычисляются в несколько этапов: предыскажение (pre-emphasis), разбиение на кадры (framing), оконная функция (windowing), быстрое преобразование Фурье (FFT) для перехода в частотную область и, наконец, дискретное косинусное преобразование (DCT). В результате получается компактное представление спектральных характеристик исходного аудиосигнала.
На изображении ниже слова YES и NO показаны в виде MFCC:

В этом видео объясняется, что такое кепстральные коэффициенты на мел-шкале (MFCC), и показано, как их вычислять.
Зачем нужны MFCC?
MFCC важны по нескольким причинам, особенно в контексте распознавания ключевых слов (KWS) и TinyML:
- Снижение размерности: MFCC сохраняют важные спектральные характеристики сигнала и заметно уменьшают размерность данных, что делает их идеальными для приложений TinyML с ограниченными ресурсами.
- Устойчивость: MFCC меньше подвержены шуму и вариациям высоты тона и амплитуды, предоставляя более стабильный и надёжный набор признаков для задач классификации звука.
- Моделирование слуха человека: мел-шкала в MFCC приближает реакцию человеческого уха на разные частоты, что делает их практичными для распознавания речи, где желательно восприятие, близкое к человеческому.
- Вычислительная эффективность: расчёт MFCC эффективен с точки зрения вычислений, что делает их хорошо подходящими для работы в реальном времени на аппаратуре с ограниченными ресурсами.
Итак, MFCC сочетают информативность и вычислительную эффективность, поэтому они популярны в задачах классификации звука, особенно в условиях ограничений, как в TinyML.
Вычисление MFCC
Вычисление кепстральных коэффициентов на мел-шкале (MFCC) включает несколько ключевых шагов. Разберём их, так как они особенно важны для задач распознавания ключевых слов (KWS) на устройствах TinyML.
-
Предыскажение (pre-emphasis): первый шаг — предыскажение, которое усиливает высокочастотные составляющие аудиосигнала и выравнивает частотный спектр. Для этого применяется фильтр, усиливающий разницу между соседними отсчётами. Формула предыскажения: , где — коэффициент предыскажения, обычно около 0,97.
-
Разбиение на кадры (framing): аудиосигнал делится на короткие кадры (длина кадра), обычно 20–40 миллисекунд. Это основано на допущении, что частоты в сигнале стационарны на коротком промежутке времени. Разбиение на кадры позволяет анализировать сигнал в таких небольших временных интервалах. Шаг кадра (frame stride) определяет смещение от одного кадра к соседнему. Кадры могут идти последовательно или перекрываться.
-
Оконная функция (windowing): каждый кадр затем умножается на оконную функцию, чтобы уменьшить разрывы на границах кадра. Часто используется окно Хэмминга. Оконная обработка готовит сигнал к преобразованию Фурье, уменьшая краевые эффекты. На изображении ниже показаны три кадра (10, 20 и 30) и отсчёты времени после оконной обработки (длина кадра и шаг кадра составляют 20 мс):

- Быстрое преобразование Фурье (FFT): к каждому кадру после оконной обработки применяется FFT, которое переводит его из временной области в частотную. FFT даёт комплексное представление, содержащее и амплитуду, и фазу. Однако для MFCC используется только амплитуда, чтобы рассчитать спектр мощности (power spectrum). Спектр мощности — это квадрат амплитудного спектра, он показывает энергию, присутствующую в каждой частотной составляющей.
Спектр мощности сигнала определяется как , где — преобразование Фурье . Возведение модуля преобразования Фурье в квадрат усиливает сильные частоты по сравнению со слабыми и тем самым лучше передаёт значимые спектральные характеристики аудиосигнала. Это важно в таких задачах, как классификация звука, распознавание речи и KWS, где нужно выделять различимые частотные паттерны, которые отличают классы звуков или фонемы речи.

- Банк мел-фильтров (Mel Filter Banks): частотная область затем отображается на мел-шкалу, которая приближает реакцию человеческого уха на разные частоты. Идея в том, чтобы извлекать больше признаков (больше фильтров) на низких частотах и меньше на высоких. Поэтому метод хорошо работает со звуками, которые различает человеческое ухо. Обычно для вычисления мел-энергий используют от 20 до 40 треугольных фильтров. Затем эти энергии логарифмируются, чтобы превратить мультипликативные факторы в аддитивные, что удобнее для дальнейшей обработки.

- Дискретное косинусное преобразование (DCT): последний шаг — применить дискретное косинусное преобразование (DCT) к логарифмам мел-энергий. DCT помогает декоррелировать энергии, эффективно сжимая данные и оставляя только наиболее информативные признаки. Обычно сохраняют первые 12–13 коэффициентов DCT, которые образуют итоговый вектор признаков MFCC.

Практика на Python
Применим рассмотренное на реальном аудиообразце. Откройте блокнот в Google Colab и извлеките признаки MFCC из ваших аудиофайлов: Открыть в Colab
Итоги
Какой метод извлечения признаков использовать?
Кепстральные коэффициенты на мел-шкале (MFCC), мел-энергии (MFE) и спектрограммы — способы представления аудиоданных, которые часто полезны в разных контекстах.
В целом MFCC сосредоточены на том, чтобы передать огибающую спектра мощности. Поэтому они менее чувствительны к мелким деталям спектра, но устойчивее к шуму. Это часто желательно для задач, связанных с речью. С другой стороны, спектрограммы и MFE сохраняют более подробную информацию о частотах, что может быть полезно в задачах, где нужно различать звуки по тонкой спектральной структуре.
MFCC особенно хороши для
- Распознавания речи: MFCC отлично подходят для выявления фонетического содержания речевых сигналов.
- Идентификации диктора: с их помощью можно отличать разных говорящих по особенностям голоса.
- Распознавания эмоций: MFCC способны передавать тонкие изменения речи, которые указывают на эмоциональное состояние.
- Распознавания ключевых слов: особенно в TinyML, где важны низкая вычислительная сложность и небольшой размер признаков.
Спектрограммы или MFE чаще подходят для
- Анализа музыки: спектрограммы передают гармоническую и тембровую структуру музыки, что важно для классификации жанров, распознавания инструментов или транскрипции музыки.
- Классификации окружающих звуков: при распознавании неречевых звуков (например, дождя, ветра, транспорта) полная спектрограмма даёт более информативные признаки.
- Идентификации птичьего пения: тонкие детали птичьих голосов часто лучше передаются спектрограммами.
- Обработки биоакустических сигналов: для анализа звуков дельфинов или летучих мышей тонкая частотная информация спектрограммы может быть критичной.
- Контроля качества аудио: спектрограммы часто используются в профессиональном анализе звука, чтобы выявлять нежелательные шумы, щелчки и другие артефакты.
Ресурсы
Уведомление об авторстве
Аппаратные наборы — русский перевод раздела «Hardware Kits» сайта учебника «Machine Learning Systems» (Harvard University).
- Оригинальный текст: Vijay Janapa Reddi, Marcelo Rovai и соавторы, Harvard University, mlsysbook.ai/kits. Исходный репозиторий:
harvard-edge/cs249r_book, папкаkits/. - Лицензия оригинала: CC BY-NC-SA 4.0. Эта книга распространяется на тех же условиях: с указанием авторства, только в некоммерческих целях и на условиях той же лицензии. Поэтому она не публикуется по общей лицензии CC BY 4.0 репозитория, см.
meta.toml. - Перевод и редакция: Сергей Монин.
Переведён весь раздел Hardware Kits: главная страница, «Начало работы», «Платформы», настройка среды разработки, лабораторные работы по Arduino Nicla Vision, Seeed XIAOML Kit, Grove Vision AI V2, Raspberry Pi и общие лабораторные работы. Переведены заголовки, текст, таблицы, подписи к рисункам и таблицам, врезки, тексты ссылок, подписи в схемах и комментарии в примерах кода. Формулы, числа, цены и названия продуктов сохранены как в оригинале. Код, команды и выводы программ остаются на английском языке. Что изменено при переносе в mdBook, какие схемы перерисованы и что осталось на языке оригинала, описано в файле PROVENANCE.md (в этой же папке).
Происхождение материалов
Источник
Материал основан на разделе Hardware Kits (папка kits/) репозитория harvard-edge/cs249r_book, сайт mlsysbook.ai/kits. Лицензия оригинала — CC BY-NC-SA 4.0 (файл kits/LICENSE).
Перевод выполнил Сергей Монин.
Что переведено
- Главная страница (
index.qmd), включая блок «Часть экосистемы MLSysBook» (shared/config/_ecosystem-cards.qmd). Блок переведён списком; интерактивная отметка «Вы здесь» не переносится, так как в mdBook нет скриптов сайта. - «Начало работы» (
getting-started.qmd), «Платформы» (platforms.qmd), «Настройка среды разработки» (ide-setup.qmd). - Arduino Nicla Vision (
arduino/nicla_vision/): обзор, настройка, классификация изображений, распознавание ключевых слов, классификация движения и обнаружение аномалий, обнаружение объектов. - Seeed XIAOML Kit, XIAO ESP32S3 (
seeed/xiao_esp32s3/): обзор, настройка, классификация изображений, распознавание ключевых слов, классификация движения, обнаружение объектов. - Grove Vision AI V2 (
seeed/grove_vision_ai_v2/): обзор, настройка и приложения без кода, классификация изображений, обнаружение объектов (в оригинале страница помечена «в разработке»). - Raspberry Pi (
raspi/): обзор, настройка (установка ОС, SSH, SWAP, камера, удалённый рабочий стол VNC, обновление системы, особенности моделей Zero и 4/5), классификация изображений, обнаружение объектов, малые языковые модели (SLM, Ollama), визуально-языковые модели (VLM, Florence-2). - Общие лабораторные работы (
shared/): обзор, инженерия признаков для распознавания ключевых слов, блок спектральных признаков DSP. - Текст, заголовки, таблицы, подписи к таблицам и рисункам, врезки, примечания, тексты ссылок и alt-тексты изображений.
- Комментарии в примерах кода переведены там, где это пояснения на естественном языке. Имена функций, переменных и библиотек и команды оставлены как в оригинале. Строки вывода (
Serial.println,ei_printf,print, подписи графиков matplotlib) в части лабораторных работ (XIAOML Kit — классификация движения, Grove Vision AI V2 — классификация изображений, общая лабораторная по спектральным признакам DSP) переведены на русский; в остальных они оставлены на английском, как в оригинале и на скриншотах.
Схемы
Схемы переведены двумя способами.
- Схема архитектуры Florence-2 (в оригинале TikZ, который mdBook не отображает) перерисована как SVG с русскими подписями:
05-raspberry-pi/vlm/images/svg/florence2-architecture.svg. - Самостоятельные схемы из курса перерисованы как SVG с русскими подписями, без логотипов и без скриншотов:
05-raspberry-pi/llm/images/svg/:llm-block-fc-proj.svg,llm-block-image.svg,llm-rag-scheme-1.svg,llm-rag-scheme-2.svg;05-raspberry-pi/object_detection/images/svg/:od-pipeline.svg,od-model-deploy.svg;04-grove-vision/setup_and_no_code_apps/images/svg/grove-person-detection.svg;04-grove-vision/image_classification/images/svg/:grove-ml-pipeline.svg,grove-inference-leds.svg;03-seeed-xiao/image_classification/images/svg/xiao-pipeline.svg.
Исходные растровые файлы этих схем остаются в папках images/png/ и images/jpeg/ рядом с главами, но в тексте они больше не используются.
Что изменено при переносе в mdBook
- Таблицы Quarto (сеточные таблицы) переписаны в формат Markdown-таблиц. Содержимое ячеек не менялось.
- Атрибуты Quarto удалены:
{#sec-…}у заголовков,{.unnumbered},{width=…},{fig-align=…},{.striped .hover}. Подпись таблицы вынесена в курсивную строку под таблицей. - Команды LaTeX и служебные маркеры Quarto удалены:
\noindent,\clearpage,\part{…}, маркеры викторин::: {.quiz-end}. - Врезки Quarto (
::: callout-…) переписаны как цитаты с жирным заголовком. - Рисунки Quarto заменены HTML-блоками
<img>с альтернативным текстом и подписью. Версии, предназначенные только для PDF (content-visible when-format="pdf"), удалены; для HTML сохранена версия для веб-страниц. - Видео Quarto (
{{< video … >}}) заменены ссылками на YouTube. Подключение общего блока ({{< include … >}}) заменено его текстом. - Перекрёстные ссылки Quarto (
@sec-…,@fig-…,@tbl-…) заменены текстом или ссылками на переведённые страницы книги. Все внутренние ссылки проверены скриптомscripts/check_links.py. - Таблица оглавления
SUMMARY.mdсоздана скриптомscripts/make_summary.py. Для названий глав книги в скрипте добавлены подписи (CHAPTER_TITLES). - Изображения скопированы в папку
images/каждой главы. Подписи и альтернативный текст переведены.
Исправления в примерах кода и опечатки оригинала
В примерах кода исправлены ошибки, которые мешали запуску или вели к неверному результату. Исправления минимальны, логика примеров не менялась.
- Импорт
from flask import …без скобок переносит строку с ошибкой синтаксиса: добавлены скобки. - Строка
mimetype=…, разорванная переносом внутри строкового литерала: собрана в одну строку. - Присваивание
url = "…" "…"без скобок: добавлены скобки, иначе вторая часть строки терялась. - Пути к моделям, разорванные обратным слэшем внутри строки (в них попадали пробелы): собраны в одну строку.
except Queue.Emptyзаменено наexcept Emptyс импортом изqueue.- В примере LLM вместо
response_2иresиспользованы правильные переменные. - В вызове
ollama.generateдобавлена пропущенная запятая передoptions=. - В функции
rag_beesудалён дублирующийprint, стоявший вне функции. - Команда
sudo apt-get installlibportaudiocpp0исправлена наsudo apt-get install libportaudiocpp0 portaudio19-dev. - В команде обучения YOLO пропущен обратный слэш в строке
epochs={EPOCHS}; добавлен. cd /Documents/YOLOзаменено наcd ~/Documents/YOLO.- В примере Florence-2
< MORE_DETAILED_CAPTION>и<CAPTION,>приведены к виду<MORE_DETAILED_CAPTION>и<CAPTION>. - В JavaScript-примере однострочный комментарий, разорванный переносом, собран в одну строку.
- Время обработки запроса в примере Ollama: в тексте оригинала указано «около 16 наносекунд», в самом выводе — 1 908 757 000 нс (около 1,9 с). Указано значение из вывода.
- В примере вызова функций модель указана как
phi3.5:3.8b(как в коде), а неllava-phi3, как было в тексте оригинала. - Пустая ссылка на скрипт
calc_distance_image.pyзаменена ссылкой на репозиторий EdgeML-with-Raspberry-Pi. - Опечатки исправлены: «Florense-2», «Movilinet», «Jupiter Notebook», «CRTL+X», «witch», «to detect bees» (в примере о коробках и колёсах) и «from 0 to 256» (исправлено на 0–255).
- В главе «Обнаружение объектов» заголовок проекта «Image Classification project» исправлен на «проект обнаружения объектов».
Дополнительные исправления после вычитки
Код:
While True:→while True:; в скетче со светодиодами (Nicla Vision, классификация изображений) типографские апострофы'uncertain’заменены на обычные, а лишний отступ передwhile True:убран.- В коде C++ для Grove Vision AI V2 комментарии
#заменены на//. - Строковые константы, разорванные переносом внутри
ei_printf(...)иSerial.printf(...)(Nicla Vision KWS, XIAO KWS), собраны из соседних литералов. sudo shutdown -h no→sudo shutdown -h now;ls -f→ls -l(длинный формат); вsudo apt-get install libatlas-base-dev\добавлен пробел перед обратным слэшем.- Имя модели в примере FOMO согласовано с командой переименования (
...-FOMO-int8.eim);/arduinor_rec.wav→/arduino_rec.wav;plt.title('...Welch's...')→ строка в двойных кавычках;{{в выводе Florence-2 →{; ключ<REGION_TO_DESCRIPTION>в выводе задачи «регион в описание». - Пути и команды с именем автора (
mjrovai) заменены наusername. - Опечатки в именах:
helloword.py→helloworld.py,HelloWord→HelloWorld,WiFI/WiFIScan/SimpleWiFIServer→WiFi/WiFiScan/SimpleWiFiServer, «Hello Word» → «Hello World»,LoRaWan→LoRaWAN,SSMA→SSCMA,YOLOV8→YOLOv8, «German Sheppard» → «German Shepherd», «Marvin Prison» → «Mervin Praison».
Числа и факты:
- Год выпуска MobileNetV1: в оригинале указан 2007, в переводе — 2017 (во всех главах про классификацию изображений).
- Число признаков для изображения : 27 640 → 27 648.
- Пиковый ток 14 мА при 5,23 В → 140 мА (при 730 мВт, XIAO) и 14 мА → 140 мА (0,7 Вт, Grove Vision AI V2, внешние светодиоды).
- Выводы I2S-микрофона XIAO: «IO41 (тактовый сигнал) и IO41 (данные)» → «IO42 (тактовый сигнал) и IO41 (данные)».
- Вывод
top_k_indices: пример с тремя индексами заменён пятью (283, 286, 282, 288, 479); размер входа92×92→96×96. - В итогах лабораторной по классификации изображений на Grove Vision AI V2: «архитектура Edge TPU» → «блок NPU Ethos-U55» (в модуле нет Edge TPU).
- Копипаст в оригинале: в XIAO «Обнаружение объектов» классы «коробка/колесо» заменены на «фрукт/жук»; в Grove «Когда XIAO подключён» заменено на Grove Vision AI (V2); в Grove повторявшийся «Шаг 3» перенумерован.
Ссылки и вёрстка:
- Ссылка на лабораторную Grove Vision AI V2 заменена относительной; в URL Colab-блокнота добавлено
%20. - Закомментированный рисунок
iic.pngв XIAO «Классификация изображений» возвращён в текст; удалены два служебных HTML-комментария, оставшихся после переноса из Quarto. - Заключённые в
<em>подписи к DALL·E больше не показывают литеральные звёздочки; цитаты после рисунков отделены пустой строкой, чтобы отображаться как цитаты.
Терминология (единообразно по книге и по глоссариям репозитория): VLM — «визуально-языковая модель»; wake word — «слово пробуждения»; fine-tuning — «дообучение»; no-code — «без кода»; SD card — «SD-карта»; Boards/Library Manager — «менеджер плат/библиотек»; power spectrum — «спектр мощности»; Impulse (Edge Impulse) не переводится. Десятичные дроби в тексте записаны через запятую; точка оставлена в версиях, идентификаторах моделей и значениях из интерфейсов и кода.
Что оставлено на языке оригинала
- Названия продуктов, компаний, библиотек и моделей: Grove Vision AI V2, XIAOML Kit, Nicla Vision, Raspberry Pi, Arduino IDE, SenseCraft AI, Edge Impulse, Roboflow, Ultralytics, Ollama, Hugging Face, Llama, Gemma, Phi, LLaVA, Florence-2, MobileNet, EfficientDet, YOLOv8, FOMO, TFLite, OpenCV, PyTorch, TensorFlow.
- Обозначения компонентов: ESP32-S3, XIAO ESP32S3 Sense, OV2640, MP34DT05, GC2145, STM32H747, Ethos-U55, PSRAM, QSPI, microSD, LSM6DS3TR-C, LSM6DSOX.
- Метрики и обозначения: mAP, IoU, NMS, FPS, CIDEr, recall@1, F1, int8, float32, Q4_0.
- Код, команды, имена файлов и переменных и подписи в веб-интерфейсах (например, Flask-страницы). Строки вывода программ оставлены на английском, кроме исключений, перечисленных выше.
- Промпты для моделей и ответы моделей (например,
What is the capital of France?). Модели обучены на английских запросах, поэтому они не переводились. - Цены в долларах (ориентировочные, как в оригинале).
- Символы ✓ в таблицах доступности и возможностей.
- Китайский иероглиф «小» в объяснении названия «XIAO»: рядом приведён перевод («крошечный»).
- Скриншоты интерфейсов сторонних программ (Edge Impulse Studio, Arduino IDE, SenseCraft AI, Roboflow, Raspberry Pi Imager, Ollama, htop и другие): текст на них остаётся на английском. Каждый рисунок имеет русскую подпись и альтернативный текст.
- Фотографии плат, устройств и изображения-иллюстрации: оставлены без изменений.
- Рисунки из научных статей и внешних публикаций (архитектура Florence-2 и распределение набора FLD-5B, сравнение LLM и SLM, графики бенчмарков). Их не перерисовывали: это чужие иллюстрации. У них русская подпись, исходный текст на рисунке остаётся английским.
- Составные схемы с фотографиями и логотипами:
kws_diagram.jpg(распознавание ключевых слов),objxclas.jpg(классификация и обнаружение объектов, на схеме фотографии яблока, апельсина и банана). Текст на них оставлен на английском; русские подписи и пояснения даны в тексте главы.
Что не перерисовано
- Растровые схемы, которые не перерисованы в SVG, в основном являются скриншотами программ и составными иллюстрациями, описанными выше. Часть растровых схем (например,
spectral_block.jpeg,pa_block.jpg,impulse-block.jpg,model_1.jpg,model_2.jpg) при переносе не просматривалась по отдельности. Их текст нужно проверить и при необходимости перерисовать.
Что требует внимания
- Цены и версии программного обеспечения (Raspberry Pi OS, Python 3.11, версии библиотек и моделей) ориентировочные. Перед работой проверьте их актуальность.
- Ссылки на блокноты и скрипты в репозитории авторов (GitHub), на страницы Edge Impulse, Hugging Face и YouTube указаны по оригиналу. Их доступность не проверялась.
- Правописание в этой книге с буквой «ё» («всё», «ещё», «развёртывание», «объём»).
- Расхождение, унаследованное от оригинала: в лабораторной по классификации изображений на Nicla Vision в коде светодиодов порог «неопределённого» класса равен 0,5, а в тексте говорится о 0,8.
- В примере RAG (Raspberry Pi, малые языковые модели) документы и запросы переведены на русский, а показанные результаты получены на английском наборе. Модель эмбеддингов
nomic-embed-textориентирована на английский язык, поэтому на русском корпусе качество поиска может отличаться.