В 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.
Аналогичные решения из кластера камер и сервера можно использовать и для других задач, например:
- Распознавание лиц. Такую систему можно использовать в школах для обеспечения безопасности и автоматической оценки посещаемости.
- Робототехника. Объединяя несколько камер и компьютерное зрение, вы можете создать прототип системы автопилота.
- Научные исследования. Кластер из множества камер позволяет проводить исследования миграции птиц и животных. При этом можно автоматически обрабатывать фотографии и видео только в случае детектирования конкретного вида, а не просматривать видеопоток целиком.
Комментарии
Применить идею можно для апгрейда измерительных микроскопов-по нажатию клавиши распознать на кадре две координаты в аналоговом формате. Если интересно, пришлю примеры...
This is plagiarism!!! https://www.pyimagesearch.com/2019/04/15/live-video-streaming-over-network-with-opencv-and-imagezmq/
We sincerely apologize, we added a link to the article. This is really a translation of the publication, we always give links to sources, we don’t know how the link was lost here. It seems that this happened when we changed the site engine and the button with the word "Translation" disappeared. In the comments you can see that the author of the translation have replied (Oct 10) to another person that it is a translation, not an original article.
Доброго дня! Хорошая статья! Автор , а как с вами можно связаться?
Это перевод публикации, ссылка на которую расположена под названием статьи (плашка Перевод). В оригинале можно найти контакты автора.
О спасибо! Реализовал не до конца подобную идею на Orange PI. Имхо за этой идеей будущее. Охранная система которая сразу отсылает уведомление что на возле дома/дачи человек это намного лучше чем пассивная запись. Сделайте ещё такую же штуку с распознаванием номеров на авто?
Постараемся реализовать в ближайшем будущем)