Pull to refresh

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

Reading time6 min
Views11K
Всем привет!

После написания не совсем серьезной и не особо полезной в практическом ключе первой части меня слегка заглодала совесть. И я решил довести начатое до конца. То есть выбрать-таки реализацию нейросети для запуска на 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.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+12
Comments11

Articles