Камера, наконец, работает, хотя первая версия тоже в техническом смысле «работала», если твоё определение работы включает отправку мне сорока трёх писем до обеда.
У меня на полке завалялась старая вебкамера, направленная на переднюю дверь, и скрипт, работающий на запасной машине, смотрит на видеопоток. Как только что-то движется в кадре, он ловит кадр и отправляет мне его по почте. Не компания безопасности, не подписка, просто Python делающий ровно то, о чём я просил, наконец работающий.
почему я вообще это хотел
Я работаю наверху большинство дней, и дверь внизу, достаточно далеко что стук в дверь при доставке или кто-то на самом деле идущий не надёжно доходит до меня. Посылки несколько раз сидели снаружи дольше чем должны были, один раз в погоду которая ящику не сделала одолжение. Исправление не нужно было быть умным. Ему просто нужно было сказать мне в тот момент когда кто-то был там.
попытка один, что не была
Моя первая версия сравнивала два кадра прямо, в цвете, пиксель на пиксель, и суммировала насколько они были разными.
import cv2
cap = cv2.VideoCapture(0)
_, prev = cap.read()
while True:
_, curr = cap.read()
diff = cv2.absdiff(prev, curr)
total_diff = diff.sum()
if total_diff > 500000:
send_alert(curr)
prev = currЭто работало хорошо примерно шесть минут дневного тестирования и потом развалилось тот момент когда солнце прошло за облако. Вся яркость кадра сдвинулась немного, каждый пиксель изменился небольшим количеством, и суммированный поперёк целого цветного изображения это достаточно чтобы прорваться через любой порог я выбрал. Я получил письмо. Потом ещё одно четыре секунды спустя потому что облако продолжало двигаться. Потом одно потому что фары машины подметали крыльцо в тот вечер. Ни одно из этих не была человек. Всё это было «вся кадр стала немного светлее или темнее».
что я на самом деле гуглил
«opencv motion detection false positive lighting change» привёл меня к учебнику pyimagesearch что видимо то что половина интернета учится основной обнаружение движения из, и чтение этого заставило меня почувствовать себя немного глупо полезным образом. Две вещи я полностью пропустил:
- Сначала конвертировать в оттенки серого и размыть перед дифом, так одиночные пиксельные шумы датчика и маленькие мерцания света получают сглажены вместо подсчитаны
- Вместо суммирования сырых пиксельных разностей, пороговый diff в чёрно-белую маску, потом посмотри связанные регионы (контуры) и забуть только об областях больше чем какой-то минимальный размер
Вторая часть та что действительно мала. Облако проходящее меняет яркость везде чуть-чуть, что после порогового в основном исчезает так как настолько маленькое изменение никогда не пересекает порог в первом месте. Человек проходящий через кадр изменяет большой, непрерывный патч пикселей много. Совершенно другая форма как только ты смотришь на это как области вместо единого суммированного числа.
версия что на самом деле работает
Обработка фона сначала. Вместо сравнения кадра с кадром, я беру один кадр как базовый и размываю его:
import cv2
cap = cv2.VideoCapture(0)
_, first_frame = cap.read()
baseline = cv2.GaussianBlur(cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY), (21, 21), 0)Потом, за кадр, размывай и дифф против того базового вместо предыдущего кадра:
def detect_regions(frame, baseline, min_area=2000):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (21, 21), 0)
diff = cv2.absdiff(baseline, gray)
thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1]
thresh = cv2.dilate(thresh, None, iterations=2)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
return [c for c in contours if cv2.contourArea(c) > min_area]И главный цикл, что также медленно обновляет базовый так долгосрочный дрейф яркости (утро на полдень, не просто проходящее облако) наконец не делает всё выглядеть как движение вечно:
while True:
_, frame = cap.read()
regions = detect_regions(frame, baseline)
if regions:
send_alert(frame)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (21, 21), 0)
baseline = cv2.addWeighted(gray.astype("float"), 0.05, baseline.astype("float"), 0.95, 0).astype("uint8")Та строка addWeighted делает медленное скользящее усреднение, толкая базовый 5% в сторону текущего кадра каждый цикл вместо замены его прямо. Настолько медленное что настоящий человек проходящий не получит впитан в базовый перед запуском, постепенно достаточное что солнце пересекающее небо через часы наконец не будет читаться как один гигантский постоянный blob движения.
холодная перезагрузка, потому что провайдеры почты имеют мнения
Другой кусок что взял вечер: даже с правильным обнаружением, один человек проходящий мимо двери три секунды может запустить пять отдельных писем так как они пересекали множество кадров. Простой cooldown таймер исправил это.
import time
last_alert = 0
COOLDOWN_SECONDS = 60
def maybe_alert(frame):
global last_alert
if time.time() - last_alert < COOLDOWN_SECONDS:
return
send_alert(frame)
last_alert = time.time()часть письма
smtplib и email.message не увлекательные библиотеки (честно говоря немного неудобные, API ощущается как не менялся со времени 2003 года, что в основном не), но они получают сообщение с прикреплённым JPEG в мой ящик вокруг четырёх линий как только образец сортируется.
import smtplib
from email.message import EmailMessage
def send_alert(image_bytes):
msg = EmailMessage()
msg["Subject"] = "motion detected"
msg["From"] = "camera@example.com"
msg["To"] = "bjorn@example.com"
msg.set_content("Something moved in frame.")
msg.add_attachment(image_bytes, maintype="image", subtype="jpeg", filename="frame.jpg")
with smtplib.SMTP_SSL("smtp.example.com", 465) as server:
server.login("camera@example.com", "app-password-not-real")
server.send_message(msg)
Первые несколько настоящих оповещений попали прямо в спам, что я вообще не учёл. Оказалось учётная запись что только отправляет, никогда не получает, с неправильно установленной SPF записью, выглядит ровно как что-то что фильтр спама должен быть подозрительным в отношении. Взял SPF и DKIM запись на отправляющем домене перед Gmail остановил тихо закапывая мои собственные оповещения от себя.
ошибка разворачивания что смутила меня больше всего
За первую неделю это «бегало без присмотра» внутри окна терминала на моём ноутбуке, в обычной сессии входа. Что означало то время когда это действительно мало, доставка в час что я случайно имел мой ноутбук закрытый для, весь кусок пошёл в спать вместе с крышкой и никогда не видел вещь. Без присмотра делал много работы в той фразе что установка не реально поддерживала. Это systemd сервис на запасной машине теперь, перезагружается на падение, выживает мне закрывающему что угодно.
почему этот ощущался другим
Большинство что я строил перед этим было самостоятельным: приложение задач, инструмент PDF, скребок что работает один раз и печатает что-то. Этот работает непрерывно, вечно, без присмотра (правильно, теперь), и должен быть действительно правильно, или это либо мисс вещь что я забочусь или спамит меня с ложными тревогами. Первая неделя это делал оба, что было раздражающе двумя совершенно разными способами.
Есть также версия этой идеи я не строил ещё, где оно отличает человека оформленного blob от чего угодно, вместо любого достаточно большого движущегося региона. Контур область получает меня большую часть пути, но большая собака или раскачивающийся куст после шторма оба пересекают тот же размер порога человек делает. Это настоящая более сложная проблема, ближе к настоящему обнаружению объектов чем дифф кадра, и это сидит на списке под действительно чтением CS50P файла I/O лекции правильно вместо перелетания это.
что дальше
Холодная перезагрузка что адаптирует вместо плоской шестидесяти секунды, так человек стоящий у двери разговаривает не перезапускается момент это истекает. И, в конечном счёте, человек-против-моль-против-куста проблема, что я подозреваю заканчивается со мной наконец учебой настоящую модель обнаружения объектов вместо наклонения на дифф кадра вечно.