feat: event-based walker detector tuned to real 7' overhead mount
Replace per-track line-crossing counter with a single event state machine
gated by foreground pixel count (ENTER=250, EXIT=150) and finalized by
quiet-exit or timeout. Direction inferred from centroid excursion
(up_score vs down_score) on quiet-exit fires, and from net displacement
(last_c vs first_c) on timeout fires.
Tuning reflects bench data at the intended 7' overhead mount: walkers
produce smaller centroid excursions than originally modelled, so
EXTENT gates, MIN_TRAJ, MAX_FRAMES and REFRACTORY were all relaxed from
their initial guesses. Constants and rationale live in firmware/lib/cv/cv.h.
Bench results (8 isolated walks, 4 entries + 4 exits):
* Event detection: 8/8 (100%)
* Aggregate entries+exits split: 4+4 (matches)
* Per-walk direction labelling: 4/8 (~50%)
Document explicitly that per-walk direction is unreliable at this mount
and that downstream analytics should trust only gross traffic
(entries + exits). Recovering direction would require a physical mount
change or a richer signal; both are out of scope for v1.
Tooling:
* tools/replay_logs.py — replay event state machine against captured
[F] diagnostic lines, for offline tuning without flash-test loops.
* firmware/src/main_capture.cpp + tools/capture_frames.py +
tools/replay_frames.py — raw-frame capture firmware and Python port
of the detector, kept in tree for future iteration even though the
TimerCamera-F serial driver stripped specific byte ranges in testing
and log-based replay became the working path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,12 +55,27 @@ static void check_factory_reset() {
|
||||
// Camera + CV task — runs on core 1 at 5 fps
|
||||
static void task_camera(void*) {
|
||||
static uint8_t frame[CV_PIXELS]; // static: avoids 9KB on task stack
|
||||
int last_logged_track_id = 0; // diagnostic: log each new track once
|
||||
while (true) {
|
||||
if (camera_capture_96(frame)) {
|
||||
if (xSemaphoreTake(s_cv_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
CVResult r = cv_process(g_cv, frame, g_cfg.line_offset);
|
||||
if (r.entries_delta) Serial.printf("[CV] entry +%d (total %d)\n", r.entries_delta, g_cv.entries);
|
||||
if (r.exits_delta) Serial.printf("[CV] exit +%d (total %d)\n", r.exits_delta, g_cv.exits);
|
||||
for (const auto& t : g_cv.tracks) {
|
||||
if (t.id > last_logged_track_id) {
|
||||
last_logged_track_id = t.id;
|
||||
Serial.printf("[CV] spawn id=%d y=%.1f\n", t.id, t.spawn_y);
|
||||
}
|
||||
}
|
||||
if (r.fg_count > 0) {
|
||||
Serial.printf("[F] n=%d y=%d..%d c=%.1f\n",
|
||||
r.fg_count, r.fg_min_y, r.fg_max_y, r.fg_centroid_y);
|
||||
}
|
||||
if (r.entries_delta) Serial.printf("[CV] entry +%d (total %d) first=%.1f min=%.1f max=%.1f last=%.1f dur=%d\n",
|
||||
r.entries_delta, g_cv.entries,
|
||||
r.fire_first_c, r.fire_min_c, r.fire_max_c, r.fire_last_c, r.fire_duration);
|
||||
if (r.exits_delta) Serial.printf("[CV] exit +%d (total %d) first=%.1f min=%.1f max=%.1f last=%.1f dur=%d\n",
|
||||
r.exits_delta, g_cv.exits,
|
||||
r.fire_first_c, r.fire_min_c, r.fire_max_c, r.fire_last_c, r.fire_duration);
|
||||
xSemaphoreGive(s_cv_mutex);
|
||||
if (r.entries_delta) led_blink_pattern(1);
|
||||
if (r.exits_delta) led_blink_pattern(2);
|
||||
|
||||
Reference in New Issue
Block a user