add 2 second window for snapshot and counter

This commit is contained in:
Alberto-Audrix committed 2026-07-24 13:51:17 +07:00
1 parent a7ce970f78
commit 7721c84406
3 files changed
+160 -95

No files matched your search

+7
View File
@@ -84,6 +84,13 @@ INHERIT_PX=60
ZONE_COOLDOWN_LEFT_SEC=3 ZONE_COOLDOWN_LEFT_SEC=3
ZONE_COOLDOWN_RIGHT_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 --- # --- Display ---
# Site name shown on the dashboard header (top-right) # Site name shown on the dashboard header (top-right)
SITE_NAME=ZenAi SITE_NAME=ZenAi
+148 -95
View File
@@ -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_LEFT_SEC = float(os.getenv("ZONE_COOLDOWN_LEFT_SEC", "0"))
ZONE_COOLDOWN_RIGHT_SEC = float(os.getenv("ZONE_COOLDOWN_RIGHT_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") DAILY_CUTOFF_TIME = os.getenv("DAILY_CUTOFF_TIME", "20:00")
EXPORT_CSV = os.getenv("EXPORT_CSV", "true").lower() == "true" EXPORT_CSV = os.getenv("EXPORT_CSV", "true").lower() == "true"
CROSS_CSV = os.getenv("CROSS_CSV", f"{OUTPUT_DIR}/crossings.csv") 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. # the counter increases.
SAVE_CROSS_SNAPSHOT = os.getenv("SAVE_CROSS_SNAPSHOT", "false").lower() == "true" 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 # 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 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): def feeder_display_name(feeder):
if feeder == "left": if feeder == "left":
return FEEDER_LEFT_NAME 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). counted: already contributed one LEFT or RIGHT count (at most one per object).
side: 'left' | 'right' | None after a count. 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): def _copy_zone_state(src):
return { return {
"counted": bool(src.get("counted", False)), "counted": bool(src.get("counted", False)),
"side": src.get("side"), "side": src.get("side"),
"dwell_feeder": src.get("dwell_feeder"),
"dwell_since": src.get("dwell_since"),
} }
@@ -1310,7 +1347,8 @@ def run():
) )
print( print(
f"Cooldownoldown: left={ZONE_COOLDOWN_LEFT_SEC}s right={ZONE_COOLDOWN_RIGHT_SEC}s " 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(f"Model: {MODEL_PATH} | imgsz={IMGSZ} | core_mask={CORE_MASK}")
print( print(
@@ -1443,9 +1481,10 @@ def run():
flush=True, flush=True,
) )
# --- zone entry detection only on DETECTED objects this frame --- # --- zone dwell counting only on DETECTED objects this frame ---
# (tracker still updates every frame to preserve IDs, but we do NOT # A sack must remain inside a feeder zone for ZONE_DWELL_*_SEC before
# count on Kalman-predicted/coasting tracks to avoid double counts) # 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)): for di in range(len(object_boxes_xyxy)):
tid = object_det_to_track.get(di) tid = object_det_to_track.get(di)
if tid is None: if tid is None:
@@ -1474,7 +1513,8 @@ def run():
f"[DEBUG F{frame_idx}] INHERIT prev for new tid={tid} " f"[DEBUG F{frame_idx}] INHERIT prev for new tid={tid} "
f"from tid={src_tid} ({inherited[0]:.1f},{inherited[1]:.1f})" f"from tid={src_tid} ({inherited[0]:.1f},{inherited[1]:.1f})"
f" counted={src_st.get('counted', False)}" 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, flush=True,
) )
@@ -1482,105 +1522,118 @@ def run():
object_zone_state[tid] = _default_zone_state() object_zone_state[tid] = _default_zone_state()
st = object_zone_state[tid] st = object_zone_state[tid]
now_left = point_in_zone(cx, cy, zone_left) feeder = resolve_feeder_at(cx, cy, zone_left, zone_right)
now_right = point_in_zone(cx, cy, zone_right)
entered_left = False if st["counted"]:
entered_right = False object_tracked[tid] = (cx, cy, mono)
if tid in object_tracked: continue
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 (entered_left or entered_right) and not st["counted"]: if feeder is None:
# Prefer the zone whose center is closer if both fire in one jump. # Left all zones — reset dwell progress.
feeder = None if st["dwell_feeder"] is not None and DEBUG_TRACKING:
if entered_left and entered_right: print(
lx1, ly1, lx2, ly2 = zone_left f"[DEBUG F{frame_idx}] DWELL RESET: tid={tid} "
rx1, ry1, rx2, ry2 = zone_right f"left feeder={st['dwell_feeder']}",
dl = abs(cx - (lx1 + lx2) / 2) + abs(cy - (ly1 + ly2) / 2) flush=True,
dr = abs(cx - (rx1 + rx2) / 2) + abs(cy - (ry1 + ry2) / 2) )
feeder = "left" if dl <= dr else "right" st["dwell_feeder"] = None
elif entered_left: st["dwell_since"] = None
feeder = "left" object_tracked[tid] = (cx, cy, mono)
else: continue
feeder = "right"
recent = ( if st["dwell_feeder"] != feeder:
recent_cross_left if feeder == "left" else recent_cross_right st["dwell_feeder"] = feeder
) st["dwell_since"] = mono
if is_duplicate_cross(recent, cx, cy, frame_idx): if DEBUG_TRACKING:
st["counted"] = True print(
st["side"] = feeder f"[DEBUG F{frame_idx}] DWELL START: tid={tid} "
if DEBUG_TRACKING: f"feeder={feeder} need={feeder_dwell_sec(feeder):.1f}s",
print( flush=True,
f"[DEBUG F{frame_idx}] DUP ZONE IGNORED: tid={tid} " )
f"cx={cx:.1f} cy={cy:.1f} feeder={feeder}", object_tracked[tid] = (cx, cy, mono)
flush=True, continue
)
object_tracked[tid] = (cx, cy, mono)
continue
if feeder_in_cooldown(feeder, mono, last_count_mono): dwell_need = feeder_dwell_sec(feeder)
st["counted"] = True dwell_elapsed = mono - (st["dwell_since"] or mono)
st["side"] = feeder if dwell_need > 0 and dwell_elapsed < dwell_need:
if DEBUG_TRACKING: object_tracked[tid] = (cx, cy, mono)
cool = feeder_cooldown_sec(feeder) continue
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
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["counted"] = True
st["side"] = feeder st["side"] = feeder
last_count_mono[feeder] = mono if DEBUG_TRACKING:
if feeder == "left": print(
counter_left += 1 f"[DEBUG F{frame_idx}] DUP ZONE IGNORED: tid={tid} "
count_left_pulse = COUNT_PULSE_FRAMES f"cx={cx:.1f} cy={cy:.1f} feeder={feeder}",
feeder_total = counter_left flush=True,
else: )
counter_right += 1 object_tracked[tid] = (cx, cy, mono)
count_right_pulse = COUNT_PULSE_FRAMES continue
feeder_total = counter_right
# Frigate-style count log, e.g. if feeder_in_cooldown(feeder, mono, last_count_mono):
# "Total karung di kandang_bawah_feeder_kanan: 5" # 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( 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, flush=True,
) )
store.record_zone_entry(tid, feeder)
if cross_logger: recent.append((frame_idx, cx, cy))
cross_logger.write_row( st["counted"] = True
[ st["side"] = feeder
store.get_counting_date(), last_count_mono[feeder] = mono
frame_idx, if feeder == "left":
feeder, counter_left += 1
tid, count_left_pulse = COUNT_PULSE_FRAMES
] feeder_total = counter_left
) else:
object_crossed_frame = True counter_right += 1
cross_events_frame.append((tid, feeder)) count_right_pulse = COUNT_PULSE_FRAMES
crossing_times.append(mono) feeder_total = counter_right
object_cross_flash[tid] = CROSS_FLASH_FRAMES # Frigate-style count log, e.g.
popups.append( # "Total karung di kandang_bawah_feeder_kanan: 5"
{ print(
"x": int(cx) - 12, f"Total {OBJECT_LABEL} di {feeder_display_name(feeder)}: {feeder_total}",
"y": int(cy), flush=True,
"born": frame_idx, )
"text": f"+1 {feeder.upper()}", 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) object_tracked[tid] = (cx, cy, mono)
for tid, (cx, cy) in object_lost_map.items(): for tid, (cx, cy) in object_lost_map.items():
+5
View File
@@ -99,6 +99,11 @@ INHERIT_PX=60
ZONE_COOLDOWN_LEFT_SEC=15 ZONE_COOLDOWN_LEFT_SEC=15
ZONE_COOLDOWN_RIGHT_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 --- # --- Video recording ---
RECORD_VIDEO=false RECORD_VIDEO=false
VIDEO_SEGMENT_SEC=3600 VIDEO_SEGMENT_SEC=3600