177 lines
7.2 KiB
Markdown
177 lines
7.2 KiB
Markdown
# 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/`
|