Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -1,181 +1,144 @@
|
|
| 1 |
-
import os
|
| 2 |
import cv2
|
| 3 |
import numpy as np
|
| 4 |
import gradio as gr
|
| 5 |
from ultralytics import YOLO
|
| 6 |
|
| 7 |
-
# -------------------------
|
| 8 |
-
# Config
|
| 9 |
-
# ----------------------------
|
| 10 |
-
MODEL_NAME = os.getenv("YOLO_MODEL", "yolov8n.pt")
|
| 11 |
-
CLASS_OF_INTEREST = "person"
|
| 12 |
-
|
| 13 |
-
# Danger zone: top-left, bottom-right (x, y)
|
| 14 |
-
DANGER_ZONE = ((100, 100), (400, 400))
|
| 15 |
-
|
| 16 |
-
# Inference config
|
| 17 |
-
CONF_THRES = 0.35
|
| 18 |
-
IMG_SIZE = 640
|
| 19 |
-
|
| 20 |
-
# ----------------------------
|
| 21 |
# Load model once (global)
|
| 22 |
-
# -------------------------
|
|
|
|
| 23 |
model = YOLO(MODEL_NAME)
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
if PERSON_CLASS_ID is None:
|
| 35 |
-
raise RuntimeError("Could not find 'person' class in model.names")
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# ----------------------------
|
| 39 |
-
# Helpers
|
| 40 |
-
# ----------------------------
|
| 41 |
-
def overlaps_zone(box_xyxy, zone):
|
| 42 |
-
"""True if box overlaps danger zone (partial overlap)."""
|
| 43 |
-
x1, y1, x2, y2 = box_xyxy
|
| 44 |
(zx1, zy1), (zx2, zy2) = zone
|
|
|
|
| 45 |
overlap_x = (x1 < zx2) and (x2 > zx1)
|
| 46 |
overlap_y = (y1 < zy2) and (y2 > zy1)
|
| 47 |
return overlap_x and overlap_y
|
| 48 |
|
| 49 |
-
|
| 50 |
-
def make_beep(sr=22050, freq=880, duration=0.25):
|
| 51 |
-
"""Return a short beep waveform for browser playback."""
|
| 52 |
-
t = np.linspace(0, duration, int(sr * duration), endpoint=False)
|
| 53 |
-
wave = 0.2 * np.sin(2 * np.pi * freq * t) # low volume
|
| 54 |
-
return (sr, wave.astype(np.float32))
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
BEEP_AUDIO = make_beep()
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
# ----------------------------
|
| 61 |
-
# Frame processor
|
| 62 |
-
# ----------------------------
|
| 63 |
-
def process_frame(frame, zone_x1, zone_y1, zone_x2, zone_y2, conf_thres):
|
| 64 |
"""
|
| 65 |
-
frame: numpy array
|
| 66 |
-
returns:
|
| 67 |
-
- annotated RGB frame
|
| 68 |
-
- grayscale RGB frame
|
| 69 |
-
- infrared frame (RGB)
|
| 70 |
-
- beep audio tuple or None
|
| 71 |
-
- status text
|
| 72 |
"""
|
| 73 |
if frame is None:
|
| 74 |
-
return None,
|
| 75 |
|
| 76 |
-
# Gradio gives RGB; OpenCV
|
| 77 |
-
|
| 78 |
-
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
|
| 87 |
-
|
| 88 |
-
for img in (bgr, gray_bgr, infrared):
|
| 89 |
-
cv2.rectangle(img, zone[0], zone[1], (0, 0, 255), 2)
|
| 90 |
-
|
| 91 |
-
# YOLO inference (stream=False for single image)
|
| 92 |
-
# verbose=False keeps logs clean
|
| 93 |
-
results = model.predict(
|
| 94 |
-
source=bgr,
|
| 95 |
-
imgsz=IMG_SIZE,
|
| 96 |
-
conf=float(conf_thres),
|
| 97 |
-
verbose=False
|
| 98 |
-
)
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
boxes = r.boxes.xyxy.cpu().numpy().astype(int)
|
| 106 |
-
cls_ids = r.boxes.cls.cpu().numpy().astype(int)
|
| 107 |
-
confs = r.boxes.conf.cpu().numpy()
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
cv2.putText(img, label, (x1, max(15, y1 - 8)),
|
| 120 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
else:
|
| 132 |
-
status = f"✅
|
| 133 |
-
beep = None
|
| 134 |
|
| 135 |
-
# Convert back to RGB for
|
| 136 |
out_rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
| 137 |
-
|
| 138 |
-
out_infra = cv2.cvtColor(infrared, cv2.COLOR_BGR2RGB)
|
| 139 |
-
|
| 140 |
-
return out_rgb, out_gray, out_infra, beep, status
|
| 141 |
|
| 142 |
|
| 143 |
-
|
| 144 |
-
# Gradio UI
|
| 145 |
-
# ----------------------------
|
| 146 |
-
with gr.Blocks(title="YOLOv8 Danger Zone Demo") as demo:
|
| 147 |
gr.Markdown(
|
| 148 |
-
""
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
- Detects **person** and triggers **alert** if they overlap the danger zone
|
| 152 |
-
"""
|
| 153 |
)
|
| 154 |
|
| 155 |
with gr.Row():
|
| 156 |
-
cam = gr.Image(sources=["webcam"],streaming=True,type="numpy",label="Webcam (Input)")
|
| 157 |
with gr.Column():
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
cam.stream(
|
| 174 |
fn=process_frame,
|
| 175 |
-
inputs=[cam,
|
| 176 |
-
outputs=[
|
| 177 |
-
|
| 178 |
)
|
| 179 |
|
| 180 |
-
demo.queue().launch()
|
| 181 |
-
|
|
|
|
|
|
|
| 1 |
import cv2
|
| 2 |
import numpy as np
|
| 3 |
import gradio as gr
|
| 4 |
from ultralytics import YOLO
|
| 5 |
|
| 6 |
+
# -------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
# Load model once (global)
|
| 8 |
+
# -------------------------
|
| 9 |
+
MODEL_NAME = "yolov8n.pt"
|
| 10 |
model = YOLO(MODEL_NAME)
|
| 11 |
|
| 12 |
+
CLASS_OF_INTEREST = "person"
|
| 13 |
+
|
| 14 |
+
def is_in_danger_zone(box, zone):
|
| 15 |
+
"""
|
| 16 |
+
box: (x1, y1, x2, y2)
|
| 17 |
+
zone: ((zx1, zy1), (zx2, zy2))
|
| 18 |
+
overlap logic: any partial overlap triggers True
|
| 19 |
+
"""
|
| 20 |
+
x1, y1, x2, y2 = box
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
(zx1, zy1), (zx2, zy2) = zone
|
| 22 |
+
|
| 23 |
overlap_x = (x1 < zx2) and (x2 > zx1)
|
| 24 |
overlap_y = (y1 < zy2) and (y2 > zy1)
|
| 25 |
return overlap_x and overlap_y
|
| 26 |
|
| 27 |
+
def process_frame(frame, zx1, zy1, zx2, zy2, conf_thres):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
"""
|
| 29 |
+
frame: numpy array (H, W, 3) from Gradio webcam (RGB)
|
| 30 |
+
returns: annotated frame (RGB), status markdown
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"""
|
| 32 |
if frame is None:
|
| 33 |
+
return None, "Waiting for webcam input…"
|
| 34 |
|
| 35 |
+
# Gradio gives RGB; OpenCV drawing expects BGR
|
| 36 |
+
bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
| 37 |
+
h, w = bgr.shape[:2]
|
| 38 |
|
| 39 |
+
# Clamp and fix zone coordinates
|
| 40 |
+
zx1 = int(np.clip(zx1, 0, w - 1))
|
| 41 |
+
zx2 = int(np.clip(zx2, 0, w - 1))
|
| 42 |
+
zy1 = int(np.clip(zy1, 0, h - 1))
|
| 43 |
+
zy2 = int(np.clip(zy2, 0, h - 1))
|
| 44 |
|
| 45 |
+
if zx2 < zx1:
|
| 46 |
+
zx1, zx2 = zx2, zx1
|
| 47 |
+
if zy2 < zy1:
|
| 48 |
+
zy1, zy2 = zy2, zy1
|
| 49 |
|
| 50 |
+
danger_zone = ((zx1, zy1), (zx2, zy2))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
# Draw danger zone
|
| 53 |
+
cv2.rectangle(bgr, danger_zone[0], danger_zone[1], (0, 0, 255), 2)
|
| 54 |
|
| 55 |
+
# Run YOLO (on original RGB frame or BGR — ultralytics handles numpy arrays)
|
| 56 |
+
results = model.predict(source=frame, conf=float(conf_thres), verbose=False)
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
alert_triggered = False
|
| 59 |
+
persons_in_zone = 0
|
| 60 |
+
persons_total = 0
|
| 61 |
|
| 62 |
+
for r in results:
|
| 63 |
+
names = r.names
|
| 64 |
+
if r.boxes is None:
|
| 65 |
+
continue
|
| 66 |
|
| 67 |
+
boxes_xyxy = r.boxes.xyxy.cpu().numpy() if hasattr(r.boxes.xyxy, "cpu") else np.array(r.boxes.xyxy)
|
| 68 |
+
cls_ids = r.boxes.cls.cpu().numpy() if hasattr(r.boxes.cls, "cpu") else np.array(r.boxes.cls)
|
| 69 |
+
confs = r.boxes.conf.cpu().numpy() if hasattr(r.boxes.conf, "cpu") else np.array(r.boxes.conf)
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
for (x1, y1, x2, y2), cls_id, cf in zip(boxes_xyxy, cls_ids, confs):
|
| 72 |
+
class_name = names[int(cls_id)]
|
| 73 |
+
if class_name != CLASS_OF_INTEREST:
|
| 74 |
+
continue
|
| 75 |
|
| 76 |
+
persons_total += 1
|
| 77 |
+
|
| 78 |
+
x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])
|
| 79 |
+
|
| 80 |
+
# Draw person bbox
|
| 81 |
+
cv2.rectangle(bgr, (x1, y1), (x2, y2), (255, 0, 0), 2)
|
| 82 |
+
label = f"{class_name}: {float(cf):.2f}"
|
| 83 |
+
cv2.putText(bgr, label, (x1, max(20, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
|
| 84 |
+
|
| 85 |
+
# Zone overlap check
|
| 86 |
+
if is_in_danger_zone((x1, y1, x2, y2), danger_zone):
|
| 87 |
+
alert_triggered = True
|
| 88 |
+
persons_in_zone += 1
|
| 89 |
+
|
| 90 |
+
if alert_triggered:
|
| 91 |
+
cv2.putText(
|
| 92 |
+
bgr,
|
| 93 |
+
f"ALERT! {persons_in_zone} person(s) in danger zone",
|
| 94 |
+
(20, 40),
|
| 95 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 96 |
+
1.0,
|
| 97 |
+
(0, 0, 255),
|
| 98 |
+
3
|
| 99 |
+
)
|
| 100 |
+
status = f"## 🔴 ALERT\n**{persons_in_zone}** person(s) inside danger zone.\n\nTotal persons detected: **{persons_total}**"
|
| 101 |
else:
|
| 102 |
+
status = f"## ✅ SAFE\nNo person inside danger zone.\n\nTotal persons detected: **{persons_total}**"
|
|
|
|
| 103 |
|
| 104 |
+
# Convert back to RGB for Gradio output
|
| 105 |
out_rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
| 106 |
+
return out_rgb, status
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
+
with gr.Blocks(title="YOLOv8 Danger Zone (Webcam)") as demo:
|
|
|
|
|
|
|
|
|
|
| 110 |
gr.Markdown(
|
| 111 |
+
"# YOLOv8 Danger Zone Detection (Webcam)\n"
|
| 112 |
+
"Use your webcam, define a rectangular danger zone, and detect if any **person** enters it.\n\n"
|
| 113 |
+
"**Note:** On Hugging Face Spaces, server-side audio (pygame) isn’t reliable. We show a clear on-screen alert instead."
|
|
|
|
|
|
|
| 114 |
)
|
| 115 |
|
| 116 |
with gr.Row():
|
|
|
|
| 117 |
with gr.Column():
|
| 118 |
+
cam = gr.Image(
|
| 119 |
+
label="Webcam Input",
|
| 120 |
+
sources=["webcam"],
|
| 121 |
+
type="numpy"
|
| 122 |
+
)
|
| 123 |
+
with gr.Column():
|
| 124 |
+
out = gr.Image(label="Annotated Output", type="numpy")
|
| 125 |
+
status_md = gr.Markdown("Waiting for webcam input…")
|
| 126 |
+
|
| 127 |
+
with gr.Accordion("Danger Zone Controls", open=True):
|
| 128 |
+
with gr.Row():
|
| 129 |
+
zx1 = gr.Slider(0, 1280, value=100, step=1, label="Zone X1 (left)")
|
| 130 |
+
zy1 = gr.Slider(0, 720, value=100, step=1, label="Zone Y1 (top)")
|
| 131 |
+
with gr.Row():
|
| 132 |
+
zx2 = gr.Slider(0, 1280, value=400, step=1, label="Zone X2 (right)")
|
| 133 |
+
zy2 = gr.Slider(0, 720, value=400, step=1, label="Zone Y2 (bottom)")
|
| 134 |
+
conf = gr.Slider(0.1, 0.9, value=0.35, step=0.05, label="Confidence Threshold")
|
| 135 |
+
|
| 136 |
+
# Stream webcam frames to backend (Gradio 5 streaming)
|
| 137 |
cam.stream(
|
| 138 |
fn=process_frame,
|
| 139 |
+
inputs=[cam, zx1, zy1, zx2, zy2, conf],
|
| 140 |
+
outputs=[out, status_md],
|
| 141 |
+
stream_every=0.1 # approx 10 fps snapshots (depends on device/network)
|
| 142 |
)
|
| 143 |
|
| 144 |
+
demo.queue().launch()
|
|
|