
Камеры видеонаблюдения стали для многих стран обыденностью, например в Китае, они могут свисать гроздьями, через каждые 5 метров, по улице. Но в провинции России это все еще может быть в новинку. Я отношусь к видеонаблюдению по большей мере положительно. Ведь вид камеры, даже превентивно может предотвратить хулиганство (однажды я использовал муляжи камер в офисе:)), а главное это возможность контролировать объект наблюдения.
Этот пост про монтаж уличной камеры, на стену многоквартирного дома и программную реализацию - вывод изображения, без использования стандартной программы, оптимизацию, для размещения на raspberry pi.
Монтаж

Внутри помещения, я уже успешно использовал камеры фирмы vstarcam, по этому, лояльное отношение, подтолкнуло сделать заказ на али vstarcam CS64. Забегая вперед скажу, что это не лучший выбор - мыльная картинка, как будто нет даже заявленных 3 МегаПикселей.
План таков: повесить на внешнюю стену электрическую распределительную коробку, внутрь нее поместить блок питания, на крышку прикрепить камеру. Сигнал передается по wi-fi, питание - провести кабель через раму окна.
Примерный бюджет: ip-камера 3500р., коробка 600р., винтики-гаечки (продаются в леруа на развес) 5р., кабель/вилка/клеммы 200р.
Порядок работ:
Блок питания закинут в коробку(не стал его там крепить), отрезан кабель питания. На клеммы прикрутил новый кусок кабеля(брал его с запасом, но в итоге понадобилась только половина), кабель вывел из коробки;
В крышке коробки(она съемная), просверлил 4 отверстия и закрепил на ней камеру болтами с гайками;
Вылез из окна во внешний мир и под окном просверлил отверстия в стене, вбил дюпеля. Прикрутил открытую коробку, из которой, пока что, болтается моток кабеля.
Взял крышку с камерой, продел и подключил внутрь коробки кабеля(питание и не нужный lan), закрыл крышку, таким образом смонтировав камеру.
Просверлил в пластиковой раме окна отверстие наружу и всунул в него кабель питания, положил кабель канал, обрезал кабель до нужной длины и прикрутил вилку. Получилось довольно сурово, но это и к лучшему :)
улица
Мотивом для дальнейшей части повествование было желание поделится с соседями видом со стены, ну и желание разобраться как захватывать видеопоток. Не было желания объяснять старшему поколению, как работает стандартное приложение eye4, по этому я решил реализовать веб страничку. Деплой будет на, уже обитавшую для домашних проектов, raspberry pi 4 4Gb.

В спецификации камеры было указано что она умеет в rtsp, его и выбрал. ip адрес камеры было просто вычислить в настройках маршрутизатора и задать его статичным. Предварительно надо было получить ссылку на видеопоток - а его нет! Я аж вспомнил nmap, а то мало ли с портом промахнулся. В документации нет ни слова, оказывается, в отличии от предыдущих моделей, в программе eye4, зайдя в настройки камеры надо включить опцию "незащищенный пароль". И как то напахнуло старыми китайскими девайсами, с непонятными настройками.

