Пролог
По мережі зараз гуляє відео – як автопілот Тесли бачить дорогу.
У мене давно свербіли руки транслювати відео, збагачене детектором, та й у реальному часі.
Проблема в тому, що транслювати відео я хочу з Raspberry, а продуктивність нейромережевого детектора залишає бажати кращого.
Intel Neural Computer Stick
Я розглядав різні варіанти рішення.
В
Незважаючи на те, що Інтел надає конвертери для основних фреймворків, тут є низка підводних каменів.
Наприклад, формат потрібної мережі може бути несумісний, а якщо сумісний, то якісь шари можуть не підтримуватись на девайсі, а якщо підтримуються, то в процесі конвертації можуть відбуватися помилки, в результаті яких на виході отримуємо якісь дивні речі.
Загалом, якщо хочеться якусь довільну нейромережу, то з NCS може не вийти. Тому я вирішив спробувати вирішити проблему через наймасовіші та найдоступніші інструменти.
Хмара
Очевидна альтернатива локально-хардварному рішенню піти в хмару.
Готових варіантів – очі розбігаються.
Усі лідери:
… І десятки менш відомих.
Вибрати серед цього різноманіття зовсім непросто.
І я вирішив не вибирати, а завернути стару добру робочу схему на OpenCV у докер і запустити його у хмарі.
Перевага такого підходу в гнучкості та контролі - можна поміняти нейромережа, хостинг, сервер - загалом, будь-яка примха.
Сервер
Почнемо із локального прототипу.
Традиційно я використовую Flask для REST API, OpenCV та MobileSSD мережу.
Поставивши на докер поточні версії, виявив, що OpenCV 4.1.2 не працює з Mobile SSD v1_coco_2018_01_28, і довелося відкотитися на перевірену 11_06_2017.
На старті сервісу завантажуємо імена класів та мережу:
def init():
tf_labels.initLabels(dnn_conf.DNN_LABELS_PATH)
return cv.dnn.readNetFromTensorflow(dnn_conf.DNN_PATH, dnn_conf.DNN_TXT_PATH)
На локальному докері (на не наймолодшому лаптопі) це займає 0.3 секунди, на Raspberry - 3.5.
Запускаємо розрахунок:
def inference(img):
net.setInput(cv.dnn.blobFromImage(img, 1.0/127.5, (300, 300), (127.5, 127.5, 127.5), swapRB=True, crop=False))
return net.forward()
Докер - 0.2 сек, Raspberry - 1.7.
Перетворюємо тензорний вихлоп на читальний json:
def build_detection(data, thr, rows, cols):
ret = []
for detection in data[0,0,:,:]:
score = float(detection[2])
if score > thr:
cls = int(detection[1])
a = {"class" : cls, "name" : tf_labels.getLabel(cls), "score" : score}
a["x"] = int(detection[3] * cols)
a["y"] = int(detection[4] * rows)
a["w"] = int(detection[5] * cols ) - a["x"]
a["h"] = int(detection[6] * rows) - a["y"]
ret.append(a)
return ret
далі
Альтернативний варіант, у якому більше роботи перекладається на сервер: він сам обводить знайдені об'єкти та повертає готову картинку.
Такий варіант хороший там, де ми не хочемо тягнути opencv на сервер.
Докер
Збираємо образ.
Код зачесаний і викладений на
Як платформа візьмемо той же Debian Stretch, що і на Raspberry - не будемо уникати перевіреного техстека.
Треба поставити flask, protobuf, requests, opencv_python, завантажити Mobile SSD, код сервера з Гітхаба і запустити сервер.
FROM python:3.7-stretch
RUN pip3 install flask
RUN pip3 install protobuf
RUN pip3 install requests
RUN pip3 install opencv_python
ADD http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v1_coco_11_06_2017.tar.gz /
RUN tar -xvf /ssd_mobilenet_v1_coco_11_06_2017.tar.gz
ADD https://github.com/tprlab/docker-detect/archive/master.zip /
RUN unzip /master.zip
EXPOSE 80
CMD ["python3", "/docker-detect-master/detect-app/app.py"]
простий
Публікація на Docker Hub
Реєстри докеру розмножуються зі швидкістю не меншою, ніж хмарні детектори.
Щоб не морочитися, ми консервативно підемо через
- реєструємося
- Авторизуємося:
вхід докера - Придумаємо змістовне ім'я:
docker tag opencv-detect tprlab/opencv-detect-ssd - Завантажуємо образ на сервер:
docker push tprlab/opencv-detect-ssd
Запускаємо у хмарі
Вибір, де запустити контейнер, теж дуже широкий.
Усі великі гравці (Гугл, Мікрософт, Амазон) пропонують мікроінстанс безкоштовно у перший рік.
Поексперементувавши з Microsoft Azure та Google Cloud, зупинився на останньому тому, що швидше злетіло.
Не став тут писати інструкцію, оскільки ця частина дуже специфічна для обраного провайдера.
Спробував різні варіанти заліза,
Низькі рівні (shared та виділені) - 0.4 - 0.5 секунди.
Машини потужніші - 0.25 - 0.3.
Що ж, навіть у гіршому випадку виграш утричі, можна спробувати.
Відео
Запускаємо простий OpenCV відеостример на Raspberry, детектируя через Google Cloud.
Для експерименту використали відеофайл, колись знятий на випадковому перехресті.
def handle_frame(frame):
return detect.detect_draw_img(frame)
def generate():
while True:
rc, frame = vs.read()
outFrame = handle_frame(frame)
if outFrame is None:
(rc, outFrame) = cv.imencode(".jpg", frame)
yield(b'--framern' b'Content-Type: image/jpegrnrn' + bytearray(outFrame) + b'rn')
@app.route("/stream")
def video_feed():
return Response(generate(), mimetype = "multipart/x-mixed-replace; boundary=frame")
З детектором виходить не більше трьох кадрів за секунду, все йде дуже повільно.
Якщо в GCloud взяти потужну машину, можна детектувати 4-5 кадрів на секунду, але різниця оком практично непомітна, все одно повільно.
Хмара та транспортні витрати тут не до того, на звичайному залозі детектор і працює з такою швидкістю.
Neural Computer Stick
Не втримався та прогнав бенчмарк на NCS.
Швидкість детектора була трохи повільніше 0.1 секунди, у будь-якому випадку в 2-3 рази швидше за хмари на слабкій машині, тобто 8-9 кадрів в секунду.
Різниця в результатах пояснюється тим, що NCS запускався Mobile SSD версії 2018_01_28.
PS Крім того, експерименти показали, що досить потужна десктопна машина з I7 процесором показує найкращі результати і на ній виявилося можливо вичавити 10 кадрів в секунду.
кластер
Експеримент пішов далі і я поставив детектор на п'яти вузлах Google Kubernetes.
Самі по собі поди були слабкі і кожен із них не міг обробити більше 2-х кадрів на секунду.
Але якщо запустити кластер на N вузлів і розбирати кадри N потоків - то при достатній кількості вузлів (5) можна досягти бажаних 10 кадрів в секунду.
def generate():
while True:
rc, frame = vs.read()
if frame is not None:
future = executor.submit(handle_frame, (frame.copy()))
Q.append(future)
keep_polling = len(Q) > 0
while(keep_polling):
top = Q[0]
if top.done():
outFrame = top.result()
Q.popleft()
if outFrame:
yield(b'--framern' b'Content-Type: image/jpegrnrn' + bytearray(outFrame) + b'rn')
keep_polling = len(Q) > 0
else:
keep_polling = len(Q) >= M
Ось що вийшло:
Трохи не так швидко як з NCS, але бадьоріше ніж в один потік.
Виграш, звичайно, не лінійний - вистрілюють накладки на синхронізацію та глибоке копіювання картинок opencv.
Висновок
Загалом експеримент дозволяє зробити висновок, що, якщо постаратися, можна викрутитися з простою хмарою.
Але потужний десктоп або локальна залізниця дозволяють досягти кращих результатів, причому без жодних хитрощів.
Посилання
Джерело: habr.com