// 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; // Event-based walker detector. Per-frame zone-flip approaches were direction- // blind at realistic mounts: a walker traversing top-to-bottom and a walker // traversing bottom-to-top produced identical zone-dominance sequences // (geometric artifact of asymmetric zones + body spanning the line). The // event approach buffers a whole walker event, then decides direction from // the centroid trajectory: sign(first_centroid_y - peak_centroid_y) > 0 means // the centroid moved upward through the frame during the event. // // Per-mount convention: UP through frame == ENTRY into store. Flip the camera // mount or invert the mapping in cv_process if the physical install differs. // fg_count thresholds that gate event start/end. Tuned against a real // 8-walk isolated test (see .agent/walk_isolated_8walks.log). Lower than // initial guesses because the 7' overhead mount produces smaller centroid // excursions than we originally modelled. static const int CV_EVENT_ENTER_THRESH = 250; static const int CV_EVENT_EXIT_THRESH = 150; // Number of consecutive sub-EXIT frames required to end an event. static const int CV_EVENT_QUIET_FRAMES = 3; // Min/max event duration in frames. Below min = too brief to be a walker // (noise burst). Above max = stationary object or stuck detection. static const int CV_EVENT_MIN_FRAMES = 5; // MAX bounds the event duration. Too low (15) cut events off while walker // was still physically in frame — every fire hit dur=MAX+1 and bg snapped // with a walker-ghost baked in, corrupting the next walk. Too high (40) // merged multiple walkers. 25 frames (5s) lets a single walker reach the // quiet-exit path (fg drops below EXIT_THRESH) before timeout, so bg snaps // on a clean empty frame. static const int CV_EVENT_MAX_FRAMES = 25; // Required vertical extent: during the event, fg must have reached near the // top of the frame (min_y <= TOP) AND near the bottom (max_y >= BOT). At a // 7' overhead mount real walkers span fg y≈0..70, not 0..95 — the original // 10/85 gates rejected most real walks. Relaxed to catch them while still // filtering small local motion that doesn't span the doorway. static const int CV_EVENT_EXTENT_TOP = 25; static const int CV_EVENT_EXTENT_BOT = 50; // Minimum centroid excursion (max of up_score/down_score) for a valid // trajectory. At overhead mount walker centroid traverses ~15-40 pixels; // 15 was too aggressive and dropped clean walks. 5 still filters wobble. static const float CV_EVENT_MIN_TRAJ = 5.0f; // Refractory period after a fire. Shorter than originally chosen — at 5 fps // a second walker can arrive within 2s of the first, especially at busy // doorways. 10 frames = 2s of back-pressure, tuned to match the gap between // consecutive isolated walks in the test log. static const uint32_t CV_EVENT_REFRACTORY_FRAMES = 10; // Diagnostic only: tracks are kept for spawn logging. Counting does NOT // depend on tracks. struct CVTrack { int id; float x, y; float spawn_y; 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; // Event state machine. bool event_active; uint32_t event_start_frame; int event_frame_count; int event_peak_n; float event_first_c; float event_last_c; float event_min_c; // min centroid_y observed during event float event_max_c; // max centroid_y observed during event int event_min_y_seen; int event_max_y_seen; int event_quiet_count; uint32_t last_fire_frame; // 0 = never; frame of last counted fire }; struct CVResult { int entries_delta; int exits_delta; // Per-frame foreground diagnostics (populated every call). int fg_count; int fg_min_y; int fg_max_y; float fg_centroid_y; // Populated only on a fire frame; zeroed otherwise. float fire_first_c; float fire_min_c; float fire_max_c; float fire_last_c; int fire_duration; }; void cv_init(CVState& state); CVResult cv_process(CVState& state, const uint8_t* frame, uint8_t line_pct); void cv_reset_counts(CVState& state);