Files

177 lines
7.2 KiB
Markdown
Raw Permalink Normal View History

2026-09-09 21:44:05 -05:00
# Bots Readme — Motion Detection (mofin) Pipeline Addition
## Overview
Added a motion detection stage (`mofin`) between `runCam` and `terp` in the video surveillance pipeline.
### Old Pipeline
```
runCam ──TCP──▶ terp ──TCP──▶ coordinator
```
### New Pipeline
```
runCam ──TCP──▶ mofin ──TCP──▶ terp ──TCP──▶ coordinator
└──TCP──▶ coordinator (no-motion clips)
```
`mofin` runs pixel-level motion detection on each clip. Clips with motion are forwarded to `terp` for classification (Haar cascade object detection). Clips without motion are sent directly to `coordinator` for storage, bypassing classification.
---
## Files Created
### `frame/motion.go`
Contains motion detection types and methods in the `frame` package.
**`Comparison` struct:**
```go
type Comparison struct {
Guid1, Guid2 []byte
PixelsChanged int
Threshold uint8
}
```
Stores the result of comparing two adjacent frames in a clip — their GUIDs, how many pixels changed, and the pixel-value threshold used.
**`(*Frame).compareTo(next *Frame, threshold uint8) (int, error)`:**
- Converts both frames to `gocv.Mat` via `ToMat()`
- Computes `AbsDiff` between the two Mats
- Converts to grayscale if multi-channel (`CvtColor`)
- Applies `Threshold` (pixel values above `threshold` count as changed)
- Returns `CountNonZero` (number of changed pixels) or `(0, error)` on failure
**`(*Clip).CountChangedPixels(threshold uint8)`:**
- Iterates over adjacent frame pairs in `c.PixelMats`
- For each pair, creates lightweight `Frame` objects from the clip's metadata and calls `compareTo`
- Logs any error returned by `compareTo` (avoiding writing to ephemeral Frame copies)
- Appends a `Comparison` entry to `c.Comparisons`
### `mofin/main.go`
A new pipeline binary that:
| Flag | Default | Purpose |
|---|---|---|
| `-listen` | `:8083` | TCP address to receive clips from `runCam` |
| `-terp` | `localhost:8081` | Forward address for clips with motion |
| `-coordinator` | `localhost:8082` | Forward address for clips without motion |
| `-threshold` | `5000` | Minimum `PixelsChanged` to consider motion present |
| `-passphrase` | `""` | Decryption/re-encryption passphrase (mirrors `terp` pattern) |
**Concurrency:** Clips are processed in parallel — each incoming clip gets its own goroutine (tracked by `procWg`). On shutdown, the listener is closed, in-flight decode goroutines drain via `acceptWg`, `clipChan` is closed, and `procWg.Wait()` blocks until all processing finishes before exiting.
**`processClip` flow:**
1. If `passphrase` is set, decrypt each frame's `PixelBytes` in-place via `Sublimate` + `Decrypt`
2. Call `clip.CountChangedPixels(30)` — pixel-value sensitivity of 30
3. Check if any `Comparison.PixelsChanged >= threshold` flag
4. Re-encrypt if passphrase was set
5. Forward to `terp` address (motion) or `coordinator` address (no motion)
### `mofin/go.mod`
Standard module setup with `replace frame => ../frame` (same pattern as `runCam`, `terp`, `coordinator`).
---
## Files Modified
### `frame/frame.go`
- Added `Errors []string` field to `Frame` — stores descriptive pipeline errors for backend review
- Added `Comparisons []Comparison` field to `Clip` — stores pairwise motion comparison results
- Removed a stale duplicate `Detect` method declaration at end of file
### `runCam/main.go`
- Renamed `-terp` flag to `-mofin` (default `localhost:8083`)
- Updated all internal references from `terpAddr``mofinAddr` in `main()`, `rebalance()`, and `runCamera()`
### `config.yaml`
Added `mofin1` node between `cam1` and `terp1`:
```yaml
- name: "mofin1"
binary: "mofin"
flags:
listen: ":8083"
terp: "localhost:8081"
coordinator: "localhost:8082"
threshold: 5000
```
Also updated `cam1` to point `mofin: "localhost:8083"` instead of `terp: "localhost:8081"`.
---
## Tests
### `frame/frame_test.go` — 19 tests
| Test | What it covers |
|---|---|
| `TestToMat_Roundtrip` | `ToMat()` returns an identical Mat from Frame data |
| `TestToMat_NonNilResult` | `ToMat()` succeeds with valid 2×2 grayscale data |
| `TestToMat_EmptyBytes` | `ToMat()` errors on zero-size buffer |
| `TestEncryptDecrypt_Roundtrip` | Encrypt then decrypt with same passphrase restores original bytes |
| `TestEncryptDecrypt_WrongPassphrase` | Decrypt with wrong passphrase fails |
| `TestEncryptDecrypt_EmptyBytes` | Encrypt/Decrypt on nil `PixelBytes` is a no-op |
| `TestDecrypt_TooShort` | Decrypt on truncated data fails |
| `TestEncrypt_UniqueSaltPerCall` | Two encryptions of same data produce different ciphertexts |
| `TestSublimate_Basic` | `Clip.Sublimate()` produces correct Frame count, metadata, GUIDs, timestamps |
| `TestSublimate_EmptyClip` | Empty clip produces zero frames |
| `TestSublimate_DetectionsCarryOver` | Frame detections are correctly mapped from Clip |
| `TestCheckLenCorrelations_Match` | Equal-length slices return true |
| `TestCheckLenCorrelations_PixelMatsGuidsMismatch` | Mismatched PixelMats/Guids returns false |
| `TestCheckLenCorrelations_TimestampsMismatch` | Mismatched Timestamps returns false |
| `TestCheckLenCorrelations_Empty` | Empty clip returns true |
| `TestClipSend` | Send/Receive roundtrip over TCP preserves Clip data |
| `TestClipSend_InvalidAddress` | Send to unreachable address returns error |
| `TestFrameFields_ZeroValues` | Zero-value Frame has nil Errors, nil Detections |
| `TestClipFields_ZeroValues` | Zero-value Clip has nil Comparisons |
### `frame/motion_test.go` — 12 tests
| Test | What it covers |
|---|---|
| `TestCompareTo_IdenticalFrames` | Two identical frames → 0 changed pixels |
| `TestCompareTo_AllPixelsChanged` | 0→255 → all 16 pixels of 4×4 detected as changed |
| `TestCompareTo_PartialChange` | 3 of 16 pixels differing → exactly 3 counted |
| `TestCompareTo_ThresholdFiltersSmallDiffs` | Pixels with diff > threshold counted; diff ≤ threshold ignored |
| `TestCompareTo_MultiChannel` | Multi-channel (BGR) → grayscale conversion works, changed pixels detected |
| `TestCompareTo_DimensionMismatch` | Different sizes return error |
| `TestCompareTo_ChannelMismatch` | Different channel counts return error |
| `TestCompareTo_IdenticalPixelData` | Same pixel bytes → 0 changed (no crash with equal data) |
| `TestCountChangedPixels_Basic` | 3 frames → 2 comparisons, correct pixel counts |
| `TestCountChangedPixels_SingleFrame` | 1 frame → 0 comparisons |
| `TestCountChangedPixels_EmptyClip` | 0 frames → 0 comparisons |
| `TestCountChangedPixels_ResetsComparisons` | Pre-existing Comparisons are cleared before run |
| `TestCountChangedPixels_ThresholdParameter` | Pixel-value threshold correctly filters small diffs |
| `TestComparison_Fields` | Comparison struct fields store/retrieve correctly |
### `frame/utils_test.go` — 16 tests
Camera-dependent tests skip with `-short` flag. Pure-logic tests (empty range, negative index, struct fields, Sscanf) always run.
### Running
```bash
go test -short ./... # fast (skips camera hardware scans)
go test ./... # full suite (attempts V4L device probes)
```
All 42+ tests pass. Pre-existing `ScanCams` tests preserved and corrected.
## Build Verification
All five modules pass `go build ./...` and `go vet ./...`:
- `frame/`
- `runCam/`
- `mofin/`
- `terp/`
- `coordinator/`