Видео с облачным детектором объектов на Raspberry Pi

    Пролог


    По сети сейчас гуляет видео — как автопилот Теслы видит дорогу.

    У меня давно чесались руки транслировать видео, обогащенное детектором, да и в реальном времени.



    Проблема в том, что транслировать видео я хочу с Raspberry, а производительность нейросетевого детектора на ней оставляет желать лучшего.

    Intel Neural Computer Stick


    Я рассматривал разные варианты решения.

    В прошлой статье экспериментировал с 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
    

    Дальше экспортируем эту операцию через Flask(на входе картинка, на выходе — результаты детектора в json).

    Альтернативный вариант, в котором больше работы перекладывается на сервер: он сам обводит найденные объекты и возвращает готовую картинку.

    Такой вариант хорош там, где мы не хотим тянуть 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"]
    

    Простой клиент для детектора на основе requests.

    Публикация на Docker Hub


    Реестры докера плодятся со скоростью не меньшей, чем облачные детекторы.

    Чтобы не заморачиваться, мы консервативно пойдем через ДокерХаб.

    1. Регистрируемся
    2. Авторизуемся:
      docker login
    3. Придумаем содержательное имя:
      docker tag opencv-detect tprlab/opencv-detect-ssd
    4. Загружаем образ на сервер:
      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'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + bytearray(outFrame) + b'\r\n')
    
    @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'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + bytearray(outFrame) + b'\r\n')
                    keep_polling = len(Q) > 0
                else:
                    keep_polling = len(Q) >= M
    

    Вот что получилось:



    Немного не так резво как с NCS, но бодрее чем в один поток.

    Выигрыш, конечно, не линеен — выстреливают накладки на синхронизацию и глубокое копирование картинок opencv.

    Заключение


    В целом, эксперимент позволяет сделать вывод, что, если постараться, можно выкрутиться с простым облаком.

    Но мощный десктоп или локальная железка позволяют добиться лучших результатов, причем без всяких ухищрений.

    Ссылки


    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 7 283 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 19

      +1

      Крутая вещь и интересная реализация! Спасибо!
      А в теории к домашней автоматизации можно прикрутить для создания отчета "пока вас не было дома": в дверь звонил вот этот человек два раза и еще проходили люди несли что-то, а детские санки в коридоре никто не трогал. Как вариант?

        0
        Область применения очень широкая, практически в любой отрасли
        0
        Не понял из статьи — можно повторить с любой камерой для малинки или нужна специфичная?
          0
          С любой, нужен только видеопоток
          0
          как всегда, самая широкая область применения будет в военном деле )
            0
            детекция объектов — необходимый шаг практически для любой задачи, связанной с машинной обработкой, так что широта применения не ограничивается военным делом.
            0
            Спасибо за статью! Простите за глупый вопрос, но как, подняв докер image, через curl отправить картинку на распознавание на сервис в докере? Попробовал через REST-клиент и через curl?
             curl -X POST -F 'image=@./detect-app/data/pic.jpg' http://127.0.0.1:8080/ddetect       
            

            Все время ошибка что буфер пуст.
            cv2.error: OpenCV(4.2.0) /io/opencv/modules/imgcodecs/src/loadsave.cpp:730: error: (-215:Assertion failed) !buf.empty() in function 'imdecode_'
            

            Пробовал и так и сяк:
            
            POST http://127.0.0.1:8080/ddetect
            Content-Type: image/jpeg
            
            < ./detect-app/data/pic.jpg
            

            Запрос на `/` выдаёт ожидаемое `DNN REST Service`.
              0
              Двойные кавычки и file называется переменная:
              curl -X POST -F «file=@cat.jpg» 192.168.1.243:8001/ddetect --output a.jpg

              PS: Хабр меняет просто кавычку " на <<
              –1
              Вы на молинке с USB3 Интел стик пробовали? Думал может это вариант.
              И ещё — какое поколение i7 го (у меня ноут с третим i7 лежит, у которого производительность вроде как на маке с пятым i5 вроде)
                +1
                Нет, у меня третья, на ней USB2. Это конечно даст прирост в скорости, но NCS — и так самый быстрый вариант.
                Про поколение I7 не в курсе, лет 5 ему уже.
                  0
                  Да, нашел отзыв, что всего процентов 20-30 прирост при подключении к USB3 вместо USB2 www.hackster.io/news/benchmarking-the-intel-neural-compute-stick-on-the-new-raspberry-pi-4-model-b-e419393f2f97
                  Малинку специально покупал 4ю, чтобы со стиком и ссд попробовать, но так пока не дошли руки (ссд попоробовал — быстро работает).
                  Про процессор спросил, поскольку почему-то не завелся на моем ноуте этот стик (под Ubuntu, устройство не нашлось чтобы intel openvino-вскими сэмпл-программами поиспользовать — буду еще смотреть позже).
                    0

                    У стика главная фича — асинхронное исполнение, не ждать пока данные отправяться, посчитаются и вернуться, а делать параллельно ещё запуск. Можно так и до x2 получить: https://github.com/opencv/opencv/pull/14516


                    Про неработающий стик на унубту — проверьте udev rules:


                    $ cat /etc/udev/rules.d/97-myriad-usbboot.rules
                    
                    SUBSYSTEM=="usb", ATTRS{idProduct}=="2150", ATTRS{idVendor}=="03e7", GROUP="users", MODE="0660", ENV{ID_MM_DEVICE_IGNORE}="1"
                    SUBSYSTEM=="usb", ATTRS{idProduct}=="2485", ATTRS{idVendor}=="03e7", GROUP="users", MODE="0660", ENV{ID_MM_DEVICE_IGNORE}="1"
                    SUBSYSTEM=="usb", ATTRS{idProduct}=="f63b", ATTRS{idVendor}=="03e7", GROUP="users", MODE="0660", ENV{ID_MM_DEVICE_IGNORE}="1"
                    
                      0
                      Асинхронное исполнение — оно только асинхронное, или стик что то может паралеллизовать?
                        0

                        Да, внутри можно запустить одну и ту же сеть обрабатывать несколько входов параллельно.

                        0
                        Спасибо, не знал этого.
                  0

                  Вместо Bare metal/VPS или кластера, попробуйте serverless computing. На каждый реквест будет спинится отдельная виртуальная машина.


                  Может не сработать, но в теории должно быть быстро (но скорее всего дорого).

                    0

                    Вы что имеете в виду?
                    Аmazon lambda?

                      0
                      Amazon Lambda, Azure Serverless, Heroku, Adobe I/O Runtime. Который будет дешевле и проще в использовании.

                      UPD: Я тут подумал, подгружать модель с OpenCV будет тяжело для serverless :/
                        +1

                        На лямбду надо тоже будет ставить все необходимое — opencv, SSD, а я не уверен что они туда легко и быстро встанут.


                        Проще тогда уж использовать готовый computer vision сервис — типа Rekognition или аналогов от Гугла и Микрософта.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое