add 2 second window for snapshot and counter
This commit is contained in:
1 parent
a7ce970f78
commit
7721c84406
3 files changed
+160
-95
No files matched your search
@@ -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
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user