# ========================================================== # CVIP2 — Computer Vision Intelligence Platform # Full merged application entrypoint # ========================================================== 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 ) # ========================================================== # App Config # ========================================================== st.set_page_config( page_title="CVIP2 – Computer Vision Intelligence Platform", layout="wide" ) DATA_DIR = Path("data") DATA_DIR.mkdir(exist_ok=True) # ========================================================== # Load configs # ========================================================== ANOM_META = load_anomaly_meta("config/anomalies.json") INDUSTRIES = json.load(open("config/industries.json", "r", encoding="utf-8")) ANOM_REGISTRY = build_registry() # ========================================================== # Helpers # ========================================================== 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() # ========================================================== # Sidebar – Industry & Anomaly Selection # ========================================================== 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"] ) # ========================================================== # Thresholds & Parameters # ========================================================== 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) # ========================================================== # ROI Configuration # ========================================================== 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), } # ========================================================== # Camera Inputs # ========================================================== 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 Configuration # ========================================================== runtime_cfg = { "industry": industry_key, "anomaly_meta": ANOM_META["anomalies"], "enabled_anomalies": set(selected_anomalies), "rois": ROIS, # thresholds "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, # constants "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 hours "working_start": 9, "working_end": 18, "after_hours_enabled": True, } # ========================================================== # Camera Worker # ========================================================== 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() # ========================================================== # Run Button # ========================================================== 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()) # ========================================================== # Dashboards # ========================================================== 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)