Spaces:
Paused
Paused
| """Korview AI — Perimeter Security Demo | |
| Upload a video or use the sample to see YOLO-based intrusion detection | |
| with configurable security zones. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import tempfile | |
| from pathlib import Path | |
| import cv2 | |
| import gradio as gr | |
| import numpy as np | |
| import torch | |
| from ultralytics import YOLO | |
| # Load model once at startup, use GPU if available | |
| DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu" | |
| MODEL = YOLO("yolov8n.pt") | |
| print(f"YOLO loaded on {DEVICE} (CUDA available: {torch.cuda.is_available()})") | |
| COCO_NAMES: dict[int, str] = { | |
| 0: "person", 1: "bicycle", 2: "car", 3: "motorcycle", | |
| 5: "bus", 7: "truck", 16: "dog", 17: "horse", | |
| } | |
| THREAT_COLORS = { | |
| "CRITICAL": (0, 0, 255), | |
| "HIGH": (0, 100, 255), | |
| "MEDIUM": (0, 200, 255), | |
| "LOW": (200, 200, 0), | |
| } | |
| def point_in_polygon(px: float, py: float, polygon: list[list[float]]) -> bool: | |
| n = len(polygon) | |
| inside = False | |
| j = n - 1 | |
| for i in range(n): | |
| xi, yi = polygon[i] | |
| xj, yj = polygon[j] | |
| if ((yi > py) != (yj > py)) and (px < (xj - xi) * (py - yi) / (yj - yi) + xi): | |
| inside = not inside | |
| j = i | |
| return inside | |
| def parse_zones(zones_json: str) -> list[dict]: | |
| try: | |
| zones = json.loads(zones_json) | |
| if isinstance(zones, dict) and "zones" in zones: | |
| zones = zones["zones"] | |
| return zones | |
| except (json.JSONDecodeError, TypeError): | |
| return [] | |
| def draw_zones(frame: np.ndarray, zones: list[dict]) -> np.ndarray: | |
| h, w = frame.shape[:2] | |
| overlay = frame.copy() | |
| for zone in zones: | |
| polygon = zone.get("polygon", []) | |
| threat = zone.get("threat_level", "MEDIUM") | |
| name = zone.get("name", zone.get("zone_id", "Zone")) | |
| color = THREAT_COLORS.get(threat, (0, 200, 255)) | |
| pts = np.array([[int(p[0] * w), int(p[1] * h)] for p in polygon], np.int32) | |
| cv2.fillPoly(overlay, [pts], color) | |
| cv2.polylines(frame, [pts], True, color, 2) | |
| cv2.putText(frame, f"{name} [{threat}]", | |
| (pts[0][0], pts[0][1] - 8), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) | |
| cv2.addWeighted(overlay, 0.15, frame, 0.85, 0, frame) | |
| return frame | |
| def check_zone(cx: float, cy: float, zones: list[dict]) -> tuple[str | None, str]: | |
| best_zone = None | |
| best_threat = "INFO" | |
| threat_order = {"INFO": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4} | |
| for zone in zones: | |
| polygon = zone.get("polygon", []) | |
| threat = zone.get("threat_level", "MEDIUM") | |
| if point_in_polygon(cx, cy, polygon): | |
| if threat_order.get(threat, 0) > threat_order.get(best_threat, 0): | |
| best_zone = zone.get("name", zone.get("zone_id")) | |
| best_threat = threat | |
| return best_zone, best_threat | |
| def process_video( | |
| video_path: str, | |
| confidence: float, | |
| zones_json: str, | |
| max_seconds: float, | |
| process_every_n: int, | |
| ) -> tuple[str, str]: | |
| if not video_path: | |
| return None, "No video provided" | |
| zones = parse_zones(zones_json) | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| return None, "Failed to open video" | |
| fps = cap.get(cv2.CAP_PROP_FPS) or 30 | |
| w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| max_frames = min(int(max_seconds * fps), total_frames) | |
| # Resize if large | |
| scale = 1.0 | |
| if w > 640: | |
| scale = 640 / w | |
| w_out, h_out = 640, int(h * scale) | |
| else: | |
| w_out, h_out = w, h | |
| out_path = tempfile.mktemp(suffix=".mp4") | |
| fourcc = cv2.VideoWriter_fourcc(*"mp4v") | |
| out_fps = fps / process_every_n # output at reduced fps | |
| writer = cv2.VideoWriter(out_path, fourcc, out_fps, (w_out, h_out)) | |
| events_log = [] | |
| frame_count = 0 | |
| processed = 0 | |
| classes = [0, 1, 2, 3, 5, 7, 16, 17] | |
| last_detections = [] # reuse for skipped frames | |
| try: | |
| while cap.isOpened() and frame_count < max_frames: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| frame_count += 1 | |
| # Skip frames for speed | |
| if frame_count % process_every_n != 0: | |
| continue | |
| processed += 1 | |
| # Resize | |
| if scale < 1.0: | |
| frame = cv2.resize(frame, (w_out, h_out)) | |
| # Draw zones | |
| if zones: | |
| frame = draw_zones(frame, zones) | |
| # YOLO detection on GPU | |
| results = MODEL.track( | |
| frame, persist=True, conf=confidence, | |
| classes=classes, device=DEVICE, verbose=False, | |
| ) | |
| for result in results: | |
| if result.boxes is None: | |
| continue | |
| for box in result.boxes: | |
| x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) | |
| cls_id = int(box.cls[0]) | |
| conf = float(box.conf[0]) | |
| track_id = int(box.id[0]) if box.id is not None else None | |
| obj_name = COCO_NAMES.get(cls_id, f"class_{cls_id}") | |
| # Normalized center | |
| cx = ((x1 + x2) / 2) / w_out | |
| cy = ((y1 + y2) / 2) / h_out | |
| # Zone check | |
| zone_name, threat = check_zone(cx, cy, zones) if zones else (None, "INFO") | |
| # Draw bbox | |
| color = THREAT_COLORS.get(threat, (0, 255, 0)) | |
| label = f"{obj_name} {conf:.0%}" | |
| if track_id is not None: | |
| label += f" #{track_id}" | |
| if zone_name: | |
| label += f" @{zone_name}" | |
| cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) | |
| (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.45, 1) | |
| cv2.rectangle(frame, (x1, y1 - th - 6), (x1 + tw, y1), color, -1) | |
| cv2.putText(frame, label, (x1, y1 - 4), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1) | |
| # Log event | |
| ts = frame_count / fps | |
| if threat != "INFO": | |
| events_log.append( | |
| f"[{ts:6.1f}s] **{threat}** — {obj_name} (conf: {conf:.0%})" | |
| f"{' in ' + zone_name if zone_name else ''}" | |
| f"{' #' + str(track_id) if track_id else ''}" | |
| ) | |
| writer.write(frame) | |
| finally: | |
| cap.release() | |
| writer.release() | |
| # Summary | |
| summary = f"## Results\n\n" | |
| summary += f"Processed **{processed}** of {frame_count} frames " | |
| summary += f"({frame_count/fps:.1f}s video, every {process_every_n}{'st' if process_every_n == 1 else 'rd'} frame)\n\n" | |
| if events_log: | |
| summary += f"**{len(events_log)} security events detected:**\n\n" | |
| for line in events_log[-50:]: | |
| summary += f"- {line}\n" | |
| if len(events_log) > 50: | |
| summary += f"\n... and {len(events_log) - 50} more\n" | |
| else: | |
| summary += "No security events detected in configured zones.\n\n" | |
| summary += "*Tip: adjust zones to cover the area where objects appear, or lower the confidence threshold.*\n" | |
| return out_path, summary | |
| DEFAULT_ZONES = json.dumps([ | |
| { | |
| "zone_id": "fence_area", | |
| "name": "Fence Perimeter", | |
| "threat_level": "HIGH", | |
| "polygon": [[0.0, 0.0], [1.0, 0.0], [1.0, 0.5], [0.0, 0.5]] | |
| }, | |
| { | |
| "zone_id": "restricted", | |
| "name": "Restricted Area", | |
| "threat_level": "CRITICAL", | |
| "polygon": [[0.2, 0.5], [0.8, 0.5], [0.8, 0.95], [0.2, 0.95]] | |
| } | |
| ], indent=2) | |
| with gr.Blocks( | |
| title="Korview AI — Perimeter Security", | |
| theme=gr.themes.Base(primary_hue="red", neutral_hue="slate"), | |
| ) as demo: | |
| gr.Markdown( | |
| "# Korview AI — Perimeter Security Detection\n" | |
| "Upload a security camera video to detect intrusions with YOLO. " | |
| "Configure security zones to classify threats." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| video_input = gr.Video(label="Upload Security Camera Video", sources=["upload"]) | |
| confidence = gr.Slider( | |
| minimum=0.1, maximum=0.9, value=0.4, step=0.05, | |
| label="Detection Confidence", | |
| ) | |
| max_seconds = gr.Slider( | |
| minimum=5, maximum=120, value=30, step=5, | |
| label="Max Video Duration (seconds)", | |
| info="T4 GPU: ~300 fps. 30s video ≈ 3s processing.", | |
| ) | |
| process_every_n = gr.Slider( | |
| minimum=1, maximum=10, value=1, step=1, | |
| label="Process Every Nth Frame", | |
| info="1 = every frame (GPU). Increase on CPU for speed.", | |
| ) | |
| zones_input = gr.Code( | |
| value=DEFAULT_ZONES, | |
| language="json", | |
| label="Security Zones (JSON — normalized 0.0-1.0 coords)", | |
| lines=12, | |
| ) | |
| run_btn = gr.Button("Run Detection", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| video_output = gr.Video(label="Detection Results") | |
| events_output = gr.Markdown(label="Security Events") | |
| run_btn.click( | |
| fn=process_video, | |
| inputs=[video_input, confidence, zones_input, max_seconds, process_every_n], | |
| outputs=[video_output, events_output], | |
| ) | |
| demo.launch(ssr_mode=False) | |