|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import streamlit as st |
|
|
import cv2 |
|
|
import time |
|
|
import json |
|
|
from pathlib import Path |
|
|
from datetime import datetime |
|
|
from concurrent.futures import ThreadPoolExecutor |
|
|
|
|
|
from core.video import VideoSource |
|
|
from core.detector import CVDetector |
|
|
from core.tracker import SimpleTracker |
|
|
from core.context import FrameContext |
|
|
|
|
|
from anomalies.registry import build_registry, load_anomaly_meta |
|
|
from storage.local_store import ( |
|
|
save_events, save_attendance, save_tickets, save_camera_metrics, |
|
|
load_events, load_attendance, load_tickets |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="CVIP2 – Computer Vision Intelligence Platform", |
|
|
layout="wide" |
|
|
) |
|
|
|
|
|
DATA_DIR = Path("data") |
|
|
DATA_DIR.mkdir(exist_ok=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ANOM_META = load_anomaly_meta("config/anomalies.json") |
|
|
INDUSTRIES = json.load(open("config/industries.json", "r", encoding="utf-8")) |
|
|
ANOM_REGISTRY = build_registry() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_roi(txt): |
|
|
try: |
|
|
if not txt: |
|
|
return None |
|
|
x1, y1, x2, y2 = map(int, txt.split(",")) |
|
|
return (x1, y1, x2, y2) |
|
|
except Exception: |
|
|
return None |
|
|
|
|
|
def now_iso(): |
|
|
return datetime.utcnow().isoformat() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.sidebar.title("CVIP2 Configuration") |
|
|
|
|
|
industry_key = st.sidebar.selectbox( |
|
|
"Industry Segment", |
|
|
list(INDUSTRIES.keys()), |
|
|
format_func=lambda k: INDUSTRIES[k]["label"] |
|
|
) |
|
|
|
|
|
industry_cfg = INDUSTRIES[industry_key] |
|
|
default_anoms = industry_cfg["default_anomalies"] |
|
|
|
|
|
st.sidebar.subheader("Anomaly Selection (Cross-Industry Allowed)") |
|
|
selected_anomalies = st.sidebar.multiselect( |
|
|
"Enable Anomalies", |
|
|
list(ANOM_META["anomalies"].keys()), |
|
|
default=default_anoms, |
|
|
format_func=lambda a: ANOM_META["anomalies"][a]["label"] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.sidebar.subheader("Thresholds / Parameters") |
|
|
|
|
|
idle_emit_sec = st.sidebar.slider("Idle emit sec", 2, 60, 10) |
|
|
sleep_idle_sec = st.sidebar.slider("Sleep idle sec", 30, 600, 120) |
|
|
|
|
|
phone_long_sec = st.sidebar.slider("Phone call max sec", 10, 3600, 180) |
|
|
break_limit_sec = st.sidebar.slider("Break limit sec", 30, 3600, 600) |
|
|
desk_absent_sec = st.sidebar.slider("Desk absent sec", 10, 3600, 120) |
|
|
|
|
|
loiter_sec = st.sidebar.slider("Loiter window sec", 10, 600, 120) |
|
|
crowd_threshold = st.sidebar.slider("Crowd threshold", 2, 50, 10) |
|
|
queue_threshold = st.sidebar.slider("Queue threshold", 2, 30, 6) |
|
|
|
|
|
entry_count_threshold = st.sidebar.slider("Zone entry count", 1, 100, 10) |
|
|
entry_window_sec = st.sidebar.slider("Entry window sec", 10, 3600, 300) |
|
|
|
|
|
meeting_capacity = st.sidebar.slider("Meeting room capacity", 1, 50, 6) |
|
|
guard_patrol_interval_sec = st.sidebar.slider("Guard patrol interval sec", 60, 7200, 900) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.sidebar.subheader("ROI Configuration (x1,y1,x2,y2)") |
|
|
|
|
|
zone_roi_txt = st.sidebar.text_input("Work Zone ROI", "") |
|
|
break_roi_txt = st.sidebar.text_input("Break Area ROI", "") |
|
|
desk_roi_txt = st.sidebar.text_input("Desk ROI", "") |
|
|
entry_roi_txt = st.sidebar.text_input("Entry ROI", "") |
|
|
perimeter_roi_txt = st.sidebar.text_input("Perimeter ROI", "") |
|
|
meeting_roi_txt = st.sidebar.text_input("Meeting Room ROI", "") |
|
|
guard_roi_txt = st.sidebar.text_input("Guard Patrol ROI", "") |
|
|
queue_roi_txt = st.sidebar.text_input("Queue ROI", "") |
|
|
qr_roi_txt = st.sidebar.text_input("QR ROI", "") |
|
|
theft_roi_txt = st.sidebar.text_input("Theft ROI", "") |
|
|
sla_roi_txt = st.sidebar.text_input("Housekeeping SLA ROI", "") |
|
|
|
|
|
ROIS = { |
|
|
"zone_roi": parse_roi(zone_roi_txt), |
|
|
"break_roi": parse_roi(break_roi_txt), |
|
|
"desk_roi": parse_roi(desk_roi_txt), |
|
|
"entry_roi": parse_roi(entry_roi_txt), |
|
|
"perimeter_roi": parse_roi(perimeter_roi_txt), |
|
|
"meeting_roi": parse_roi(meeting_roi_txt), |
|
|
"guard_roi": parse_roi(guard_roi_txt), |
|
|
"queue_roi": parse_roi(queue_roi_txt), |
|
|
"qr_roi": parse_roi(qr_roi_txt), |
|
|
"theft_roi": parse_roi(theft_roi_txt), |
|
|
"sla_roi": parse_roi(sla_roi_txt), |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("Multi-Camera Inputs") |
|
|
|
|
|
cam_sources = st.text_area( |
|
|
"Camera Sources (one per line: RTSP / file path / webcam index)", |
|
|
value="0" |
|
|
).splitlines() |
|
|
|
|
|
use_parallel = st.checkbox("Run cameras in parallel (Local only)", value=False) |
|
|
max_workers = st.slider("Max camera workers", 1, 16, 4) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
runtime_cfg = { |
|
|
"industry": industry_key, |
|
|
"anomaly_meta": ANOM_META["anomalies"], |
|
|
"enabled_anomalies": set(selected_anomalies), |
|
|
"rois": ROIS, |
|
|
|
|
|
|
|
|
"idle_emit_sec": idle_emit_sec, |
|
|
"sleep_idle_sec": sleep_idle_sec, |
|
|
"phone_long_sec": phone_long_sec, |
|
|
"break_limit_sec": break_limit_sec, |
|
|
"desk_absent_sec": desk_absent_sec, |
|
|
"loiter_sec": loiter_sec, |
|
|
"crowd_threshold": crowd_threshold, |
|
|
"queue_threshold": queue_threshold, |
|
|
"entry_count_threshold": entry_count_threshold, |
|
|
"entry_window_sec": entry_window_sec, |
|
|
"meeting_capacity": meeting_capacity, |
|
|
"guard_patrol_interval_sec": guard_patrol_interval_sec, |
|
|
|
|
|
|
|
|
"idle_motion_px": 3.0, |
|
|
"phone_overlap_min": 0.1, |
|
|
"theft_diff_thr": 25, |
|
|
"theft_change_ratio": 0.08, |
|
|
"theft_baseline_alpha": 0.05, |
|
|
"freeze_diff_mean": 1.5, |
|
|
"freeze_frames": 30, |
|
|
"tamper_low_contrast": 8.0, |
|
|
"tamper_dark_mean": 15.0, |
|
|
"tamper_bright_mean": 240.0, |
|
|
"tamper_frames": 20, |
|
|
"sla_seconds": 900, |
|
|
|
|
|
|
|
|
"working_start": 9, |
|
|
"working_end": 18, |
|
|
"after_hours_enabled": True, |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_camera(camera_id, source): |
|
|
detector = CVDetector() |
|
|
tracker = SimpleTracker() |
|
|
vs = VideoSource(source) |
|
|
|
|
|
frame_count = 0 |
|
|
event_count = 0 |
|
|
|
|
|
for frame in vs.frames(): |
|
|
frame_count += 1 |
|
|
detections = detector.detect(frame) |
|
|
tracks = tracker.update(detections) |
|
|
|
|
|
ctx = FrameContext( |
|
|
frame=frame, |
|
|
detections=detections, |
|
|
tracks=tracks, |
|
|
camera_id=str(camera_id), |
|
|
zone="default", |
|
|
fps=vs.fps, |
|
|
face_identities=detector.face_identities, |
|
|
now_ts=time.time() |
|
|
) |
|
|
|
|
|
events = [] |
|
|
for aid in runtime_cfg["enabled_anomalies"]: |
|
|
anomaly = ANOM_REGISTRY.get(aid) |
|
|
if anomaly: |
|
|
events.extend(anomaly.evaluate(ctx, runtime_cfg)) |
|
|
|
|
|
if events: |
|
|
save_events([e.__dict__ for e in events]) |
|
|
event_count += len(events) |
|
|
|
|
|
save_camera_metrics([{ |
|
|
"ts": now_iso(), |
|
|
"camera_id": str(camera_id), |
|
|
"frames_processed": frame_count, |
|
|
"events": event_count, |
|
|
"attendance_changes": 0, |
|
|
"tickets": 0, |
|
|
"industry": industry_key |
|
|
}]) |
|
|
|
|
|
vs.release() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if st.button("▶ Start CVIP2 Monitoring"): |
|
|
st.success("Monitoring started") |
|
|
if use_parallel and len(cam_sources) > 1: |
|
|
with ThreadPoolExecutor(max_workers=max_workers) as ex: |
|
|
for idx, src in enumerate(cam_sources): |
|
|
ex.submit(run_camera, idx, src.strip()) |
|
|
else: |
|
|
for idx, src in enumerate(cam_sources): |
|
|
run_camera(idx, src.strip()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("📊 Dashboards") |
|
|
|
|
|
tab1, tab2, tab3 = st.tabs(["Events", "Attendance", "Tickets"]) |
|
|
|
|
|
with tab1: |
|
|
df = load_events() |
|
|
st.dataframe(df, use_container_width=True) |
|
|
|
|
|
with tab2: |
|
|
df = load_attendance() |
|
|
st.dataframe(df, use_container_width=True) |
|
|
|
|
|
with tab3: |
|
|
df = load_tickets() |
|
|
st.dataframe(df, use_container_width=True) |
|
|
|