ipcam-mog2.py raw

   1  #!/usr/bin/env python3
   2  # MOG2 background subtraction motion detector
   3  # Reads video, outputs motion segment timestamps (start end) one per line
   4  # Usage: ipcam-mog2.py input.mkv [min_area_pct] [gap_sec] [history] [var_threshold]
   5  import sys
   6  import cv2
   7  import numpy as np
   8  
   9  if len(sys.argv) < 2:
  10      print("usage: ipcam-mog2.py input.mkv [min_area%] [gap_sec] [history] [var_thresh]", file=sys.stderr)
  11      sys.exit(1)
  12  
  13  path = sys.argv[1]
  14  min_area_pct = float(sys.argv[2]) if len(sys.argv) > 2 else 0.5
  15  gap_sec = float(sys.argv[3]) if len(sys.argv) > 3 else 1.0
  16  history = int(sys.argv[4]) if len(sys.argv) > 4 else 500
  17  var_thresh = float(sys.argv[5]) if len(sys.argv) > 5 else 16.0
  18  
  19  cap = cv2.VideoCapture(path)
  20  if not cap.isOpened():
  21      print(f"cannot open {path}", file=sys.stderr)
  22      sys.exit(1)
  23  
  24  fps = cap.get(cv2.CAP_PROP_FPS)
  25  w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
  26  h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
  27  total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
  28  
  29  # downscale for speed - process at 320px wide
  30  scale = 320.0 / w if w > 320 else 1.0
  31  sw, sh = int(w * scale), int(h * scale)
  32  total_pixels = sw * sh
  33  min_pixels = total_pixels * (min_area_pct / 100.0)
  34  
  35  mog = cv2.createBackgroundSubtractorMOG2(
  36      history=history,
  37      varThreshold=var_thresh,
  38      detectShadows=True,
  39  )
  40  # shadow detection marks shadow pixels as 127 vs foreground 255
  41  # learning rate: -1 = automatic, or set explicitly (0.001-0.01 typical)
  42  learn_rate = -1
  43  
  44  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
  45  
  46  segments = []
  47  in_motion = False
  48  seg_start = 0.0
  49  seg_end = 0.0
  50  frame_idx = 0
  51  motion_frames = 0
  52  still_frames = 0
  53  
  54  while True:
  55      ret, frame = cap.read()
  56      if not ret:
  57          break
  58      t = frame_idx / fps
  59  
  60      small = cv2.resize(frame, (sw, sh)) if scale < 1.0 else frame
  61      mask = mog.apply(small, learningRate=learn_rate)
  62  
  63      # threshold: only foreground (255), ignore shadows (127)
  64      _, mask = cv2.threshold(mask, 200, 255, cv2.THRESH_BINARY)
  65  
  66      # morphological open to kill noise, close to fill gaps
  67      mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
  68      mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
  69  
  70      fg_pixels = cv2.countNonZero(mask)
  71      motion = fg_pixels > min_pixels
  72  
  73      if motion:
  74          still_frames = 0
  75          motion_frames += 1
  76          if not in_motion:
  77              in_motion = True
  78              seg_start = t
  79          seg_end = t
  80      else:
  81          still_frames += 1
  82          motion_frames = 0
  83          if in_motion and (t - seg_end) > gap_sec:
  84              segments.append((seg_start, seg_end))
  85              in_motion = False
  86  
  87      frame_idx += 1
  88  
  89  cap.release()
  90  
  91  if in_motion:
  92      segments.append((seg_start, seg_end))
  93  
  94  # merge segments that are closer than gap_sec
  95  merged = []
  96  for s, e in segments:
  97      if merged and s - merged[-1][1] <= gap_sec:
  98          merged[-1] = (merged[-1][0], e)
  99      else:
 100          merged.append((s, e))
 101  
 102  if not merged:
 103      print("no motion detected", file=sys.stderr)
 104      sys.exit(1)
 105  
 106  print(f"frames={frame_idx} fps={fps:.1f} area_thresh={min_area_pct}% "
 107        f"({int(min_pixels)}px) gap={gap_sec}s history={history} "
 108        f"var_thresh={var_thresh}", file=sys.stderr)
 109  print(f"{len(merged)} segments", file=sys.stderr)
 110  
 111  for s, e in merged:
 112      sh_, sm, ss = int(s // 3600), int((s % 3600) // 60), s % 60
 113      eh, em, es = int(e // 3600), int((e % 3600) // 60), e % 60
 114      print(f"{sh_:02d}:{sm:02d}:{ss:06.3f} {eh:02d}:{em:02d}:{es:06.3f}")
 115