Распознавание на 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/
Машинка там слабая, чисто для примера...
Автор: JBFW