Улучшаем систему видеонаблюдения, ч.3

8035dd6a2ea236518fcebf631051393d

Распознавание на python работало хорошо, но хотелось еще как-то это ускорить.
Спрашивается: если есть некоторая оболочка, позволяющая запустить модель на python — может быть есть оболочка позволяющая запустить ее на C/C++?
И такая нашлась: https://github.com/Geekgineer/YOLOs-CPP

Копируем:

git clone https://github.com/Geekgineer/YOLOs-CPP
cd YOLOs-CPP

Нам нужны дополнительно некоторые пакеты:

apt install curl libopencv-dev cmake g++

В файле build.sh нужно найти ONNXRUNTIME_VERSION -, а потом посмотреть, на что реально она влияет. Описание процесса установки уже отстало от жизни, поэтому придётся ручками.
Скрипт должен скачать соответствующий версии файл -, но там, откуда он его скачивает, версия более новая, к тому же скрипт желает загрузить версию для arm64, а там — aarch64.
В общем, вот это надо выполнить вручную: скачать, распаковать, сделать симлинк с нужным названием, закомментировать уже выполненное

ln -s onnxruntime-linux-aarch64–1.20.1 onnxruntime-linux-arm64–1.20.1

В этом пакете лежат include-файлы и so-библиотеки, нужные для сборки и работы.
Библиотеки *so имеет смысл скопировать в /usr/local/lib/

В каталоге YOLOs-CPP/src — три файла-примера использования детектора: для изображений, для видеофайлов и для видеопотока, например с камеры.
Каталог models содержит модели yolo в формате onnx, а include — *.hpp-файлы для работы с ними.

В файлах примеров необходимо правильно выбрать нужную версию модели — если используем yolo11 — то нужны будут YOLO11.hpp и указатели типа YOLO11* в коде *.cpp

В результате компиляции должны получится соответствующие исполняемые файлы. Но есть нюансы:

1 — примеры рассчитаны на запуск на десктопе, они должны показывать картинку с нарисованными рамками обьектов. Десктопа нет, работать это не будет.
Тут по сути нужен только пример работы с картинками — его надо переписать так, чтобы вместо вывода на экран сохранялся бы файл на диске:

cv::imwrite("out.jpg", image);
//cv::imshow("Detections", image);
//cv::waitKey(0); // Wait for a key press to close the window

2 — «из коробки» не заработал файл модели yolo11n.onnx.
Но там же, в models, есть скрипт export_onnx.py, а в прошлый раз, при запуске скриптов в python, мы уже получили работающий yolo11n.pt.
Можно перейти туда:

cd /root/yolo
. bin/activate
cp XXX/models/export_onnx.py ./
vi export_onnx.py

from ultralytics import YOLO

# Load the YOLOv11n model
model = YOLO("yolo11l.pt")

# Export the model to ONNX format
model.export(format="onnx")

./export_onnx.py
mv yolo11n.onnx XXX/models/

Если всё было сделано правильно — программа image_inference запустится, прочитает указанный в коде файл, и запишет out.jpg с рамками обьектов.
В принципе, тут можно было бы чуть доработать ее до указания в командной строке входного и выходного файлов -, но это неинтересно, потому что сам процесс загрузки программы и модели занимает значительное время, а цель была в его уменьшении.

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

И такая библиотека есть: https://github.com/davidmoreno/onion

wget https://github.com/davidmoreno/onion/archive/refs/heads/master.zip
unzip master.zip
cd onion-master
mkdir build
cd build
cmake …
make
sudo make install

По умолчанию полученные include и so-библиотеки устанавливаются в /usr/local/include и /usr/local/lib.
Теперь надо обновить кеш в ОС:

ldconfig /usr/local/lib

В examples можно найти примеры использования библиотеки. В данном случае нам нужен post — пример обработки POST-запросов.
Принцип работы простой: создается сервер, прописываются url которые он обрабатывает и функции, которые при этом вызываются.
В данном примере запрос к /data обрабатывает функция post_data, которая получает значение переменной text

...
  const char *user_data = onion_request_get_post(req, "text");
...

и отправляет его обратно клиенту.
Нам нужен файл — и беглый поиск по include-файлам сразу дает нам функцию

const char *path = onion_request_get_file(req, "file");

Причем path в данном случае указывает на временный файл, куда сохранено загруженное изображение, а этот файл находится в каталоге /tmp, который по сути tmpfs, т.е. в памяти.
То есть то, что и требовалось: минимальное время записи-чтения.

Теперь остается совместить ежа с ужом: если у нас есть программа, способная загружать файлы и обрабатывать их, и есть программа, способная принимать файлы и сохранять их куда-то временно — можно принимать файл, передавать его на обработку, и возвращать результат клиенту, что и требуется.

Перепишем код примера так:

#include 
#include 
#include 

#include "YOLO11.hpp"

#include 
#include 
#include 
#include 

onion *o = NULL;

// указываем используемые модели
const std::string modelPath = "/etc/yolo/models/yolo11n.onnx";
const std::string labelsPath = "/etc/yolo/models/coco.names";

// создаем один постоянный обьект
YOLO11Detector detector(modelPath, labelsPath, false); // no GPU

// это список names, соответствующих типу обьекта
// по сути он уже создается внутри detector, но используется только для
// отрисовки рамок внутри него же, поэтому создадим внешний
std::vector classNames;

// ==========================================================
void onexit(int _) {
  ONION_INFO("Exit");
  if (o)
    onion_listen_stop(o);
}

