Улучшаем систему видеонаблюдения, ч.2
Вариант детекции обьектов с помощью 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);
Теперь все работает почти как раньше, только без оболочки в виде дополнительного сервера с не очень понятными ошибками обновления.
И чуть быстрее.