Add TFLite object detection to reduce false positives

Motion detection now optionally runs MobileNet V2 SSD (COCO, quantized)
on frames that trigger motion, identifying objects like people, cats, and
cars. Events without detected objects are suppressed by default. Snapshots
include bounding box annotations. New MCP tool vision_get_detections()
enables label-based queries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex
2026-02-08 17:04:10 -06:00
parent 68c7e9772f
commit e1171e8ff8
11 changed files with 687 additions and 50 deletions

View File

@@ -13,6 +13,7 @@ Runs as a service on Mac mini, listens for POSTs from Pis.
"""
import os
import json
import sqlite3
import base64
import logging
@@ -20,7 +21,7 @@ import threading
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional
from typing import Optional, List
from contextlib import contextmanager
from fastapi import FastAPI, HTTPException, Header
@@ -87,6 +88,16 @@ def init_db():
logger.info(f"Database initialized: {DB_PATH}")
def migrate_db():
"""Add new columns if they don't exist (idempotent)"""
with get_db() as conn:
columns = [row[1] for row in conn.execute("PRAGMA table_info(events)").fetchall()]
if "detections" not in columns:
conn.execute("ALTER TABLE events ADD COLUMN detections TEXT")
conn.commit()
logger.info("Migration: added 'detections' column to events table")
@contextmanager
def get_db():
"""Database connection context manager"""
@@ -171,6 +182,12 @@ def stop_cleanup_thread():
# === Models ===
class DetectionItem(BaseModel):
label: str
confidence: float
bbox: List[float]
class EventData(BaseModel):
timestamp: str
camera_id: str
@@ -178,6 +195,7 @@ class EventData(BaseModel):
confidence: float = 0.0
region: str = "full"
area_percent: float = 0.0
detections: Optional[List[DetectionItem]] = None
class IncomingEvent(BaseModel):
@@ -190,6 +208,7 @@ class IncomingEvent(BaseModel):
@app.on_event("startup")
def startup():
init_db()
migrate_db()
start_cleanup_thread()
logger.info(f"🦊 Event collector started on port {PORT}")
logger.info(f" Data directory: {DATA_DIR}")
@@ -257,14 +276,19 @@ def receive_event(
except Exception as e:
logger.error(f"Failed to save snapshot: {e}")
# Serialize detections to JSON if present
detections_json = None
if event.detections:
detections_json = json.dumps([d.model_dump() for d in event.detections])
# Store in database
try:
with get_db() as conn:
conn.execute("""
INSERT INTO events
(event_id, timestamp, camera_id, event_type, confidence,
area_percent, snapshot_path, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO events
(event_id, timestamp, camera_id, event_type, confidence,
area_percent, snapshot_path, detections, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
event_id,
event.timestamp,
@@ -273,6 +297,7 @@ def receive_event(
event.confidence,
event.area_percent,
snapshot_path,
detections_json,
now.isoformat() + "Z"
))
conn.commit()
@@ -357,10 +382,15 @@ def get_stats():
FROM events GROUP BY event_type
""").fetchall()
with_detections = conn.execute(
"SELECT COUNT(*) FROM events WHERE detections IS NOT NULL"
).fetchone()[0]
return {
"total_events": total,
"annotated": annotated,
"unannotated": total - annotated,
"with_detections": with_detections,
"by_camera": {row[0]: row[1] for row in by_camera},
"by_type": {row[0]: row[1] for row in by_type},
"data_dir": str(DATA_DIR),