import cv2 import numpy as np from movement_tracker.motion import MotionDetector, ScanDetector rng = np.random.default_rng(0) BG = rng.integers(60, 120, (720, 1280, 3), dtype=np.uint8) def noisy(frame: np.ndarray) -> np.ndarray: return np.clip(frame.astype(int) + rng.integers(-6, 7, frame.shape), 0, 255).astype( np.uint8 ) def warmed_up() -> MotionDetector: d = MotionDetector() for _ in range(40): d.detect(noisy(BG)) return d def test_static_scene_has_no_motion(): assert warmed_up().detect(noisy(BG)) is None def test_picks_largest_blob(): frame = BG.copy() frame[100:300, 900:1100] = 250 frame[500:560, 100:160] = 250 x, y = warmed_up().detect(noisy(frame)) assert abs(x - 1000) < 10 and abs(y - 200) < 10 def test_ignores_motion_during_warmup(): frame = BG.copy() frame[100:300, 900:1100] = 250 assert MotionDetector().detect(frame) is None def test_detects_object_slightly_darker_than_background(): # MOG2 shadow detection would discard this as a shadow bg = np.full((720, 1280, 3), 150, np.uint8) d = MotionDetector() for _ in range(30): d.detect(bg) frame = bg.copy() frame[200:500, 400:700] = 110 x, y = d.detect(frame) assert abs(x - 550) < 10 and abs(y - 350) < 10 def textured_world(width: int) -> np.ndarray: """A scene with enough texture for the scan detector to track features.""" rng = np.random.default_rng(1) world = rng.integers(0, 255, (720, width, 3), dtype=np.uint8) world = cv2.GaussianBlur(world, (0, 0), 6) return cv2.normalize(world, None, 30, 220, cv2.NORM_MINMAX) def pan(world: np.ndarray, frames: int, obj_x=None) -> list: """Detections while the camera pans right 6px per frame across the world.""" d, hits = ScanDetector(), [] for i in range(frames): w = world.copy() if obj_x is not None: x = obj_x(i) w[250:480, x : x + 90] = 240 hits.append(d.detect(w[:, i * 6 : i * 6 + 1280])) return hits def test_scan_ignores_camera_motion(): assert not any(pan(textured_world(1800), 60)) def test_scan_finds_moving_object(): # object walks right 8px/frame; in view coordinates its center is 545 + 2i hits = pan(textured_world(1800), 40, obj_x=lambda i: 500 + 8 * i) x, y = hits[-1] assert abs(x - (545 + 2 * 39)) < 60 and abs(y - 365) < 30