From e6843584cf8d333cea98dc02cd21f2f80629b724 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Mon, 13 Apr 2026 14:26:34 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20CV=20module=20=E2=80=94=20frame=20diff?= =?UTF-8?q?=20+=20threshold=20(blob=20tracking=20TODO)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- firmware/lib/cv/cv.cpp | 58 +++++++++++++++++++++++++ firmware/lib/cv/cv.h | 40 ++++++++++++++++++ firmware/test/test_cv/test_cv.cpp | 70 +++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 firmware/lib/cv/cv.cpp create mode 100644 firmware/lib/cv/cv.h create mode 100644 firmware/test/test_cv/test_cv.cpp diff --git a/firmware/lib/cv/cv.cpp b/firmware/lib/cv/cv.cpp new file mode 100644 index 0000000..46f1fff --- /dev/null +++ b/firmware/lib/cv/cv.cpp @@ -0,0 +1,58 @@ +// firmware/lib/cv/cv.cpp +#include "cv.h" +#include +#include +#include + +void cv_init(CVState& state) { + memset(&state, 0, sizeof(CVState)); + state.next_id = 1; +} + +void cv_reset_counts(CVState& state) { + state.entries = 0; + state.exits = 0; +} + +static void frame_diff(const uint8_t* frame, const uint8_t* bg, + uint8_t* fg, int pixels) { + for (int i = 0; i < pixels; i++) { + int diff = (int)frame[i] - (int)bg[i]; + if (diff < 0) diff = -diff; + fg[i] = (diff > CV_DIFF_THRESH) ? 1 : 0; + } +} + +CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct) { + CVResult result = {0, 0}; + state.frame_index++; + + if (!state.bg_valid) { + memcpy(state.background, frame, CV_PIXELS); + state.bg_valid = true; + return result; + } + + uint8_t fg[CV_PIXELS]; + frame_diff(frame, state.background, fg, CV_PIXELS); + + int fg_count = 0; + for (int i = 0; i < CV_PIXELS; i++) fg_count += fg[i]; + + bool motion = fg_count > CV_MIN_BLOB_PX; + if (!motion) { + if (state.frame_index - state.last_motion_frame > 10) { + memcpy(state.background, frame, CV_PIXELS); + } + for (auto& t : state.tracks) t.missed++; + state.tracks.erase( + std::remove_if(state.tracks.begin(), state.tracks.end(), + [](const CVTrack& t){ return t.missed > CV_MAX_MISSED; }), + state.tracks.end()); + return result; + } + + state.last_motion_frame = state.frame_index; + // Blob detection and tracking added in Tasks 5-6 + return result; +} diff --git a/firmware/lib/cv/cv.h b/firmware/lib/cv/cv.h new file mode 100644 index 0000000..30de5b1 --- /dev/null +++ b/firmware/lib/cv/cv.h @@ -0,0 +1,40 @@ +// firmware/lib/cv/cv.h +#pragma once +#include +#include + +static const int CV_W = 96; +static const int CV_H = 96; +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 { + int id; + float x, y; + bool above_line; + int missed; +}; + +struct CVState { + uint8_t background[CV_PIXELS]; + bool bg_valid; + uint32_t last_motion_frame; + uint32_t frame_index; + int next_id; + std::vector tracks; + int entries; + int exits; +}; + +struct CVResult { + int entries_delta; + int exits_delta; +}; + +void cv_init(CVState& state); +CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct); +void cv_reset_counts(CVState& state); diff --git a/firmware/test/test_cv/test_cv.cpp b/firmware/test/test_cv/test_cv.cpp new file mode 100644 index 0000000..ecdc9be --- /dev/null +++ b/firmware/test/test_cv/test_cv.cpp @@ -0,0 +1,70 @@ +// firmware/test/test_native/test_cv.cpp +#include +#include +#include "cv.h" + +static void fill_frame(uint8_t* f, uint8_t val) { + memset(f, val, CV_PIXELS); +} + +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); + + 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; + cv_init(state); + TEST_ASSERT_EQUAL_INT(0, state.entries); + TEST_ASSERT_EQUAL_INT(0, state.exits); + TEST_ASSERT_FALSE(state.bg_valid); +} + +void test_cv_reset_counts() { + 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); +} + +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_cv_init_clears_state); + RUN_TEST(test_cv_reset_counts); + return UNITY_END(); +}