Privacy-First, Visual-to-Emoji Yard Monitor
Open-source edge AI on a Raspberry Pi 5 β watches your yard and sends emoji alerts.
No video saved. No cloud inference. No surveillance. Just signal.
Rook is a sensor system that watches your yard and sends emoji summaries of what's happening β ππ¦ for a delivery, π¦
for a hawk, πΊβ οΈ for a possible coyote, π
at sunrise, ππ for a car parked over an hour.
It runs 24/7 on a Raspberry Pi 5 with a Sony STARVIS camera and YOLO26n, processing everything on-device.
| Principle | Implementation |
|---|---|
| Privacy by design | No video saved or transmitted. Frames exist only in RAM during inference. |
| Signal over noise | Solo cars counted silently. Static fixtures auto-suppressed. Only meaningful activity fires alerts. |
| Always on | Headless Pi 5 + Slack/Email. No subscriptions, no cloud GPU, no monthly cost. |
Every frame passes through a four-stage pipeline. Each stage acts as a progressive filter β only meaningful events survive to trigger a notification.
The camera feed is downscaled to 640Γ360 and passed through OpenCV's MOG2 background subtractor (~3ms). Two checks must pass:
- Pixel count: Total changed pixels must exceed
MOTION_THRESHOLD_PIXELS - Blob analysis: The largest contiguous blob must exceed
MOTION_BLOB_MIN_PIXELS
If neither passes, the frame is discarded and YOLO never runs β saving ~150ms of CPU per idle frame. A forced YOLO scan every 5 minutes bypasses this gate to catch objects that MOG2 has absorbed into the background model (e.g., a car that parked and stopped moving).
Qualifying frames are upscaled to 1088px and passed through YOLO26n exported to NCNN format for ~3Γ faster inference vs. PyTorch on CPU (~150ms per frame).
| Parameter | Value | Rationale |
|---|---|---|
| Confidence threshold | 0.70 |
Uniform across all hours β minimizes false positives |
| Airborne gate | 0.75 |
Birds, airplanes, kites require extra certainty (distant sky noise) |
| Ignored classes | train, traffic light, boat |
Site-specific false positives (see Suppressed Classes) |
Detections pass through four enrichment layers before scoring:
| Module | Purpose |
|---|---|
| SceneFixtureFilter | Auto-suppresses objects appearing in β₯80% of recent inferences at the same screen position. Resets daily. |
| LingererTracker | Monitors dwell time per object per zone. Fires delayed alerts for parked cars (60 min), loiterers (5 min). 3-frame grace period prevents eviction from brief occlusions. |
| Open-Meteo Weather | 15-minute cached weather enrichment. Extreme conditions (storms, heat, snow) add score bonuses. |
| Bird Color ID | HSV analysis on bird bounding boxes. Identifies cardinals (π΄π¦), blue jays (π΅π¦), dark raptors (π¦ ). |
Each detection set is scored by calculate_image_score(). Score determines notification routing:
Score β₯ 30 β Slack + Email (with annotated image) Score < 30 β Logged to daily digest only
Score bonuses ensure multi-subject events always notify:
| Trigger | Bonus | Floor |
|---|---|---|
| 3+ objects in scene | +15 | 30 |
| 5+ objects in scene | +25 | 30 |
| 3+ people | +10 | 30 |
| 5+ people | +30 | 30 |
| Unaccompanied animal | +15 | 30 |
| Any person during quiet hours | +20 | 30 |
Solo cars, bicycles, and horses are silently counted β no real-time alert.
---
## Emoji Vocabulary
Rook translates detections into contextual emoji summaries. Composite heuristics combine class, count, time-of-day, proximity, and motion data.
### People
| Emoji | Event | Trigger |
|---|---|---|
| ποΈ | Large crowd | 5+ people |
| π₯ | Group | 2β4 people |
| π | Runner | Solo person + large motion blob |
| π΄ | Cyclist | Bicycle + person |
| ππΆ | Night walker | Person during nighttime |
| ππΆ | Rain walk | Umbrella + person |
| ππ¦ | Moving day | Suitcase + person |
| πβ½ | Street play | Sports ball or frisbee + person |
### Wildlife
| Emoji | Event | Trigger |
|---|---|---|
| π» | Bear | Immediate high-priority (score 100) |
| πΊβ οΈ | Possible coyote | Spatially isolated dog, quiet hours or dawn |
| πβ οΈ | Loose dog | Spatially isolated dog, daytime |
| π | Accompanied dog | Dog near a person (assumed on-leash) |
| π¦
| Raptor | Solo bird, dark plumage |
| π΄π¦ | Cardinal | Bird with red HSV signature |
| π΅π¦ | Blue jay | Bird with blue HSV signature |
| πΎπΎπΎ | Animal cluster | 3+ animals in scene |
### Vehicles & Atmospheric
| Emoji | Event | Trigger |
|---|---|---|
| ππ | Parked car | Lingering > 60 min in same zone |
| πΆβ±οΈ | Loiterer | Person lingering > 5 min in same zone |
| βοΈβ¬οΈ | Low-flying aircraft | Airplane detection |
| πͺπ¬οΈ | Kite / wind event | Kite + person |
| π
| Sunrise | Exposure mode switch to daytime |
| π | Sunset | Exposure mode switch to nighttime |
---
## Notifications
### Channels
| Channel | Threshold | Behavior |
|---|---|---|
| **Slack** | Score β₯ 30 | Real-time emoji alert. Active 24/7. |
| **Email/MMS** | Score β₯ 30 | High-priority events with annotated image. **Suppressed during quiet hours** (11 PMβ6 AM). |
| **Lingerer alerts** | Threshold-based | Direct Slack + Email. Bypass score gate. Re-alert every 15 min. |
| **Daily Digest** | Automatic | 3 AM email: yesterday's activity counts, cumulative stats, top event image, Beast Cam crops. |
| **Heartbeat** | Every 6 hours | Slack ping confirming the engine is alive. |
### Quiet Hours (11 PM β 6 AM)
During quiet hours, email is suppressed (no phone buzzing overnight). Slack alerts continue with a π prefix. Any person detected during quiet hours automatically gets a +20 score bonus and a floor of 30.
### Cooldown
Alerts use a score-adaptive cooldown to prevent spam:
- High-priority events (score β₯ 60): **10-second** cooldown
- Low-priority events (score ~8): **52-second** cooldown
- Redundant scenes (same classes as last alert) are always skipped
### SMS β Opt-In & Compliance
> The canonical, single-page opt-in disclosure for Rook's Twilio A2P 10DLC campaign lives at **[`docs/SMS_COMPLIANCE.md`](docs/SMS_COMPLIANCE.md)**. The summary below is reproduced from that page.
**Who can receive SMS from a Rook deployment?** Exactly one person: the **device owner** who installs the software, provisions the Twilio sender, and runs the Pi. Rook is a personal-use, open-source IoT sensor β there is no signup form, no customer list, and no mechanism for enrolling anyone other than the deployer themselves. Entering a third party's number is a violation of these terms and of Twilio's A2P 10DLC policy.
**How is consent collected?** Self-enrollment via local configuration. After cloning the repo and running `setup_pi.sh`, the device owner edits `~/rook-env/.env` on their own Raspberry Pi and writes their **own** mobile number into the `NOTIFY_TO_NUMBER` field (see [Β§4 Configure Environment](#4-configure-environment) for the exact disclosure shown at the moment of entry). The disclosure block in that section β visible in this public repository **before** any number is entered β names the program, message types, frequency, cost disclosure, opt-out keywords/methods, help keyword, and links to Privacy & Terms. Saving the file and starting the `rook.service` systemd unit constitutes the device owner's express written consent to receive SMS from their own device.
**Program details (required A2P disclosures):**
| Field | Value |
|---|---|
| Program name | Rook Sensor Alerts |
| Brand type | Sole Proprietor (single device owner / single recipient) |
| Message types | Emoji-based yard activity alerts, thermal warnings, system status |
| Marketing? | **No.** Marketing messages are never sent. |
| Message frequency | Variable, activity-driven. Typically **0β20 messages/day**. |
| Cost | **Message and data rates may apply** (per your mobile carrier). |
| Opt-out keywords | STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT (Twilio default) |
| Opt-out (additional) | Remove `NOTIFY_TO_NUMBER` from `~/rook-env/.env` and restart the service, **or** power off the device. |
| Help keywords | HELP, INFO (Twilio default) |
| Third-party sharing | **None.** Mobile numbers are not shared with third parties or affiliates for marketing or promotional purposes. See [PRIVACY.md](PRIVACY.md). |
| Privacy Policy | <https://github.com/Cook4986/rook-sensor/blob/main/PRIVACY.md> |
| Terms & Conditions | <https://github.com/Cook4986/rook-sensor/blob/main/TERMS.md> |
---
## Scene Intelligence
### Fixture Suppression
`SceneFixtureFilter` maintains a rolling window of the last 60 YOLO inferences. Any `(class, zone)` pair appearing in β₯80% of those inferences is promoted to a **fixture** and silently dropped from future detections β preventing a static houselight, signpost, or long-parked neighbor's car from consuming the alert budget.
> **Note**: Classes tracked by the `LingererTracker` (car, motorcycle, bicycle, person) are **exempt** from fixture suppression. This ensures a parked car can accumulate the full 60-minute dwell time needed to trigger a lingering alert, even though it would otherwise be promoted to a fixture after ~48 seconds.
Fixture lists reset daily at the digest rollover.
### Lingerer Detection
`LingererTracker` observes objects across consecutive YOLO scans using a coarse 4Γ4 grid-cell zone system.
| Class | Threshold | Alert |
|---|---|---|
| Car | 60 min | ππ Parked vehicle |
| Motorcycle | 30 min | ποΈπ Parked motorcycle |
| Bicycle | 30 min | π²π Unattended bicycle |
| Person | 5 min | πΆβ±οΈ Loitering individual |
A **3-frame grace period** prevents premature eviction from brief occlusions (a passing car, a confidence dip, or thermal frame-skipping). Re-alerts fire every 15 minutes if the object is still present. The forced YOLO timer (every 5 min) ensures objects absorbed into MOG2's background model are still observed.
### Suppressed Classes
These COCO classes are permanently ignored in this deployment:
| Class | Reason |
|---|---|
| `train` | No rail infrastructure β misclassifies dark boxy vehicles at distance |
| `traffic light` | Park houselight across the street β persistent false positive |
| `boat` | No navigable water β park fence / reflective surface triggers |
### Thermal Management
The Pi 5 runs warm under continuous inference. The engine implements progressive throttling:
| SoC Temperature | Behavior |
|---|---|
| < 65Β°C | Full speed (~6 FPS) |
| 65β72Β°C | Process 1 in 3 frames |
| 72β80Β°C | Process 1 in 6 frames |
| β₯ 80Β°C | Shutdown (hardware protection) |
Temperature is checked every 30 seconds. Thermal state is logged and included in the daily evaluation report.
---
## Hardware
| Component | Part | Notes |
|---|---|---|
| SBC | Raspberry Pi 5 (2GB+) | 2GB sufficient; 4GB provides headroom |
| Camera | Arducam B0444 (IMX462 STARVIS, 1/2.8") | Excellent low-light sensitivity |
| OS | Debian Trixie (64-bit) | Required for Pi 5 kernel support |
| Power | 5V/5A USB-C PD | Underpowering causes throttling |
See [`device/bom.md`](device/bom.md) for the full bill of materials.
---
## Setup
### 1. Flash OS
Flash **Debian Trixie (64-bit)** via Raspberry Pi Imager. Enable SSH and set hostname to `rook`.
### 2. Configure Camera
Add to `/boot/firmware/config.txt`:
```ini
camera_auto_detect=0
dtoverlay=arducam-pivariety
git clone https://github.com/Cook4986/rook-sensor.git
cd rook-sensor/app
chmod +x setup_pi.sh && ./setup_pi.shsetup_pi.sh installs Python, OpenCV, Ultralytics, creates a virtual environment at ~/rook-env, exports the NCNN model, and registers the systemd service.
Program: Rook Sensor Alerts (operated by the device owner under a Twilio A2P 10DLC Sole Proprietor campaign).
Single recipient, self-enrollment only. Rook is a personal-use, open-source IoT device. The only phone number eligible to receive SMS from a Rook deployment is the device owner's own mobile number β i.e., the same person who installs the software, configures the Pi, and provisions the Twilio sender. You may not enter anyone else's number. By entering your own mobile number in
NOTIFY_TO_NUMBERbelow and starting the service, you (the device owner) provide express written consent to receive automated SMS alerts from your own Rook device.
- Message types: Emoji-based detection alerts (e.g.,
π¦π,π¦), thermal warnings, and system status. No marketing.- Message frequency: Variable, driven by yard activity. Typically 0β20 messages/day.
- Cost: Message and data rates may apply (per your mobile carrier).
- Opt-out: Reply STOP to any Rook message, or remove
NOTIFY_TO_NUMBERfrom this.envfile and restart the service, or power off the device.- Help: Reply HELP to any Rook message.
- Privacy: Your mobile number is not shared with third parties or affiliates for marketing or promotional purposes. See PRIVACY.md.
- Terms: See TERMS.md.
Canonical disclosure:
docs/SMS_COMPLIANCE.md. The block above mirrors that page and is referenced by Rook's Twilio A2P 10DLC campaign registration.
Create ~/rook-env/.env:
# ββ Notification channels ββ
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
NOTIFY_EMAIL=you@example.com
# NOTIFY_TO_NUMBER: enter ONLY your own mobile number (E.164, e.g. +15551234567).
# By entering it here you (the device owner) consent to receive automated SMS
# from Rook Sensor Alerts. Msg freq varies (0β20/day typical). Msg & data rates
# may apply. Reply STOP to cancel, HELP for help. No marketing. No third-party
# sharing. Terms: https://github.com/Cook4986/rook-sensor/blob/main/TERMS.md
# Privacy: https://github.com/Cook4986/rook-sensor/blob/main/PRIVACY.md
NOTIFY_TO_NUMBER=+15551234567
# ββ Twilio (Required for SMS) ββ
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=...
TWILIO_FROM_NUMBER=+15559876543
# ββ SMTP (Gmail app password recommended) ββ
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com
SMTP_PASS=your-app-password
# ββ Location (for weather enrichment + sunrise/sunset) ββ
LATITUDE=40.71
LONGITUDE=-74.00
# ββ Camera ββ
FLIP_180=1 # Set to 0 if camera is right-side-up
# ββ Diagnostics (uncomment as needed) ββ
# TEST_EMAIL=1 # Email a live frame on next restart (one-shot)sudo systemctl enable --now rook.serviceThe engine starts automatically on boot. Logs are written to ~/rook.log.
After editing rook_engine.py locally, deploy to the Pi:
# From your Mac β uses scp to push scripts and restarts the service
bash app/deploy_to_pi.sh # defaults to rook@rook.local
bash app/deploy_to_pi.sh user@host # custom hostCaptures a single frame, runs inference, and optionally emails the annotated result. Requires the engine to be stopped (camera conflict).
source ~/rook-env/bin/activate
python3 ~/frame_test.py --email # Capture β infer β email annotated frame
python3 ~/frame_test.py --benchmark # 10-iteration inference timingSet TEST_EMAIL=1 in ~/rook-env/.env and restart the service. The engine sends a live annotated frame on startup, then resumes normal operation. Remove the flag after use.
Analyzes rook.log to produce a comprehensive report covering detection volume, alert rates, fixture suppression efficiency, ghost-motion gate performance, thermal behavior, and automated recommendations.
# Run from Mac or Pi
python3 app/rook_eval.py # reads ~/rook.log
python3 app/rook_eval.py /path/to/rook.log --json # explicit path + JSON outputRuns continuous YOLO inference for a set duration while logging SoC temperature. Useful for validating cooling solutions.
python3 app/thermal_stress_test.py # default 5-minute stress runThe engine uses YOLO26n exported to NCNN format at imgsz=1088 for ~3Γ faster inference vs. PyTorch on CPU. The export is handled automatically by setup_pi.sh, but to regenerate manually:
source ~/rook-env/bin/activate
python3 -c "from ultralytics import YOLO; YOLO('yolo26n.pt').export(format='ncnn', imgsz=1088)"
mv yolo26n_ncnn_model yolo26n_1088_ncnn_modelThe engine automatically falls back to yolo26n.pt (PyTorch) if the NCNN directory is not found.
Rook archives two categories of images on the Pi for downstream model training and review:
| Archive | Path on Pi | Content |
|---|---|---|
| Unclassified motion | ~/rook-archive/unclassified/ |
Frames where MOG2 detected motion but YOLO found nothing. Gated by a multi-frame persistence filter. |
| Beast Cam | ~/beast_cam/ |
Cropped wildlife detections (birds, animals). Auto-purged after 7 days. |
A launchd agent runs every 15 minutes, pulling new files from the Pi via scp to a local staging directory, then copying into Dropbox:
Pi β scp β ~/rook-staging/ β cp β ~/Library/CloudStorage/Dropbox/Rook/archive/
Installation:
# Install the launchd agent (replaces any prior crontab entry)
cp app/com.rook.sync.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.rook.sync.plist
# Verify
launchctl list | grep rook
# Logs
tail -f /tmp/rook_sync.logNote: The sync script lives at
~/bin/rook_sync.sh(not inside Dropbox) because macOS restrictslaunchdaccess to~/Library/CloudStorage/. If you updateapp/sync_archive.sh, copy it to~/bin/rook_sync.sh.
After syncing, run reclassify_archive.py on your Mac to re-infer unclassified frames with a larger YOLO model (YOLOv11l) using Apple MPS acceleration. Finds objects the Pi's nano model missed β especially distant park subjects.
python3 app/reclassify_archive.py # process all pending frames
python3 app/reclassify_archive.py --model x # use yolo11x.pt (maximum quality)
python3 app/reclassify_archive.py --dry-run # report without moving files
python3 app/reclassify_archive.py --slack # send Slack digest of findings# Clear old files on the Pi to free SD card space
ssh rook@rook.local "find ~/rook-archive/unclassified/ -name '*.jpg' -mtime +7 -delete"
ssh rook@rook.local "find ~/beast_cam/ -name '*.jpg' -mtime +7 -delete"
ssh rook@rook.local "df -h ~" # verify space freedThe base YOLO26n model covers 80 COCO classes but cannot distinguish site-specific objects (Amazon van vs. generic truck, trash truck vs. delivery). See rook_custom_model_proposal.md for the full pipeline:
- Data mining β Unclassified archive + Beast Cam crops provide training data
- Annotation β Roboflow or CVAT for bounding box labeling (target: 300β500 samples per class)
- Fine-tuning β Transfer learning on
yolo26n.ptwithimgsz=1088to match deployment resolution - NCNN export β Required to maintain ~150ms inference on the Pi's CPU
- Integration β Add new classes to
SCORE_MAP,EMOJI_MAP, and scene heuristics
rook-sensor/
βββ README.md # This file
βββ PRIVACY.md # Privacy policy
βββ TERMS.md # Terms of service
βββ LICENSE # MIT
βββ rook_custom_model_proposal.md # Custom YOLO training plan
β
βββ app/
β βββ rook_engine.py # Core detection engine (runs on Pi)
β βββ rook_eval.py # Log analysis & performance evaluation
β βββ rook_weather.py # Weather enrichment module
β βββ frame_test.py # Single-frame diagnostic tool
β βββ reclassify_archive.py # Mac-side re-inference with larger model
β βββ thermal_stress_test.py # SoC thermal validation
β βββ setup_pi.sh # Pi first-boot setup script
β βββ deploy_to_pi.sh # Mac β Pi deployment script
β βββ sync_archive.sh # Pi β Mac archive sync (scp-based)
β βββ com.rook.sync.plist # macOS launchd agent for sync scheduling
β
βββ assets/
β βββ rook_logo.png # Project logo
β βββ architecture.png # Pipeline architecture diagram
β
βββ device/
β βββ bom.md # Bill of materials
β
βββ docs/
βββ camera_calibration.md # IMX462 tuning notes
βββ emoji_vocabulary.md # Extended emoji reference
βββ refinements.md # Historical engineering decisions
MIT β see LICENSE.
Built by Matthew Cook

