Пишем программу для автоматического распознавания объектов с веб-камер

Разбираемся, как с помощью Python и OpenCV захватывать видео с нескольких веб-камер, передавать материалы на сервер и распознавать объекты.

В OpenCV существует множество вариантов для трансляции видеопотока. Можно использовать один из них – IP-камеры, но с ними бывает довольно трудно работать. Так, некоторые IP-камеры не позволяют получить доступ к RTSP-потоку (англ. Real Time Streaming Protocol). Другие IP-камеры не работают с функцией OpenCV cv2.VideoCapture. В конце концов, такой вариант может быть слишком дорогостоящим для ваших задач, особенно, если вы хотите построить сеть из нескольких камер.

Как отправлять видеопоток со стандартной веб-камеры с помощью OpenCV? Одним из удобных способов является использование протоколов передачи сообщений и соответствующих библиотек ZMQ и ImageZMQ.

Поэтому сначала мы кратко обсудим транспорт видеопотока вместе с ZMQ, библиотекой для асинхронной передачи сообщений в распределенных системах. Далее, мы реализуем два скрипта на Python:

  1. Клиент, который будет захватывать кадры с простой веб-камеры.
  2. Сервер, принимающий кадры и ищущий на них выбранные типы объектов (например, людей, собак и автомобили).

Для демонстрации работы узлов применяются четыре платы 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

Начнем с реализации клиента. Что он будет делать:

  1. Захватывать видеопоток с камеры (USB или RPi-модуль).
  2. Отправлять кадры по сети через 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

Далее запускаем клиенты, следуя инструкции (будьте внимательны, в вашей системе имена и адреса могут отличаться):

  1. Откройте SSH-соединение с клиентом: ssh pi@192.168.1.10
  2. Запустите экран клиента: screen
  3. Перейдите к профилю: source ~/.profile
  4. Активируйте окружение: workon py3cv4
  5. Установите ImageZMQ, следуя инструкциям библиотеки по установке
  6. Запустите клиент: python client.py --server-ip 192.168.1.5

Ниже представлено демо-видео панели с процессом стриминга и распознавания объектов с четырех камер на Raspberry Pi.

Аналогичные решения из кластера камер и сервера можно использовать и для других задач, например:

  • Распознавание лиц. Такую систему можно использовать в школах для обеспечения безопасности и автоматической оценки посещаемости.
  • Робототехника. Объединяя несколько камер и компьютерное зрение, вы можете создать прототип системы автопилота.
  • Научные исследования. Кластер из множества камер позволяет проводить исследования миграции птиц и животных. При этом можно автоматически обрабатывать фотографии и видео только в случае детектирования конкретного вида, а не просматривать видеопоток целиком.

А какие у вас есть идеи для использования нескольких камер и OpenCV?

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
14 июля 2017

Пишем свою нейросеть: пошаговое руководство

Отличный гайд про нейросеть от теории к практике. Вы узнаете из каких элеме...