Раз уж Вы заинтересовались данной статьёй, то ожидается, что Вы умеете программировать на с++ с использованием библиотеки Qt и разрабатывать нейросети на Python c использованием библиотеки tensorflow.
Соответственно остаётся только понять как использовать обученные Вами нейросетевые модели в Qt проектах.

А сделать нужно следующее:

Скачать и откомпилировать для нужной платформы 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 lite

Оригинальный tensorflow

Плюсы:

  • Использование оригинального файла сохранения модели

  • Возможность обучать модель

Минусы:

  • Сложный интерфейс. Тут именно tensorflow без keras, то есть Вы оперируете не понятиями модель, слой, а понятиями вычислительный граф, вычислительная операция

  • Огромный размер библиотеки около 300 MB

  • Нет настроек для компиляции под мобильные ОС

Tensorflow lite

Плюсы:

  • Простой интерфейс, хоть и не такой как keras

  • Маленький размер библиотеки - несколько мегабайт

  • Есть настройки компиляции под мобильные ОС

Минусы:

  • Необходимость преобразовывать формат ".h5" в формат ".tflite" (делается одной командой на python).

  • Могут поддерживаться не все операции (в моей модели таких не было).

Компиляция библиотеки под оригинальный 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",

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

Комментарии (15)


  1. losacef
    00.00.0000 00:00

    Я ждал хоть какой-то демонстрации дизайна интерфейса


    1. AlexeyUral
      00.00.0000 00:00

      А я минимального размера приложения которое получается


      1. Kapping Автор
        00.00.0000 00:00

        Приложение вместе со обученной нейросетью получается примерно 30MB плюс размер весов нейросети.


    1. Kapping Автор
      00.00.0000 00:00

      Целью статьи было поделится знаниями по применению нейросетей на c++ с использованием Qt, не демонстрация конкретного приложения, в статье есть ссылка на github с готовым проектом приложения.


  1. RomanSt
    00.00.0000 00:00
    +2

    Отмечу достаточно важный, с точки зрения, конечного результата факт. Так собранный Tensorflow будет очень медленно работать. Моя команда 2 года назад сравнивала inference и получалось, что на Python работает в 2 раза быстрее. И, вопрос: зачем тогда C++? Так вот, чтобы собрать правильно, есть смысл покапаться в том, как собирают pip-пакет с Tensorflow, взять оттуда настройки bazel и собирать с ними.


    1. Kapping Автор
      00.00.0000 00:00
      +1

      Если вы разбирались может поделитесь как это сделать, ещё отмечу, что я пытался использовать делегат nnapi для чтобы задействовать аппаратное ускорение на моём redmi note 7, там в базеле для этого нужно nnapi как зависимость добавить, но программа при этом крашилась, пока не разобрался почему.


      1. RomanSt
        00.00.0000 00:00

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


        1. Kapping Автор
          00.00.0000 00:00

          Я замерил что код python и c++ у меня дают примерно одинаковое время инференса.


          1. RomanSt
            00.00.0000 00:00

            Год назад проблема еще была, у меня есть подтверждение от третьей стороны. Может сейчас TF собирается по другому и работает теперь быстрее после дефолтной сборки. А может вы замеры не так делаете.

            Если хотите, могу посмотреть. Можете оформить в виде репо ваши замеры времени на обоих ЯП и приложить список команд для сборки TF. Очень хочется верить, что Вы правы, но мой внутренний скептик кричит, что это не так :)


            1. Kapping Автор
              00.00.0000 00:00

              Я не знаю как оформить в виде репо, я замерял текущее время непосредственно до и после инференса на обоих моделях у меня получилось следующее:

              Если я при компиляции tensorflow не использовал опцию --copt=-march=native
              то есть не оптимизировал для своего процессора, то в обоих случаях у меня получалось около 100-110 мс. Если же я использовал данную опцию, то на с++ у меня получалось 50-60мс.

              python писал: This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operati
              ons:  AVX2 FMA

              То есть если бы я компилировал TensorFlow и для python у меня скорее всего тоже получилось бы примерно 50-60мс.

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


  1. Rewesand
    00.00.0000 00:00

    Спасибо автору огромное. Прям ложка к обеду. Работаю над проектом на с++/qt с применением нейросетей. Qt был выбран как кроссплатформенной решение, с перспективой написания приложений под андроид. И вот стать как раз кстати. Мне как раз и нужна была инструкция для входа в реализацию приложения для андроид.

    Кстати, размер в 300мб я не считаю минусом в современное время. Думаю должно быть понимание того, что приложения должны ставится на мощные телефоны. Это как с играми, никто делая киберпанк. Хочешь играть - купи мощный комп. Тоже мы и должны делать с телефонами.

    Игра геншинтмпакт весит более 15гб и мелочится на +-300мб не стоит.

    А как насчёт PyTorch?


    1. Kapping Автор
      00.00.0000 00:00

      Я знаю только Tensorflow, можно ли так же с PyTorch не знаю. Я мог что то упустить из того что делал, так что если что не получается спрашивайте.


    1. Kapping Автор
      00.00.0000 00:00

      Обратите внимание, я ещё про оптимизацию под андрюшку добавил.


  1. nikitakruglovx
    00.00.0000 00:00
    +1

    Зачем демонстрировать .pro файлы? Гораздо полезнее было бы реализовать и показать на cmake! Никто уже .pro не использует в нормальных больших проектах, лишний апендикс

    В целом и общем статья может показаться полезной.


    1. DSRussell
      00.00.0000 00:00
      +1

      Не кричите. Еще как используют.