Раз уж Вы заинтересовались данной статьёй, то ожидается, что Вы умеете программировать на с++ с использованием библиотеки Qt и разрабатывать нейросети на Python c использованием библиотеки tensorflow.
Соответственно остаётся только понять как использовать обученные Вами нейросетевые модели в Qt проектах.
А сделать нужно следующее:
-
Скачать и откомпилировать для нужной платформы tensorflow
-
Скачать и откомпилировать для нужной платформы opencv
-
Скачать android SDK и настроить Qt для работы с ним
-
В pro файле Qt указать местоположение директории с заголовочными файлами и сами откомпилированные библиотеки
-
Написать программу
-
Для Android задействовать аппаратное ускорение
Скачать и откомпилировать для нужной платформы tensorflow
Скачать исходные коды можно с официального сайта.
Для этого в папке, в которой Вы планируете хранить проекты, выполните (в консоли, конечно) git clone https://github.com/tensorflow/tensorflow.git и командойgit checkout branch_name выберите нужную версию tensorflow.
Для компиляции tensorflow Вам понадобится bazel. Но каждая версия tensorflow требует своей версии basel. Чтобы не париться самому с версиями, стоит установить bazelisk. Для чего скачиваем бинарник с https://github.com/bazelbuild/bazelisk/releases, переименовываем в bazelisk и кладём в системную папку с программами, например в /usr/local/bin (для linux), после чего в командах компиляции вместо bazel пишем bazelisk.
Есть два варианта использования tensorflow:
Оригинальный tensorflow
Плюсы:
Минусы:
-
Сложный интерфейс. Тут именно tensorflow без keras, то есть Вы оперируете не понятиями модель, слой, а понятиями вычислительный граф, вычислительная операция
-
Огромный размер библиотеки около 300 MB
-
Нет настроек для компиляции под мобильные ОС
Tensorflow lite
Плюсы:
-
Простой интерфейс, хоть и не такой как keras
-
Маленький размер библиотеки - несколько мегабайт
-
Есть настройки компиляции под мобильные ОС
Минусы:
Компиляция библиотеки под оригинальный tensorflow
Для компиляции необходимо выполнить следующие команды:
Устанавливаем protobuf:
git clone https://github.com/protocolbuffers/protobuf.git cd protobufgit checkout 3.9.x ./autogen.sh ./confugure make -j$(nproc) sudo make install sudo ldconfig git clone https://github.com/tensorflow/tensorflowcd tensorflow git checkout r2.7 git clone https://github.com/abseil/abseil-cpp.git ln -s abseil-cpp/absl ./absl/
Добавить googleprotobuf*; в tensorflow/tensorflow/tf_version_script.lds
После чего:
./confugure bazelisk build --jobs=10 --verbose_failures -c opt --config=monolithic //tensorflow:libtensorflow_cc.so
Следует отметить, что версия protobuf должна соответствовать версии tensorflow. Я нашёл соответствие по выдаваемой при компиляции tensorflow ошибке.
Компиляция библиотеки под tensorflow lite
Для компиляции необходимо выполнить следующие команды:
git clone https://github.com/tensorflow/tensorflow cd tensorflow git checkout r2.7 ./confugure bazelisk build -c opt --config=android_arm64 --config=monolithic //tensorflow/lite:libtensorflowlite.so
./confugure в интерактивном режиме настроит сборку, если настройка для компиляции под android, Вам потребуется указать местоположение Android NDK, которую вы скачали в составе Android studio.
Здесь android_arm64 - настройки для компиляции под 64 битную версию android.
Замените на android_arm для компиляции под 32 битную версию android.
Уберите --config=android_arm64 для компиляции под ту ОС, в которой Вы ведёте разработку.
Ссылка по теме на официальный сайт https://www.tensorflow.org/lite/android/development
После компиляции появится директория bazel-bin, в которой скомпилированная библиотека будет находится в директории tensorflow/lite
В команду компиляции можно добавить следующие флаги оптимизации, что процентов на 20 может повысить быстродействие:
Набор инструкций | Флаги |
---|
AVX | --copt=-mavx |
AVX2 | --copt=-mavx2 |
FMA | --copt=-mfma |
SSE 4.1 | --copt=-msse4.1 |
SSE 4.2 | --copt=-msse4.2 |
Все поддерживаемые процессором | --copt=-march=native |
но в таком случае на некоторых компьютерах ПО может не работать.
В pro файле проекта на Qt для android следует добавить следующие строки:
INCLUDEPATH += "Путь к папке с tensorflow" INCLUDEPATH += "Путь к папке с tensorflow"/bazel-bin/ INCLUDEPATH += "Путь к папке с tensorflow"/bazel-tensorflow/external INCLUDEPATH += "Путь к папке с tensorflow"/bazel-bin/external/flatbuffers/_virtual_includes/flatbuffers LIBS += -L"Путь к папке с tensorflow"/bazel-bin/tensorflow/lite -ltensorflowlite
Скачать и откомпилировать для нужной платформы opencv
Выполнить git clone https://github.com/opencv/opencv.gitПерейти на нужную версию git checkout "Ветка", для каждой версии opencv есть ветка.Рядом с директорией opencv создать директорию, например opencv_build, в которой для каждой платформы создать свою директорию.
В директории opencv_build для платформы Android 64 создавать скрипты следующего содержания, заменяя
"Папка куда будет вестись компиляция под конкретную ОС"
"Папка с конкретной версией android NDK"
"Папка с результатом сборки",
Вашими названиями папок.
# !/bin/bash cd "Папка куда будет вестись компиляция под конкретную ОС" rm -R * PATH=$PATH:"Папка с конкретной версией android NDK"/toolchains/llvm/prebuilt/linux-x86_64/bin export ANDROID_HOME="Папка с SDK" export ANDROID_SDK_ROOT="Папка с SDK" export CMAKE_CONFIG_GENERATOR="Unix Makefiles" cmake -DCMAKE_BUILD_TYPE=Debug -DANDROID_NATIVE_API_LEVEL=lastest -DANDROID_ABI=arm64-v8a -DCMAKE_BUILD_TYPE=Debug -G"$CMAKE_CONFIG_GENERATOR" -DANDROID_ARM_NEON=ON -DANDROID_STL=c++_static -DBUILD_ANDROID_PROJECTS:BOOL=ON -DBUILD_opencv_world:BOOL=OFF -DBUILD_PERF_TESTS:BOOL=OFF -DBUILD_TESTS:BOOL=OFF -DBUILD_DOCS:BOOL=OFF -DWITH_CUDA:BOOL=ON -DBUILD_EXAMPLES:BOOL=OFF -DENABLE_PRECOMPILED_HEADERS=OFF -DWITH_IPP=ON -DWITH_MSMF=ON -DOPENCV_ENABLE_NONFREE:BOOL=ON -DWITH_OPENEXR=OFF -DWITH_CAROTENE=ON -DINSTALL_CREATE_DISTRIB=ON -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules -DCMAKE_TOOLCHAIN_FILE="Папка с конкретной версией android NDK"/build/cmake/android.toolchain.cmake ../../opencv make -j16 cmake --install . --prefix "Папка с результатом сборки"
Для 32-битной Android платформы нужно arm64-v8a заменить на armeabi-v7a
Для desktop платформы скрипт будет следующий:
#!/bin/bash cd "Папка куда будет вестись компиляция под конкретную ОС" rm -R * cmake ../../opencv make -j16 cmake --install . --prefix "Папка с результатом сборки"
В pro файле проекта следует добавить для desktop следующее:
INCLUDEPATH += "Папка с результатом сборки"/include/opencv4 LIBS += -L"Папка с результатом сборки"/lib -lopencv_dnn -lopencv_videoio -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_flann -lopencv_imgproc -lopencv_core
В pro файле проекта следует добавить для android следующее:
OPENCV_ANDROID = "Папка с результатом сборки" INCLUDEPATH += "$$OPENCV_ANDROID/sdk/native/jni/include" LIBS += -lmediandkcontains(ANDROID_TARGET_ARCH,armeabi-v7a){ LIBS += -L"$$OPENCV_ANDROID/sdk/native/3rdparty/libs/$$ANDROID_TARGET_ARCH" -ltbb -lIlmImf } LIBS += -L"$$OPENCV_ANDROID/sdk/native/libs/$$ANDROID_TARGET_ARCH" -L"$$OPENCV_ANDROID/sdk/native/staticlibs/$$ANDROID_TARGET_ARCH" -L"$$OPENCV_ANDROID/sdk/native/3rdparty/libs/$$ANDROID_TARGET_ARCH" -lade -littnotify -llibjpeg-turbo -llibwebp -llibpng -llibtiff -llibopenjp2 -lquirc -ltegra_hal -lopencv_dnn -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_flann -lopencv_imgproc -lopencv_core -lopencv_videoio -lcpufeatures -llibprotobuf ANDROID_EXTRA_LIBS = $$OPENCV_ANDROID/sdk/native/libs/arm64-v8a/libopencv_java4.so
Так же для компиляции под desktop tensorflow необходим flatbuffers
Установим его в систему глобально следующими командами:
git clone https://github.com/google/flatbuffers.git cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release make sudo make install sudo ldconfig
Вы можете установить flatbuffers локально заменив
sudo make install
cmake --install . --prefix "Папка с результатом сборки",
а затем с помощью INCLUDEPATH += и LIBS += добавить к проекту Qt
Разработка ПО
Здесь я опишу работу с tensorflow lite
Для работы с tensorflow lite преобразуем модель в его формат:
pred_center_model.save('pred_center_model_full') converter = tf.lite.TFLiteConverter.from_saved_model('pred_center_model_full') # path to the SavedModel directory tflite_model = converter.convert() # Save the model. with open('pred_center_model2.tflite', 'wb') as f: f.write(tflite_model)
Теперь подключим необходимые библиотеки:
#include <opencv2/opencv.hpp> #include "tensorflow/lite/interpreter.h" #include "tensorflow/lite/kernels/register.h" #include "tensorflow/lite/c/c_api_types.h"
Теперь загрузим модель:
std::unique_ptr<tflite::FlatBufferModel> m_model; tflite::ops::builtin::BuiltinOpResolver resolver; std::unique_ptr<tflite::Interpreter> interpreter; m_model = tflite::FlatBufferModel::BuildFromFile("путь к модели"); tflite::InterpreterBuilder builder(*m_model, resolver); TfLiteStatus tatus = builder(&interpreter); interpreter->AllocateTensors();
Если status == kTfLiteOk, то можем выполнять инференс модели.
К сожалению, я не нашёл как получить размерности входного и выходного слоя из самой модели, поэтому их нужно просто знать. В данном примере вход берётся видеофрейм cv:Mat , выходом же будет массив из 9 чисел (вероятностей конкретного класса).
// загружаем данные на входной слой сonst size_t DATA_SIZE 224*224*3 float* input = interpreter->typed_input_tensor<float>(0); auto *from_data = (uint8_t*)frame.data;. copy(from_data, from_data + DATA_SIZE, input); // делаем инференс auto status = interpreter->Invoke(); // разбираем данные с выходного слоя float* output = interpreter->typed_output_tensor<float>(0); if (status == kTfLiteOk) { auto size = 9; int max_idx {0}; float max = output[0]; static const vector<string> emo_names = {"злость", "презрение", "отвращение", "страх", "радость", "норма", "печаль", "удивление", "неуверенность"}; vector<string> emotions; for (int i = 0; i < size; ++i) { float curr_val = output[i]; if (curr_val > 0.2) emotions.push_back(emo_names[i]); if (curr_val > max) { max_idx = i; max = curr_val; } } return emotions; } else return {"predict error"};
Очень часто распознавать требуется кадры из видео-потока веб камеры или камеры смартфона, что программно одно и тоже. Захват можно делать либо средствами opencv, либо средствами Qt. Делать захват средствами opencv заманчиво, так как с полученным кадром можно удобно делать множество операций, например, вырезать заданную область, но у меня так и не получилось заставить работать захват через opencv под android. Поэтому я сделал захват и вывод видео-потока средствами Qt, а преобразования кадров средствами opencv. Так работает под все платформы.
Для захвата камеры нужно создать 3 объекта:
QScopedPointer<QCamera> m_camera; QVideoSink *m_video_sink{new QVideoSink{this}}; QMediaCaptureSession m_captureSession;
После чего выбрать камеру (например, камеру по умолчанию) и связать данные объекты:
m_camera.reset(QMediaDevices::defaultVideoInput()); m_captureSession.setCamera(m_camera.data()); m_camera->start(); m_captureSession.setVideoSink(m_video_sink);
После чего периодически опрашивать видео-поток и преобразовывать изображение в cv:Mat:
m_curr_image = m_video_sink->videoFrame().toImage(); m_frame = QImage2Mat(m_curr_image);
Вот функции преобразования из QImage в cv::Mat и обратно:
using namespace cv; QImage Mat2QImage(cv::Mat const& src) { cv::Mat temp; // make the same cv::Mat cvtColor(src, temp, COLOR_BGR2RGBA); // cvtColor Makes a copt, that what i need QImage dest((const uchar *) temp.data, temp.cols, temp.rows, temp.step, QImage::Format_RGB32); dest.bits(); // enforce deep copy, see documentation // of QImage::QImage ( const uchar * data, int width, int height, Format format ) return dest; } cv::Mat QImage2Mat(QImage const& src) { cv::Mat tmp(src.height(),src.width(),CV_8UC4,(uchar*)src.bits(),src.bytesPerLine()); cv::Mat result; // deep copy just in case (my lack of knowledge with open cv) cvtColor(tmp, result, COLOR_RGBA2BGR); return result; }
Кроме своих моделей полезно использовать чужие, уже обученные, например, в opencv встроена модель детекции лиц cv::dnn::Net, вот пример:
auto prepared_frame = cv::dnn::blobFromImage(frame, 1.0, Size(300,300), Scalar(104.0, 177.0, 123.0)); m_face_detect_model.setInput(prepared_frame); Mat output = m_face_detect_model.forward(); const int SHIFT = 7; using currTp = Vec<float,SHIFT>; auto it = output.begin<currTp>(); while(it != output.end<currTp>()) { currTp pred = *it; if (pred[2] < 0.5) break; int x = pred[3]*m_img_width; int y = pred[4]*m_img_height; int width = (pred[5] - pred[3])*m_img_width; int height = (pred[6] - pred[4])*m_img_height; coords.push_back(Rect{x, y, width, height}); it+=SHIFT; }
Правда под Android модели opencv у меня работали крайне неэффективно, раз в 40 хуже, чем обученные мной tensorflow модели. Если кто-то знает как это исправить пишите, буду рад.
Tensorflow предоставляет кучу готовых, обученных моделей компьютерного зрения в проекте mediapipe и под python их можно удобно использовать, но под с++ планируется, что Вы интегрируетесь в mediapipe, а не наоборот, так как планируется, что если Вы используете с++, то Вам необходим минимальный объём приложения.
Идея mediapipe заключается в том, что пишется текстовый файл в специальном формате, в котором описывается путь данных от ввода, например, с камеры, до вывода на экран устройства этот файл подаётся на вход программе, реализующей общий код приложения. Если какое-то преобразование не существует в mediapipe, то пишется класс наследуемый от mediapipe::CalculatorBase, в котором реализуется данное преобразование. Подробнее можно прочитать по ссылке. Но это уже тема отдельной статьи.
Вот ссылка на готовый проект В нём есть ветки с разными реализациями.
Использование аппаратного ускорения на android устройствах
Для использования аппаратного ускорения нужно применять делегаты.
Ссылка по ним на сайте tensorflow https://www.tensorflow.org/lite/performance/delegates?hl=ruВ данном примере добавим nnapi и gpu делегаты, делается это следующим образом :
Подключаем библиотеки(только под android)
#if defined(Q_OS_ANDROID) #include "tensorflow/lite/delegates/nnapi/nnapi_delegate.h" #include "tensorflow/lite/delegates/gpu/delegate.h" #endif // ANDROID
При создании модели создаём делегатов и указываем их модели
std::unique_ptr<tflite::FlatBufferModel> m_model; tflite::ops::builtin::BuiltinOpResolver resolver; std::unique_ptr<tflite::Interpreter> interpreter; m_model = tflite::FlatBufferModel::BuildFromFile("Имя файла с моделью tflite"); tflite::InterpreterBuilder builder(*m_model, resolver); #ifdef Q_OS_ANDROID auto* delegate = TfLiteGpuDelegateV2Create(/*default options=*/nullptr); builder.AddDelegate(delegate); builder.AddDelegate(tflite::NnApiDelegate()); #endif m_status = builder(&interpreter); interpreter->AllocateTensors();
После этого пользуемся моделью как обычно.
Однако для того чтобы библиотеки с делегатами были доступны необходимо перед компиляцией tensorflow добавить их в проект, делается это следующим образом:
В директории с tensorflow заходим в tensorflow/lite/
там находим файл BUILD
в нём по ключевому слову tensorflowlite ищем раздел tflite_cc_shared_object в котором
name = "tensorflowlite" вот пример:
tflite_cc_shared_object( name = "tensorflowlite", # Until we have more granular symbol export for the C++ API on Windows, # export all symbols. features = ["windows_export_all_symbols"], linkopts = select({ "//tensorflow:macos": [ "-Wl,-exported_symbols_list,$(location //tensorflow/lite:tflite_exported_symbols.lds)", ], "//tensorflow:windows": [], "//conditions:default": [ "-Wl,-z,defs", "-Wl,--version-script,$(location //tensorflow/lite:tflite_version_script.lds)", ], }), per_os_targets = True, deps = [ ":framework", ":tflite_exported_symbols.lds", ":tflite_version_script.lds", "//tensorflow/lite/kernels:builtin_ops_all_linked", ], )
В нём в раздел deps добавляем следующие строки
"//tensorflow/lite/nnapi:nnapi_lib", "//tensorflow/lite/delegates/nnapi:nnapi_delegate", "//tensorflow/lite/delegates/gpu:delegate", "//tensorflow/lite/delegates/gpu:gl_delegate",
После чего компилируем библиотеку.
Если при создании модели после добавления делегатов ваша программа стала крашится, возможно ваша модель слишком велика для аппаратного ускорения на данном устройстве, у меня было именно так, после того как я её уменьшил всё стало хорошо.