пралог
Па сетцы зараз гуляе відэа - як аўтапілот Теслы бачыць дарогу.
У мяне даўно свярбелі рукі трансляваць відэа, узбагачанае дэтэктарам, ды і ў рэальным часе.
Праблема ў тым, што трансляваць відэа я хачу з 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.
P.S. Акрамя таго, эксперыменты паказалі, што дастаткова магутная дэсктопная машына з 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