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:
@@ -5,6 +5,14 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
// File-local defaults. Runtime values live in CVState::tuning and can be
|
||||||
|
// overridden via config_load_tuning() on boot or server push at runtime.
|
||||||
|
static constexpr uint8_t CV_DEFAULT_DIFF_THRESH = 30;
|
||||||
|
static constexpr int CV_DEFAULT_MIN_BLOB_PX = 64;
|
||||||
|
static constexpr float CV_DEFAULT_MAX_MOVE = 15.0f;
|
||||||
|
static constexpr int CV_DEFAULT_MAX_MISSED = 10;
|
||||||
|
static constexpr uint8_t CV_DEFAULT_LINE_OFFSET = 50;
|
||||||
|
|
||||||
void cv_init(CVState& state) {
|
void cv_init(CVState& state) {
|
||||||
// Initialize members directly — avoid CVState{} temporary which puts 9KB on stack
|
// Initialize members directly — avoid CVState{} temporary which puts 9KB on stack
|
||||||
memset(state.background, 0, sizeof(state.background));
|
memset(state.background, 0, sizeof(state.background));
|
||||||
@@ -15,11 +23,11 @@ void cv_init(CVState& state) {
|
|||||||
state.tracks.clear();
|
state.tracks.clear();
|
||||||
state.entries = 0;
|
state.entries = 0;
|
||||||
state.exits = 0;
|
state.exits = 0;
|
||||||
state.tuning.diff_thresh = CV_DIFF_THRESH;
|
state.tuning.diff_thresh = CV_DEFAULT_DIFF_THRESH;
|
||||||
state.tuning.min_blob_px = CV_MIN_BLOB_PX;
|
state.tuning.min_blob_px = CV_DEFAULT_MIN_BLOB_PX;
|
||||||
state.tuning.max_move = CV_MAX_MOVE;
|
state.tuning.max_move = CV_DEFAULT_MAX_MOVE;
|
||||||
state.tuning.max_missed = CV_MAX_MISSED;
|
state.tuning.max_missed = CV_DEFAULT_MAX_MISSED;
|
||||||
state.tuning.line_offset = 50;
|
state.tuning.line_offset = CV_DEFAULT_LINE_OFFSET;
|
||||||
state.tuning.cfg_version = 0;
|
state.tuning.cfg_version = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +40,9 @@ struct Point { int x, y; };
|
|||||||
|
|
||||||
// Note: queue may grow to CV_PIXELS entries (~72KB) on large blobs.
|
// Note: queue may grow to CV_PIXELS entries (~72KB) on large blobs.
|
||||||
// Requires PSRAM (enabled via -DBOARD_HAS_PSRAM in platformio.ini).
|
// Requires PSRAM (enabled via -DBOARD_HAS_PSRAM in platformio.ini).
|
||||||
// BFS flood fill. Marks visited pixels (sets fg to 0). Returns {-1,-1} if blob < CV_MIN_BLOB_PX.
|
// BFS flood fill. Marks visited pixels (sets fg to 0). Returns {-1,-1} if blob < min_blob_px.
|
||||||
static std::pair<float,float> extract_blob(uint8_t* fg, int start_x, int start_y) {
|
static std::pair<float,float> extract_blob(uint8_t* fg, int start_x, int start_y,
|
||||||
|
int min_blob_px) {
|
||||||
std::vector<Point> queue;
|
std::vector<Point> queue;
|
||||||
queue.reserve(512);
|
queue.reserve(512);
|
||||||
queue.push_back({start_x, start_y});
|
queue.push_back({start_x, start_y});
|
||||||
@@ -58,11 +67,12 @@ static std::pair<float,float> extract_blob(uint8_t* fg, int start_x, int start_y
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count < CV_MIN_BLOB_PX) return {-1.0f, -1.0f};
|
if (count < min_blob_px) return {-1.0f, -1.0f};
|
||||||
return {sum_x / count, sum_y / count};
|
return {sum_x / count, sum_y / count};
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::vector<std::pair<float,float>> find_centroids(const uint8_t* fg) {
|
static std::vector<std::pair<float,float>> find_centroids(const uint8_t* fg,
|
||||||
|
int min_blob_px) {
|
||||||
std::vector<std::pair<float,float>> result;
|
std::vector<std::pair<float,float>> result;
|
||||||
static uint8_t fg_copy[CV_PIXELS]; // static to avoid 9KB stack allocation
|
static uint8_t fg_copy[CV_PIXELS]; // static to avoid 9KB stack allocation
|
||||||
memcpy(fg_copy, fg, CV_PIXELS);
|
memcpy(fg_copy, fg, CV_PIXELS);
|
||||||
@@ -70,7 +80,7 @@ static std::vector<std::pair<float,float>> find_centroids(const uint8_t* fg) {
|
|||||||
for (int y = 0; y < CV_H; y++) {
|
for (int y = 0; y < CV_H; y++) {
|
||||||
for (int x = 0; x < CV_W; x++) {
|
for (int x = 0; x < CV_W; x++) {
|
||||||
if (!fg_copy[y * CV_W + x]) continue;
|
if (!fg_copy[y * CV_W + x]) continue;
|
||||||
auto c = extract_blob(fg_copy, x, y);
|
auto c = extract_blob(fg_copy, x, y, min_blob_px);
|
||||||
if (c.first >= 0) result.push_back(c);
|
if (c.first >= 0) result.push_back(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,15 +88,15 @@ static std::vector<std::pair<float,float>> find_centroids(const uint8_t* fg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void frame_diff(const uint8_t* frame, const uint8_t* bg,
|
static void frame_diff(const uint8_t* frame, const uint8_t* bg,
|
||||||
uint8_t* fg, int pixels) {
|
uint8_t* fg, int pixels, uint8_t diff_thresh) {
|
||||||
for (int i = 0; i < pixels; i++) {
|
for (int i = 0; i < pixels; i++) {
|
||||||
int diff = (int)frame[i] - (int)bg[i];
|
int diff = (int)frame[i] - (int)bg[i];
|
||||||
if (diff < 0) diff = -diff;
|
if (diff < 0) diff = -diff;
|
||||||
fg[i] = (diff > CV_DIFF_THRESH) ? 1 : 0;
|
fg[i] = (diff > diff_thresh) ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct) {
|
CVResult cv_process(CVState& state, const uint8_t* frame) {
|
||||||
CVResult result = {0, 0};
|
CVResult result = {0, 0};
|
||||||
state.frame_index++;
|
state.frame_index++;
|
||||||
|
|
||||||
@@ -96,13 +106,18 @@ CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uint8_t diff_thresh = state.tuning.diff_thresh;
|
||||||
|
const int min_blob_px = state.tuning.min_blob_px;
|
||||||
|
const float max_move = state.tuning.max_move;
|
||||||
|
const int max_missed = state.tuning.max_missed;
|
||||||
|
|
||||||
static uint8_t fg[CV_PIXELS]; // static: avoids 9KB on task stack
|
static uint8_t fg[CV_PIXELS]; // static: avoids 9KB on task stack
|
||||||
frame_diff(frame, state.background, fg, CV_PIXELS);
|
frame_diff(frame, state.background, fg, CV_PIXELS, diff_thresh);
|
||||||
|
|
||||||
int fg_count = 0;
|
int fg_count = 0;
|
||||||
for (int i = 0; i < CV_PIXELS; i++) fg_count += fg[i];
|
for (int i = 0; i < CV_PIXELS; i++) fg_count += fg[i];
|
||||||
|
|
||||||
bool motion = fg_count > CV_MIN_BLOB_PX;
|
bool motion = fg_count > min_blob_px;
|
||||||
if (!motion) {
|
if (!motion) {
|
||||||
if (state.frame_index - state.last_motion_frame > 10) {
|
if (state.frame_index - state.last_motion_frame > 10) {
|
||||||
memcpy(state.background, frame, CV_PIXELS);
|
memcpy(state.background, frame, CV_PIXELS);
|
||||||
@@ -110,19 +125,19 @@ CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct) {
|
|||||||
for (auto& t : state.tracks) t.missed++;
|
for (auto& t : state.tracks) t.missed++;
|
||||||
state.tracks.erase(
|
state.tracks.erase(
|
||||||
std::remove_if(state.tracks.begin(), state.tracks.end(),
|
std::remove_if(state.tracks.begin(), state.tracks.end(),
|
||||||
[](const CVTrack& t){ return t.missed > CV_MAX_MISSED; }),
|
[max_missed](const CVTrack& t){ return t.missed > max_missed; }),
|
||||||
state.tracks.end());
|
state.tracks.end());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.last_motion_frame = state.frame_index;
|
state.last_motion_frame = state.frame_index;
|
||||||
|
|
||||||
auto centroids = find_centroids(fg);
|
auto centroids = find_centroids(fg, min_blob_px);
|
||||||
|
|
||||||
std::vector<bool> centroid_matched(centroids.size(), false);
|
std::vector<bool> centroid_matched(centroids.size(), false);
|
||||||
|
|
||||||
for (auto& track : state.tracks) {
|
for (auto& track : state.tracks) {
|
||||||
float best_dist = CV_MAX_MOVE * CV_MAX_MOVE;
|
float best_dist = max_move * max_move;
|
||||||
int best_idx = -1;
|
int best_idx = -1;
|
||||||
|
|
||||||
for (int i = 0; i < (int)centroids.size(); i++) {
|
for (int i = 0; i < (int)centroids.size(); i++) {
|
||||||
@@ -145,10 +160,10 @@ CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct) {
|
|||||||
|
|
||||||
state.tracks.erase(
|
state.tracks.erase(
|
||||||
std::remove_if(state.tracks.begin(), state.tracks.end(),
|
std::remove_if(state.tracks.begin(), state.tracks.end(),
|
||||||
[](const CVTrack& t){ return t.missed > CV_MAX_MISSED; }),
|
[max_missed](const CVTrack& t){ return t.missed > max_missed; }),
|
||||||
state.tracks.end());
|
state.tracks.end());
|
||||||
|
|
||||||
float line_y = (line_pct / 100.0f) * CV_H;
|
float line_y = (state.tuning.line_offset / 100.0f) * CV_H;
|
||||||
for (int i = 0; i < (int)centroids.size(); i++) {
|
for (int i = 0; i < (int)centroids.size(); i++) {
|
||||||
if (centroid_matched[i]) continue;
|
if (centroid_matched[i]) continue;
|
||||||
CVTrack t;
|
CVTrack t;
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ static const int CV_W = 96;
|
|||||||
static const int CV_H = 96;
|
static const int CV_H = 96;
|
||||||
static const int CV_PIXELS = CV_W * CV_H;
|
static const int CV_PIXELS = CV_W * CV_H;
|
||||||
|
|
||||||
static const uint8_t CV_DIFF_THRESH = 30;
|
|
||||||
static const int CV_MIN_BLOB_PX = 64;
|
|
||||||
static const float CV_MAX_MOVE = 15.0f;
|
|
||||||
static const int CV_MAX_MISSED = 10;
|
|
||||||
|
|
||||||
struct CVTrack {
|
struct CVTrack {
|
||||||
int id;
|
int id;
|
||||||
float x, y;
|
float x, y;
|
||||||
@@ -46,5 +41,5 @@ struct CVResult {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void cv_init(CVState& state);
|
void cv_init(CVState& state);
|
||||||
CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct);
|
CVResult cv_process(CVState& state, const uint8_t* frame);
|
||||||
void cv_reset_counts(CVState& state);
|
void cv_reset_counts(CVState& state);
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ bool config_load(DeviceConfig& cfg) {
|
|||||||
cfg.hmac_secret = prefs.getString("hmac_secret", "");
|
cfg.hmac_secret = prefs.getString("hmac_secret", "");
|
||||||
cfg.wifi_ssid = prefs.getString("wifi_ssid", "");
|
cfg.wifi_ssid = prefs.getString("wifi_ssid", "");
|
||||||
cfg.wifi_pass = prefs.getString("wifi_pass", "");
|
cfg.wifi_pass = prefs.getString("wifi_pass", "");
|
||||||
cfg.line_offset = (uint8_t)prefs.getUInt("line_offset", 50);
|
|
||||||
|
|
||||||
prefs.end();
|
prefs.end();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ struct DeviceConfig {
|
|||||||
String hmac_secret; // 32-byte hex string
|
String hmac_secret; // 32-byte hex string
|
||||||
String wifi_ssid;
|
String wifi_ssid;
|
||||||
String wifi_pass;
|
String wifi_pass;
|
||||||
uint8_t line_offset; // 0-100, percent of frame height for virtual line
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load all config from NVS. Returns false if device_id/location_id/hmac_secret missing.
|
// Load all config from NVS. Returns false if device_id/location_id/hmac_secret missing.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ static void task_camera(void*) {
|
|||||||
while (true) {
|
while (true) {
|
||||||
if (camera_capture_96(frame)) {
|
if (camera_capture_96(frame)) {
|
||||||
if (xSemaphoreTake(s_cv_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
if (xSemaphoreTake(s_cv_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||||
CVResult r = cv_process(g_cv, frame, g_cfg.line_offset);
|
CVResult r = cv_process(g_cv, frame);
|
||||||
if (r.entries_delta) Serial.printf("[CV] entry +%d (total %d)\n", r.entries_delta, g_cv.entries);
|
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);
|
if (r.exits_delta) Serial.printf("[CV] exit +%d (total %d)\n", r.exits_delta, g_cv.exits);
|
||||||
xSemaphoreGive(s_cv_mutex);
|
xSemaphoreGive(s_cv_mutex);
|
||||||
@@ -140,6 +140,11 @@ void setup() {
|
|||||||
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
|
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
|
||||||
|
|
||||||
cv_init(g_cv);
|
cv_init(g_cv);
|
||||||
|
if (config_load_tuning(g_cv.tuning)) {
|
||||||
|
Serial.printf("[CFG] tuning loaded from NVS, cfg_version=%u\n", g_cv.tuning.cfg_version);
|
||||||
|
} else {
|
||||||
|
Serial.println("[CFG] no persisted tuning, using defaults");
|
||||||
|
}
|
||||||
|
|
||||||
if (!camera_init()) {
|
if (!camera_init()) {
|
||||||
Serial.println("FATAL: camera init failed");
|
Serial.println("FATAL: camera init failed");
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ void test_frame_diff_no_change_gives_no_fg() {
|
|||||||
uint8_t frame[CV_PIXELS];
|
uint8_t frame[CV_PIXELS];
|
||||||
fill_frame(frame, 128);
|
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);
|
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.entries_delta);
|
||||||
TEST_ASSERT_EQUAL_INT(0, r2.exits_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(bg, 100);
|
||||||
fill_frame(fg_frame, 200);
|
fill_frame(fg_frame, 200);
|
||||||
|
|
||||||
cv_process(state, bg, 50);
|
cv_process(state, bg);
|
||||||
CVResult r = cv_process(state, fg_frame, 50);
|
CVResult r = cv_process(state, fg_frame);
|
||||||
|
|
||||||
// Tracking not yet implemented — just verify no crash and result is zero
|
// 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.entries_delta);
|
||||||
@@ -66,7 +66,7 @@ void test_tracking_spawns_track_for_new_blob() {
|
|||||||
|
|
||||||
uint8_t bg[CV_PIXELS];
|
uint8_t bg[CV_PIXELS];
|
||||||
fill_frame(bg, 100);
|
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
|
// Frame with a bright 30x30 blob in top-left quadrant
|
||||||
uint8_t blob_frame[CV_PIXELS];
|
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++)
|
for (int x = 5; x < 35; x++)
|
||||||
blob_frame[y * CV_W + x] = 200;
|
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_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].x);
|
||||||
@@ -94,20 +94,20 @@ void test_blob_crossing_line_top_to_bottom_is_entry() {
|
|||||||
CVState state;
|
CVState state;
|
||||||
cv_init(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];
|
uint8_t bg[CV_PIXELS];
|
||||||
fill_frame(bg, 100);
|
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)
|
// Walk blob from y=20 toward line; crossing occurs at y=48 (above→below)
|
||||||
// Stop at crossing frame and assert its result
|
// Stop at crossing frame and assert its result
|
||||||
int setup[] = {20, 34};
|
int setup[] = {20, 34};
|
||||||
for (int i = 0; i < 2; i++) {
|
for (int i = 0; i < 2; i++) {
|
||||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, setup[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);
|
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(1, r.entries_delta);
|
||||||
TEST_ASSERT_EQUAL_INT(0, r.exits_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);
|
cv_init(state);
|
||||||
|
|
||||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
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)
|
// Walk blob from y=76 toward line; crossing occurs at y=34 (below→above)
|
||||||
// Stop at crossing frame and assert its result
|
// Stop at crossing frame and assert its result
|
||||||
int setup[] = {76, 62, 48};
|
int setup[] = {76, 62, 48};
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
uint8_t f[CV_PIXELS]; make_blob_frame(f, 48, setup[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);
|
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(0, r.entries_delta);
|
||||||
TEST_ASSERT_EQUAL_INT(1, r.exits_delta);
|
TEST_ASSERT_EQUAL_INT(1, r.exits_delta);
|
||||||
@@ -147,26 +147,54 @@ void test_cv_init_populates_tuning_defaults() {
|
|||||||
|
|
||||||
cv_init(state);
|
cv_init(state);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_UINT8(CV_DIFF_THRESH, state.tuning.diff_thresh);
|
// Values mirror CV_DEFAULT_* constants in cv.cpp (now file-local).
|
||||||
TEST_ASSERT_EQUAL_INT(CV_MIN_BLOB_PX, state.tuning.min_blob_px);
|
TEST_ASSERT_EQUAL_UINT8(30, state.tuning.diff_thresh);
|
||||||
TEST_ASSERT_EQUAL_FLOAT(CV_MAX_MOVE, state.tuning.max_move);
|
TEST_ASSERT_EQUAL_INT(64, state.tuning.min_blob_px);
|
||||||
TEST_ASSERT_EQUAL_INT(CV_MAX_MISSED, state.tuning.max_missed);
|
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_UINT8(50, state.tuning.line_offset);
|
||||||
TEST_ASSERT_EQUAL_UINT32(0, state.tuning.cfg_version);
|
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() {
|
void test_no_crossing_same_side_no_count() {
|
||||||
CVState state;
|
CVState state;
|
||||||
cv_init(state);
|
cv_init(state);
|
||||||
|
|
||||||
uint8_t bg[CV_PIXELS]; fill_frame(bg, 100);
|
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
|
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
|
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.entries_delta);
|
||||||
TEST_ASSERT_EQUAL_INT(0, r.exits_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_blob_crossing_line_bottom_to_top_is_exit);
|
||||||
RUN_TEST(test_no_crossing_same_side_no_count);
|
RUN_TEST(test_no_crossing_same_side_no_count);
|
||||||
RUN_TEST(test_cv_init_populates_tuning_defaults);
|
RUN_TEST(test_cv_init_populates_tuning_defaults);
|
||||||
|
RUN_TEST(test_cv_process_respects_runtime_min_blob);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user