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:
@@ -518,7 +518,7 @@ def vision_get_events(
|
||||
|
||||
events = []
|
||||
for row in rows:
|
||||
events.append({
|
||||
event_dict = {
|
||||
"id": row["id"],
|
||||
"event_id": row["event_id"],
|
||||
"timestamp": row["timestamp"],
|
||||
@@ -529,7 +529,14 @@ def vision_get_events(
|
||||
"snapshot_path": row["snapshot_path"],
|
||||
"annotation": row["annotation"],
|
||||
"tags": row["tags"],
|
||||
})
|
||||
}
|
||||
# Include detections if present
|
||||
try:
|
||||
det_raw = row["detections"]
|
||||
event_dict["detections"] = json.loads(det_raw) if det_raw else None
|
||||
except (KeyError, json.JSONDecodeError, TypeError):
|
||||
event_dict["detections"] = None
|
||||
events.append(event_dict)
|
||||
|
||||
logger.info(f"Retrieved {len(events)} events")
|
||||
return events
|
||||
@@ -541,6 +548,99 @@ def vision_get_events(
|
||||
conn.close()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def vision_get_detections(
|
||||
label: str = None,
|
||||
camera_id: str = None,
|
||||
since: str = None,
|
||||
min_confidence: float = 0.0,
|
||||
limit: int = 20
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Query events that contain specific object detections.
|
||||
|
||||
Filters events to only those where the AI detected objects
|
||||
(person, cat, dog, car, etc.). More targeted than raw motion events.
|
||||
|
||||
Args:
|
||||
label: Filter by detected object type (e.g., "person", "cat", "dog")
|
||||
camera_id: Filter by camera
|
||||
since: ISO timestamp - only events after this time
|
||||
min_confidence: Minimum detection confidence (0.0-1.0)
|
||||
limit: Maximum events to return (default 20)
|
||||
|
||||
Returns:
|
||||
List of events with their detections
|
||||
|
||||
Examples:
|
||||
vision_get_detections(label="cat")
|
||||
vision_get_detections(label="person", camera_id="basement")
|
||||
vision_get_detections(min_confidence=0.8)
|
||||
"""
|
||||
conn = get_events_db()
|
||||
if not conn:
|
||||
return [{"error": f"Events database not found: {EVENTS_DB}"}]
|
||||
|
||||
try:
|
||||
query = "SELECT * FROM events WHERE detections IS NOT NULL"
|
||||
params = []
|
||||
|
||||
if since:
|
||||
query += " AND timestamp >= ?"
|
||||
params.append(since)
|
||||
|
||||
if camera_id:
|
||||
query += " AND camera_id = ?"
|
||||
params.append(camera_id)
|
||||
|
||||
# Fetch more than limit to allow for client-side filtering
|
||||
query += " ORDER BY timestamp DESC LIMIT ?"
|
||||
params.append(limit * 5)
|
||||
|
||||
rows = conn.execute(query, params).fetchall()
|
||||
|
||||
events = []
|
||||
for row in rows:
|
||||
try:
|
||||
dets = json.loads(row["detections"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
continue
|
||||
|
||||
# Filter by label and confidence
|
||||
if label or min_confidence > 0:
|
||||
matching = [
|
||||
d for d in dets
|
||||
if (not label or d.get("label") == label)
|
||||
and d.get("confidence", 0) >= min_confidence
|
||||
]
|
||||
if not matching:
|
||||
continue
|
||||
else:
|
||||
matching = dets
|
||||
|
||||
events.append({
|
||||
"event_id": row["event_id"],
|
||||
"timestamp": row["timestamp"],
|
||||
"camera_id": row["camera_id"],
|
||||
"confidence": row["confidence"],
|
||||
"annotation": row["annotation"],
|
||||
"tags": row["tags"],
|
||||
"detections": matching,
|
||||
})
|
||||
|
||||
if len(events) >= limit:
|
||||
break
|
||||
|
||||
logger.info(f"Retrieved {len(events)} detection events")
|
||||
return events
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error querying detections: {e}")
|
||||
return [{"error": str(e)}]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def vision_get_event_snapshot(event_id: str) -> Union[MCPImage, str]:
|
||||
"""
|
||||
@@ -702,9 +802,36 @@ def vision_event_stats() -> Dict[str, Any]:
|
||||
WHERE timestamp >= datetime('now', '-1 day')
|
||||
""").fetchone()[0]
|
||||
|
||||
# Detection stats
|
||||
try:
|
||||
with_detections = conn.execute(
|
||||
"SELECT COUNT(*) FROM events WHERE detections IS NOT NULL"
|
||||
).fetchone()[0]
|
||||
stats["with_detections"] = with_detections
|
||||
|
||||
if with_detections > 0:
|
||||
det_rows = conn.execute(
|
||||
"SELECT detections FROM events WHERE detections IS NOT NULL"
|
||||
).fetchall()
|
||||
label_counts = {}
|
||||
for det_row in det_rows:
|
||||
try:
|
||||
dets = json.loads(det_row[0])
|
||||
for d in dets:
|
||||
lbl = d.get("label", "unknown")
|
||||
label_counts[lbl] = label_counts.get(lbl, 0) + 1
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
if label_counts:
|
||||
stats["detected_objects"] = dict(
|
||||
sorted(label_counts.items(), key=lambda x: -x[1])
|
||||
)
|
||||
except Exception:
|
||||
pass # Column may not exist on older databases
|
||||
|
||||
# Most recent event
|
||||
row = conn.execute("""
|
||||
SELECT event_id, timestamp, camera_id
|
||||
SELECT event_id, timestamp, camera_id
|
||||
FROM events ORDER BY timestamp DESC LIMIT 1
|
||||
""").fetchone()
|
||||
if row:
|
||||
|
||||
Reference in New Issue
Block a user