Котики vs нейросеть 2. Или запускаем SqueezeNet v.1.1 на Raspberry Zero в realtime (почти)

    Всем привет!

    После написания не совсем серьезной и не особо полезной в практическом ключе первой части меня слегка заглодала совесть. И я решил довести начатое до конца. То есть выбрать-таки реализацию нейросети для запуска на Rasperry Pi Zero W в реальном времени (конечно, насколько это возможно на таком железе). Прогнать её на данных из реальной жизни и осветить на Хабре полученные результаты.

    Осторожно! Под катом работоспособный код и немного больше котиков, чем в первой части. На картинке коТ и коД соответственно.

    image

    Какую же сеть выбрать?


    Напомню, что в связи с немощностью железа малинки, выбор реализаций нейросети невелик. А именно:

    1. SqueezeNet.
    2. YOLOv3 Tiny.
    3. MobileNet.
    4. ShuffleNet.

    Насколько правильным был выбор в пользу SqueezeNet в первой части?.. Прогонять каждую из вышеозначенных нейросетей на своём железе — довольно долгое мероприятие. Поэтому, терзаемый смутными сомнениями, я решил погуглить, не задавался ли кто-то подобным вопросом до меня. Оказалось, что задавался и исследовал его довольно подробно. Желающие могут обратиться к первоисточнику. Я же ограничусь единственной картинкой из него:

    image

    Из картинки следует, что время обработки одного изображения для разных моделей, обученных по датасету ImageNet, меньше всего у SqueezeNet v.1.1. Примем это в качестве руководства к действию. В сравнение не вошла YOLOv3, но, насколько я помню, YOLO более затратна, чем MobileNet. Т.е. по скорости она тоже должна уступать и SqueezeNet.

    Реализация выбранной сети


    Веса и топологию SqueezeNet, обученной на наборе данных ImageNet (фреймворк Caffe), можно найти на GitHub. Я на всякий случай скачал обе версии, чтобы потом их можно было сравнить. Почему именно ImageNet? Этот набор из всех доступных обладает максимальным количеством классов (1000 шт.), поэтому результаты работы нейросети обещают быть довольно интересными.

    На этот раз посмотрим, как Raspberry Zero справляется с распознаванием кадров с камеры. Вот он, наш скромный труженик сегодняшнего поста:

    image

    За основу кода я взял исходник из блога Adrian Rosebrock, упоминавшегося в первой части, а именно вот отсюда. Но пришлось значительно его перепахать:

    1. Заменить используемую модель с MobileNetSSD на SqueezeNet.
    2. Выполнение п.1 привело к расширению числа классов до 1000. Но при этом функцию выделения объектов разноцветными рамками (SSD функционал) пришлось, увы, убрать.
    3. Убрать прием аргументов через командную строку (почему-то напрягает меня такой ввод параметров).
    4. Убрать метод VideoStream, а с ним и горячо любимую Адрианом библиотеку imutils. Исходно метод использовался для получения видеопотока с камеры. Но у меня с камерой, подключенной к Raspberry Zero, он тупо не заработал, выдавая что-то вроде «Illegal instruction».
    5. Добавить на распознанную картинку частоту кадров (FPS), переписать вычисление FPS.
    6. Сделать сохранение кадров, чтобы написать этот пост.

    На малинке с ОС Rapbian Stretch, Python 3.5.3 и установленной через pip3 install OpenCV 3.4.1 получилось и запустилось следующее:

    Код здесь
    import picamera
    from picamera.array import PiRGBArray
    import numpy as np
    import time
    from time import sleep
    import datetime as dt
    import cv2
    
    # загружаем параметры сети
    prototxt = 'models/squeezenet_v1.1.prototxt'
    model = 'models/squeezenet_v1.1.caffemodel'
    labels = 'models/synset_words.txt'
    
    # загружаем распознаваемые классы
    rows = open(labels).read().strip().split("\n")
    classes = [r[r.find(" ") + 1:].split(",")[0] for r in rows]
    
    # загружаем модель сети
    print("[INFO] loading model...")
    net = cv2.dnn.readNetFromCaffe(prototxt, model)
    
    print("[INFO] starting video stream...")
    # инициализируем камеру
    camera = picamera.PiCamera()
    camera.resolution = (640, 480)
    camera.framerate = 25
    
    # прогреваем камеру
    camera.start_preview()
    sleep(1)
    camera.stop_preview()
    
    # инициализируем кадр в формате raw 
    rawCapture = PiRGBArray(camera)
    # сбрасываем счетчик FPS
    t0 = time.time()
    
    # цикл обработки видео потока
    for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):
        # захватываем кадр как blob
        frame = rawCapture.array
        blob = cv2.dnn.blobFromImage(frame, 1, (224, 224), (104, 117, 124))
    
        # загружаем в сеть blob, получаем класс и вероятность
        net.setInput(blob)
        preds = net.forward()
        preds = preds.reshape((1, len(classes)))
        idxs = int(np.argsort(preds[0])[::-1][:1])
        
        # вычисляем FPS
        FPS = 1/(time.time() - t0)
        t0 = time.time()
    
        # помещаем на кадр класс, вероятность и FPS, выводим в консоль
        text = "Label: {}, p = {:.2f}%, fps = {:.2f}".format(classes[idxs], preds[0][idxs] * 100, FPS)
        cv2.putText(frame, text, (5, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
        print(text)
    
        cv2.imshow("Frame", frame)     # выводим кадр на дисплее Raspberry 
        fname = 'pic_' + dt.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.jpg'
        cv2.imwrite(fname, frame)      # сохраняем кадр на SD диске
        key = cv2.waitKey(1) & 0xFF
    
        # если нажата кнопка `q` выходим из цикла
        if key == ord("q"): break
    
        # очищаем поток raw данных с камеры перед следующим циклом
        rawCapture.truncate(0)
    
    print("[INFO] video stream is terminated")
    
    # прибираем за собой
    cv2.destroyAllWindows()
    camera.close()


    Результаты


    Код выводит на экран монитора, подключенного к Raspberry, очередной распознанный кадр вот в таком виде. Вверху кадра отображен только самый вероятный класс.

    image

    Итак, компьютерная мышь была определена как мышь с очень высокой вероятностью. При этом обновление изображений происходит с частотой 0,34 FPS (т.е. примерно раз в три секунды). Немного напрягает держать камеру и ждать, когда обработается очередной кадр, но жить можно. Кстати, если убрать сохранение кадра на SD карту, скорость обработки возрастет до 0,37...0,38 FPS. Наверняка, есть и другие пути разгона. Поживем — увидим, в любом случае, оставим этот вопрос для следующих постов.

    Отдельно извинюсь за баланс белого. Дело в том, что к Rapberry была подключена IR камера с включенной подсветкой, поэтому бОльшая часть кадров выглядит довольно странно. Но тем ценнее каждое попадание нейросети. Очевидно, что баланс белого на обучающей выборке был более правильным. Кроме того, я решил вставить именно необработанные кадры, чтобы читатель видел их примерно также, как видит и нейросеть.

    Для начала сравним работу SqueezeNet версий 1.0 (на левом кадре) и 1.1 (на правом):

    image

    Видно, что версия 1.1 работает в два с четвертью раза быстрее 1.0 (0,34 FPS против 0,15). Выигрыш по скорости ощутимый. Выводов о точности распознавания по этому примеру делать не стОит, поскольку точность сильно зависит от положения камеры относительно объекта, освещения, бликов, теней и т.п.

    Ввиду столь значительного скоростного преимущества v1.1 над v.1.0 в дальнейшем использовалась только SqueezeNet v.1.1. Для оценки работы модели я наводил камеру на различные попавшиеся под руку предметы и получил на выходе следующие кадры:

    image

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

    image

    Сотовый телефон определяется довольно прилично, если включить экран. Сотовый с выключенным экраном нейросеть за сотовый не считает.

    image

    Пустая чашка вполне сносно определилась как кофейная чашка. Пока всё идет довольно неплохо.

    image

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

    Усложним задачу


    Попробуем подложить нейросети свинью нечто каверзное. Мне как раз попалась самодельная детская игрушка. Полагаю, что большинство читателей признают в ней игрушечную кошку. Интересно, а чем её сочтёт наш зачаточный искусственный разум?

    image

    На кадре слева ИК подсветка стерла все полоски с ткани. В результате игрушка определилась как кислородная маска с довольно приличной вероятностью. Почему бы и нет? Форма игрушки и вправду напоминает кислородную маску.

    На кадре справа я закрыл пальцами ИК подстветку, поэтому на игрушке проявились полосы, а баланс белого стал более правдоподобным. Собственно, это единственный в этом посте выглядящий более-менее нормально кадр. Но нейросеть такое обилие подробностей на изображении сбило с толку. Она определила игрушку как фуфайку (толстовку). Надо сказать, что это тоже не похоже на «пальцем в небо». Попадание если не в «яблоньку», то хотя бы в яблоневый сад).

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

    image

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

    image

    Ага, если кошку сфотографировать сверху, она определяется корректно, но стоит только поменять немного положение кошачьего тела на кадре, для нейросети оно становится собачьим — сибирского хаски и маламута (эскимосская ездовая собака), соответственно.

    image

    А эта подборка прекрасна тем, что на каждом отдельном кадре кошка определена собака разных пород. Причем породы не повторяются )

    image

    Кстати, существуют позы, при которых нейросети становится очевидно, что это все таки кошка, а не собака. То есть SqueezeNet v.1.1 всё таки удалось себя проявить даже на таком сложном для анализа объекте. С учетом успехов нейросети в распознавании предметов в начале теста и признание кошки кошкой в конце на этот раз объявляем твердую боевую ничью )

    Ну вот, собственно, и всё. Предлагаю всем желающим испытать предложенный код на своей малинке и любых, попавших в поле зрения одушевленных и неодушевленных объектах. Особенно буду благодарен тем, кто замерит FPS на Rapberry Pi B+. Обещаю включить результаты в данный пост со ссылкой на приславшего данные. Полагаю, что должно получиться ощутимо больше 1 FPS!

    Надеюсь, что кому-то информация из этого поста будет полезна в развлекательных или образовательных целях, а кого-то, может даже, натолкнёт на новые идеи.

    Всем удачной трудовой недели! И до новых встреч )

    image

    UPD1: На Raspberry Pi 3B+ приведенный выше скрипт работает с частотой 2 с небольшим FPS.

    UPD2: На RPi 3B+ с Movidius NCS скрипт работает с частотой 6 FPS.
    Поделиться публикацией

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

      +1
      На первой картинке мы видим питона, который проглатывает кота… Да и вообще вся статья о том, как кортить питона котиками :)
        +1
        Верно подмечено )
        0
        Критерий котиков для определения качества работы нейросети. Как только нейросеть достигнет того уровня прогресса, который позволит безошибочно определять котиков на фото, она будет признана совершенной.
          0
          Коты оказались интересными объектами для проверки нейросети. Почему-то на них она сильно ошибается. Но, если честно, скорее всего ошибается и на других объектах, просто кот оказался ближе остальных )
          0
          Macbook Air 2017, max ~20 fps, avg 14
          image
          Код адаптирован под Macbook (камеру) ниже.

          import cv2
          import numpy as np
          import time
          from time import sleep
          import datetime as dt
          
          cap = cv2.VideoCapture(0)
          
          prototxt = 'models/solver.prototxt'
          model = 'models/squeezenet_v1.1.caffemodel'
          labels = 'models/synset_words.txt'
          
          rows = open(labels).read().strip().split("\n")
          classes = [r[r.find(" ") + 1:].split(",")[0] for r in rows]
          
          # 
          net = cv2.dnn.readNetFromCaffe(prototxt, model)
          
          # reset FPS meter
          t0 = time.time()
          
          while(True):
          
              # take each frame
              _, frame = cap.read()
              hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
          
              # only for the colors:
              lower_blue = np.array([0,0,0])
              upper_blue = np.array([255,255,255])
          
              # threshold the HSV image to get only blue colors
              mask = cv2.inRange(hsv, lower_blue, upper_blue)
          
              # putting frame into blob
              res = cv2.bitwise_and(frame,frame, mask= mask)
              blob = cv2.dnn.blobFromImage(res, 1, (224, 224), (104, 117, 124))
          
              # measuring the FPS 
              FPS = 1/(time.time() - t0)
              t0 = time.time()
          
              # trying to find the object
              net.setInput(blob)
              preds = net.forward()
              preds = preds.reshape((1, len(classes)))
              idxs = int(np.argsort(preds[0])[::-1][:1])
          
              # creating text for label
              text = "Label: {}, p = {:.2f}%, fps = {:.2f}".format(classes[idxs], preds[0][idxs] * 100, FPS)
              # putting text on frame
              cv2.putText(res, text, (5, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
          
              # prewiev
              cv2.imshow('res',res)
          
              # saving results 1-image for ~2.5 secconds
              # fname = 'pic_' + dt.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.jpg'
              # cv2.imwrite(fname, res)
          
              # exit from frame capturing
              k = cv2.waitKey(5) & 0xFF
              if k == 27:
                  break
          
          cv2.destroyAllWindows()


            0
            Спасибо, обладатели Macbook точно оценят!
            0
            Кот на белом фоне. Постоянный белый фон на разных изображениях вероятнее всего — снег. На фоне снега гораздо вероятнее увидеть собаку чем кота, а еще вероятнее северную ездовую собаку. Все очень логично, хотя и не совсем верно)
              0
              Да, аргумент резонный. Кстати, Imagenet, на котором была обучена описываемая сеть, лежит в открытом доступе. Можно посмотреть исходные изображения здесь: image-net.org/synset?wnid=n02084071#
              0
              Лучше сделать распознавание объектов(именно объектов) а после определение их класса.
              Работает прикольней, визуализирует интересней.



              Данные о движении объектов можно записывать в базу.
              Например перемещение котиков по комнате.
                0
                Добрый день! Идея статьи была в оценке качества классификации на нейронке с широким набором классов. Да, можно поставить SSD детектор на Raspberry, например MobileNet SSD (на примере с картинки, похоже, именно оно и есть). С рамочками изображение гораздо информативнее, плюс детектируются сразу несколько объектов в кадре. Но, надо заметить, что классов значительно меньше (в районе 20). Работает такой детектор также ощутимо медленнее. На CPU RPI B+ будет примерно 0,5...1 FPS. На PRI B+ с Movidius NCS в USB слоте получается 4 с небольшим FPS.

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

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