#!/usr/bin/env python3 # MOG2 background subtraction motion detector # Reads video, outputs motion segment timestamps (start end) one per line # Usage: ipcam-mog2.py input.mkv [min_area_pct] [gap_sec] [history] [var_threshold] import sys import cv2 import numpy as np if len(sys.argv) < 2: print("usage: ipcam-mog2.py input.mkv [min_area%] [gap_sec] [history] [var_thresh]", file=sys.stderr) sys.exit(1) path = sys.argv[1] min_area_pct = float(sys.argv[2]) if len(sys.argv) > 2 else 0.5 gap_sec = float(sys.argv[3]) if len(sys.argv) > 3 else 1.0 history = int(sys.argv[4]) if len(sys.argv) > 4 else 500 var_thresh = float(sys.argv[5]) if len(sys.argv) > 5 else 16.0 cap = cv2.VideoCapture(path) if not cap.isOpened(): print(f"cannot open {path}", file=sys.stderr) sys.exit(1) fps = cap.get(cv2.CAP_PROP_FPS) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # downscale for speed - process at 320px wide scale = 320.0 / w if w > 320 else 1.0 sw, sh = int(w * scale), int(h * scale) total_pixels = sw * sh min_pixels = total_pixels * (min_area_pct / 100.0) mog = cv2.createBackgroundSubtractorMOG2( history=history, varThreshold=var_thresh, detectShadows=True, ) # shadow detection marks shadow pixels as 127 vs foreground 255 # learning rate: -1 = automatic, or set explicitly (0.001-0.01 typical) learn_rate = -1 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) segments = [] in_motion = False seg_start = 0.0 seg_end = 0.0 frame_idx = 0 motion_frames = 0 still_frames = 0 while True: ret, frame = cap.read() if not ret: break t = frame_idx / fps small = cv2.resize(frame, (sw, sh)) if scale < 1.0 else frame mask = mog.apply(small, learningRate=learn_rate) # threshold: only foreground (255), ignore shadows (127) _, mask = cv2.threshold(mask, 200, 255, cv2.THRESH_BINARY) # morphological open to kill noise, close to fill gaps mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) fg_pixels = cv2.countNonZero(mask) motion = fg_pixels > min_pixels if motion: still_frames = 0 motion_frames += 1 if not in_motion: in_motion = True seg_start = t seg_end = t else: still_frames += 1 motion_frames = 0 if in_motion and (t - seg_end) > gap_sec: segments.append((seg_start, seg_end)) in_motion = False frame_idx += 1 cap.release() if in_motion: segments.append((seg_start, seg_end)) # merge segments that are closer than gap_sec merged = [] for s, e in segments: if merged and s - merged[-1][1] <= gap_sec: merged[-1] = (merged[-1][0], e) else: merged.append((s, e)) if not merged: print("no motion detected", file=sys.stderr) sys.exit(1) print(f"frames={frame_idx} fps={fps:.1f} area_thresh={min_area_pct}% " f"({int(min_pixels)}px) gap={gap_sec}s history={history} " f"var_thresh={var_thresh}", file=sys.stderr) print(f"{len(merged)} segments", file=sys.stderr) for s, e in merged: sh_, sm, ss = int(s // 3600), int((s % 3600) // 60), s % 60 eh, em, es = int(e // 3600), int((e % 3600) // 60), e % 60 print(f"{sh_:02d}:{sm:02d}:{ss:06.3f} {eh:02d}:{em:02d}:{es:06.3f}")