Пишем программу для автоматического распознавания объектов с веб-камер
Разбираемся, как с помощью Python и OpenCV захватывать видео с нескольких веб-камер, передавать материалы на сервер и распознавать объекты.
В OpenCV существует множество вариантов для трансляции видеопотока. Можно использовать один из них – IP-камеры, но с ними бывает довольно трудно работать. Так, некоторые IP-камеры не позволяют получить доступ к RTSP-потоку (англ. Real Time Streaming Protocol). Другие IP-камеры не работают с функцией OpenCV cv2.VideoCapture
. В конце концов, такой вариант может быть слишком дорогостоящим для ваших задач, особенно, если вы хотите построить сеть из нескольких камер.
Как отправлять видеопоток со стандартной веб-камеры с помощью OpenCV? Одним из удобных способов является использование протоколов передачи сообщений и соответствующих библиотек ZMQ и ImageZMQ.
Поэтому сначала мы кратко обсудим транспорт видеопотока вместе с ZMQ, библиотекой для асинхронной передачи сообщений в распределенных системах. Далее, мы реализуем два скрипта на Python:
- Клиент, который будет захватывать кадры с простой веб-камеры.
- Сервер, принимающий кадры и ищущий на них выбранные типы объектов (например, людей, собак и автомобили).
Для демонстрации работы узлов применяются четыре платы Raspberry Pi с подключенными модулями камер. На их примере мы покажем, как использовать дешевое оборудование в создании распределенной сети из камер, способных отправлять кадры на более мощную машину для дополнительной обработки.
Передача сообщений и ZMQ
Передача сообщений – парадигма программирования, традиционно используемая в многопроцессорных распределенных системах. Концепция предполагает, что один процесс может взаимодействовать с другими процессами через посредника – брокера сообщений (англ. message broker). Посредник получает запрос, а затем обрабатывает акт пересылки сообщения другому процессу/процессам. При необходимости брокер сообщений также отправляет ответ исходному процессу.
ZMQ является высокопроизводительной библиотекой для асинхронной передачи сообщений, используемой в распределенных системах. Этот пакет обеспечивает высокую пропускную способность и малую задержку. На основе ZMQ Джефом Бассом создана библиотека ImageZMQ, которую сам Джеф использует для компьютерного зрения на своей ферме вместе с теми же платами Raspberry Pi.
Начнем с того, что настроим клиенты и сервер.
Конфигурирование системы и установка необходимых пакетов
Сначала установим opencv и ZMQ. Чтобы избежать конфликтов, развертывание проведем в виртуальной среде:
$ workon <env_name> # например, py3cv4 $ pip install opencv-contrib-python $ pip install zmq $ pip install imutils
Теперь нам нужно клонировать репозиторий с ImageZMQ:
$ cd ~ $ git clone https://github.com/jeffbass/imagezmq.git
Далее, можно скопировать директорию с исходником или связать ее с вашим виртуальным окружением. Рассмотрим второй вариант:
$ cd ~/.virtualenvs/py3cv4/lib/python3.6/site-packages $ ln -s ~/imagezmq/imagezmq imagezmq
Библиотеку ImageZMQ нужно установить и на сервер, и на каждый клиент.
Примечание: чтобы быть увереннее в правильности введенного пути, используйте дополнение через табуляцию.
Подготовка клиентов для ImageZMQ
В этом разделе мы осветим важное отличие в настройке клиентов.
Наш код будет использовать имя хоста клиента для его идентификации. Для этого достаточно и IP-адреса, но настройка имени хоста позволяет проще считать назначение клиента.
В нашем примере для определенности мы предполагаем, что вы используете Raspberry Pi с операционной системой Raspbian. Естественно, что клиент может быть построен и на другой ОС.
Чтобы сменить имя хоста, запустите терминал (это можно сделать через SSH-соединение) и введите команду raspi-config
:
$ sudo raspi-config
Вы увидите следующее окно терминала. Перейдите к пункту 2 Network Options.
На следующем шаге выберите опцию N1 Hostname.
На этом этапе задайте осмысленное имя хоста (например, pi-livingroom, pi-bedroom, pi-garage). Так вам будет легче ориентироваться в клиентах сети и сопоставлять имена и IP-адреса.
Далее, необходимо согласиться с изменениями и перезагрузить систему.
В некоторых сетях вы можете подключиться через SSH, не предоставляя IP-адрес явным образом:
$ ssh pi@pi-frontporch
Определение отношений сервер-клиент
Прежде чем реализовать стриминг потокового видео по сети, определим отношения клиентов и сервера. Для начала уточним терминологию:
- Клиент – устройство, отвечающее за захват кадров с веб-камеры с использованием OpenCV, а затем за отправку кадров на сервер.
- Сервер — компьютер, принимающий кадры от всех клиентов.
Конечно, и сервер, и клиент могут и принимать, и отдавать какие-то данные (не только видеопоток), но для нас важно следующее:
- Существует как минимум одна (а скорее всего, несколько) система, отвечающая за захват кадров (клиент).
- Существует только одна система, используемая для получения и обработки этих кадров (сервер).
Структура проекта
Структура проекта будет состоять из следующих файлов:
$ $ tree . ├── MobileNetSSD_deploy.caffemodel ├── MobileNetSSD_deploy.prototxt ├── client.py └── server.py 0 directories, 4 files
Два первых файла из списка соответствуют файлам предобученной нейросети Caffe MobileNet SSD для распознавания объектов. В репозитории по ссылке можно найти соответствующие файлы, чьи названия, правда, могут отличаться от приведенных (*.caffemodel
и deploy.prototxt
). Сервер (server.py
) использует эти файлы Caffe в DNN-модуле OpenCV.
Скрипт client.py
будет находиться на каждом устройстве, которое отправляет поток на сервер.
Реализация клиентского стримера на OpenCV
Начнем с реализации клиента. Что он будет делать:
- Захватывать видеопоток с камеры (USB или RPi-модуль).
- Отправлять кадры по сети через ImageZMQ.
Откроем файл client.py
и вставим следующий код:
# импортируем необходимые библиотеки from imutils.video import VideoStream # захват кадров с камеры import imagezmq import argparse # обработка аргумента командной строки, содержащего IP-адрес сервера import socket # получение имени хоста Raspberry Pi import time # для учета задержки камеры перед отправкой кадров # создаем парсер аргументов и парсим ap = argparse.ArgumentParser() ap.add_argument("-s", "--server-ip", required=True, help="ip address of the server to which the client will connect") args = vars(ap.parse_args()) # инициализируем объект ImageSender с адресом сокета сервера sender = imagezmq.ImageSender(connect_to="tcp://{}:5555".format( args["server_ip"]))
Назначение импортируемых модулей описано в комментариях. В последних строчках создается объект-отправитель, которому передаются IP-адрес и порт сервера. Указанный порт 5555
обычно не вызывает конфликтов.
Инициализируем видеопоток и начнем отправлять кадры на сервер.
# получим имя хоста, инициализируем видео поток, # дадим датчику камеры прогреться rpiName = socket.gethostname() vs = VideoStream(usePiCamera=True).start() #vs = VideoStream(src=0).start() time.sleep(2.0) # задержка для начального разогрева камеры while True: # прочитать кадр с камеры и отправить его на сервер frame = vs.read() sender.send_image(rpiName, frame)
Теперь у нас есть объект VideoStream
, созданный для захвата фреймов с RPi-камеры. Если вы используете USB-камеру, раскомментируйте следующую строку и закомментируйте ту, что активна сейчас.
В этом месте вы также можете установить разрешение камеры. Мы будем использовать максимальное, так что аргумент не передастся. Если вы обнаружите задержку, надо уменьшить разрешение, выбрав одно из доступных значений, представленных в таблице. Например:
vs = VideoStream(usePiCamera=True, resolution=(320, 240)).start()
Для USB-камеры такой аргумент не предусмотрен. В следующей строке после считывания кадра можно изменить его размер:
frame = imutils.resize(frame, width=320)
В последних строках скрипта происходит захват и отправка кадров на сервер.
Реализация сервера
На стороне сервера необходимо обеспечить:
- Прием кадров от клиентов.
- Детектирование объектов на каждом из входящих кадров.
- Подсчет количества объектов для каждого из кадров.
- Отображение смонтированного кадра (панели), содержащего изображения от всех активных устройств.
Последовательно заполним файл с описанием сервера server.py
:
# импортируем необходимые библиотеки from imutils import build_montages # монтаж всех входящих кадров from datetime import datetime import numpy as np import imagezmq import argparse import imutils import cv2 # создаем парсер аргументов и парсим их ap = argparse.ArgumentParser() ap.add_argument("-p", "--prototxt", required=True, help="path to Caffe 'deploy' prototxt file") ap.add_argument("-m", "--model", required=True, help="path to Caffe pre-trained model") ap.add_argument("-c", "--confidence", type=float, default=0.2, help="minimum probability to filter weak detections") ap.add_argument("-mW", "--montageW", required=True, type=int, help="montage frame width") ap.add_argument("-mH", "--montageH", required=True, type=int, help="montage frame height") args = vars(ap.parse_args())
Библиотека imutils
упрощает работу с изображениями (есть на GitHub и PyPi).
Пять аргументов, обрабатываемых с помощью парсера argparse
:
--prototxt
: путь к файлу прототипа глубокого изучения Caffe.--model
: путь к предообученной модели нейросети Caffe.--confidence
: порог достоверности для фильтрации случаев нечеткого обнаружения.--montageW
: количество столбцов для монтажа общего кадра, состоящего в нашем примере из 2х2 картинок (то есть montageW = 2) . Часть ячеек может быть пустой.--montageH
: аналогично предыдущему пункту — количество строк в общем кадре.
Вначале инициализируем объект ImageHub
для работы с детектором объектов. Последний построен на базе MobileNet Single Shot Detector.
imageHub = imagezmq.ImageHub() # инициализируем список меток классов сети MobileNet SSD, обученной # для детектирования, генерируем набор ограничивающих прямоугольников # разного цвета для каждого класса CLASSES = ["background", "aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"] # загружаем сериализованную модель Caffe с диска print("[INFO] loading model...") net = cv2.dnn.readNetFromCaffe(args["prototxt"], args["model"])
Объект ImageHub
используется сервером для приема подключений от каждой платы Raspberry Pi. По существу, для получения кадров по сети и отправки назад подтверждений здесь используются сокеты и ZMQ .
Предположим, что в системе безопасности мы отслеживаем только три класса подвижных объектов: собаки, люди и автомобили. Эти метки мы запишем в множество CONSIDER
, чтобы отфильтровать прочие неинтересные нам классы (стулья, растения и т. д.).
Кроме того, необходимо следить за активностью клиентов, проверяя время отправки тем или иным клиентом последнего кадра.
# инициализируем выбранный набор подсчитываемых меток классов, # словарь-счетчик и словарь фреймов CONSIDER = set(["dog", "person", "car"]) objCount = {obj: 0 for obj in CONSIDER} frameDict = {} # инициализируем словарь, который будет содержать информацию # о том когда устройство было активным в последний раз lastActive = {} lastActiveCheck = datetime.now() # храним ожидаемое число клиентов, период активности # вычисляем длительность ожидания между проверкой # на активность устройства ESTIMATED_NUM_PIS = 4 ACTIVE_CHECK_PERIOD = 10 ACTIVE_CHECK_SECONDS = ESTIMATED_NUM_PIS * ACTIVE_CHECK_PERIOD # назначаем ширину и высоту монтажного кадра # чтобы просматривать потоки от всех клиентов вместе mW = args["montageW"] mH = args["montageH"] print("[INFO] detecting: {}...".format(", ".join(obj for obj in CONSIDER)))
Далее необходимо зациклить потоки, поступающие от клиентов и обработку данных на сервере.
# начинаем цикл по всем кадрам while True: # получаем имя клиента и кадр, # подтверждаем получение (rpiName, frame) = imageHub.recv_image() imageHub.send_reply(b'OK') # если устройства нет в словаре lastActive, # это новое подключенное устройство if rpiName not in lastActive.keys(): print("[INFO] receiving data from {}...".format(rpiName)) # записываем время последней активности для устройства, # от которого мы получаем кадр lastActive[rpiName] = datetime.now()
Итак, сервер забирает изображение в imageHub
, высылает клиенту сообщение о подтверждении получения. Принятое сервером сообщениеimageHub.recv_image
содержит имя хоста rpiName
и кадр frame
. Остальные строки кода нужны для учета активности клиентов.
Затем мы работаем с кадром, формируя блоб (о функции blobFromImage
читайте подробнее в посте pyimagesearch). Блоб передается нейросети для детектирования объектов.
Замечание: мы продолжаем рассматривать цикл, поэтому здесь и далее будьте внимательны с отступами в коде.
# изменяем размер кадра, чтобы ширина была не больше 400 пикселей, # захватываем размеры кадров и создаем блоб frame = imutils.resize(frame, width=400) (h, w) = frame.shape[:2] blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 0.007843, (300, 300), 127.5) # передаем блоб нейросети, получаем предсказания net.setInput(blob) detections = net.forward() # сбрасываем число объектов для интересующего набора objCount = {obj: 0 for obj in CONSIDER}
Теперь мы хотим пройтись по детектированным объектам, чтобы посчитать и выделить их цветными рамками:
# циклически обходим детектированные объекты for i in np.arange(0, detections.shape[2]): # извлекаем вероятность соответствующего предсказания confidence = detections[0, 0, i, 2] # отфильтруем слабые предсказания, # гарантируя минимальную достоверность if confidence > args["confidence"]: # извлекаем индекс метки класса idx = int(detections[0, 0, i, 1]) # проверяем, что метка класса в множестве интересных нам if CLASSES[idx] in CONSIDER: # подсчитываем детектированный объект objCount[CLASSES[idx]] += 1 # вычисляем координаты рамки, ограничивающей объект box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (startX, startY, endX, endY) = box.astype("int") # рисуем рамку вокруг объекта cv2.rectangle(frame, (startX, startY), (endX, endY), (255, 0, 0), 2)
Далее, аннотируем каждый кадр именем хоста и количеством объектов. Наконец, монтируем из нескольких кадров общую панель:
# отобразим имя клиента на кадре cv2.putText(frame, rpiName, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) # отобразим число объектов на кадре label = ", ".join("{}: {}".format(obj, count) for (obj, count) in objCount.items()) cv2.putText(frame, label, (10, h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255,0), 2) # обновим кадр в словаре кадров клиентов frameDict[rpiName] = frame # построим общий кадр из словаря кадров montages = build_montages(frameDict.values(), (w, h), (mW, mH)) # покажем смонтированный кадр на экране for (i, montage) in enumerate(montages): cv2.imshow("Home pet location monitor ({})".format(i), montage) # детектируем нажатие какой-либо клавиши key = cv2.waitKey(1) & 0xFF
Остался заключительный блок для проверки последних активностей всех клиентов. Эти операции особенно важны в системах безопасности, чтобы при отключении клиента вы не наблюдали неизменный последний кадр.
# если разница между текущим временем и временем последней активности # больше порога, производим проверку if (datetime.now() - lastActiveCheck).seconds > ACTIVE_CHECK_SECONDS: # циклично обходим все ранее активные устройства for (rpiName, ts) in list(lastActive.items()): # удаляем клиент из словарей кадров и последних активных # если устройство неактивно if (datetime.now() - ts).seconds > ACTIVE_CHECK_SECONDS: print("[INFO] lost connection to {}".format(rpiName)) lastActive.pop(rpiName) frameDict.pop(rpiName) # устанавливаем время последней активности lastActiveCheck = datetime.now() # если нажата клавиша `q`, выходим из цикла if key == ord("q"): break # закрываем окна и освобождаем память cv2.destroyAllWindows()
Запускаем стриминг видео с камер
Теперь, когда мы реализовали и клиент, и сервер, проверим их. Загрузим клиент на каждую плату Raspberry Pi с помощью SCP-протокола:
$ scp client.py pi@192.168.1.10:~ $ scp client.py pi@192.168.1.11:~ $ scp client.py pi@192.168.1.12:~ $ scp client.py pi@192.168.1.13:~
Проверьте, что на всех машинах установлены импортируемые клиентом или сервером библиотеки. Первым нужно запускать сервер. Сделать это можно следующей командой:
$ python server.py --prototxt MobileNetSSD_deploy.prototxt \ --model MobileNetSSD_deploy.caffemodel --montageW 2 --montageH 2
Далее запускаем клиенты, следуя инструкции (будьте внимательны, в вашей системе имена и адреса могут отличаться):
- Откройте SSH-соединение с клиентом:
ssh pi@192.168.1.10
- Запустите экран клиента:
screen
- Перейдите к профилю:
source ~/.profile
- Активируйте окружение:
workon py3cv4
- Установите ImageZMQ, следуя инструкциям библиотеки по установке
- Запустите клиент:
python client.py --server-ip 192.168.1.5
Ниже представлено демо-видео панели с процессом стриминга и распознавания объектов с четырех камер на Raspberry Pi.
Аналогичные решения из кластера камер и сервера можно использовать и для других задач, например:
- Распознавание лиц. Такую систему можно использовать в школах для обеспечения безопасности и автоматической оценки посещаемости.
- Робототехника. Объединяя несколько камер и компьютерное зрение, вы можете создать прототип системы автопилота.
- Научные исследования. Кластер из множества камер позволяет проводить исследования миграции птиц и животных. При этом можно автоматически обрабатывать фотографии и видео только в случае детектирования конкретного вида, а не просматривать видеопоток целиком.