Итоговая ссылка rtsp://admin:password@192.168.0.119:10554/tcp/av0_0
Можно проверить ее подключившись например vlc
Пароль задавался в фирменной утилите.
Код
Программная часть будет использовать python (не судите строго, только год приручаю питона:)). Веб фреймворк Flask был выбран из-за простоты (для одностраничника больше и не надо); Для оптимизации, захват и генерацию кадров было решено разделить на разные процессы, с помощью multiprocessing (в надежде, что это поможет хилому rpi); Для захвата кадров видеопотока и их кодирования, оказалось лучшим вариантом будет использование библиотеки OpenCV.
Непосредственно код:
Файл скрипта на питоне webstreaming.py :
from flask import Response, Flask, render_template
from multiprocessing import Process, Manager
import time
import cv2
app: Flask = Flask(__name__)
source: str = "rtsp://admin:password@192.168.0.119:10554/tcp/av0_1"
def cache_frames(source: str, last_frame: list, running) -> None:
""" Кэширование кадров """
cap = cv2.VideoCapture(source)
#cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) #в некоторых случаях это позволяет избавится от старых кадров
fps = cap.get(cv2.CAP_PROP_FPS)
while running.value:
ret, frame = cap.read() # Чтение кадра
if ret: # Если кадр считан
#frame = cv2.resize(frame, (640, 360)) # Изменение размера кадра, по необходимости
_, buffer = cv2.imencode('.jpg', frame,
[int(cv2.IMWRITE_JPEG_QUALITY), 85]) # Кодирование кадра в JPEG
last_frame[0] = buffer.tobytes() # Кэширование кадра
else:
# Здесь можно обрабатывать ошибки захвата кадра
break # Если не удалось захватить кадр
time.sleep(1 / (fps+1)) # Интервал между кадрами
cap.release()
def generate(shared_last_frame: list):
""" Генератор кадров """
frame_data = None
while True:
if frame_data != shared_last_frame[0]: # Если кадр изменился
frame_data = shared_last_frame[0]
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame_data + b'\r\n') # HTTP ответ для потоковой передачи
time.sleep(1/15) # Задержка
@app.route("/")
def index() -> str:
# Возвращаем отрендеренный шаблон
return render_template("index.html")
@app.route("/video_feed")
def video_feed() -> Response:
return Response(generate(last_frame),
mimetype="multipart/x-mixed-replace; boundary=frame") # Запуск генератора
if __name__ == '__main__':
with Manager() as manager:
last_frame = manager.list([None]) # Кэш последнего кадра
running = manager.Value('i', 1) # Управляемый флаг для контроля выполнения процесса
# Создаём процесс для кэширования кадров
p = Process(target=cache_frames, args=(source, last_frame, running))
p.start()
# Запуск Flask-приложения в блоке try/except
try:
app.run(host='0.0.0.0', port=8000, debug=False, threaded=True, use_reloader=False)
except KeyboardInterrupt:
p.join() # Ожидаем завершения процесса
finally:
running.value = 0 # Устанавливаем флаг в 0, сигнализируя процессу о необходимости завершения
p.terminate() # Принудительно завершаем процесс, если он все еще выполняется
p.join() # Убедимся, что процесс завершился
Файл шаблона templates/index.html :
<html>
<head>
<title>Моя улица вэб стриминг</title>
</head>
<body>
<h1>Моя улица вэб стриминг</h1>
<h3>парковочка</h3>
<img src="{{ url_for('video_feed') }}">
</body>
</html>
Шаблон, состоит из нескольких тегов хтмл и думаю в объяснении не нуждается, по скрипту пройдемся более детально.
Здесь фласк приложение, при открытие страницы клиентом, обращается к генератору кадров, который выбирает изображение, постоянно создаваемое в отдельном процессе, захватывая видеопоток камеры.
Кеширование реализовано с помощью глобальной переменной last_frame, которая для обмена между процессами представляет из себя manager(данные внутри обернуты в list, так как это условие его использования). Это позволяет не генерировать для каждого нового клиента уникальные данные, они смотрят одни и те же картинки, не увеличивая нагрузку.
Сначала запускается процесс p, это позволит параллельно создавать кадры, не нагружая основной процесс.
Далее запускается фласк приложение app.run. Блок try, я добавил для того что бы нормально обработать ctrl-c в терминале. По его завершению, происходят методы завершения созданного процесса.
Функция создания кадра cache_frames. Именно в ней происходит основная нагрузка, которую надо оптимизировать, для маломощного одноплатника. Будем резать качество! Если у Вас будет довольно мощный сервер, вероятно не стоит повторять все советы(оставив хотя бы нормальное разрешение). Для начала я пробовал снижать частоту кадров, это приводило к появлению старых кадров и очевидному замедлению воспроизведения. Обнулить буфер камеры в VideoCapture можно только вытащив из него все кадры. Запускать cap.grab() в цикле это действенный механизм, но это приводит к недопустимой для меня нагрузке. В моей камере есть второй поток с более низким разрешением, это позволило снизить разрешение без cv2.resize, что существенно уменьшило нагрузку, позволив оставить штатную частоту кадров камеры. Все эти моменты могут различаться в разных моделях камер. Давайте пройдемся по строкам главной функции. Сначала мы открываем видеопоток(cap) и узнаем какой у него fps. Далее идет цикл в котором мы читаем кадр(cap.read). Закомментирована строка с изменением размера, так как удалось это сделать на стороне камеры. Далее происходит кодирование в jpeg, с уменьшением качества(imencode). По итогу мы преобразуем массив в необработанную строку байтов, так как именно такой результирующий вид требуется, и размещаем в наш кеш last_frame. Цикл каждый раз засыпает, что бы снизить нагр��зку, интервал чуть выше фпс, что бы вычитывать все кадры из буфера камеры. По выходу из цикла ресурсы видеопотока будут освобождены(release).
Функция generate, при подключении клиента, генерирует хттп mjpeg ответ изображения с кадрами из кеша, который будет отображаться в браузере.
Вы можете заметить, что здесь нет работы над стабильностью. Например если соединение с камерой пропадет на время, скрипт просто сломается и такие ситуации надо обрабатывать.
Так же стоит провести работу по адаптации для нормального wsgi сервера. Это сделано, что бы не раздувать текущий текст и на своем гитхабе я постараюсь выложить доработанную версию.
Перекинув файлы на распберри пай и запустив их, нагрузка составила:

Я посчитал, что чуть более 20% использования cpu(BCM2711), хороший результат, не стеснит остальные проекты.
Осталось только пробросить порт на маршрутизаторе и можно делиться видео наблюдениями. Соседи рады, я рад :-)
Этот текст я написал, так как увидел скудность ру доков по rtsp+python. Возможно кого то это мотивирует на эксперименты с наблюдением и обработкой видеозахвата:) Всем удачи!
