Время прочтения: 4 мин.

Для построения детектора движения будем использовать микрокомпьютер Raspberry Pi 3 Model B, оснащенный четырехъядерным процессором ARMv7 и оперативной памятью объемом 1 Гбайт. Ещё нам потребуется карта памяти MicroSD, блок питания с разъемом MicroUSB и веб-камера. Будем считать, что на компьютер уже установлена операционная система Raspberry Pi OS и настроено подключение к Интернет.

Перед написанием скрипта необходимо пройти несколько подготовительных шагов.

Обновляем программные пакеты и устанавливаем библиотеку opencv.

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install python3-opencv

Переходим в домашний каталог, создаем папку проекта motion_detector, переходим в эту папку, в ней создаем папку images для сохранения кадров с обнаруженным движением.

cd ~
mkdir motion_detector
cd motion_detector
mkdir images

Создаем пустой файл скрипта simple_detector.py и задаем права на его запуск.

touch simple_detector.py
chmod +x simple_detector.py

Далее переходим к разработке скрипта для обнаружения движения.

Сначала задаем путь к интерпретатору Python и импортируем необходимые библиотеки.

#!/usr/bin/python3

import cv2
import numpy as np
from datetime import datetime

Создаем экземпляр класса для отсечения фона.

backSub = cv2.createBackgroundSubtractorMOG2(50, 16, True)

Инициируем видеозахват с камеры и устанавливаем разрешение кадра.

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

Захват и обработку изображений будем выполнять в бесконечном цикле. Каждая итерация цикла начинается с захвата изображения с камеры.

while True:
    _, frame = cap.read()

Изображение frame — тензор размерности 720x1280x3.

Далее отсекаем фон.

    fg_mask = backSub.apply(frame)

Результатом этой операции является изображение с тремя градациями яркости, где черной области соответствует фон, белой — движущиеся объекты, а серой — тени.

Преобразуем это изображение в черно-белое таким образом, чтобы тени также были отнесены к движущимся объектам.

    _, mask_thr = cv2.threshold(fg_mask, 100, 255, 0)

На изображении могут быть артефакты в виде мелких точек, вызванные изменениями освещенности, атмосферными осадками, отражениями разных источников света. Для исключения таких артефактов применим к изображению операцию размыкания (opening) с квадратным ядром 5×5. В результате изображение будет очищено от мелких артефактов.

    kernel_open = np.ones((5,5), np.uint8)
    mask_open = cv2.morphologyEx(mask_thr, cv2.MORPH_OPEN, kernel_open

Также применяем операцию замыкания (closing) с квадратным ядром 9×9. Операция замыкания позволяет объединить близко расположенные области белого цвета.

    kernel_close = np.ones((9,9), np.uint8)
    mask_close = cv2.morphologyEx(mask_open, cv2.MORPH_CLOSE, kernel_close)

Результат двух последних действий приведен ниже.

Далее выполняем поиск контуров.

    _, contours, _ = cv2.findContours(mask_close, cv2.RETR_EXTERNAL,\
                              cv2.CHAIN_APPROX_SIMPLE)

Поскольку часть контуров имеет очень малый размер, будем считать, что это также артефакты.  Выполняем отбор найденных контуров по критерию минимальной площади. В качестве порога используем значение 100, что эквивалентно квадратному контуру размером 10×10 пикселей.

    area_threshold = 100
    contours_sel = [cnt for cnt in contours if cv2.contourArea(cnt) > area_threshold]

После этой операции контуры малой площади исключаются из процесса обработки.

Оцениваем отношение суммарной площади контуров к площади всего кадра.

total_area = 0
for cnt in contours_sel:
    total_area += cv2.contourArea(cnt)
rel_area = total_area / (frame.shape[0] * frame.shape[1]) * 100

В случае, если относительная площадь областей белого цвета превысит некоторое пороговое значение, будем считать, что в кадре есть движение. Для таких кадров обводим контуры прямоугольниками, добавляем текущую дату и время, а также сохраняем полученное изображение в папку images.

motion_threshold = 0.5

if rel_area > motion_threshold:

    frame_boxes = frame.copy()
    for cnt in contours_sel:
        x,y,w,h = cv2.boundingRect(cnt)
        cv2.rectangle(frame_boxes, (x, y), (x + w, y + h), (0, 0, 255), 2)

    dt = datetime.now()
    dt_image = dt.strftime('%d.%m.%Y %H:%M:%S.%f')[:-3]
    cv2.putText(frame_boxes, dt_image, (20,40),\
                       cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)

    dt_file = dt.strftime('%Y-%m-%d_%H-%M-%S.%f')[:-3]
    fname_out = 'images/' + dt_file + '.jpg'
    cv2.imwrite(fname_out, frame_boxes)

В результате получаем набор исходных кадров с обозначенными областями движения и метками времени.

На этом разработку простого детектора движения можно считать завершенной. По результатам опытной эксплуатации установлены следующие факты:

  • быстродействие детектора составляет около 1,5 кадров в секунду;
  • за одни сутки накапливается около 5 тысяч изображений объемом до 700 Мбайт, таким образом, карта памяти объемом 32 Гбайта позволит хранить кадры с признаками движения приблизительно за один месяц работы системы.

Пример работы детектора приведен на видеозаписи. Cкрипт детектора выложен в репозитории github.com/mporuchikov/motion_detector.