Время прочтения: 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.