Компьютерное зрение на C++: пишем приложение для поиска объектов под Android |
||
МЕНЮ Главная страница Поиск Регистрация на сайте Помощь проекту Архив новостей ТЕМЫ Новости ИИ Голосовой помощник Разработка ИИГородские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Искусственный интеллект Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Нейронные сети начинающим Психология ИИ Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Творчество ИИ Техническое зрение Чат-боты Авторизация |
2024-10-18 19:00 Привет, Хабр! Меня зовут Кирилл Колодяжный, я пишу код на С++ для систем хранения данных в YADRO. Помимо основной работы, интересуюсь машинным обучением и его возможностями, в том числе на «плюсах». Недавно мне стало интересно разобраться, как развернуть модель компьютерного зрения на мобильном устройстве с операционной системой Android. Я изучил доступные инструменты, чтобы понять, какие части приложения можно реализовать на С++, и написать само приложение для телефона. Ни в одном из материалов на подобную тему не описывают реализацию такого приложения от начала до конца, поэтому я собрал свой опыт в серию статей. Расскажу, как реализовать обнаружение объектов в реальном времени с помощью камеры на мобильной платформе Android с использованием библиотек PyTorch и NCNN и моделей компьютерного зрения YOLOv5 и YOLOv4. Шаблон моего приложения пригодится тем, кто хочет проверить прототип функциональности для компьютерного зрения на С++, использующий OpenCV на Android, но не хочет глубоко погружаться в программирование под Android. В первой части цикла мы:
Почему я пишу это приложение на С++ Программы на С++ получаются более быстрыми и компактными. Нам доступно больше вычислительных ресурсов, поскольку современные компиляторы могут оптимизировать программу в соответствии с архитектурой целевого процессора. C++ не использует дополнительный сборщик мусора для управления памятью — это существенно влияет на производительность программы. Размер программы также можно уменьшить, поскольку C++ не использует дополнительную виртуальную машину и компилируется непосредственно в машинный код. Поэтому, на мой взгляд, это хороший выбор для мобильных устройств с ограниченным объемом ресурсов, например, телефонов или маломощных плат, которые используются в робототехнике или видеоаналитике. Инструменты, которые я использовал для решения задачи YOLO (You Only Look Once) — семейство современных моделей обнаружения объектов на изображениях и видео, которые работают с высокой точностью и скоростью. Модели относительно небольшие и легкие, их просто развернуть на устройствах с ограниченными ресурсами. Также они достаточно производительны, что важно для приложений, которые анализируют видеопоток в реальном времени. На Android мы можем использовать разные фреймворки машинного обучения: PyTorch, ExecuTorch, TensorFlow Lite, NCNN, ONNX runtime frameworks или другие. Для этой задачи использую платформу PyTorch, а именно — TorchScript и NCNN. Она позволяет задействовать практически любую модель PyTorch на мобильных платформах с минимальными функциональными ограничениями. К тому же это мой основной инструмент для ML. Ссылки на инструменты:
Исходный код проекта для этой статьи найдете в репозитории GitFlic. Проект Android Studio В этом разделе рассмотрим, как использовать IDE Android Studio для создания мобильного приложения. Сначала используем мастер Native C++ Activity в IDE Android Studio для создания заглушки приложения. Если назвать проект objectdetection и выбрать Kotlin как язык разработки, то Android Studio создаст определенную структуру проекта. В следующем примере показаны наиболее важные ее части:
В этом проекте основная функциональность приложения будет реализована в нативной части на С++. Там будут функции для детекции объектов и отрисовки захваченного изображения, bounding boxes и меток классов обнаруженных объектов. Таким образом, Kotlin-часть будет максимально сокращена и не будет содержать никакого кода для пользовательского интерфейса. Kotlin-часть проекта: пишем основу приложения Kotlin-часть будем использовать для запроса и проверки необходимых разрешений для доступа к камере. К сожалению, подобная функциональность отсутствует в C++ API для Android NDK. Можно, конечно, вызывать ее из С++, используя JNI. Но, думаю, это только усложнит код. Кроме того, из Kotlin будет запускаться сеанс захвата изображения с камеры, если необходимые разрешения предоставлены. Весь код на Kotlin находится в одном файле — MainActivity.kt. Сохраняем ориентации камеры В этом проекте я не буду полностью реализовывать обработку поворота устройства, чтобы упростить код и показать только самые интересные моменты работы с моделью обнаружения объектов. Итак, чтобы сделать код стабильным, я отключу ландшафтный режим. Это можно сделать в файле AndroidManifest.xml следующим образом: Я только добавил инструкцию по ориентации экрана к объекту activity, который описывает поведения нашего приложения.
Обрабатываем запрос разрешения доступа к камере Первым шагом изменим объявление класса MainActivity, чтобы обрабатывать результаты запроса для получения разрешения. Здесь я унаследовал класс MainActivity от интерфейса OnRequestPermissionsResultCallback. Это позволило переопределить метод onRequestPermissionsResult, где можно проверить результат запроса разрешений. Но чтобы получить результат, я должен сначала сделать запрос следующим образом: Я переопределил метод onResume класса Activity. Этот метод вызывается каждый раз, когда приложение начинает работать или возобновляется из фонового режима. Я инициализировал переменную cameraPermission константой для получения разрешения камеры. Затем методом checkSelfPermission проверил, предоставлено ли уже это разрешение.
В методе onRequestPermissionsResult я проверяю, было ли предоставлено требуемое разрешение следующим образом: Сначала я использовал родительский метод, чтобы сохранить стандартное поведение приложения. Затем проверил идентификационный код разрешения CAM_PERMISSION_CODE и убедился, было ли разрешение предоставлено. Если в разрешении отказано, я просто показываю сообщение об ошибке и завершаю работу приложения. Как я писал ранее, в случае получения разрешения я нахожу идентификатор камеры, обращенной назад: Сначала я получаю экземпляр объекта CameraManager. И использую его для перебора каждой доступной камеры на устройстве. Для каждого объекта камеры я запрашиваю его характеристики, поддерживаемый аппаратный уровень и место, куда обращена камера. Если камера — обычное устройство и обращена назад, возвращаем ее идентификатор. Если не нашел подходящее устройство, возвращаю пустую строку. Получив разрешение на доступ к камере и идентификатор камеры, вызываю функцию initObjectDetection, чтобы начать захват изображений и обнаружение объектов. Эта функция, как stopObjectDetection, предоставляется через Java Native Interface из C++ в Kotlin. Функцию stopObjectDetection использую для остановки сеанса захвата изображений: Этот метод вызывается каждый раз, когда приложение Android закрывается или переходит в фоновый режим. Загружаем native-библиотеки У нас есть два метода — initObjectDetection и stopObjectDetection, которые являются JNI-вызовами функций, реализованных в C ++. Чтобы соединить нативную библиотеку с кодом Java или Kotlin, я использую Java Native Interface (JNI). Это стандартный механизм, который используется для вызова функций C/C++ из Kotlin или Java. Сначала я загружаю нативную библиотеку с помощью функции System.LoadLibrary в инициализации companion объекта для нашей Activity. Затем я определяю методы, которые реализованы в нативной библиотеке, объявив их как «внешние». Следующий фрагмент показывает, как это cделать в Kotlin: Эти объявления позволяют Kotlin найти соответствующий бинарный файл нативной библиотеки, загрузить его и получить доступ к функциям. В следующем разделе рассмотрим часть проекта на C++ — она отвечает за создание сессии непрерывного захвата изображений. Нативная часть проекта на C++: создаем сессию непрерывного захвата изображений Подключаем библиотеку OpenCV Основная функциональность этого примера проекта реализована в нативной части, написанной на C++. Она разработана с использованием библиотеки OpenCV для обработки изображений с камеры. Такой подход позволяет при необходимости перенести решение на другую платформу. То есть мы, например, можем использовать стандартные инструменты для разработки и отладки на настольной платформе, а потом легко перенести их на мобильную. Чтобы использовать библиотеку OpenCV на Android, можно скачать уже скомпилированный SDK из официального релиза, архив opencv-4.10.0-android-sdk.zip. А затем просто распаковать в удобную вам директорию. Он подключится к проекту стандартным для CMake способом: Чтобы сборка Android-проекта смогла найти OpenCV SDK, нам надо установить значение параметра OpenCV_DIR для CMake. Это можно сделать, изменив скрипты сборки Android-проекта. Так как по умолчанию для сборки Android-проектов используется система Gradle, можно изменить файл build.gradle.kts: Заводить переменную opencvDir необязательно. Мне показалось, что так удобнее. Посмотрите на детали файлов CMakeLists.txt и build.gradle.kts в репозитории проекта для уточнения деталей. Инициализируем сессии обнаружения объектов с помощью JNI Я закончил обсуждение Kotlin-части объявлениями JNI-функций. Соответствующие реализации для initObjectDetection и stopObjectDetection на C++ расположены в файле native-lib.cpp. Он автоматически создается IDE Android Studio для нативных проектов. Следующий фрагмент кода показывает определение функции initObjectDetection: Я сделал объявление функции, следуя правилам именования JNI, чтобы оно было видимым из Java/Kotlin-части. Имя функции включает полное имя пакета Java, включая пространства имен, а также первые два обязательных параметра — типы JNIEnv* и jobject. Третий параметр — строка, он соответствует идентификатору камеры и существует в объявлении функции на Kotlin. В реализации функции проверяю, создан ли уже экземпляр объекта CameraCapture. Вызываю метод allow_camera_session с идентификатором камеры, а затем вызываю метод configure_resources. Эти вызовы настраивают соответствующую камеру, окно вывода и инициализируют конвейер захвата изображения в объекте CameraCapture. Вторая функция, которую я использовали в Kotlin-части — stopObjectDetection, и ее реализация выглядит так: Здесь я только освобождаю ресурсы, используемые для конвейера захвата изображений, потому что при приостановке приложения доступ к устройству камеры блокируется. Когда приложение будет активировано снова, повторятся и вызов функции initObjectDetection, и инициализация конвейера захвата изображений. Вы можете видеть, что я использовал функции LOGI и LOGE. Они определены в файле log.h следующим образом: Я определил эти функции для упрощения логирования в подсистеме Android logcat. Эта серия функций использует один и тот же тег для ведения журнала, и у них меньше аргументов, чем у __android_log_xxx. Кроме того, уровень логирования закодирован в имени функции. Основной цикл приложения В этом проекте я буду использовать библиотеку NativeAppGlue. Это библиотека для разработчиков Android, которая помогает создавать нативные приложения. Она реализует определенный слой абстракции между кодом Java/Kotlin и нативным кодом, упрощая разработку приложений с использованием обоих языков. Эта библиотека позволяет нам определить функцию android_main, схожую по функциональности со стандартной main. В ней можно реализовать цикл для обновления пользовательского интерфейса, обработки пользовательского ввода и системных событий. Следующий фрагмент кода показывает, как я реализовал эту функцию в файле native-lib.cpp: Функция android_main принимает экземпляр типа android_app вместо обычных параметров argc и argv. android_app — это класс C ++, который предоставляет доступ к платформе Android и позволяет взаимодействовать с системными службами. Также его можно использовать для доступа к оборудованию устройства, такому как датчики и камеры. Функция android_main — входная точка для нашего нативного модуля. Я в ней инициализирую глобальный объект camera_capture_, чтобы он стал доступен для функций initObjectDetection и stopObjectDetection. Для инициализации CameraCapture я делаю следующее:
В этом цикле я использовал функцию Android NDK ALooper_pollOnce, чтобы получить указатель на объект опроса команд (событий). С помощью метода process этого объекта я инициирую вызов функций process_android_cmd и process_android_input через объект app. И в конце цикла я использую наш объект camera_capture для захвата изображений с камеры и их обработки в методе draw_frame. Функция process_android_cmd реализована следующим образом: Здесь я обрабатываю только две команды, которые соответствуют инициализации и закрытию окна приложения. Они используются для инициализации и очистки конвейера захвата изображения. Когда окно создано, я изменяю его размеры в соответствии с выбранным разрешением камеры. Команда завершения работы окна позволяет нам очистить ресурсы используемые захвата изображений, чтобы предотвратить доступ к уже заблокированному устройству камеры. Функция process_android_input используется только для переключения моделей детекции объектов и реализована следующим образом: То есть, если мы получим любое событие пользовательского ввода, например, нажатие на экран, то произойдет переключение детектора объектов по кругу. Я использовал эту функциональность, чтобы сравнивать производительность детекторов в реальном времени. В следующих подразделах рассмотрим детали реализации класса CameraCapture. Обзор класса CameraCapture Это главный фасад всего конвейера обнаружения объектов. Функциональность, которую он реализует:
Прежде чем начать рассматривать детали этого класса, давайте посмотрим, как реализованы его конструктор, деструктор и некоторые вспомогательные методы. Реализация конструктора выглядит так: Тут я сохраняю указатель на объект android_app и создаю объекты, реализующие детекцию объектов с помощью inference-моделей на базе архитектуры YOLO. Также из объекта android_app я получил указатель на объект типа AAssetManager, который используется для загрузки файлов, упакованных в приложение Android (в APK). Деструктор реализован следующим образом: Здесь я использовал метод release_resources, в котором закрываю открытое устройство камеры и очищаю объекты конвейера захвата изображений. Следующий фрагмент кода показывает метод, который косвенно вызывается в Kotlin-части с помощью вызова функции initObjectDetection: В этой функции я сохраняю строку идентификатора камеры, позже устройство с этим идентификатором будет открыто в методе configure_resources. Как мы уже знаем, идентификатор камеры будет передан в объект CameraCapture только в том случае, если было предоставлено требуемое разрешение и на устройстве Android есть подходящая камера. Далее в коде для проверки доступности камеры будет использоваться следующий метод: Тут я просто проверяю, не является ли идентификатор камеры пустым. В следующих подразделах подробно покажу реализации основных частей функциональности класса CameraCapture. Конфигурация устройства камеры и окна приложения Для создания объекта диспетчера камеры и открытия устройства камеры в классе CameraCapture есть метод create_camera: Здесь camera_mgr_ является членом класса CameraCapture и после инициализации используется для управления камерой. Указатель на открытое устройство камеры будет сохранен в переменной camera_device_. Также обратите внимание, что я использовал строку идентификатора камеры для открытия конкретного устройства. Переменная camera_device_callbacks определяется так: Это объект типа ACameraDevice_stateCallbacks со ссылками на разные обработчики событий камеры. У меня они просто показывают, когда камера отключается и сообщают об ошибке. Это обязательные обработчики, которые необходимо инициализировать в любом случае в соответствии с требованиями API. И, конечно же, в реальном продукте они могут быть реализованы с большей пользой. Метод create_camera вызывается в методе configure_resources каждый раз, когда приложение активируется: Вначале я проверяю наличие всех необходимых ресурсов: идентификатора камеры, объекта android_app и того, что у этого объекта есть указатель на окно приложения. Затем я создаю объект диспетчера камер и открываю устройство камеры. Используя диспетчер камер, получаю ориентацию датчика камеры, чтобы настроить соответствующую ширину и высоту окна приложения. Далее, с помощью значений ширины и высоты разрешения камеры (camera resolution), я настраиваю размеры окна следующим образом: Здесь я использую функцию ACameraManager_getCameraCharacteristics для получения объекта характеристик метаданных камеры. Из полученных характеристик я получаю значение для ориентации сенсора с помощью функции ACameraMetadata_getConstEntry и соответствующей константы ACAMERA_SENSOR_ORIENTATION. Далее в зависимости от ориентации я выбираю соответствующий порядок ширины и высоты. Если ориентация горизонтальная, я поменяю местами ширину и высоту. Точные значения определены в заголовочном файле и равны 800 для высоты и 600 для ширины в режиме portrait. Это очень упрощенная обработка ориентации, она необходима только для корректной работы с буфером окна вывода. Как вы помните, в начале статьи я отключил ландшафтный режим для приложения, поэтому буду игнорировать ориентацию датчика камеры при декодировании изображения. В завершение, используя функцию ANativeWindow_setBuffersGeometry, я задаю размеры окна приложения, куда будет происходить отрисовка и форматирование буфера изображения. Я выбрал 32-битный RGBA-формат. В конце метода configure_resources создается объект camera reader и инициализируется сессия захвата изображений. Инициализация сессии захвата изображений Ранее перед инициализацией конвейера захвата я создавал объект image reader. Это делается в методе create_image_reader: Использую функцию AImageReader_new для создания объекта типа AImageReader с параметрами определенной ширины, высоты, формата YUV и с четырьмя буферами изображений. Значения ширины и высоты я использовал те же, что использовались для настройки размеров окна вывода. Формат YUV использован потому, что это родной формат изображения для большинства камер. Четыре буфера изображений используются, чтобы сделать захват изображений независимым от их обработки. Это означает, что процесс захвата изображений будет заполнять один буфер изображения данными камеры, пока мы считываем и обрабатываем другой буфер. Инициализация сессии захвата — более комплексный процесс, который требует создания нескольких объектов и их соединения друг с другом. Метод create_session реализует это так: Я начал с получения объекта окна для объекта image reader и захвата владения им. Это означает, что я взял ссылку на окно и системе не следует его удалять. Это окно будет использоваться как целевое для чтения изображений конвейером захвата, то есть в него будут попадать изображения с камеры. Затем я создаю объект контейнера для выходных потоков сессии и сам объект выходного потока для сессии. Сессия захвата может иметь несколько выходных потоков, и их все следует поместить в контейнер. Каждый выходной поток сессии представляет собой объект для подключения конкретной поверхности или окна вывода, в нашем случае это окно объекта image reader. Настроив выходные потоки сессии, я создаю объект запроса захвата изображения. И делаю так, чтобы целью его вывода было окно объекта image reader. Это делается созданием и добавлением объекта целевого выхода (см. функции ACameraOutputTarget_create и ACaptureRequest_addTarget). Я настраиваю запрос на захват изображений с открытой камеры в режиме предварительного просмотра. После этого я создаю экземпляр объекта ACaptureSession, который будет использовать открытое устройство камеры. В нем заполнен контейнер с выходными потоками, который я создал ранее. И наконец, запускаю захват изображений, установив повторяющийся запрос для сессии. Связь между сессией и запросом на захват изображений заключается в следующем: создавая сессию захвата, мы настраиваем ее списком возможных выходных потоков, а в запросе (ACaptureRequest) на захват указываем, какие конкретно потоки будут использоваться. Может быть несколько запросов на захват и выходных потоков. В нашем случае у нас один запрос завязывается с одним выходом. Также наш запрос будет непрерывно повторяться, то есть я буду получать изображения в реальном времени как видеопоток. На следующем рисунке показана логическая схема потоков данных в сессии захвата изображений: Это не реальная схема потока данных, а логическая, которая демонстрирует, как связаны объекты сессии захвата. Пунктирной линией показан путь запроса, а сплошной линией — логический путь передачи данных изображения. Управление буфером захвата изображений и буфером окна вывода Когда я рассказывал про основной цикл приложения, я упомянул метод draw_frame, который вызывается в этом цикле после обработки команд. Этот метод используется для:
Следующий фрагмент кода показывает реализацию этого метода: Здесь я получаю следующее доступное изображение с камеры от объекта image_reader_. Помните, я инициализировал его так, чтобы в нем было четыре буфера изображений? Таким образом я получаю изображения из этих буферов одно за другим. Пока обрабатывается одно изображение, следующее уже захватывается и попадает в другой буфер. И так по кругу. Получив изображение с камеры, я получаю и блокирую окно приложения для отрисовки результатов в него. Если блокировка не удалась, то я удаляю текущую ссылку на изображение, останавливаю обработку, и цикл повторяется. Если окно удачно заблокировалось, я обрабатываю текущее изображение. С помощью метода process_image я нахожу объекты и вывожу результаты обнаружения в окно приложения. Метод process_image принимает на вход объекты AImage и ANativeWindow_Buffer. Когда я блокирую окно приложения, то получаю указатель на внутренний буфер, который и будет использоваться для рисования. После того, как я обработал изображение и отобразил результаты, я разблокирую окно приложения, чтобы сделать его буфер доступным для системы. Для этого освобождаю ссылку на окно и удаляю ссылку на объект изображения. Этот метод в основном связан с управлением ресурсами, а реальная обработка изображений выполняется методом process_image, про который я расскажу в следующем подразделе. Обработка полученного изображения Метод process_image решает следующие задачи:
Давайте посмотрим на реализацию этих задач. Сигнатура метода process_image выглядит следующим образом: Этот метод использует объект буфера окна приложения для отрисовки результатов и объект изображения для фактической обработки. Чтобы в дальнейшем обработать изображение, преобразовываю его в соответствующую структуру данных — в нашем случае это матрица OpenCV. Реализация метода начинается с проверки свойств формата изображения и окна: Сначала я проверяю, что буфер окна имеет формат RGB (A/X). Далее — уточняю формат изображения (YUV) и то, что у него три канала. Затем я получаю размеры изображения, они пригодятся дальше. После проверки входных данных я получаю данные для каналов YUV: Для начала я получаю значения stride, размер данных и указатель на фактические данные для каждого канала YUV. В этом формате данные изображения разделяются на три компонента: яркость Y и два компонента цветности U и V. Компонент Y обычно сохраняется в полном разрешении, в то время как компоненты U и V могут иметь уменьшенный размер. Это обеспечивает более эффективное хранение и передачу видеоданных. В изображении Android YUV используется половинное разрешение для каналов U и V. Полученные значения stride позволят нам корректно получить доступ к строкам данных в буферах, значения stride зависят от разрешения изображения и расположения памяти данных. Данные каналов YUV, значения stride ширины и длины я преобразую в матрицы OpenCV: Я создал два объекта типа cv::Size, чтобы сохранить исходный размер изображения для канала Y и половинный размер для каналов U и V. Затем я использовал эти размеры, указатели на данные и значения stride для создания матриц OpenCV для каждого канала.
Используя функцию OpenCV cvtColorTwoPlane, мы можем преобразовать эти матрицы каналов в формат RGBA: Для определения порядка компоновки формата YUV я использую разницу адресов данных для каналов U и V, при этом положительная разница соответствует формату NV12, а отрицательная — формату NV21. NV12 и NV21 — это типы формата YUV, которые отличаются порядком расположения U- и V-компонентов в канале цветности. В NV12 компонент U предшествует компоненту V, в то время как в NV21 все наоборот. Такая компоновка каналов играет роль в размере используемой памяти и производительности обработки изображений, поэтому выбор, что использовать, зависит от реальной задачи и проекта. Кроме того, компоновка может зависеть от устройства камеры, поэтому я добавил ее обработку. Функция cvtColorTwoPlane преобразует матрицы Y-канала и матрицу совмещенных UV-каналов в RGBА- или RGB-изображения. Последний аргумент этой функции — флаг, который указывает, какое фактическое преобразование она должна выполнить. Как я уже говорил, наше приложение работает только в портретном режиме, но для придания изображению нормального вида нам все равно нужно повернуть его: Камеры в Android возвращают изображения повернутыми, даже если я зафиксировал ориентацию, поэтому я и использую функцию cv::rotate, чтобы придать им вертикальный вид. Подготовив изображение RGBA, я передаю его в детектор объектов и получаю результаты обнаружения объектов. Для каждого результирующего элемента я рисую прямоугольник и название класса объекта на матрице изображения, которая у нас уже есть и которую я использовал для обнаружения. Эти шаги реализуются так: Я вызываю метод detect объекта ObjectDetector и получаю контейнер results. Этот метод мы рассмотрим позже. Затем для каждого элемента в контейнере я рисую ограничивающую рамку и текстовую метку для обнаруженного объекта. Обращаюсь к функциям OpenCV rectangle и putText, где целевое изображение использует переменную rgba_img_ — наше исходное изображение. Результатом обнаружения является структура, определенная в заголовочном файле detector_interface.h: В ней присутствуют значения индекса класса, строка с названием класса, оценка достоверности, ограничивающая рамку в координатах изображения. Для визуализации наших результатов я использовал только свойства rectangle и class_name. Последняя задача, которую выполняет метод process_image, — это рендеринг результирующего изображения в буфер окна приложения: Я создаю матрицу OpenCV buffer_mat таким образом, чтобы ее данные были данными буфера окна. Для этого я передаю в конструктор указатель buf->bit на данные буфера окна b и значение смещения buf->stride*4. Размер и тип данных соответствуют формату буфера, который я настроил в методе configure_resources, в формате WINDOW_FORMAT_RGBA_8888. Затем я просто использую метод матрицы CopyTo, чтобы скопировать изображение визуализированными прямоугольниками и метками классов в объект buffer_mat. Как вы поняли, buffer_mat представляет буфер Android окна как объект OpenCV. Такой подход позволяет писать нам меньше кода и использовать процедуры OpenCV для обработки изображений и управления памятью. Что будет дальше Мы рассмотрели главный фасад нашего приложения для обнаружения объектов. Вот так выглядит интерфейс в процессе работы: В продолжении статьи я расскажу, как:
Также мы сравним производительность PyTorch с NCNN и использованных моделей. Подписывайтесь, чтобы не пропустить вторую часть. Источник: habr.com Комментарии: |
|