feat(cv): directional once-per-track counting + detection LED blinks

A single person walking under the overhead camera was generating both an
entry and an exit within a few seconds — the line-crossing logic treated
a blob's traversal into one side of the frame and out the other as two
separate events whenever the track spawned near the line, oscillated
against shadows, or churned at creation.

Replaced line-crossing semantics with directional traversal:
- Each track records spawn_y at creation and a counted flag.
- An event fires only if the track is not yet counted, spawned firm on
  one side of the line (|spawn_y - line_y| > CV_TRAVERSAL_MARGIN_PX),
  and is now firm on the opposite side. Direction of travel determines
  entry vs exit. The track is then flagged counted — one trip, one count.
- Cooldown remains as a secondary safety net.

main.cpp: single/double LED pulse on entry/exit detections. Saves and
restores the current LED state so upload (yellow-on) and no-WiFi
indicators aren't clobbered.

Tests updated to walk blobs beyond the margin and register two new
cases: wobble-at-line doesn't count, and a reversed full traversal
doesn't double-count on the same track.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 09:46:59 -07:00
parent 24aaae6ff2
commit 3b471992f2
6 changed files with 180 additions and 62 deletions

View File

@@ -21,7 +21,8 @@ pio run -t upload --upload-port /dev/ttyUSB0
| Module | Behavior |
|--------|----------|
| CV pipeline | 5 fps, 96×96 grayscale, blob tracking, line-crossing count with per-direction cooldown |
| CV pipeline | 5 fps, 96×96 grayscale, blob tracking, directional traversal count (origin→destination, once per track) with per-direction cooldown safety net |
| Detection LED | Single blink on entry, double blink on exit (preserves upload/no-WiFi status LED) |
| BLE scanner | Continuous passive scan; deinits during hourly upload to free heap |
| Reporter | Hourly HMAC-signed POST; 60s boot report for fast connectivity check |
| Provisioning | Captive portal AP on first boot for WiFi setup |
@@ -32,16 +33,31 @@ pio run -t upload --upload-port /dev/ttyUSB0
- **First report**: 60 seconds after NTP sync (connectivity check)
- **Subsequent reports**: every 3600 seconds
### Crossing cooldown
### Directional counting
To suppress double-counts from track churn (a blob briefly dropping below the
minimum-blob-pixel threshold, causing the tracker to kill and respawn a track
that then re-crosses the line), each direction enforces a cooldown window
between counted crossings. Default: `CV_CROSSING_COOLDOWN_FRAMES = 5`, which
suppresses any second crossing in the same direction whose frame gap is `< 5`
— ≈0.8s at 5 fps. Entries and exits maintain separate cooldowns, so a real entry
immediately followed by a real exit still counts both. See
`firmware/lib/cv/cv.h`.
Each tracked blob fires at most **one** event over its lifetime, and only
when it has genuinely traversed the frame — specifically, when its spawn
position and current position are both at least `CV_TRAVERSAL_MARGIN_PX`
(14 px ≈ 15% of the 96×96 frame) from the line, and on opposite sides.
- Top half → bottom half traversal = **entry**
- Bottom half → top half traversal = **exit**
A blob that appears near the line and wobbles across it does not count
(both positions are within the margin band). A blob that fully traverses
then reverses under the same track also does not double-count (the track
is flagged `counted`). If tracking churns — the track dies mid-traversal
and respawns on the other side — a new track with a new spawn on the
crossed side is the normal path to a correct count.
See `firmware/lib/cv/cv.h` for margin and `cv.cpp` for the crossing logic.
### Crossing cooldown (safety net)
On top of directional counting, each direction enforces a cooldown between
counted events. Default: `CV_CROSSING_COOLDOWN_FRAMES = 5` (≈0.8s at 5 fps).
Entries and exits maintain separate cooldowns, so a real entry immediately
followed by a real exit still counts both.
## Operator Setup
@@ -87,7 +103,7 @@ python tools/ota_push.py \
3. Connect phone to `DoorCounter-Setup` WiFi
4. Browser opens automatically → enter store WiFi password → done
**LED indicators**: Red = no WiFi · Blue = counting · Yellow = uploading
**LED indicators**: Red = no WiFi · Blue = counting · Yellow = uploading · Brief flash (×1) on entry · Brief flash (×2) on exit
## API