#!/usr/bin/env python3 """ Webcam motion presence detector using frame differencing. Exit codes: 0 = motion detected, 1 = no motion, 2 = camera error (busy/unavailable) Usage: presence_detect.py [camera_id] """ import sys import time import cv2 import numpy as np FRAMES_TO_CHECK = 8 DIFFS_NEEDED = 2 # require motion in at least N consecutive-frame diffs PIXEL_DELTA_THRESHOLD = 18 # per-pixel grayscale delta to count as "changed" MOTION_AREA_RATIO = 0.008 # fraction of pixels that must change to call it motion BLUR_KSIZE = (21, 21) # Gaussian blur kernel to suppress sensor noise # Spacing between the frames we diff. Without it the frames are grabbed # back-to-back within a few milliseconds, so (a) slow, small movements barely # differ between adjacent frames and (b) reading off the ffmpeg v4l2loopback # mirror can hand back the SAME buffered frame twice → a zero delta even while # you're moving. Spacing the grabs out gives slow motion a real temporal # baseline and guarantees each compared frame is a fresh one. INTER_FRAME_DELAY = 0.06 # seconds between grabs (~0.5s total observation window) def detect(camera_id: int) -> int: cap = cv2.VideoCapture(camera_id) if not cap.isOpened(): return 2 motion_diffs = 0 prev_gray = None try: for i in range(FRAMES_TO_CHECK): if i > 0: time.sleep(INTER_FRAME_DELAY) ok, frame = cap.read() if not ok: continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, BLUR_KSIZE, 0) if prev_gray is not None: delta = cv2.absdiff(prev_gray, gray) changed = np.count_nonzero(delta > PIXEL_DELTA_THRESHOLD) if changed / delta.size >= MOTION_AREA_RATIO: motion_diffs += 1 prev_gray = gray finally: cap.release() return 0 if motion_diffs >= DIFFS_NEEDED else 1 if __name__ == "__main__": camera_id = int(sys.argv[1]) if len(sys.argv) > 1 else 0 sys.exit(detect(camera_id))