Распознавание на 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/
Добавить комментарий