refactor(cv): read thresholds from runtime tuning + load from NVS on boot
cv_process and helpers (frame_diff, extract_blob, find_centroids) now read diff_thresh, min_blob_px, max_move, max_missed, and line_offset from state.tuning instead of file-scope static const constants. The four thresholds are promoted to file-local constexpr defaults in cv.cpp (CV_DEFAULT_*) and are no longer part of the public cv.h API — external code can't depend on them. cv_process signature drops the line_pct parameter; callers use state.tuning.line_offset instead. This eliminates the drift hazard of having two sources of truth (DeviceConfig.line_offset vs CVTuning.line_offset); the former is deleted. main.cpp now calls config_load_tuning(g_cv.tuning) after cv_init on boot so previously persisted tuning survives reboot; logs whether tuning came from NVS or defaults. The legacy NVS key "line_offset" is intentionally left alone — harmless and flash_device.py may still write it during provisioning. Migration is out of scope. Tests: 12/12 passing (11 existing + 1 new test_cv_process_respects_runtime_min_blob proving the runtime-read path). Flash: 1,414,069 bytes (89.9%). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,10 +17,10 @@ void test_frame_diff_no_change_gives_no_fg() {
|
||||
uint8_t frame[CV_PIXELS];
|
||||
fill_frame(frame, 128);
|
||||
|
||||
CVResult r1 = cv_process(state, frame, 50);
|
||||
CVResult r1 = cv_process(state, frame);
|
||||
TEST_ASSERT_EQUAL_INT(0, r1.entries_delta);
|
||||
|
||||
CVResult r2 = cv_process(state, frame, 50);
|
||||
CVResult r2 = cv_process(state, frame);
|
||||
TEST_ASSERT_EQUAL_INT(0, r2.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, r2.exits_delta);
|
||||
}
|
||||
@@ -33,8 +33,8 @@ void test_frame_diff_large_change_detected_no_crash() {
|
||||
fill_frame(bg, 100);
|
||||
fill_frame(fg_frame, 200);
|
||||
|
||||
cv_process(state, bg, 50);
|
||||
CVResult r = cv_process(state, fg_frame, 50);
|
||||
cv_process(state, bg);
|
||||
CVResult r = cv_process(state, fg_frame);
|
||||
|
||||
// Tracking not yet implemented — just verify no crash and result is zero
|
||||
TEST_ASSERT_EQUAL_INT(0, r.entries_delta);
|
||||
@@ -66,7 +66,7 @@ void test_tracking_spawns_track_for_new_blob() {
|
||||
|
||||
uint8_t bg[CV_PIXELS];
|
||||
fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50); // init background
|
||||
cv_process(state, bg); // init background
|
||||
|
||||
// Frame with a bright 30x30 blob in top-left quadrant
|
||||
uint8_t blob_frame[CV_PIXELS];
|
||||
@@ -75,7 +75,7 @@ void test_tracking_spawns_track_for_new_blob() {
|
||||
for (int x = 5; x < 35; x++)
|
||||
blob_frame[y * CV_W + x] = 200;
|
||||
|
||||
cv_process(state, blob_frame, 50);
|
||||
cv_process(state, blob_frame);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, (int)state.tracks.size());
|
||||
TEST_ASSERT_FLOAT_WITHIN(5.0f, 20.0f, state.tracks[0].x);
|
||||
@@ -94,20 +94,20 @@ void test_blob_crossing_line_top_to_bottom_is_entry() {
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
|
||||
// Line at 50% = y=48; step ≤14px per frame to stay within CV_MAX_MOVE
|
||||
// Line at 50% = y=48; step ≤14px per frame to stay within max_move (default 15)
|
||||
uint8_t bg[CV_PIXELS];
|
||||
fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50); // init background
|
||||
cv_process(state, bg); // init background
|
||||
|
||||
// Walk blob from y=20 toward line; crossing occurs at y=48 (above→below)
|
||||
// Stop at crossing frame and assert its result
|
||||
int setup[] = {20, 34};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, setup[i]);
|
||||
cv_process(state, f, 50);
|
||||
cv_process(state, f);
|
||||
}
|
||||
uint8_t fcross[CV_PIXELS]; make_blob_frame(fcross, 48, 48);
|
||||
CVResult r = cv_process(state, fcross, 50);
|
||||
CVResult r = cv_process(state, fcross);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, r.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, r.exits_delta);
|
||||
@@ -119,17 +119,17 @@ void test_blob_crossing_line_bottom_to_top_is_exit() {
|
||||
cv_init(state);
|
||||
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50);
|
||||
cv_process(state, bg);
|
||||
|
||||
// Walk blob from y=76 toward line; crossing occurs at y=34 (below→above)
|
||||
// Stop at crossing frame and assert its result
|
||||
int setup[] = {76, 62, 48};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, setup[i]);
|
||||
cv_process(state, f, 50);
|
||||
cv_process(state, f);
|
||||
}
|
||||
uint8_t fcross[CV_PIXELS]; make_blob_frame(fcross, 48, 34);
|
||||
CVResult r = cv_process(state, fcross, 50);
|
||||
CVResult r = cv_process(state, fcross);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, r.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(1, r.exits_delta);
|
||||
@@ -147,12 +147,40 @@ void test_cv_init_populates_tuning_defaults() {
|
||||
|
||||
cv_init(state);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(CV_DIFF_THRESH, state.tuning.diff_thresh);
|
||||
TEST_ASSERT_EQUAL_INT(CV_MIN_BLOB_PX, state.tuning.min_blob_px);
|
||||
TEST_ASSERT_EQUAL_FLOAT(CV_MAX_MOVE, state.tuning.max_move);
|
||||
TEST_ASSERT_EQUAL_INT(CV_MAX_MISSED, state.tuning.max_missed);
|
||||
TEST_ASSERT_EQUAL_UINT8(50, state.tuning.line_offset);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, state.tuning.cfg_version);
|
||||
// Values mirror CV_DEFAULT_* constants in cv.cpp (now file-local).
|
||||
TEST_ASSERT_EQUAL_UINT8(30, state.tuning.diff_thresh);
|
||||
TEST_ASSERT_EQUAL_INT(64, state.tuning.min_blob_px);
|
||||
TEST_ASSERT_EQUAL_FLOAT(15.0f, state.tuning.max_move);
|
||||
TEST_ASSERT_EQUAL_INT(10, state.tuning.max_missed);
|
||||
TEST_ASSERT_EQUAL_UINT8(50, state.tuning.line_offset);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, state.tuning.cfg_version);
|
||||
}
|
||||
|
||||
void test_cv_process_respects_runtime_min_blob() {
|
||||
// Proves cv_process reads min_blob_px from state.tuning at runtime
|
||||
// (not from a compile-time constant). With a very high threshold, the
|
||||
// same blob-producing frame that spawns a track in other tests must NOT
|
||||
// spawn one here.
|
||||
CVState state;
|
||||
cv_init(state);
|
||||
state.tuning.min_blob_px = 10000; // larger than CV_PIXELS → no blob can qualify
|
||||
|
||||
uint8_t bg[CV_PIXELS];
|
||||
fill_frame(bg, 100);
|
||||
cv_process(state, bg); // init background
|
||||
|
||||
// Same 30x30 blob used by test_tracking_spawns_track_for_new_blob
|
||||
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;
|
||||
|
||||
CVResult r = cv_process(state, blob_frame);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, (int)state.tracks.size());
|
||||
TEST_ASSERT_EQUAL_INT(0, r.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, r.exits_delta);
|
||||
}
|
||||
|
||||
void test_no_crossing_same_side_no_count() {
|
||||
@@ -160,13 +188,13 @@ void test_no_crossing_same_side_no_count() {
|
||||
cv_init(state);
|
||||
|
||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
||||
cv_process(state, bg, 50);
|
||||
cv_process(state, bg);
|
||||
|
||||
uint8_t f1[CV_PIXELS]; make_blob_frame(f1, 48, 20); // above line
|
||||
cv_process(state, f1, 50);
|
||||
cv_process(state, f1);
|
||||
|
||||
uint8_t f2[CV_PIXELS]; make_blob_frame(f2, 48, 30); // still above line, moved closer
|
||||
CVResult r = cv_process(state, f2, 50);
|
||||
CVResult r = cv_process(state, f2);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, r.entries_delta);
|
||||
TEST_ASSERT_EQUAL_INT(0, r.exits_delta);
|
||||
@@ -183,5 +211,6 @@ int main() {
|
||||
RUN_TEST(test_blob_crossing_line_bottom_to_top_is_exit);
|
||||
RUN_TEST(test_no_crossing_same_side_no_count);
|
||||
RUN_TEST(test_cv_init_populates_tuning_defaults);
|
||||
RUN_TEST(test_cv_process_respects_runtime_min_blob);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user