From ff87201a76944dac73ee2572a627f0c2fb7dda55 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Fri, 10 Jul 2026 06:28:53 +0700 Subject: [PATCH] Motion detect fraction small area of frame --- config.env.example | 17 +++++++++++++---- counter_live_rknn.py | 19 +++++++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/config.env.example b/config.env.example index 1d1a19c..d5423b2 100644 --- a/config.env.example +++ b/config.env.example @@ -138,11 +138,20 @@ CROSS_SNAPSHOT_CLEANUP_SEC=60 # --- Rate / performance --- # Enable motion detection pre-filter: skip inference on frames with no movement -# (true/false, default: false). When enabled, frames below MOTION_THRESHOLD are -# skipped, saving NPU/CPU load. +# (true/false, default: false), saving NPU/CPU load. Motion is measured by the +# fraction of pixels that changed (localized-motion aware), NOT the whole-frame +# average, so an object entering the edge of the frame is detected immediately. MOTION_DETECTION_ENABLED=false -# Mean absolute pixel difference threshold (0–255) to consider a frame as having -# motion. Lower = more sensitive. Default 5.0. +# Per-pixel intensity change (0-255) for a pixel to count as "moved". Lower = more +# sensitive to subtle movement. Default 25. +MOTION_PIXEL_DELTA=25 +# Fraction of frame pixels (0-1) that must change to trigger inference. Lower = +# more sensitive / detects smaller or farther objects sooner. Default 0.002 (0.2%). +MOTION_MIN_AREA_FRAC=0.002 +# Heartbeat: always run inference at least every N frames even with no detected +# motion, so a slow or barely-moving object is never missed for long. Default 15. +MOTION_HEARTBEAT_FRAMES=15 +# (Deprecated) old whole-frame mean-difference threshold; no longer used. MOTION_THRESHOLD=5.0 # Sliding window in seconds for computing the crossing rate (objects/minute) RATE_WINDOW_SEC=60 diff --git a/counter_live_rknn.py b/counter_live_rknn.py index 9a6cbdf..4847a97 100644 --- a/counter_live_rknn.py +++ b/counter_live_rknn.py @@ -111,6 +111,14 @@ IS_LIVE = SOURCE.lower().startswith(("rtsp://", "http://")) MOTION_DETECTION_ENABLED = os.getenv("MOTION_DETECTION_ENABLED", "false").lower() == "true" MOTION_THRESHOLD = float(os.getenv("MOTION_THRESHOLD", "5.0")) +# Per-pixel intensity change (0-255) for a pixel to count as "moved". +MOTION_PIXEL_DELTA = int(os.getenv("MOTION_PIXEL_DELTA", "25")) +# Fraction of frame pixels that must change (0-1) to trigger inference. Small, +# so an object entering the edge of the frame is detected immediately. +MOTION_MIN_AREA_FRAC = float(os.getenv("MOTION_MIN_AREA_FRAC", "0.002")) +# Always run inference at least every N frames even if no motion (heartbeat), so a +# slow/stationary object is never missed for long. +MOTION_HEARTBEAT_FRAMES = int(os.getenv("MOTION_HEARTBEAT_FRAMES", "15")) CROSS_FLASH_FRAMES = 12 POPUP_LIFETIME = 20 @@ -1078,6 +1086,7 @@ def run(): frame_idx = 0 inf_ms = 0.0 prev_gray = None + frames_since_infer = 0 video_writer = None crossing_times = deque() counter_in = 0 @@ -1138,15 +1147,21 @@ def run(): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) if prev_gray is not None: diff = cv2.absdiff(gray, prev_gray) - mean_diff = cv2.mean(diff)[0] - skip_inference = mean_diff < MOTION_THRESHOLD + moved = int(np.count_nonzero(diff > MOTION_PIXEL_DELTA)) + moved_frac = moved / diff.size + skip_inference = moved_frac < MOTION_MIN_AREA_FRAC + if frames_since_infer >= MOTION_HEARTBEAT_FRAMES: + skip_inference = False prev_gray = gray detections = [] if not skip_inference: + frames_since_infer = 0 inf_start = time.time() detections = model(frame) inf_ms = inf_ms * 0.9 + (time.time() - inf_start) * 1000 * 0.1 + else: + frames_since_infer += 1 object_boxes_xyxy = [] object_scores = []