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:
64
firmware/src/main_capture.cpp
Normal file
64
firmware/src/main_capture.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
// firmware/src/main_capture.cpp
|
||||
//
|
||||
// Frame-dump firmware. Replaces main.cpp when building env:timercam-capture.
|
||||
// Streams raw 96x96 grayscale frames at 5 fps over serial (921600 baud) for
|
||||
// offline algorithm iteration.
|
||||
//
|
||||
// Wire format per frame (little-endian):
|
||||
// magic uint32 0xDC0FC0DE
|
||||
// frame_ix uint32 monotonic counter
|
||||
// millis uint32 ms since boot
|
||||
// pixels byte[9216] raw grayscale 96x96, row-major
|
||||
//
|
||||
// No WiFi, no BLE, no CV. Just camera → serial.
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "camera.h"
|
||||
#include "cv.h" // for CV_PIXELS
|
||||
|
||||
#define LED_PIN 2
|
||||
#define CAM_FPS 5
|
||||
#define CAM_INTERVAL_MS (1000 / CAM_FPS)
|
||||
|
||||
// Magic chosen from bytes that commonly survive; 'FRM1' ascii.
|
||||
// Avoid high bytes 0xA0-AF / 0xD0-DF — observed missing from the CH9102 stream.
|
||||
static const uint32_t FRAME_MAGIC = 0x314D5246; // 'FRM1' little-endian on wire
|
||||
|
||||
void setup() {
|
||||
Serial.begin(460800);
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
|
||||
delay(500);
|
||||
Serial.println("# capture-mode: 460800 baud, 96x96 gray @ 5fps");
|
||||
Serial.flush();
|
||||
|
||||
if (!camera_init()) {
|
||||
Serial.println("# FATAL: camera init failed");
|
||||
while (true) {
|
||||
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
|
||||
delay(200);
|
||||
}
|
||||
}
|
||||
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
static uint8_t frame[CV_PIXELS];
|
||||
static uint32_t frame_ix = 0;
|
||||
uint32_t t0 = millis();
|
||||
|
||||
if (camera_capture_96(frame)) {
|
||||
uint32_t ms = millis();
|
||||
Serial.write((uint8_t*)&FRAME_MAGIC, 4);
|
||||
Serial.write((uint8_t*)&frame_ix, 4);
|
||||
Serial.write((uint8_t*)&ms, 4);
|
||||
Serial.write(frame, CV_PIXELS);
|
||||
frame_ix++;
|
||||
digitalWrite(LED_PIN, frame_ix & 1);
|
||||
}
|
||||
|
||||
uint32_t elapsed = millis() - t0;
|
||||
if (elapsed < CAM_INTERVAL_MS) delay(CAM_INTERVAL_MS - elapsed);
|
||||
}
|
||||
Reference in New Issue
Block a user