From 7721c8440625c4b41ede213c60275744917ceca8 Mon Sep 17 00:00:00 2001 From: Alberto-Audrix Date: Fri, 24 Jul 2026 13:51:17 +0700 Subject: [PATCH] add 2 second window for snapshot and counter --- config.env.example | 7 ++ counter_live_rknn.py | 243 ++++++++++++++++++++++++++----------------- env.example | 5 + 3 files changed, 160 insertions(+), 95 deletions(-) diff --git a/config.env.example b/config.env.example index 3c71f59..42343d9 100644 --- a/config.env.example +++ b/config.env.example @@ -84,6 +84,13 @@ INHERIT_PX=60 ZONE_COOLDOWN_LEFT_SEC=3 ZONE_COOLDOWN_RIGHT_SEC=3 +# --- Feeder dwell (seconds) --- +# Sack must remain continuously inside the feeder zone for this many seconds +# before it is counted and a cross snapshot is saved. Leaving the zone resets +# the timer. 0 = count on the first frame inside the zone. +ZONE_DWELL_LEFT_SEC=2 +ZONE_DWELL_RIGHT_SEC=2 + # --- Display --- # Site name shown on the dashboard header (top-right) SITE_NAME=ZenAi diff --git a/counter_live_rknn.py b/counter_live_rknn.py index 4b578df..3e21577 100644 --- a/counter_live_rknn.py +++ b/counter_live_rknn.py @@ -99,12 +99,17 @@ INHERIT_PX = float(os.getenv("INHERIT_PX", "60")) ZONE_COOLDOWN_LEFT_SEC = float(os.getenv("ZONE_COOLDOWN_LEFT_SEC", "0")) ZONE_COOLDOWN_RIGHT_SEC = float(os.getenv("ZONE_COOLDOWN_RIGHT_SEC", "0")) +# Per-feeder dwell: sack must remain inside the zone for N seconds before it is +# counted (and a cross snapshot is saved). 0 = count on first frame in zone. +ZONE_DWELL_LEFT_SEC = float(os.getenv("ZONE_DWELL_LEFT_SEC", "2")) +ZONE_DWELL_RIGHT_SEC = float(os.getenv("ZONE_DWELL_RIGHT_SEC", "2")) + DAILY_CUTOFF_TIME = os.getenv("DAILY_CUTOFF_TIME", "20:00") EXPORT_CSV = os.getenv("EXPORT_CSV", "true").lower() == "true" CROSS_CSV = os.getenv("CROSS_CSV", f"{OUTPUT_DIR}/crossings.csv") -# Save an annotated frame snapshot each time an object enters a feeder zone and +# Save an annotated frame snapshot each time a sack finishes its zone dwell and # the counter increases. SAVE_CROSS_SNAPSHOT = os.getenv("SAVE_CROSS_SNAPSHOT", "false").lower() == "true" # Also save one snapshot the first time each object is detected (before it enters @@ -738,6 +743,29 @@ def feeder_cooldown_sec(feeder): return ZONE_COOLDOWN_RIGHT_SEC +def feeder_dwell_sec(feeder): + if feeder == "left": + return ZONE_DWELL_LEFT_SEC + return ZONE_DWELL_RIGHT_SEC + + +def resolve_feeder_at(cx, cy, zone_left, zone_right): + """Return 'left' | 'right' | None for the zone containing (cx, cy).""" + now_left = point_in_zone(cx, cy, zone_left) + now_right = point_in_zone(cx, cy, zone_right) + if now_left and now_right: + lx1, ly1, lx2, ly2 = zone_left + rx1, ry1, rx2, ry2 = zone_right + dl = abs(cx - (lx1 + lx2) / 2) + abs(cy - (ly1 + ly2) / 2) + dr = abs(cx - (rx1 + rx2) / 2) + abs(cy - (ry1 + ry2) / 2) + return "left" if dl <= dr else "right" + if now_left: + return "left" + if now_right: + return "right" + return None + + def feeder_display_name(feeder): if feeder == "left": return FEEDER_LEFT_NAME @@ -781,14 +809,23 @@ def _default_zone_state(): counted: already contributed one LEFT or RIGHT count (at most one per object). side: 'left' | 'right' | None after a count. + dwell_feeder / dwell_since: which zone the track is currently dwelling in, + and when continuous presence in that zone began. """ - return {"counted": False, "side": None} + return { + "counted": False, + "side": None, + "dwell_feeder": None, + "dwell_since": None, + } def _copy_zone_state(src): return { "counted": bool(src.get("counted", False)), "side": src.get("side"), + "dwell_feeder": src.get("dwell_feeder"), + "dwell_since": src.get("dwell_since"), } @@ -1310,7 +1347,8 @@ def run(): ) print( f"Cooldownoldown: left={ZONE_COOLDOWN_LEFT_SEC}s right={ZONE_COOLDOWN_RIGHT_SEC}s " - f"(0=off)" + f"(0=off) | Dwell: left={ZONE_DWELL_LEFT_SEC}s right={ZONE_DWELL_RIGHT_SEC}s " + f"(0=immediate)" ) print(f"Model: {MODEL_PATH} | imgsz={IMGSZ} | core_mask={CORE_MASK}") print( @@ -1443,9 +1481,10 @@ def run(): flush=True, ) - # --- zone entry detection only on DETECTED objects this frame --- - # (tracker still updates every frame to preserve IDs, but we do NOT - # count on Kalman-predicted/coasting tracks to avoid double counts) + # --- zone dwell counting only on DETECTED objects this frame --- + # A sack must remain inside a feeder zone for ZONE_DWELL_*_SEC before + # it is counted (and a cross snapshot is saved). Leaving the zone + # resets the dwell timer. Cooldown still applies after a successful count. for di in range(len(object_boxes_xyxy)): tid = object_det_to_track.get(di) if tid is None: @@ -1474,7 +1513,8 @@ def run(): f"[DEBUG F{frame_idx}] INHERIT prev for new tid={tid} " f"from tid={src_tid} ({inherited[0]:.1f},{inherited[1]:.1f})" f" counted={src_st.get('counted', False)}" - f" side={src_st.get('side')}", + f" side={src_st.get('side')}" + f" dwell={src_st.get('dwell_feeder')}", flush=True, ) @@ -1482,105 +1522,118 @@ def run(): object_zone_state[tid] = _default_zone_state() st = object_zone_state[tid] - now_left = point_in_zone(cx, cy, zone_left) - now_right = point_in_zone(cx, cy, zone_right) + feeder = resolve_feeder_at(cx, cy, zone_left, zone_right) - entered_left = False - entered_right = False - if tid in object_tracked: - prev_cx, prev_cy = object_tracked[tid][0], object_tracked[tid][1] - prev_left = point_in_zone(prev_cx, prev_cy, zone_left) - prev_right = point_in_zone(prev_cx, prev_cy, zone_right) - entered_left = now_left and not prev_left - entered_right = now_right and not prev_right - else: - # First observation: count if the centroid already sits in a zone. - entered_left = now_left - entered_right = now_right + if st["counted"]: + object_tracked[tid] = (cx, cy, mono) + continue - if (entered_left or entered_right) and not st["counted"]: - # Prefer the zone whose center is closer if both fire in one jump. - feeder = None - if entered_left and entered_right: - lx1, ly1, lx2, ly2 = zone_left - rx1, ry1, rx2, ry2 = zone_right - dl = abs(cx - (lx1 + lx2) / 2) + abs(cy - (ly1 + ly2) / 2) - dr = abs(cx - (rx1 + rx2) / 2) + abs(cy - (ry1 + ry2) / 2) - feeder = "left" if dl <= dr else "right" - elif entered_left: - feeder = "left" - else: - feeder = "right" + if feeder is None: + # Left all zones — reset dwell progress. + if st["dwell_feeder"] is not None and DEBUG_TRACKING: + print( + f"[DEBUG F{frame_idx}] DWELL RESET: tid={tid} " + f"left feeder={st['dwell_feeder']}", + flush=True, + ) + st["dwell_feeder"] = None + st["dwell_since"] = None + object_tracked[tid] = (cx, cy, mono) + continue - recent = ( - recent_cross_left if feeder == "left" else recent_cross_right - ) - if is_duplicate_cross(recent, cx, cy, frame_idx): - st["counted"] = True - st["side"] = feeder - if DEBUG_TRACKING: - print( - f"[DEBUG F{frame_idx}] DUP ZONE IGNORED: tid={tid} " - f"cx={cx:.1f} cy={cy:.1f} feeder={feeder}", - flush=True, - ) - object_tracked[tid] = (cx, cy, mono) - continue + if st["dwell_feeder"] != feeder: + st["dwell_feeder"] = feeder + st["dwell_since"] = mono + if DEBUG_TRACKING: + print( + f"[DEBUG F{frame_idx}] DWELL START: tid={tid} " + f"feeder={feeder} need={feeder_dwell_sec(feeder):.1f}s", + flush=True, + ) + object_tracked[tid] = (cx, cy, mono) + continue - if feeder_in_cooldown(feeder, mono, last_count_mono): - st["counted"] = True - st["side"] = feeder - if DEBUG_TRACKING: - cool = feeder_cooldown_sec(feeder) - elapsed = mono - (last_count_mono.get(feeder) or mono) - print( - f"[DEBUG F{frame_idx}] COOLDOWN IGNORED: tid={tid} " - f"feeder={feeder} elapsed={elapsed:.2f}s / {cool}s", - flush=True, - ) - object_tracked[tid] = (cx, cy, mono) - continue + dwell_need = feeder_dwell_sec(feeder) + dwell_elapsed = mono - (st["dwell_since"] or mono) + if dwell_need > 0 and dwell_elapsed < dwell_need: + object_tracked[tid] = (cx, cy, mono) + continue - recent.append((frame_idx, cx, cy)) + # Dwell complete — apply dedup / cooldown, then count + snapshot. + recent = recent_cross_left if feeder == "left" else recent_cross_right + if is_duplicate_cross(recent, cx, cy, frame_idx): st["counted"] = True st["side"] = feeder - last_count_mono[feeder] = mono - if feeder == "left": - counter_left += 1 - count_left_pulse = COUNT_PULSE_FRAMES - feeder_total = counter_left - else: - counter_right += 1 - count_right_pulse = COUNT_PULSE_FRAMES - feeder_total = counter_right - # Frigate-style count log, e.g. - # "Total karung di kandang_bawah_feeder_kanan: 5" + if DEBUG_TRACKING: + print( + f"[DEBUG F{frame_idx}] DUP ZONE IGNORED: tid={tid} " + f"cx={cx:.1f} cy={cy:.1f} feeder={feeder}", + flush=True, + ) + object_tracked[tid] = (cx, cy, mono) + continue + + if feeder_in_cooldown(feeder, mono, last_count_mono): + # Keep dwelling; count once cooldown expires if still in zone. + if DEBUG_TRACKING: + cool = feeder_cooldown_sec(feeder) + elapsed = mono - (last_count_mono.get(feeder) or mono) + print( + f"[DEBUG F{frame_idx}] DWELL WAIT COOLDOWN: tid={tid} " + f"feeder={feeder} elapsed={elapsed:.2f}s / {cool}s", + flush=True, + ) + object_tracked[tid] = (cx, cy, mono) + continue + + if DEBUG_TRACKING: print( - f"Total {OBJECT_LABEL} di {feeder_display_name(feeder)}: {feeder_total}", + f"[DEBUG F{frame_idx}] DWELL COUNT: tid={tid} " + f"cx={cx:.1f} cy={cy:.1f} feeder={feeder} " + f"dwell={dwell_elapsed:.2f}s", flush=True, ) - store.record_zone_entry(tid, feeder) - if cross_logger: - cross_logger.write_row( - [ - store.get_counting_date(), - frame_idx, - feeder, - tid, - ] - ) - object_crossed_frame = True - cross_events_frame.append((tid, feeder)) - crossing_times.append(mono) - object_cross_flash[tid] = CROSS_FLASH_FRAMES - popups.append( - { - "x": int(cx) - 12, - "y": int(cy), - "born": frame_idx, - "text": f"+1 {feeder.upper()}", - } + + recent.append((frame_idx, cx, cy)) + st["counted"] = True + st["side"] = feeder + last_count_mono[feeder] = mono + if feeder == "left": + counter_left += 1 + count_left_pulse = COUNT_PULSE_FRAMES + feeder_total = counter_left + else: + counter_right += 1 + count_right_pulse = COUNT_PULSE_FRAMES + feeder_total = counter_right + # Frigate-style count log, e.g. + # "Total karung di kandang_bawah_feeder_kanan: 5" + print( + f"Total {OBJECT_LABEL} di {feeder_display_name(feeder)}: {feeder_total}", + flush=True, + ) + store.record_zone_entry(tid, feeder) + if cross_logger: + cross_logger.write_row( + [ + store.get_counting_date(), + frame_idx, + feeder, + tid, + ] ) + object_crossed_frame = True + cross_events_frame.append((tid, feeder)) + crossing_times.append(mono) + object_cross_flash[tid] = CROSS_FLASH_FRAMES + popups.append( + { + "x": int(cx) - 12, + "y": int(cy), + "born": frame_idx, + "text": f"+1 {feeder.upper()}", + } + ) object_tracked[tid] = (cx, cy, mono) for tid, (cx, cy) in object_lost_map.items(): diff --git a/env.example b/env.example index e7c3eae..84239be 100644 --- a/env.example +++ b/env.example @@ -99,6 +99,11 @@ INHERIT_PX=60 ZONE_COOLDOWN_LEFT_SEC=15 ZONE_COOLDOWN_RIGHT_SEC=15 +# --- Feeder dwell (seconds) --- +# Sack must stay inside the zone this long before count + snapshot. 0 = immediate. +ZONE_DWELL_LEFT_SEC=2 +ZONE_DWELL_RIGHT_SEC=2 + # --- Video recording --- RECORD_VIDEO=false VIDEO_SEGMENT_SEC=3600