// 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; // Directional counting margin: a track only counts if it spawned and is now // both at least this far from the line (in pixels). Prevents counting blobs // that wobble around the line or spawn on top of it. Value chosen at ~15% of // the 96px frame: 14px ≈ the typical torso half-width overhead. static const float CV_TRAVERSAL_MARGIN_PX = 14.0f; // Per-direction crossing cooldown. Any same-direction crossing whose frame gap // is strictly less than this value is dropped. At 5 fps, a value of 5 → ≈0.8s // suppression window. Purpose: mask track churn (blob briefly drops below // min_blob_px, track dies & respawns, re-crosses). static const uint32_t CV_CROSSING_COOLDOWN_FRAMES = 5; struct CVTrack { int id; float x, y; float spawn_y; // y at track creation — used for directional counting bool above_line; bool counted; // fires at most once per track (one track = one trip) 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; uint32_t last_entry_frame; // 0 = never; frame_index of last counted entry uint32_t last_exit_frame; // 0 = never; frame_index of last counted exit }; 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);