feat: CV module — frame diff + threshold (blob tracking TODO)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
58
firmware/lib/cv/cv.cpp
Normal file
58
firmware/lib/cv/cv.cpp
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// firmware/lib/cv/cv.cpp
|
||||||
|
#include "cv.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
40
firmware/lib/cv/cv.h
Normal file
40
firmware/lib/cv/cv.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// firmware/lib/cv/cv.h
|
||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<CVTrack> 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);
|
||||||
70
firmware/test/test_cv/test_cv.cpp
Normal file
70
firmware/test/test_cv/test_cv.cpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// firmware/test/test_native/test_cv.cpp
|
||||||
|
#include <unity.h>
|
||||||
|
#include <string.h>
|
||||||
|
#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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user