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:
@@ -7,258 +7,290 @@ static void fill_frame(uint8_t* f, uint8_t val) {
|
||||
memset(f, val, CV_PIXELS);
|
||||
}
|
||||
|
||||
// Draw a rectangular walker-blob spanning rows [y0, y1], columns [cx-hw, cx+hw].
|
||||
// Pixel value 200 over background 100 -> frame_diff threshold (30) is cleared.
|
||||
static void draw_walker(uint8_t* f, int y0, int y1, int cx, int hw) {
|
||||
fill_frame(f, 100);
|
||||
for (int y = y0; y <= y1; y++) {
|
||||
if (y < 0 || y >= CV_H) continue;
|
||||
for (int x = cx - hw; x <= cx + hw; x++) {
|
||||
if (x < 0 || x >= CV_W) continue;
|
||||
f[y * CV_W + x] = 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void prime_bg(CVState& state) {
|
||||
uint8_t bg[CV_PIXELS];
|
||||
fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50);
|
||||
}
|
||||
|
||||
// Let the event state machine see QUIET_FRAMES+1 empty frames so any active
|
||||
// event finalizes before the next test assertion.
|
||||
static void quiesce(CVState& state) {
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
for (int i = 0; i < CV_EVENT_QUIET_FRAMES + 1; i++) cv_process(state, bg, 50);
|
||||
}
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
void test_frame_diff_no_change_gives_no_fg() {
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
|
||||
uint8_t frame[CV_PIXELS];
|
||||
fill_frame(frame, 128);
|
||||
|
||||
void test_no_change_no_event() {
|
||||
CVState state; cv_init(state);
|
||||
uint8_t frame[CV_PIXELS]; fill_frame(frame, 128);
|
||||
CVResult r1 = cv_process(state, frame, 50);
|
||||
TEST_ASSERT_EQUAL_INT(0, r1.entries_delta);
|
||||
|
||||
CVResult r2 = cv_process(state, frame, 50);
|
||||
TEST_ASSERT_EQUAL_INT(0, r2.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, r2.exits_delta);
|
||||
}
|
||||
|
||||
void test_frame_diff_large_change_detected_no_crash() {
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
|
||||
uint8_t bg[CV_PIXELS], fg_frame[CV_PIXELS];
|
||||
fill_frame(bg, 100);
|
||||
fill_frame(fg_frame, 200);
|
||||
|
||||
cv_process(state, bg, 50);
|
||||
CVResult r = cv_process(state, fg_frame, 50);
|
||||
|
||||
// Tracking not yet implemented — just verify no crash and result is zero
|
||||
TEST_ASSERT_EQUAL_INT(0, r.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, r.exits_delta);
|
||||
}
|
||||
|
||||
void test_cv_init_clears_state() {
|
||||
CVState state;
|
||||
state.entries = 99; state.exits = 88;
|
||||
state.entries = 99; state.exits = 88; state.event_active = true;
|
||||
cv_init(state);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.exits);
|
||||
TEST_ASSERT_FALSE(state.bg_valid);
|
||||
TEST_ASSERT_FALSE(state.event_active);
|
||||
}
|
||||
|
||||
void test_cv_reset_counts() {
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
state.entries = 5;
|
||||
state.exits = 3;
|
||||
CVState state; cv_init(state);
|
||||
state.entries = 5; state.exits = 3;
|
||||
cv_reset_counts(state);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.exits);
|
||||
}
|
||||
|
||||
void test_tracking_spawns_track_for_new_blob() {
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
void test_walker_up_through_frame_is_entry() {
|
||||
// Simulate a walker traversing from bottom to top of frame.
|
||||
// Per-frame fg_count and centroid (11-wide column, height H -> n=11*H):
|
||||
// t0 y=60..95 n=396 c=77 <- event starts (n >= ENTER=300)
|
||||
// t1 y=30..95 n=726 c=62
|
||||
// t2 y=0..95 n=1056 c=47
|
||||
// t3 y=0..60 n=671 c=30
|
||||
// t4 y=0..25 n=286 c=12 (below EXIT=200, quiet=1)
|
||||
// t5 y=0..10 n=121 c=5 (below EXIT, quiet=2)
|
||||
// t6 empty quiet=3 -> finalize
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
uint8_t bg[CV_PIXELS];
|
||||
fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50); // init background
|
||||
|
||||
// Frame with a bright 30x30 blob in top-left quadrant
|
||||
uint8_t blob_frame[CV_PIXELS];
|
||||
fill_frame(blob_frame, 100);
|
||||
for (int y = 5; y < 35; y++)
|
||||
for (int x = 5; x < 35; x++)
|
||||
blob_frame[y * CV_W + x] = 200;
|
||||
|
||||
cv_process(state, blob_frame, 50);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, (int)state.tracks.size());
|
||||
TEST_ASSERT_FLOAT_WITHIN(5.0f, 20.0f, state.tracks[0].x);
|
||||
TEST_ASSERT_FLOAT_WITHIN(5.0f, 20.0f, state.tracks[0].y);
|
||||
}
|
||||
|
||||
static void make_blob_frame(uint8_t* f, int cx, int cy) {
|
||||
fill_frame(f, 100);
|
||||
for (int y = cy - 12; y <= cy + 12; y++)
|
||||
for (int x = cx - 12; x <= cx + 12; x++)
|
||||
if (y >= 0 && y < CV_H && x >= 0 && x < CV_W)
|
||||
f[y * CV_W + x] = 200;
|
||||
}
|
||||
|
||||
void test_blob_crossing_line_top_to_bottom_is_entry() {
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
|
||||
// Line at 50% = y=48, traversal margin = 14px. Spawn must be y<34, final y>62.
|
||||
// Step ≤14px per frame to stay within CV_MAX_MOVE.
|
||||
uint8_t bg[CV_PIXELS];
|
||||
fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50); // init background
|
||||
|
||||
int setup[] = {20, 34, 48, 62}; // spawn firm above, walk across, not yet firm below
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, setup[i]);
|
||||
int rows[][2] = {{60,95},{30,95},{0,95},{0,60},{0,25},{0,10}};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
// Still no count — y=62 is not firm below (needs >62)
|
||||
TEST_ASSERT_EQUAL_INT(0, state.entries);
|
||||
quiesce(state);
|
||||
|
||||
// One more step: y=70 is firm below → entry fires now
|
||||
uint8_t fcross[CV_PIXELS]; make_blob_frame(fcross, 48, 70);
|
||||
CVResult r = cv_process(state, fcross, 50);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, r.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, r.exits_delta);
|
||||
TEST_ASSERT_EQUAL_INT(1, state.entries);
|
||||
}
|
||||
|
||||
void test_blob_crossing_line_bottom_to_top_is_exit() {
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50);
|
||||
|
||||
// Spawn firm below (y=76 > 62), walk toward and across line (y=48), continue
|
||||
// until firm above (y<34). Each step ≤14px.
|
||||
int setup[] = {76, 62, 48, 34};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, setup[i]);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
// y=34 not firm above (needs <34) — no count yet
|
||||
TEST_ASSERT_EQUAL_INT(0, state.exits);
|
||||
|
||||
uint8_t fcross[CV_PIXELS]; make_blob_frame(fcross, 48, 22); // firm above
|
||||
CVResult r = cv_process(state, fcross, 50);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, r.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(1, r.exits_delta);
|
||||
}
|
||||
|
||||
void test_track_spawned_near_line_does_not_count_on_wobble() {
|
||||
// Simulates a blob that appears right on the line (e.g. shadow or noise)
|
||||
// and wobbles across it. With directional margin, no count should fire —
|
||||
// this is the false-positive pattern the feature guards against.
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
void test_walker_down_through_frame_is_exit() {
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50);
|
||||
|
||||
// Spawn within margin of line (y=44, margin=14 so 44 ∈ [34,62])
|
||||
// then wobble above to y=38, below to y=58. Both within margin.
|
||||
int setup[] = {44, 38, 58, 42, 56};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, setup[i]);
|
||||
int rows[][2] = {{0,35},{0,65},{0,95},{35,95},{70,95},{85,95}};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(1, state.exits);
|
||||
}
|
||||
|
||||
void test_approach_retreat_without_full_extent_does_not_fire() {
|
||||
// Walker approaches from bottom, reaches y=30, retreats, never reaches top.
|
||||
// Extent gate requires min_y_seen <= 10; this event tops out at y=30 so
|
||||
// extent never clears and no fire occurs regardless of trajectory score.
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
int rows[][2] = {{60,95},{40,95},{30,95},{40,95},{60,95},{80,95}};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.exits);
|
||||
}
|
||||
|
||||
void test_track_counts_at_most_once_even_if_it_wobbles_back() {
|
||||
// A track that traverses fully should count once. If it then reverses and
|
||||
// crosses back, the track should NOT fire again — it's already counted.
|
||||
// (A separate new track on the return trip would count as exit, but while
|
||||
// the same track persists, it's one trip.)
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
void test_brief_burst_below_min_duration_does_not_fire() {
|
||||
// One frame of large fg, then gone. Event starts, immediately quiesces,
|
||||
// duration ends up below CV_EVENT_MIN_FRAMES.
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50);
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, 0, 95, 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
quiesce(state);
|
||||
|
||||
// Full traversal top→bottom
|
||||
int walk_down[] = {20, 34, 48, 62, 70};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, walk_down[i]);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(1, state.entries);
|
||||
|
||||
// Same track reverses back to top. counted=true prevents a second event.
|
||||
int walk_up[] = {62, 48, 34, 22};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, walk_up[i]);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(1, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.exits);
|
||||
}
|
||||
|
||||
void test_cooldown_suppresses_rapid_re_entry() {
|
||||
// Cooldown is a safety net on top of directional counting. Construct two
|
||||
// DIFFERENT tracks (each counts once on its own) whose crossings happen
|
||||
// within the cooldown window — the second should still be suppressed.
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
state.bg_valid = true;
|
||||
memset(state.background, 100, CV_PIXELS);
|
||||
state.frame_index = 100;
|
||||
state.entries = 1;
|
||||
state.last_entry_frame = 100;
|
||||
void test_stationary_large_blob_does_not_fire() {
|
||||
// Static large blob in frame for many frames, then removed. Centroid
|
||||
// never moves -> MIN_TRAJ gate blocks fire.
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
// Track at y=50 (just below line), spawn_y=20 (firm above) — a valid trajectory.
|
||||
CVTrack t;
|
||||
t.id = 1; t.x = 48; t.y = 50; t.spawn_y = 20;
|
||||
t.above_line = false; t.counted = false; t.missed = 0;
|
||||
state.tracks.push_back(t);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, 0, 95, 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
|
||||
// Frame 101: blob at y=64 (delta=14, matches; firm below line+margin=62).
|
||||
// Would count but cooldown (101-100=1 < 5) suppresses.
|
||||
uint8_t f1[CV_PIXELS]; make_blob_frame(f1, 48, 64);
|
||||
CVResult r1 = cv_process(state, f1, 50);
|
||||
TEST_ASSERT_EQUAL_INT(0, r1.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.exits);
|
||||
}
|
||||
|
||||
// Wait out the refractory period with bg-only frames so the next walker
|
||||
// event is accepted.
|
||||
static void wait_refractory(CVState& state) {
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
for (uint32_t i = 0; i < CV_EVENT_REFRACTORY_FRAMES + 2; i++) {
|
||||
cv_process(state, bg, 50);
|
||||
}
|
||||
}
|
||||
|
||||
void test_two_sequential_walkers_count_twice() {
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
int rows[][2] = {{60,95},{30,95},{0,95},{0,60},{0,25},{0,10}};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
wait_refractory(state);
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(2, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.exits);
|
||||
}
|
||||
|
||||
void test_full_reversal_counts_entry_then_exit() {
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
int up_rows[][2] = {{60,95},{30,95},{0,95},{0,60},{0,25},{0,10}};
|
||||
int down_rows[][2] = {{0,35},{0,65},{0,95},{35,95},{70,95},{85,95}};
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, up_rows[i][0], up_rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
wait_refractory(state);
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, down_rows[i][0], down_rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(1, state.exits);
|
||||
}
|
||||
|
||||
void test_refractory_suppresses_back_to_back_fire() {
|
||||
// After a fire, a second event attempted within CV_EVENT_REFRACTORY_FRAMES
|
||||
// is suppressed. Simulates walker lingering / ghost re-triggering.
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
int rows[][2] = {{60,95},{30,95},{0,95},{0,60},{0,25},{0,10}};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
TEST_ASSERT_EQUAL_INT(1, state.entries);
|
||||
|
||||
// Advance past cooldown; reset a fresh track (previous one had counted=true
|
||||
// set only if it actually counted — cooldown path leaves counted=false so
|
||||
// we reuse the same track).
|
||||
state.frame_index = 200;
|
||||
state.tracks[0].y = 50;
|
||||
state.tracks[0].spawn_y = 20;
|
||||
state.tracks[0].counted = false;
|
||||
state.tracks[0].above_line = false;
|
||||
uint8_t f2[CV_PIXELS]; make_blob_frame(f2, 48, 64);
|
||||
CVResult r2 = cv_process(state, f2, 50);
|
||||
TEST_ASSERT_EQUAL_INT(1, r2.entries_delta);
|
||||
// Immediate second walker within refractory window — should NOT count.
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
TEST_ASSERT_EQUAL_INT(1, state.entries);
|
||||
}
|
||||
|
||||
void test_event_counts_after_refractory_expires() {
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
int rows[][2] = {{60,95},{30,95},{0,95},{0,60},{0,25},{0,10}};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
TEST_ASSERT_EQUAL_INT(1, state.entries);
|
||||
|
||||
// Wait out the refractory period.
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
for (uint32_t i = 0; i < CV_EVENT_REFRACTORY_FRAMES + 2; i++) {
|
||||
cv_process(state, bg, 50);
|
||||
}
|
||||
|
||||
// Second walker — should now count.
|
||||
for (int i = 0; i < 6; i++) {
|
||||
uint8_t f[CV_PIXELS]; draw_walker(f, rows[i][0], rows[i][1], 48, 5);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
TEST_ASSERT_EQUAL_INT(2, state.entries);
|
||||
}
|
||||
|
||||
void test_no_crossing_same_side_no_count() {
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
void test_noise_below_enter_thresh_does_not_start_event() {
|
||||
// Tiny 5x5 blob (25 px) never crosses ENTER=300, event never starts.
|
||||
CVState state; cv_init(state);
|
||||
prime_bg(state);
|
||||
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50);
|
||||
auto small = [](uint8_t* f, int cy) {
|
||||
fill_frame(f, 100);
|
||||
for (int y = cy-2; y <= cy+2; y++)
|
||||
for (int x = 46; x <= 50; x++)
|
||||
if (y>=0 && y<CV_H && x>=0 && x<CV_W) f[y*CV_W+x] = 200;
|
||||
};
|
||||
for (int cy = 10; cy <= 90; cy += 8) {
|
||||
uint8_t f[CV_PIXELS]; small(f, cy);
|
||||
cv_process(state, f, 50);
|
||||
}
|
||||
quiesce(state);
|
||||
|
||||
uint8_t f1[CV_PIXELS]; make_blob_frame(f1, 48, 20); // above line
|
||||
cv_process(state, f1, 50);
|
||||
|
||||
uint8_t f2[CV_PIXELS]; make_blob_frame(f2, 48, 30); // still above line, moved closer
|
||||
CVResult r = cv_process(state, f2, 50);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, r.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, r.exits_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.entries);
|
||||
TEST_ASSERT_EQUAL_INT(0, state.exits);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_frame_diff_no_change_gives_no_fg);
|
||||
RUN_TEST(test_frame_diff_large_change_detected_no_crash);
|
||||
RUN_TEST(test_no_change_no_event);
|
||||
RUN_TEST(test_cv_init_clears_state);
|
||||
RUN_TEST(test_cv_reset_counts);
|
||||
RUN_TEST(test_tracking_spawns_track_for_new_blob);
|
||||
RUN_TEST(test_blob_crossing_line_top_to_bottom_is_entry);
|
||||
RUN_TEST(test_blob_crossing_line_bottom_to_top_is_exit);
|
||||
RUN_TEST(test_track_spawned_near_line_does_not_count_on_wobble);
|
||||
RUN_TEST(test_track_counts_at_most_once_even_if_it_wobbles_back);
|
||||
RUN_TEST(test_no_crossing_same_side_no_count);
|
||||
RUN_TEST(test_cooldown_suppresses_rapid_re_entry);
|
||||
RUN_TEST(test_walker_up_through_frame_is_entry);
|
||||
RUN_TEST(test_walker_down_through_frame_is_exit);
|
||||
RUN_TEST(test_approach_retreat_without_full_extent_does_not_fire);
|
||||
RUN_TEST(test_brief_burst_below_min_duration_does_not_fire);
|
||||
RUN_TEST(test_stationary_large_blob_does_not_fire);
|
||||
RUN_TEST(test_two_sequential_walkers_count_twice);
|
||||
RUN_TEST(test_full_reversal_counts_entry_then_exit);
|
||||
RUN_TEST(test_refractory_suppresses_back_to_back_fire);
|
||||
RUN_TEST(test_event_counts_after_refractory_expires);
|
||||
RUN_TEST(test_noise_below_enter_thresh_does_not_start_event);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user