fix(cv): add per-direction crossing cooldown to suppress track-churn double-counts

When a blob briefly drops below CV_MIN_BLOB_PX, its track is killed and respawns,
causing the same person to generate multiple counts per visit (~50/min observed
in field). Add a per-direction cooldown (default 5 frames ≈ 0.8s @ 5 fps) that
drops subsequent entries (or exits) within the window of the last counted one.
Entry and exit cooldowns are tracked independently.

Fixed at compile time for now; exposing as a server-push tunable is deferred
until the server-push-config branch lands. See docs/server-prompt-crossing-
cooldown.md for the server-side coordination notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 06:33:11 -07:00
parent 9d5b588231
commit 62931e26ff
6 changed files with 150 additions and 5 deletions

View File

@@ -15,6 +15,8 @@ void cv_init(CVState& state) {
state.tracks.clear();
state.entries = 0;
state.exits = 0;
state.last_entry_frame = 0;
state.last_exit_frame = 0;
}
void cv_reset_counts(CVState& state) {
@@ -160,12 +162,22 @@ CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct) {
if (now_above != track.above_line) {
if (!now_above) {
// was above, now below → entry
state.entries++;
result.entries_delta++;
bool in_cooldown = state.last_entry_frame != 0 &&
(state.frame_index - state.last_entry_frame) < CV_CROSSING_COOLDOWN_FRAMES;
if (!in_cooldown) {
state.entries++;
result.entries_delta++;
state.last_entry_frame = state.frame_index;
}
} else {
// was below, now above → exit
state.exits++;
result.exits_delta++;
bool in_cooldown = state.last_exit_frame != 0 &&
(state.frame_index - state.last_exit_frame) < CV_CROSSING_COOLDOWN_FRAMES;
if (!in_cooldown) {
state.exits++;
result.exits_delta++;
state.last_exit_frame = state.frame_index;
}
}
}
track.above_line = now_above;