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

от автора

Распознавание на 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 <opencv2/highgui/highgui.hpp> #include <iostream> #include <string>  #include "YOLO11.hpp"  #include <signal.h> #include <onion/log.h> #include <onion/onion.h> #include <onion/shortcuts.h>  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<std::string> 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<Detection> results = detector.detect(image);   auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(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, "",                        "<html>\n"                        "<head>\n"                        " <title>Image analyzer</title>\n"                        "</head>\n"                        "\n"                        "Just upload file<br>\n"                        "<form method=\"POST\" action=\"detect\" enctype=\"multipart/form-data\">\n"                        "<input type=\"file\" name=\"file\">\n"                        "<input type=\"submit\">\n"                        "</form>\n" "\n" "</html>\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/
Машинка там слабая, чисто для примера…


ссылка на оригинал статьи https://habr.com/ru/articles/865632/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *