Детектор движения на Raspberry Pi

На основе одноплатного микрокомпьютера можно собрать 3D-принтер, построить медиацентр и даже разработать настольную игру. Такие компьютеры можно применить и для построения систем безопасности, в частности, систем видеонаблюдения. Пользуясь возможностями микрокомпьютера Raspberry Pi и алгоритмами машинного зрения, разработаем простой детектор движения.

Для построения детектора движения будем использовать микрокомпьютер 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.

0
3 комментария
maguto dev

Прикольная идея.
Было бы интересно узнать какой объём ресурсов raspbery остаются доступными, чтоб прикрутить функционал.

Ответить
Развернуть ветку
NTA
Автор

Шестичасовой эксперимент показал, что при работе скрипта средняя загрузка процессора на Raspberry Pi 3 Model B составляет около 44%. Таким образом, более половины ресурсов процессора остаются свободными.

Ответить
Развернуть ветку
maguto dev

Благодарю!

Ответить
Развернуть ветку
0 комментариев
Раскрывать всегда