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

0bbebc9c01fff614e2924ab31020b8b2

Вариант детекции обьектов с помощью CodeProject.AI работал хорошо, но пришлось отдать под него отдельный, хоть и старый, ноутбук, который требовал отдельного питания, заметно грелся, жужжал вентилятором.
Поэтому, с появлением компактного девайса с arm64 и 4 Гб ОЗУ, захотелось перенести всё на него.

К счастью, оказалось что есть готовый Docker и для arm64, достаточно только при создании указать codeproject/ai-server: arm64.

Всё установилось и заработало, причем даже чуть побыстрее чем на старом ноутбуке, и всё в маленькой бесшумной коробочке с питанием от USB.
Но хотелось что-то улучшить…

Стандартно, там внутри для распознавания обьектов используется нечто под названием YOLO 6.5. (да, я понятия не имел что это такое. Работает и хорошо, а что?)
При этом через пользовательский интерфейс система сообщает, что можно обновить до более свежей версии, но при попытке обновить штатным образом, нажатием кнопочек на экране, всё только портится и работать перестает: что-то не удалось найти, что-то не удалось загрузить и так далее.

При этом сама система — это как бы универсальный AI-сервер, на котором можно запускать разные модули, из которых по сути нужен только один.
А что если найти и запустить его отдельно?

Так выяснилось, что YOLO — это довольно известная штука от Ultralytics https://github.com/ultralytics/ultralytics, и актуальная версия там уже 11, а не 6.5.
И что всё это можно запустить под python.
Правда, есть нюанс: я не знаю python, но когда это кого останавливало?

Собирать всё это решено было также в докере, хотя бы для того чтобы ничего не поломать в работающей системе своими экспериментами.

docker run -ti --name t1 -v /tmp:/tmp -p 2222:22 -p 5000:5000 debian

За основу берем Дебиан, подключая туда системный /tmp, пробрасывая порт ssh 22 как 2222 и 5000 для веб-доступа.
В данном случае /tmp проброшен был ради упрощения обмена файлами, но как оказалось это была отличная идея, которая затем пригодится.

apt update
apt install vim openssh-server

vim /etc/startup

#!/bin/sh

mkdir -p /var/run/sshd
/usr/sbin/sshd -D

chmod 755 /etc/startup

Устанавливаем пароль — заходить и работать удаленно:
passwd

Разрешаем логин рутом (всё равно это тест и сюда посторонние не ходят)
vim /etc/ssh/ssdh_config

...
PermitRootLogin yes
...

Теперь можно спокойно ставить всё что хочется:
apt install python3-pip
apt install python3.11-venv

Найденные в интернетах инструкции советуют установить окружение:

python3 -m venv yolo
cd yolo
. bin/activate

Устанавливаю пакет от Ultralytics и отладочный вебсервер Flask
pip install ultralytics
pip install Flask

И пишу свою первую программу на python:

from flask import Flask, request, jsonify, make_response

app = Flask(__name__)

import ultralytics
import os

model = ultralytics.YOLO('yolo11n.pt')

@app.route('/detect', methods=['POST'])
def predict():

    file = request.files['file']
    filename = file.filename
    file.save(os.path.join('/tmp/', file.filename))

    results = model.predict(os.path.join('/tmp/', file.filename))

    os.remove(os.path.join('/tmp/', file.filename))
    for result in results:
        #result.show()  # display to screen
        #result.save(os.path.join('/tmp/', "result_"+file.filename))
        # Return response
        response = make_response(result.to_json())
        response.headers["Access-Control-Allow-Origin"] = "*"
        return response

В данном случае создаем примитивный веб-сервер, умеющий принимать файл по URL /detect, запускающий обработку на модели yolo11n.pt.
Файл в процессе сохраняется в каталоге /tmp, который отображен из основной ОС и по факту является ОЗУ (tmpfs), то есть работает максимально быстро.
Результат обработки вроде как мог бы быть выведен на экран -, но у нас сервер, поэтому эта функция просто закомментирована.
Также, результатом мог бы быть файл с отрисованными рамками -, но я уже их рисую отдельно, и так как мне нужно было — поэтому эта функция тоже закомментирована.

Остается собственно список обьектов, который выводится в формате JSON. А заголовок «Access-Control-Allow-Origin=*» позволит обойти ограничения на кросс-доменные ссылки.

Пробный запуск:
flask --app detect run --host 0.0.0.0

По 5000 порту удается подключить и передать картинку, а в ответ получить список обьектов — всё работает.
Но в документации пишут, что Flask — это отладочный сервер, а нужно запустить что-то более серьезное.

pip install uwsgi
uwsgi --http:5000 --wsgi-file detect.py --callable app --processes 4 --threads 4 --stats:9191

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

#!/bin/sh
PATH=/root/yolo8/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH

cd /root/yolo

. bin/activate

setsid uwsgi --http :5000 --wsgi-file detect.py \
        --callable app --processes 4 --threads 4 \
        --stats :9191 > /dev/null 2>/var/log/yolo_error.log &

И вызывать его из файла /etc/startup

Перезапускаем контейнер:
docker commit t1 yolo
docker stop t1
docker rm t1
docker run -d --name yolo -v /tmp:/tmp -p 2222:22 -p 5000:5000 yolo /etc/startup

Теперь по порту 5000 доступен детектор изображений, а по порту 2222 можно подключиться по ssh и поправить что-то, если потребуется.

Немного изменился формат JSON-ответа, поэтому вносим изменения в модуль MyImgAI.pm, примерно так:

my $d = from_json($1);
-- if($d->{count}){
++ if(defined $d){

my $cnt = 0;
-- foreach my $x (@{$d->{predictions}}){
++ foreach my $x (@$d){

-- my $px_max   = $x->{x_max};
-- my $px_min   = $x->{x_min};
-- my $py_max   = $x->{y_max};
-- my $py_min   = $x->{y_min};
++ my $px_max   = $x->{box}->{x1};
++ my $px_min   = $x->{box}->{x2};
++ my $py_max   = $x->{box}->{y1};
++ my $py_min   = $x->{box}->{y2};

-- my $key = $ch.'_'.$x->{label}.'_'.$px_max.'_'.$px_min.'_'.$py_max.'_'.$py_min;
++ my $key = $ch.'_'.$x->{name}.'_'.$px_max.'_'.$px_min.'_'.$py_max.'_'.$py_min;

-- if($x->{label} eq 'person'){
-- $im->rectangle($x->{x_min},$x->{y_min},$x->{x_max},$x->{y_max},$red);
++ if($x->{name} eq 'person'){
++ $im->rectangle($x->{box}->{x1},$x->{box}->{y1},$x->{box}->{x2},$x->{box}->{y2},$red);

-- elsif($x->{label} eq 'car'){
-- $im->rectangle($x->{x_min},$x->{y_min},$x->{x_max},$x->{y_max},$blue);
++ elsif($x->{name} eq 'car'){
++ $im->rectangle($x->{box}->{x1},$x->{box}->{y1},$x->{box}->{x2},$x->{box}->{y2},$blue);

-- $im->rectangle($x->{x_min},$x->{y_min},$x->{x_max},$x->{y_max},$green);
++ $im->rectangle($x->{box}->{x1},$x->{box}->{y1},$x->{box}->{x2},$x->{box}->{y2},$green);

Теперь все работает почти как раньше, только без оболочки в виде дополнительного сервера с не очень понятными ошибками обновления.
И чуть быстрее.

© Habrahabr.ru