С распространением и развитием нейронный сетей все чаще возникает потребность их использования на встроенных и маломощных устройствах, роботах и дронах. Устройство Neural Compute Stick в связке с фреймворком OpenVINO от компании Intel позволяет решить эту задачу, беря тяжелые вычисления нейросетей на себя. Благодаря этому можно без особых усилий запустить нейросетевой классификатор или детектор на маломощном устройстве вроде Raspberry Pi практически в реальном времени, при этом не сильно повышая энергопотребление. В данной публикации я расскажу, как использовать фреймворк OpenVINO (на C++) и Neural Compute Stick, чтобы запустить простую систему обнаружения лиц на Raspberry Pi.
Как обычно, весь код доступен на GitHub.
Летом 2017 года компания Intel выпустила устройство Neural Compute Stick (NCS), предназначенное для запуска нейронных сетей на маломощных устройствах, и уже через пару месяцев его можно было приобрести и испытать, что я и сделал. NCS представляет собой небольшой вычислительный модуль с корпусом лазурного цвета (выполняющим также роль радиатора), подключаемый к основному устройству по USB. Внутри, помимо всего прочего, находится Intel Myriad VPU, по сути являющийся 12-ядерным параллельным процессором, заточенным под операции, часто возникающие в нейросетях. NCS не пригодна для обучения нейросетей, но вот инференс в уже обученных нейросетях сравним по скорости с таковым на GPU. Все вычисления в NCS проводятся над 16-bit float числами, что позволяет повысить скорость. NCS для работы требуется всего 1 Ватт мощности, то есть при 5 В на USB разъеме потребляется ток до 200 мА — это даже меньше, чем у камеры для Raspberry Pi (250 мА).
Для работы с первой NCS использовался Neural Compute SDK (NCSDK): в него включены инструменты для компиляции нейросетей в форматах Caffe и TensorFlow в формат NCS, инструменты для измерения их производительности, а также Python и С++ API для инференса.
Затем была выпущена новая версия фреймворка для работы с NCS: NCSDK2. В ней довольно сильно изменился API, и хотя некоторые изменения показались мне странными, были и полезные нововведения. В частности, было добавлено автоматическое преобразование из float 32 bit в float 16 bit в C++ (раньше для этого приходилось вставлять костыли в виде кода из Numpy). Также появились очереди изображений и результатов их обработки.
В мае 2018 Intel выпустила OpenVINO (который ранее именовался Intel Computer Vision SDK). Этот фреймворк предназначен для эффективного запуска нейросетей на различных устройствах: процессорах и графических картах Intel, FPGA, а также Neural Compute Stick.
В ноябре 2018 увидела свет новая версия ускорителя: Neural Compute Stick 2. Вычислительная мощность устройства была повышена: в описании на сайте обещают ускорение до 8x, однако новую версию устройства мне не довелось протестировать. Ускорение достигается за счет увеличения числа ядер с 12 до 16, а также добавления новых вычислительных устройств, оптимизированных под нейросети. Правда, про потребляемую мощность информации я не нашел.
Вторая версия NCS уже несовместима с NCSDK или NCSDK2: их полномочия перешли OpenVINO, который способен помимо обеих версий NCS работать с множеством других устройств. Сам OpenVINO обладает огромным функционалом и включает следующие компоненты:
В своих предыдущих статьях я рассказывал о том, как запустить детектор лиц YOLO на NCS (первая статья), а также о том, как обучить свой SSD детектор лиц и запустить его на Raspberry Pi и NCS (вторая статья). В этих статьях я использовал NCSDK и NCSDK2. В данной статье я расскажу, как проделать нечто похожее, но уже с помощью OpenVINO, проведу небольшое сравнение как разных детекторов лиц, так и двух фреймворков для их запуска, и укажу на некоторые подводные камни. Я пишу на C++, так как верю, что таким способом можно добиться большей производительности, что будет важно в случае Raspberry Pi.
Не самая сложная задача, хотя есть тонкости. OpenVINO на момент написания статьи поддерживает только Ubuntu 16.04 LTS, CentOS 7.4 и Windows 10. У меня стоит Ubuntu 18, и для установки в ней нужны небольшие костыли. Также я хотел сравнить OpenVINO с NCSDK2, с установкой которого тоже есть проблемы: в частности, он подтягивает свои версии Caffe и TensorFlow и может слегка поломать настройки окружения. В итоге я решил пойти по простому пути и установить оба фреймворка в виртуальную машину с Ubuntu 16 (я использую VirtualBox).
Стоит заметить, что для успешного подключения NCS к виртуальной машине нужно установить гостевые дополнения VirtualBox и включить поддержку USB 3.0. Также я добавил универсальный фильтр USB устройств, в результате чего NCS подключалась без проблем (хотя веб-камеру все еще приходится подключать в настройках виртуальной машины). Для установки и компиляции OpenVINO нужно завести учетную запись Intel, выбрать вариант фреймворка (с поддержкой FPGA или без) и следовать инструкциям. С NCSDK еще проще: он загружается с GitHub (не забудьте выбрать ветку ncsdk2 для новой версии фреймворка), после чего нужно сделать
Единственная проблема, с которой я столкнулся при запуске NCSDK2 в виртуальной машине, это ошибка следующего вида:
Она возникает в конце корректного выполнения программы и (вроде) ни на что не влияет. Судя по всему, это небольшой баг, связанный с VM (на Raspberry такого не должно быть).
Установка на Raspberry Pi существенно отличается. Для начала убедитесь, что у вас стоит Raspbian Stretch: оба фреймворка официально работают только на этой ОС. NCSDK2 нужно скомпилировать в API-only режиме, иначе он попытается установить Caffe и TensorFlow, что вряд ли понравится вашей Raspberry. В случае OpenVINO есть уже собранная версия для Raspberry, которую нужно лишь распаковать и настроить переменные окружения. В этой версии есть только C++ и Python API, а также библиотека OpenCV, все остальные инструменты недоступны. Это значит, что для обоих фреймворков модели нужно конвертировать заранее на машине с Ubuntu. Моя демо-программа с обнаружением лиц работает как на Raspberry, так и на десктопе, поэтому я просто добавил конвертированные файлы нейросетей в свой репозиторий на GitHub, чтобы их было проще синхронизировать с Raspberry. У меня Raspberry Pi 2 model B, но должно взлететь и с другими моделями.
Есть еще одна тонкость, касающаяся взаимодействия Raspberry Pi и Neural Compute Stick: если в случае ноутбука достаточно просто ткнуть NCS в ближайший USB 3.0 порт, то для Raspberry придется найти USB кабель, иначе NSC своим корпусом заблокирует оставшиеся три USB разъема. Также стоит помнить, что на Raspberry все USB версии 2.0, поэтому скорость инференса будет ниже из-за задержек коммуникации (подробное сравнение будет позже). А вот если вы захотите подсоединить к Raspberry две или больше NCS, скорее всего, придется найти USB-hub с дополнительными питанием.
Довольно громоздко. Нужно сделать много разных действий, начиная с загрузки плагина и заканчивая самим инференсом — поэтому я написал класс-обертку для детектора. Полный код можно посмотреть на GitHub, а здесь я просто перечислю основные моменты. Начнем по порядку:
Определения всех нужных нам функций находятся в файле
Следующие переменные будут нужны постоянно.
Теперь загрузим необходимый плагин — нам нужен тот, что отвечает за NCS и NCS2, его можно получить по имени «MYRIAD». Напомню, что в контексте OpenVINO плагин — это просто динамическая библиотека, подключающаяся по явному запросу. Параметром функции
Теперь создадим объект для загрузки нейросети, считаем ее описание и установим размер батча (число одновременно обрабатываемых изображений). Нейросеть в формате OpenVINO задается двумя файлами: .xml с описанием структуры и .bin с весами. Пока будем использовать готовые детекторы из OpenVINO, позже создадим свой. Здесь
Далее происходит следующее:
Теперь самый важный момент: загружаем нейросеть в плагин (то есть, в NCS). Судя по всему, компиляция в нужный формат происходит налету. Если на этой функции программа падает, вероятно, нейросеть не подходит для данного устройства.
И напоследок — произведем пробный инференс и получим размеры входа (возможно, это можно сделать и более изящно). Сначала открываем запрос на инференс, затем от него получаем ссылку на входной блок данных, и уже у него запрашиваем размер.
Попробуем загрузить картинку в NCS. Точно так же создаем запрос на инференс, от него получаем указатель на блок данных, и уже оттуда достаем указатель на сам массив. Далее просто копируем данные из нашей картинки (здесь она уже приведена к нужному размеру). Стоит заметить, что в
Зачем асинхронный? Это позволит оптимизировать распределение ресурсов. Пока NCS считает нейросеть, можно обрабатывать следующий кадр — это приведет к заметному ускорению на Raspberry Pi.
Если вы хорошо знакомы с нейросетями, у вас мог возникнуть вопрос о том, в какой момент мы масштабируем значения входных пикселей нейросети (например, приводим к диапазону ). Дело в том, что в моделях OpenVINO это преобразование уже включено в описание нейросети, а при использовании своего детектора мы сделаем что-то похожее. А поскольку и конвертацию в float, и масштабирование входов производит OpenVINO, нам остается только изменить размер изображения.
Теперь (после выполнения некоторой полезной работы) завершим запрос на инференс. Программа блокируется, пока не придут результаты выполнения. Получаем указатель на результат.
Теперь самое время задуматься о том, в каком формате NCS возвращает результат работы детектора. Стоит заметить, что формат немного отличается от того, что был при использовании NCSDK. Вообще говоря, выход детектора четырехмерный и имеет размерность (1 x 1 x максимальное число детекций x 7), можно считать, что это массив размера (
Параметр
Семь значений в описании одной детекции представляют собой следующее:
Теперь о том, как выглядит общая схема инференса в реальном времени. Сначала инициализируем нейросеть и камеру, заводим
В примерах InferenceEngine мне не понравились громоздкие CMake файлы, и я решил компактно переписать все в свой Makefile:
Эта команда будет работать как на Ubuntu, так и на Raspbian, благодаря паре трюков. Пути для поиска заголовков и динамических библиотек я указал и для Raspberry, и для машины с Ubuntu. Из библиотек, помимо OpenCV, надо подключить также
Отдельно стоит отметить, что при компиляции на Raspberry нужен флаг
NCS поддерживает из коробки только SSD детекторы в формате Caffe, хотя с помощью пары грязных трюков мне удавалось запустить на ней YOLO из формата Darknet. Single Shot Detector (SSD) является популярной архитектурой среди легковесных нейросеток, а с помощью разных энкодеров (или backbone сетей) можно достаточно гибко варьировать соотношение скорости и качества.
Я буду экспериментировать с разными детекторами лиц:
Для детекторов из OpenVINO нет весов ни в формате Caffe, ни в формате NCSDK, поэтому их я смогу запустить только в OpenVINO.
У меня есть два файла в формате Caffe: .prototxt с описанием сети и .caffemodel с весами. Мне нужно получить из них два файла в формате OpenVINO: .xml и .bin с описанием и весами соответственно. Для этого необходимо использовать скрипт mo.py из OpenVINO (он же Model Optimizer):
В данном случае происходит приведение значений из диапазона в диапазон . Вообще у этого скрипта очень много параметров, некоторые из которых специфичны для отдельных фреймворков, рекомендую посмотреть мануал к скрипту.
В дистрибутиве OpenVINO для Raspberry нет готовых моделей, но их достаточно просто скачать.
Я использовал три варианта сравнения: 1) NCS + Виртуальная машина с Ubuntu 16.04, процессор Core i7, разъем USB 3.0; 2) NCS + Та же машина, разъем USB 3.0 + кабель USB 2.0 (будут больше задержки на обмен с устройством); 3) NCS + Raspberry Pi 2 model B, Raspbian Stretch, разъем USB 2.0 + кабель USB 2.0.
Свой детектор я запускал как с OpenVINO, так и с NCSDK2, детекторы из OpenVINO только с их родным фреймворком, YOLO — только с NCSDK2 (скорее всего, его можно запустить и на OpenVINO).
Таблица FPS для разных детекторов выглядит так (числа приблизительные):
Примечание: производительность измерялась для всей демо-программы целиком, включая обработку и визуализацию кадров.
YOLO оказался самым медленным и самым неустойчивым из всех. Он очень часто пропускает детекции и не может работать с засвеченными кадрами.
Детектор, который я обучал, работает вдвое быстрее, более устойчив к искажениям на кадрах и обнаруживает даже мелкие лица. Тем не менее, он все равно иногда пропускает детекции, а иногда обнаруживает ложные. Если отрезать от него несколько последних слоев, он станет чуть быстрее, но крупные лица видеть перестанет. Тот же детектор, запущенный через OpenVINO, становится немного быстрее при использовании USB 2.0, качество визуально не меняется.
Детекторы из OpenVINO, конечно, намного превосходят и YOLO, и мой детектор. (Я бы даже не стал обучать свой детектор, если бы OpenVINO существовал в его текущем виде в то время). Модель retail-0004 существенно быстрее и при этом практически не пропускает лица, но зато мне удалось ее слегка обмануть (хотя confidence у этих детекций низкий):
Соревновательная атака естественного интеллекта на искусственный
Детектор adas-0001 существенно медленнее, но при этом работает с изображениями большого размера и должен быть точнее. Я разницы не заметил, но проверял я на довольно простых кадрах.
В целом, очень приятно, что на маломощном устройстве вроде Raspberry Pi можно использовать нейросети, да еще и почти в реальном времени. OpenVINO предоставляет очень обширный функционал для инференса нейросетей на множестве разных устройств — гораздо шире, чем я описал в статье. Думаю, Neural Compute Stick и OpenVINO будут очень полезны в моих робототехнических изысканиях.
Как обычно, весь код доступен на GitHub.
Немного о Neural Compute Stick и OpenVINO
Летом 2017 года компания Intel выпустила устройство Neural Compute Stick (NCS), предназначенное для запуска нейронных сетей на маломощных устройствах, и уже через пару месяцев его можно было приобрести и испытать, что я и сделал. NCS представляет собой небольшой вычислительный модуль с корпусом лазурного цвета (выполняющим также роль радиатора), подключаемый к основному устройству по USB. Внутри, помимо всего прочего, находится Intel Myriad VPU, по сути являющийся 12-ядерным параллельным процессором, заточенным под операции, часто возникающие в нейросетях. NCS не пригодна для обучения нейросетей, но вот инференс в уже обученных нейросетях сравним по скорости с таковым на GPU. Все вычисления в NCS проводятся над 16-bit float числами, что позволяет повысить скорость. NCS для работы требуется всего 1 Ватт мощности, то есть при 5 В на USB разъеме потребляется ток до 200 мА — это даже меньше, чем у камеры для Raspberry Pi (250 мА).
Для работы с первой NCS использовался Neural Compute SDK (NCSDK): в него включены инструменты для компиляции нейросетей в форматах Caffe и TensorFlow в формат NCS, инструменты для измерения их производительности, а также Python и С++ API для инференса.
Затем была выпущена новая версия фреймворка для работы с NCS: NCSDK2. В ней довольно сильно изменился API, и хотя некоторые изменения показались мне странными, были и полезные нововведения. В частности, было добавлено автоматическое преобразование из float 32 bit в float 16 bit в C++ (раньше для этого приходилось вставлять костыли в виде кода из Numpy). Также появились очереди изображений и результатов их обработки.
В мае 2018 Intel выпустила OpenVINO (который ранее именовался Intel Computer Vision SDK). Этот фреймворк предназначен для эффективного запуска нейросетей на различных устройствах: процессорах и графических картах Intel, FPGA, а также Neural Compute Stick.
В ноябре 2018 увидела свет новая версия ускорителя: Neural Compute Stick 2. Вычислительная мощность устройства была повышена: в описании на сайте обещают ускорение до 8x, однако новую версию устройства мне не довелось протестировать. Ускорение достигается за счет увеличения числа ядер с 12 до 16, а также добавления новых вычислительных устройств, оптимизированных под нейросети. Правда, про потребляемую мощность информации я не нашел.
Вторая версия NCS уже несовместима с NCSDK или NCSDK2: их полномочия перешли OpenVINO, который способен помимо обеих версий NCS работать с множеством других устройств. Сам OpenVINO обладает огромным функционалом и включает следующие компоненты:
- Model Optimizer: Python скрипт, позволяющий конвертировать нейросети из популярных фреймворков для глубокого обучения в универсальный формат OpenVINO. Список поддерживаемых фреймворков: Caffe, TensorFlow, MXNET, Kaldi (фреймворк для распознавания речи), ONNX (открытый формат представления нейросетей).
- Inference Engine: C++ и Python API для инференса нейронных сетей, абстрагированный от конкретного устройства инференса. Код API будет выглядеть почти идентично для CPU, GPU, FPGA и NCS.
- Набор плагинов для разных устройств. Плагины являются динамическими библиотеками, подгружаемыми в явном виде в коде основной программы. Нас больше всего интересует плагин для NCS.
- Набор предобученных моделей в универсальном формате OpenVINO (полный список здесь). Внушительная коллекция качественных нейросеток: детекторы лиц, пешеходов, объектов; распознавание ориентации лиц, особых точек лиц, позы человека; super resolution; и другие. Стоит заметить, что не все они поддерживаются NCS/FPGA/GPU.
- Model Downloader: еще один скрипт, упрощающий загрузку моделей в формате OpenVINO по сети (хотя можно легко обойтись и без него).
- Библиотека компьютерного зрения OpenCV, оптимизированная под аппаратуру Intel.
- Библиотека компьютерного зрения OpenVX.
- Intel Compute Library for Deep Neural Networks.
- Intel Math Kernel Library for Deep Neural Networks.
- Инструмент для оптимизации нейросетей под FPGA (опционально).
- Документация и примеры программ.
В своих предыдущих статьях я рассказывал о том, как запустить детектор лиц YOLO на NCS (первая статья), а также о том, как обучить свой SSD детектор лиц и запустить его на Raspberry Pi и NCS (вторая статья). В этих статьях я использовал NCSDK и NCSDK2. В данной статье я расскажу, как проделать нечто похожее, но уже с помощью OpenVINO, проведу небольшое сравнение как разных детекторов лиц, так и двух фреймворков для их запуска, и укажу на некоторые подводные камни. Я пишу на C++, так как верю, что таким способом можно добиться большей производительности, что будет важно в случае Raspberry Pi.
Установка OpenVINO
Не самая сложная задача, хотя есть тонкости. OpenVINO на момент написания статьи поддерживает только Ubuntu 16.04 LTS, CentOS 7.4 и Windows 10. У меня стоит Ubuntu 18, и для установки в ней нужны небольшие костыли. Также я хотел сравнить OpenVINO с NCSDK2, с установкой которого тоже есть проблемы: в частности, он подтягивает свои версии Caffe и TensorFlow и может слегка поломать настройки окружения. В итоге я решил пойти по простому пути и установить оба фреймворка в виртуальную машину с Ubuntu 16 (я использую VirtualBox).
Стоит заметить, что для успешного подключения NCS к виртуальной машине нужно установить гостевые дополнения VirtualBox и включить поддержку USB 3.0. Также я добавил универсальный фильтр USB устройств, в результате чего NCS подключалась без проблем (хотя веб-камеру все еще приходится подключать в настройках виртуальной машины). Для установки и компиляции OpenVINO нужно завести учетную запись Intel, выбрать вариант фреймворка (с поддержкой FPGA или без) и следовать инструкциям. С NCSDK еще проще: он загружается с GitHub (не забудьте выбрать ветку ncsdk2 для новой версии фреймворка), после чего нужно сделать
make install
.Единственная проблема, с которой я столкнулся при запуске NCSDK2 в виртуальной машине, это ошибка следующего вида:
E: [ 0] dispatcherEventReceive:236 dispatcherEventReceive() Read failed -1
E: [ 0] eventReader:254 Failed to receive event, the device may have reset
Она возникает в конце корректного выполнения программы и (вроде) ни на что не влияет. Судя по всему, это небольшой баг, связанный с VM (на Raspberry такого не должно быть).
Установка на Raspberry Pi существенно отличается. Для начала убедитесь, что у вас стоит Raspbian Stretch: оба фреймворка официально работают только на этой ОС. NCSDK2 нужно скомпилировать в API-only режиме, иначе он попытается установить Caffe и TensorFlow, что вряд ли понравится вашей Raspberry. В случае OpenVINO есть уже собранная версия для Raspberry, которую нужно лишь распаковать и настроить переменные окружения. В этой версии есть только C++ и Python API, а также библиотека OpenCV, все остальные инструменты недоступны. Это значит, что для обоих фреймворков модели нужно конвертировать заранее на машине с Ubuntu. Моя демо-программа с обнаружением лиц работает как на Raspberry, так и на десктопе, поэтому я просто добавил конвертированные файлы нейросетей в свой репозиторий на GitHub, чтобы их было проще синхронизировать с Raspberry. У меня Raspberry Pi 2 model B, но должно взлететь и с другими моделями.
Есть еще одна тонкость, касающаяся взаимодействия Raspberry Pi и Neural Compute Stick: если в случае ноутбука достаточно просто ткнуть NCS в ближайший USB 3.0 порт, то для Raspberry придется найти USB кабель, иначе NSC своим корпусом заблокирует оставшиеся три USB разъема. Также стоит помнить, что на Raspberry все USB версии 2.0, поэтому скорость инференса будет ниже из-за задержек коммуникации (подробное сравнение будет позже). А вот если вы захотите подсоединить к Raspberry две или больше NCS, скорее всего, придется найти USB-hub с дополнительными питанием.
Как выглядит код OpenVINO
Довольно громоздко. Нужно сделать много разных действий, начиная с загрузки плагина и заканчивая самим инференсом — поэтому я написал класс-обертку для детектора. Полный код можно посмотреть на GitHub, а здесь я просто перечислю основные моменты. Начнем по порядку:
Определения всех нужных нам функций находятся в файле
inference_engine.hpp
в пространстве имен InferenceEngine
.#include <inference_engine.hpp>
using namespace InferenceEngine;
Следующие переменные будут нужны постоянно.
inputName
и outputName
нам нужны для того, чтобы адресовать вход и выход нейросети. Вообще говоря, у нейросети может быть много входов и выходов, но в наших детекторах их будет по одному. Переменная net
— это сама сеть, request
— указатель на последний запрос инференса, inputBlob
— указатель на массив данных входа нейросети. Остальные переменные говорят сами за себя.string inputName;
string outputName;
ExecutableNetwork net;
InferRequest::Ptr request;
Blob::Ptr inputBlob;
//input shape
int netInputWidth;
int netInputHeight;
int netInputChannels;
//output shape
int maxNumDetectedFaces;
//return code
StatusCode ncsCode;
Теперь загрузим необходимый плагин — нам нужен тот, что отвечает за NCS и NCS2, его можно получить по имени «MYRIAD». Напомню, что в контексте OpenVINO плагин — это просто динамическая библиотека, подключающаяся по явному запросу. Параметром функции
PluginDispatcher
является список директорий, в которых следует искать плагины. Если вы настроили переменные среды по инструкции, пустой строки будет достаточно. Для справки, плагины находятся в [OpenVINO_install_dir]/deployment_tools/inference_engine/lib/ubuntu_16.04/intel64/
InferencePlugin plugin = PluginDispatcher({""}).getPluginByDevice("MYRIAD");
Теперь создадим объект для загрузки нейросети, считаем ее описание и установим размер батча (число одновременно обрабатываемых изображений). Нейросеть в формате OpenVINO задается двумя файлами: .xml с описанием структуры и .bin с весами. Пока будем использовать готовые детекторы из OpenVINO, позже создадим свой. Здесь
std::string filename
— это имя файла без расширения. Также нужно иметь в виду, что NCS поддерживает размер батча только равный 1. CNNNetReader netReader;
netReader.ReadNetwork(filename+".xml");
netReader.ReadWeights(filename+".bin");
netReader.getNetwork().setBatchSize(1);
Далее происходит следующее:
- Для входа нейросети устанавливаем тип данных unsigned char 8 bit. Это значит, что мы можем подавать на вход изображение в таком формате, в котором оно приходит с камеры, а InferenceEngine позаботится о конвертации (NCS производит вычисления в формате float 16 bit). Это позволит немного ускориться на Raspberry Pi — как я понял, конвертация производится на NCS, поэтому задержки на передачу данных по USB меньше.
- Получаем имена входа и выхода, чтобы потом обращаться к ним.
- Получаем описание выходов (это map из имени выхода в указатель на блок данных). Получаем указатель на блок данных первого (единственного) выхода.
- Получаем его размер: 1 x 1 x максимальное число детекций x длина описания детекции (7). О формате описания детекций — позже.
- Устанавливаем формат выхода в float 32 bit. Опять же, преобразование из float 16 bit берет на себя InferenceEngine.
//we can set input type to unsigned char: conversion will be performed on device
netReader.getNetwork().getInputsInfo().begin()->second->setPrecision(Precision::U8);
//get input and output names and their info structures
inputName = netReader.getNetwork().getInputsInfo().begin()->first;
outputName = netReader.getNetwork().getOutputsInfo().begin()->first;
OutputsDataMap outputInfo(netReader.getNetwork().getOutputsInfo());
InputsDataMap inputInfo(netReader.getNetwork().getInputsInfo());
DataPtr &outputData = (outputInfo.begin()->second);
//get output shape: (1 x 1 x maxNumDetectedFaces x faceDescriptionLength(7))
const SizeVector outputDims = outputData->getTensorDesc().getDims();
maxNumDetectedFaces = outputDims[2];
//set input type to float32: calculations are all in float16, conversion is performed on device
outputData->setPrecision(Precision::FP32);
Теперь самый важный момент: загружаем нейросеть в плагин (то есть, в NCS). Судя по всему, компиляция в нужный формат происходит налету. Если на этой функции программа падает, вероятно, нейросеть не подходит для данного устройства.
net = plugin.LoadNetwork(netReader.getNetwork(), {});
И напоследок — произведем пробный инференс и получим размеры входа (возможно, это можно сделать и более изящно). Сначала открываем запрос на инференс, затем от него получаем ссылку на входной блок данных, и уже у него запрашиваем размер.
//perform single inference to get input shape (a hack)
request = net.CreateInferRequestPtr(); //open inference request
//we need the blob size: (batch(1) x channels(3) x H x W)
inputBlob = request->GetBlob(inputName);
SizeVector blobSize = inputBlob->getTensorDesc().getDims();
netInputWidth = blobSize[3];
netInputHeight = blobSize[2];
netInputChannels = blobSize[1];
request->Infer(); //close request
Попробуем загрузить картинку в NCS. Точно так же создаем запрос на инференс, от него получаем указатель на блок данных, и уже оттуда достаем указатель на сам массив. Далее просто копируем данные из нашей картинки (здесь она уже приведена к нужному размеру). Стоит заметить, что в
cv::Mat
и inputBlob
измерения хранятся в разном порядке (в OpenCV индекс канала меняется быстрее всех, в OpenVINO — медленнее всех), поэтому одним memcpy не обойтись. Затем начинаем асинхронный инференс. Зачем асинхронный? Это позволит оптимизировать распределение ресурсов. Пока NCS считает нейросеть, можно обрабатывать следующий кадр — это приведет к заметному ускорению на Raspberry Pi.
cv::Mat data;
... //get image somehow
//create request, get data blob
request = net.CreateInferRequestPtr();
inputBlob = request->GetBlob(inputName);
unsigned char* blobData = inputBlob->buffer().as<unsigned char*>();
//copy from resized frame to network input
int wh = netInputHeight*netInputWidth;
for (int c = 0; c < netInputChannels; c++)
for (int h = 0; h < wh; h++)
blobData[c * wh + h] = data.data[netInputChannels*h + c];
//start asynchronous inference
request->StartAsync();
Если вы хорошо знакомы с нейросетями, у вас мог возникнуть вопрос о том, в какой момент мы масштабируем значения входных пикселей нейросети (например, приводим к диапазону ). Дело в том, что в моделях OpenVINO это преобразование уже включено в описание нейросети, а при использовании своего детектора мы сделаем что-то похожее. А поскольку и конвертацию в float, и масштабирование входов производит OpenVINO, нам остается только изменить размер изображения.
Теперь (после выполнения некоторой полезной работы) завершим запрос на инференс. Программа блокируется, пока не придут результаты выполнения. Получаем указатель на результат.
float * output;
ncsCode = request->Wait(IInferRequest::WaitMode::RESULT_READY);
output = request->GetBlob(outputName)->buffer().as<float*>();
Теперь самое время задуматься о том, в каком формате NCS возвращает результат работы детектора. Стоит заметить, что формат немного отличается от того, что был при использовании NCSDK. Вообще говоря, выход детектора четырехмерный и имеет размерность (1 x 1 x максимальное число детекций x 7), можно считать, что это массив размера (
maxNumDetectedFaces
x 7). Параметр
maxNumDetectedFaces
задается в описании нейросети, и его несложно изменить, например, в .prototxt описании сети в формате Caffe. Ранее мы получили его из объекта, представляющего детектор. Этот параметр связан со спецификой работы класса детекторов SSD (Single Shot Detector), к которому относятся все поддерживаемые NCS детекторы. SSD всегда рассматривает одинаковое (и очень большое) число ограничивающих рамок для каждого изображения, а после отсеивания детекций с низкой оценкой уверенности и удаления перекрывающихся рамок с помощью Non-maximum Suppression обычно оставляют 100-200 лучших. Именно за это и отвечает параметр.Семь значений в описании одной детекции представляют собой следующее:
- номер изображения в батче, на котором обнаружен объект (в нашем случае должен быть равен нулю);
- класс объекта (0 — фон, начиная с 1 — остальные классы, возвращаются только детекции с положительным классом);
- уверенность в наличии детекции (в диапазоне );
- нормированная x-координата верхнего левого угла ограничивающей рамки (в диапазоне );
- аналогично — y-координата;
- нормированная ширина ограничивающей рамки (в диапазоне );
- аналогично — высота;
Код извлечения ограничивающих рамок из выхода детектора
void get_detection_boxes(const float* predictions,
int numPred, int w, int h, float thresh,
std::vector<float>& probs,
std::vector<cv::Rect>& boxes)
{
float score = 0;
float cls = 0;
float id = 0;
//predictions holds numPred*7 values
//data format: image_id, detection_class, detection_confidence,
//box_normed_x, box_normed_y, box_normed_w, box_normed_h
for (int i=0; i<numPred; i++)
{
score = predictions[i*7+2];
cls = predictions[i*7+1];
id = predictions[i*7 ];
if (id>=0 && score>thresh && cls<=1)
{
probs.push_back(score);
boxes.push_back(Rect(predictions[i*7+3]*w, predictions[i*7+4]*h,
(predictions[i*7+5]-predictions[i*7+3])*w,
(predictions[i*7+6]-predictions[i*7+4])*h));
}
}
}
numPred
мы узнаем из самого детектора, а w,h
— размеры изображения для визуализации.Теперь о том, как выглядит общая схема инференса в реальном времени. Сначала инициализируем нейросеть и камеру, заводим
cv::Mat
для сырых кадров и еще один для кадров, приведенных к нужному размеру. Заполняем наши кадры нулями — это прибавит уверенности в том, что на холостом запуске нейросеть ничего не найдет. Затем запускаем цикл инференса:- Загружаем текущий кадр в нейросеть с помощью асинхронного запроса — NCS уже начала работать, и в это время у нас есть возможность сделать полезную работу основным процессором.
- Отображаем все предыдущие детекции на предыдущем кадре, рисуем кадр (если надо).
- Получаем новый кадр с камеры, сжимаем его до нужного размера. Для Raspberry я рекомендую использовать самый простой алгоритм изменения размера — в OpenCV это Nearest neighbors interpolation. На качество работы детектора это не сильно повлияет, но может накинуть немного скорости. Также я зеркально отражаю кадр для удобства визуализации (опционально).
- Теперь самое время получить результат с NCS, завершив запрос на инференс. Программа заблокируется до получения результата.
- Обрабатываем новые детекции, выделяем рамки.
- Остальное: отработка нажатий клавиш, подсчет кадров и др.
Как это компилировать
В примерах InferenceEngine мне не понравились громоздкие CMake файлы, и я решил компактно переписать все в свой Makefile:
g++ $(RPI_ARCH) \
-I/usr/include -I. \
-I$(OPENVINO_PATH)/deployment_tools/inference_engine/include \
-I$(OPENVINO_PATH_RPI)/deployment_tools/inference_engine/include \
-L/usr/lib/x86_64-linux-gnu \
-L/usr/local/lib \
-L$(OPENVINO_PATH)/deployment_tools/inference_engine/lib/ubuntu_16.04/intel64 \
-L$(OPENVINO_PATH_RPI)/deployment_tools/inference_engine/lib/raspbian_9/armv7l \
vino.cpp wrapper/vino_wrapper.cpp \
-o demo -std=c++11 \
`pkg-config opencv --cflags --libs` \
-ldl -linference_engine $(RPI_LIBS)
Эта команда будет работать как на Ubuntu, так и на Raspbian, благодаря паре трюков. Пути для поиска заголовков и динамических библиотек я указал и для Raspberry, и для машины с Ubuntu. Из библиотек, помимо OpenCV, надо подключить также
libinference_engine
и libdl
— библиотеку для динамической линковки других библиотек, она нужна, чтобы сработала загрузка плагина. При этом сам libmyriadPlugin
указывать не надо. Помимо прочего, для Raspberry я также подключаю библиотеку Raspicam для работы с камерой (это $(RPI_LIBS)
). Также пришлось использовать стандарт C++11.Отдельно стоит отметить, что при компиляции на Raspberry нужен флаг
-march=armv7-a
(это $(RPI_ARCH)
). Если его не указать, программа скомпилируется, но будет падать с тихим сегфолтом. А еще можно добавить оптимизации с помощью -O3
, это прибавит скорости.Какие есть детекторы
NCS поддерживает из коробки только SSD детекторы в формате Caffe, хотя с помощью пары грязных трюков мне удавалось запустить на ней YOLO из формата Darknet. Single Shot Detector (SSD) является популярной архитектурой среди легковесных нейросеток, а с помощью разных энкодеров (или backbone сетей) можно достаточно гибко варьировать соотношение скорости и качества.
Я буду экспериментировать с разными детекторами лиц:
- YOLO, взятый отсюда, конвертированный сначала в формат Caffe, затем в формат NCS (только с NCSDK). Изображение 448 x 448.
- Свой Mobilenet + SSD детектор, про обучение которого я рассказывал в предыдущей публикации. У меня еще есть подрезанный вариант этого детектора, который видит только мелкие лица, и при этом чуть быстрее. Полную версию своего детектора я проверю и на NCSDK, и на OpenVINO. Изображение 300 x 300.
- Детектор face-detection-adas-0001 из OpenVINO: MobileNet + SSD. Изображение 384 x 672.
- Детектор face-detection-retail-0004 из OpenVINO: легковесный SqueezeNet + SSD. Изображение 300 x 300.
Для детекторов из OpenVINO нет весов ни в формате Caffe, ни в формате NCSDK, поэтому их я смогу запустить только в OpenVINO.
Трансформируем свой детектор в формат OpenVINO
У меня есть два файла в формате Caffe: .prototxt с описанием сети и .caffemodel с весами. Мне нужно получить из них два файла в формате OpenVINO: .xml и .bin с описанием и весами соответственно. Для этого необходимо использовать скрипт mo.py из OpenVINO (он же Model Optimizer):
mo.py \
--framework caffe \
--input_proto models/face/ssd-face.prototxt \
--input_model models/face/ssd-face.caffemodel \
--output_dir models/face \
--model_name ssd-vino-custom \
--mean_values [127.5,127.5,127.5] \
--scale_values [127.5,127.5,127.5] \
--data_type FP16
output_dir
задает директорию, в которой будут созданы новые файлы, model_name
— имя для новых файлов без расширения, data_type (FP16/FP32)
— тип весов в нейросети (NCS поддерживает только FP16). Параметры mean_values, scale_values
задают среднее и масштаб для предобработки изображений перед их запуском в нейросеть. Конкретное преобразование выглядит так:В данном случае происходит приведение значений из диапазона в диапазон . Вообще у этого скрипта очень много параметров, некоторые из которых специфичны для отдельных фреймворков, рекомендую посмотреть мануал к скрипту.
В дистрибутиве OpenVINO для Raspberry нет готовых моделей, но их достаточно просто скачать.
Например, вот так.
wget --no-check-certificate \
https://download.01.org/openvinotoolkit/2018_R4/open_model_zoo/face-detection-retail-0004/FP16/face-detection-retail-0004.xml \
-O ./models/face/vino.xml; \
wget --no-check-certificate \
https://download.01.org/openvinotoolkit/2018_R4/open_model_zoo/face-detection-retail-0004/FP16/face-detection-retail-0004.bin \
-O ./models/face/vino.bin
Сравнение детекторов и фреймворков
Я использовал три варианта сравнения: 1) NCS + Виртуальная машина с Ubuntu 16.04, процессор Core i7, разъем USB 3.0; 2) NCS + Та же машина, разъем USB 3.0 + кабель USB 2.0 (будут больше задержки на обмен с устройством); 3) NCS + Raspberry Pi 2 model B, Raspbian Stretch, разъем USB 2.0 + кабель USB 2.0.
Свой детектор я запускал как с OpenVINO, так и с NCSDK2, детекторы из OpenVINO только с их родным фреймворком, YOLO — только с NCSDK2 (скорее всего, его можно запустить и на OpenVINO).
Таблица FPS для разных детекторов выглядит так (числа приблизительные):
Model | USB 3.0 | USB 2.0 | Raspberry Pi |
---|---|---|---|
Custom SSD with NCSDK2 | 10.8 | 9.3 | 7.2 |
Custom longrange SSD with NCSDK2 | 11.8 | 10.0 | 7.3 |
YOLO v2 with NCSDK2 | 5.3 | 4.6 | 3.6 |
Custom SSD with OpenVINO | 10.6 | 9.9 | 7.9 |
OpenVINO face-detection-retail-0004 | 15.6 | 14.2 | 9.3 |
OpenVINO face-detection-adas-0001 | 5.8 | 5.5 | 3.9 |
Примечание: производительность измерялась для всей демо-программы целиком, включая обработку и визуализацию кадров.
YOLO оказался самым медленным и самым неустойчивым из всех. Он очень часто пропускает детекции и не может работать с засвеченными кадрами.
Детектор, который я обучал, работает вдвое быстрее, более устойчив к искажениям на кадрах и обнаруживает даже мелкие лица. Тем не менее, он все равно иногда пропускает детекции, а иногда обнаруживает ложные. Если отрезать от него несколько последних слоев, он станет чуть быстрее, но крупные лица видеть перестанет. Тот же детектор, запущенный через OpenVINO, становится немного быстрее при использовании USB 2.0, качество визуально не меняется.
Детекторы из OpenVINO, конечно, намного превосходят и YOLO, и мой детектор. (Я бы даже не стал обучать свой детектор, если бы OpenVINO существовал в его текущем виде в то время). Модель retail-0004 существенно быстрее и при этом практически не пропускает лица, но зато мне удалось ее слегка обмануть (хотя confidence у этих детекций низкий):
Соревновательная атака естественного интеллекта на искусственный
Детектор adas-0001 существенно медленнее, но при этом работает с изображениями большого размера и должен быть точнее. Я разницы не заметил, но проверял я на довольно простых кадрах.
Заключение
В целом, очень приятно, что на маломощном устройстве вроде Raspberry Pi можно использовать нейросети, да еще и почти в реальном времени. OpenVINO предоставляет очень обширный функционал для инференса нейросетей на множестве разных устройств — гораздо шире, чем я описал в статье. Думаю, Neural Compute Stick и OpenVINO будут очень полезны в моих робототехнических изысканиях.