Вариант детекции обьектов с помощью 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);
Теперь все работает почти как раньше, только без оболочки в виде дополнительного сервера с не очень понятными ошибками обновления.
И чуть быстрее.
Комментарии (16)
aborouhin
08.12.2024 13:11Автору за отвагу и достигнутый результат, несомненно, плюсик, но зачем sshd внутри контейнера?.. Ну и делать docker-контейнер на debian (даже не -slim версии), с установкой внутрь него и vim, который был нужен один раз, и flask, который в итоге оказался не нужен... (в питоновской части тоже наверняка можно более минимальным вариантом ограничиться, но тут я не специалист) - это вот вообще не best practice. Контейнер должен быть минимальным. А потом ещё про его обновление подумать бы...
JBFW Автор
08.12.2024 13:11Контейнер можно использовать для масштабирования сервисов, а можно - как аналог виртуалки.
Вот в данном случае это и есть виртуалка - быстро создается, и сравнительно безопасно - можно понаустанавливать всяких программ, не рискуя снести рабочую систему. Накосячил - удалить образ и создать заново.Поэтому и ssh, чтобы подключаться напрямую, и vim, чтобы был нормальный редактор.
Насчет flack вообще не в курсе, но так показалось что uwsgi именно его использует внутри себя. Но я python не знаю ))aborouhin
08.12.2024 13:11Как аналог виртуалки обычно используют LXC, но хозяин - барин :) В любом случае поднимать внутри контейнера SSH несколько бессмысленно, т.к. можно просто вызвать "docker exec -it my_container /bin/sh". А так Вы пробросили SSH (да ещё и с рутом и парольным доступом) в контейнер, который собран руками и в нём даже критичные обновления безопасности устанавливаться не будут, пока Вы его вручную не пересоберёте.
JBFW Автор
08.12.2024 13:11Совершенно верно. Потому что стоит это физически в отдельном удаленном помещении, в отдельной сети, и для того чтобы можно было загрузить туда какой-нибудь файл теперь достаточно простого scp filename servername, а не плясок с бубном с заходами в ОС самого девайса.
Поскольку это у нас контейнер - не вижу принципиальной разницы между рутом в контейнере и юзером в этом же контейнере - и тот и другой могут при проникновении злоумышленника что-нибудь напортить, но и того и другого можно так же снести вместе с зараженным контейнером из базовой ОС, и восстановить из сохраненной копии.
А вот автообновления, даже критические - это зло, которое хуже чем вообще ничего не обновлять. Не должно оно САМО обновляться, иначе вас рано или поздно ждет сюрприз "у нас почему-то всё сломалось!!!111"
aborouhin
08.12.2024 13:11А вот автообновления, даже критические - это зло, которое хуже чем вообще ничего не обновлять.
Смелое утверждение. Конечно, лучше иметь тестовый контур, на котором предварительно раскатываются обновления и тестируется, что ничего не сломалось. Но это такое дорогое удовольствие и по времени, и по деньгам, поэтому если пара часов простоя не критичны - Debian stable / Ubuntu LTS с unattended-upgrades вполне себе компромисс. У меня годами так две дюжины серверов автообновляются, инциденты были единичными и для меня приемлемыми. Да и даже апгрейд на новый релиз (это уже вручную, само собой) редко что-то ломает.
NAI
08.12.2024 13:11а можно - как аналог виртуалки.
Нельзя. Потому, контейнеры для сервисов - это кусок архитектуры. Достаточно задаться вопросами: что произойдет если pid 1 (а его назначаете вы, при билде) в контейнере завершит работу (или умрет), что произойдет с вашим vim\ssh-соединением и прочим софтом
JBFW Автор
08.12.2024 13:11и что страшного? Оно вылетит. У вас никогда компьютер не выключался по питанию?
Вот, практический эксперимент буквально онлайн:
Захожу, PID 1 = /etc/startup, скрипт висит за счет sshd -D.
Убиваю sshd - всё, скрипт завершился, контейнер вырубился.Что критичного произошло?
Во-первых, его можно перезапустить снаружи.
Во-вторых, можно завернуть sshd в бесконечный цикл while...Просто тут контейнер используется не для запуска сервиса, а для запуска рабочей среды для программирования и отладки, вот и вся разница.
Правильнее назвать это "песочницей"NAI
08.12.2024 13:11Убиваю sshd - всё, скрипт завершился, контейнер вырубился.
Что критичного произошло? Во-первых, его можно перезапустить снаружи.
Вы ответили на первую половину вопроса, но проигнорировали вторую:
...произойдет с вашим vim\ssh-соединением и прочим софтом
Ответ банален - он умрет. Все еще не видите проблемы? Давайте чтобы было понятнее рассмотрим через призму "У вас никогда компьютер не выключался по питанию?" Итак у вас есть ПК который при старте запускает ну... скажем word и при закрытии окна выключается. Когда это будет удобно? Когда вы писатель и кроме ворда ничем не пользуетесь - включил ПК, набрал текст, закрыл окно - все. Даже больше, система мониторит процесс ворда, и если он завис, система сама перезапустит весь ПК - вот это вот из перовой линии поддержки "перегрузите ПК".
Но вам лично мало ворда и накатываете внутрь Акцесс (БД), Эксель, почту и еще файловую шару делаете. И тут вы получаете оооогромный пучек проблем:
Во-первых, если кто-то писал в БД\файловую шару, а вы закрыли ворд, то что произойдет с остальными процессами? kill -9? а данные? Восстанавливали когда-нибудь поломанные БД? Отличный опыт (не для прода). Что произойдет если процессы подняты, как нормальные сервисы (или отдельные контейнеры) - kill 15, т.е. процессы могут спокойно записать данные, закрыть транзакции, соединения и спокойно умереть. И даже больше, если у вас упал любой сервис, можно написать обработчик ошибок, ну там не знаю "БД упала, пишем в текстовые файлики" или nginx отвалился - вот вам заглушка.
Во-вторых, мониторинг. У вас докер (или подман, или куб) мониторят этот самый pid 1, окей, допустим это сферический идеальный цикл. Но другие сервисы внутри контейнера берут и падают. Докер про них ничего не знает, по этому вы вкорячиваете туда агента мониторинга. Допустим monit - маленький, легкий, сам рестартит сервисы. И тут получается "отличная" история- ваши сервисами управляют аж две системы мониторинга. KISS нервно курит в сторонке
Есть еще пучек проблем типа распределения ресурсов, доступов (это вроде победили, но осадочек остался). В общем, перечисленные проблемы должны сказать вам что забивать гвозди микроскопом конечно можно, но
нах*язачем если есть молоток?ProstoSerge
08.12.2024 13:11Слушай чел, у него домашнее видеонаблюдение и он эксперементирует. Ты пришел сюда рассказывать как нужно випродакшене развернуть докер. Иди своему тимлиду рассказывай как ты умеешь.
ProstoSerge
08.12.2024 13:11Посмотрите в сторону Alpine Linux, для докера на Arm отлично подойдет. Маленький шустрый дистрибутив.
NAI
08.12.2024 13:11Контейнер должен быть минимальным.
Как тебе такое Илон Маск?
REPOSITORY TAG IMAGE ID CREATED SIZE build/trassir-daemon-cuda11.1 4.5.24.0-1232707-Release 695927c06b84 6 weeks ago 22.1GB
Официальная сборка системы видеонаблюдения (все по трассировским инструкциям). Про compose они видимо не слышали
aborouhin
08.12.2024 13:11Ааа!... :) Таких монстров не встречал в природе. Но вот полно любителей запихать в свой образ контейнера какой-нибудь nginx замшелой версии исключительно как reverse proxy, добавляющий HTTPS, и пофигу, что у меня свой nginx для этой задачи вообще на отдельной машине... бесят.
Dynasaur
что за девайс?
JBFW Автор
Опыты с TV-box, позже напишу