| """ |
| Rule-based anomaly detection layer applied on top of YOLO detections. |
| Flags unusual behaviours: wrong zone entry, repeated appearances, overspeeding (future). |
| """ |
|
|
| from collections import defaultdict |
| from datetime import datetime, timedelta |
|
|
| |
| _plate_history: dict[str, list[datetime]] = defaultdict(list) |
| REPEAT_WINDOW_MINUTES = 10 |
| REPEAT_COUNT_THRESHOLD = 5 |
|
|
| |
| RESTRICTED_HEAVY_ZONES = {"VIP_LOT", "COMPACT_ONLY"} |
|
|
|
|
| def analyse(plate: str, vehicle_class: str, zone: str) -> tuple[str, str]: |
| """ |
| Evaluate one detection event and return (status, notes). |
| |
| status: "normal" | "flagged" | "unauthorized" | "anomaly" |
| """ |
| now = datetime.now() |
|
|
| |
| cutoff = now - timedelta(minutes=REPEAT_WINDOW_MINUTES) |
| _plate_history[plate] = [t for t in _plate_history[plate] if t > cutoff] |
|
|
| |
| _plate_history[plate].append(now) |
|
|
| notes_parts = [] |
| status = "normal" |
|
|
| |
| if vehicle_class in ("bus", "truck") and zone.upper() in RESTRICTED_HEAVY_ZONES: |
| status = "unauthorized" |
| notes_parts.append(f"Heavy vehicle ({vehicle_class}) in restricted zone {zone}") |
|
|
| |
| count = len(_plate_history[plate]) |
| if count > REPEAT_COUNT_THRESHOLD: |
| status = "anomaly" |
| notes_parts.append(f"Plate {plate} seen {count}x in last {REPEAT_WINDOW_MINUTES} min") |
|
|
| |
| if not plate or plate == "UNKNOWN": |
| status = "flagged" |
| notes_parts.append("Unreadable plate — manual review required") |
|
|
| return status, "; ".join(notes_parts) if notes_parts else "" |
|
|