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



Всем привет!

После написания не совсем серьезной и не особо полезной в практическом ключе первой части меня слегка заглодала совесть. И я решил довести начатое до конца. То есть выбрать-таки реализацию нейросети для запуска на 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

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (8):

  1. fifonik
    /#19353858 / +1

    На первой картинке мы видим питона, который проглатывает кота… Да и вообще вся статья о том, как кортить питона котиками :)

  2. RangerXP
    /#19353896

    Критерий котиков для определения качества работы нейросети. Как только нейросеть достигнет того уровня прогресса, который позволит безошибочно определять котиков на фото, она будет признана совершенной.

    • Walker2000
      /#19353902

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

  3. AndriiHolovko
    /#19356412

    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()


    • Walker2000
      /#19356424

      Спасибо, обладатели Macbook точно оценят!

  4. palchik
    /#19368038

    Кот на белом фоне. Постоянный белый фон на разных изображениях вероятнее всего — снег. На фоне снега гораздо вероятнее увидеть собаку чем кота, а еще вероятнее северную ездовую собаку. Все очень логично, хотя и не совсем верно)

    • Walker2000
      /#19368140

      Да, аргумент резонный. Кстати, Imagenet, на котором была обучена описываемая сеть, лежит в открытом доступе. Можно посмотреть исходные изображения здесь: image-net.org/synset?wnid=n02084071#