// преобразуем обьект Detection в string
std::string toJsonString(const Detection& det) {
  std::ostringstream os;
  std::string name = classNames[det.classId];
  os << "{"
     << "\"name\":\"" << name << "\","
     << "\"class\":" << det.classId << ","
     << "\"confidence\":" << det.conf << ","
     << "\"box\":{"
     << "\"x1\":" << det.box.x << ","
     << "\"y1\":" << det.box.y << ","
     << "\"x2\":" << (det.box.width + det.box.x) << ","
     << "\"y2\":" << (det.box.height + det.box.y)
     << "}"
     << "}";
  return os.str();
}

// обработка файла
onion_connection_status post_data(void *_, onion_request * req, onion_response * res) {

  onion_response_set_header(res,"Access-Control-Allow-Origin","*");

  if (onion_request_get_flags(req) & OR_HEAD) {
    onion_response_write_headers(res);
    return OCS_PROCESSED;
  }

  const char * imagePath = onion_request_get_file(req, "file");

  cv::Mat image = cv::imread(imagePath);
  if (image.empty()){
    onion_response_printf(res, "[]");
    return OCS_PROCESSED;
  }

  auto start = std::chrono::high_resolution_clock::now();
  std::vector results = detector.detect(image);
  auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start);

  std::cerr << "Detection completed in: " << duration.count() << " ms" << std::endl;

  std::ostringstream os;
  os << "[";
  int i = 0;
  for (const auto& det : results) {
    if(i > 0) os << ",";
    std::string str = toJsonString(det);
    os << str;
    i++;
  }
  os << "]";

  std::string out = os.str();

  // не используем, хотя можем
  // detector.drawBoundingBox(image, results); // simple bbox drawing
  // detector.drawBoundingBoxMask(image, results); // Uncomment for mask drawing
  // cv::imwrite("/tmp/out.jpg", image);

  onion_response_printf(res, out.c_str() );
  return OCS_PROCESSED;

}

// ==========================================================

int main(){

  // загружаем свой список типов объектов
  classNames = utils::getClassNames(labelsPath);

  // запускаем сервер, мультитредовый вариант
  //o = onion_new(O_ONE_LOOP);
  o = onion_new(O_THREADED);
  onion_url *urls = onion_root_url(o);

  // вместо index.html
  onion_url_add_static(urls, "",
                       "\n"
                       "\n"
                       " Image analyzer\n"
                       "\n"
                       "\n"
                       "Just upload file
\n" "
\n" "\n" "\n" "
\n" "\n" "\n", HTTP_OK); // url обработки onion_url_add(urls, "detect", (void*)post_data); signal(SIGTERM, onexit); signal(SIGINT, onexit); onion_listen(o); onion_free(o); return 0; }

Теперь надо всё это скомпилировать.
Так как все необходимые библиотеки уже установлены на свои места в системе — создаем просто Makefile с подключением нужных (и ненужных)

CXX_INCLUDES = -I/root/yolo/YOLOs-CPP/include -I/root/yolo/YOLOs-CPP/onnxruntime-linux-arm64-1.20.1/include -isystem /usr/include/opencv4

CXX_FLAGS = -O3 -march=native

LD_FLAGS = -L/usr/lib/aarch64-linux-gnu -L/usr/local/lib -lonion -lonioncpp -lrt -lpthread -lonnxruntime -lopencv_stitching -lopencv_alphamat -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_cvv -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_shape -lopencv_stereo -lopencv_structured_light -lopencv_superres -lopencv_surface_matching -lopencv_tracking -lopencv_videostab -lopencv_viz -lopencv_wechat_qrcode -lopencv_xobjdetect -lopencv_xphoto -lopencv_highgui -lopencv_datasets -lopencv_plot -lopencv_text -lopencv_ml -lopencv_phase_unwrapping -lopencv_optflow -lopencv_ximgproc -lopencv_video -lopencv_videoio -lopencv_imgcodecs -lopencv_objdetect -lopencv_calib3d -lopencv_dnn -lopencv_features2d -lopencv_flann -lopencv_photo -lopencv_imgproc -lopencv_core

all:
  ${CXX} ${CXX_FLAGS} ${CXX_INCLUDES} ${CXX_DEFINES} yolo_server.cpp -o yolo_server ${LD_FLAGS}

make all
cp yolo_server /usr/local/bin/

По умолчанию onion-сервер работает на 8080 порту, это можно изменить указав явно порт в коде, а можно просто пробросить нужный порт при запуске Docker.
В итоге получаем все тот же yolo11, но работающий теперь на C++ вместо python. Судя по сообщениям программы — обработка изображений ускорилась примерно в 2 раза.

Более того, этих моделей yolo11 несколько:

  • yolo11n — nano

  • yolo11s — small

  • yolo11m — medium

  • yolo11l — large

  • yolo11x — extra large

Чем «больше» модель — тем больше памяти она требует, и тем дольше обрабатывает изображения. Выбрать модель можно простой заменой соответствующего файла при запуске сервера.
С точки зрения результата — разница примерно такая, что на фотографии толпы людей nano выделяет только первый ряд, а вот small уже цепляет и тех, кто стоит во втором и виден частично. Но каждая ступень увеличивает время детектировани примерно вдвое.
С другой стороны, те кто там за спинами — их всё равно видно плохо, так что практическая польза от этого сильно зависит от применения, зачем и для чего это надо: посмотреть или посчитать.

Ну и пример как оно детектирует: http://jbfw.duckdns.org/
Машинка там слабая, чисто для примера…

© Habrahabr.ru