Spaces:
Running
Running
| import time | |
| from typing import Optional, Tuple | |
| import cv2 | |
| import gradio as gr | |
| import numpy as np | |
| FACE_CASCADE = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml") | |
| CUSTOM_CSS = """ | |
| :root { --radius-xl: 22px; } | |
| .gradio-container { | |
| max-width: 1500px !important; | |
| margin: auto !important; | |
| background: radial-gradient(circle at 15% 15%, rgba(38, 166, 191, .22), transparent 30%), | |
| linear-gradient(135deg, #101827 0%, #273142 48%, #10232b 100%) !important; | |
| color: #eef6ff !important; | |
| } | |
| #component-0, .contain, .block, .panel { | |
| border-radius: var(--radius-xl) !important; | |
| } | |
| .prose h1, h1 { | |
| font-size: clamp(2.2rem, 5vw, 4.4rem) !important; | |
| text-align: center !important; | |
| letter-spacing: -0.04em !important; | |
| } | |
| .prose p, .prose li { color: #c9d6e8 !important; } | |
| label, .label-wrap span { font-weight: 800 !important; } | |
| button { | |
| border-radius: 999px !important; | |
| font-weight: 800 !important; | |
| } | |
| .image-container, .wrap, .block { | |
| overflow: hidden !important; | |
| } | |
| #status_box textarea, #status_box .prose { | |
| font-size: 1.05rem !important; | |
| } | |
| @media (max-width: 760px) { | |
| .gradio-container { padding: 8px !important; } | |
| h1 { font-size: 2.35rem !important; } | |
| .image-container img, .image-container video { max-height: 62vh !important; object-fit: contain !important; } | |
| } | |
| """ | |
| HEADER = """ | |
| # FaceSense Live | |
| Real-time face boxes for desktop and mobile webcam testing. | |
| Click **Record** on the camera panel to start live analysis. On phones, allow camera permission; your browser may offer front/rear camera selection. | |
| """ | |
| def draw_corner_box(img: np.ndarray, x: int, y: int, w: int, h: int) -> None: | |
| color = (0, 238, 255) | |
| shadow = (6, 18, 28) | |
| thickness = 4 | |
| line = max(18, int(min(w, h) * 0.22)) | |
| # shadow first for readability | |
| cv2.rectangle(img, (x, y), (x + w, y + h), shadow, 7) | |
| # corner style box | |
| cv2.line(img, (x, y), (x + line, y), color, thickness) | |
| cv2.line(img, (x, y), (x, y + line), color, thickness) | |
| cv2.line(img, (x + w, y), (x + w - line, y), color, thickness) | |
| cv2.line(img, (x + w, y), (x + w, y + line), color, thickness) | |
| cv2.line(img, (x, y + h), (x + line, y + h), color, thickness) | |
| cv2.line(img, (x, y + h), (x, y + h - line), color, thickness) | |
| cv2.line(img, (x + w, y + h), (x + w - line, y + h), color, thickness) | |
| cv2.line(img, (x + w, y + h), (x + w, y + h - line), color, thickness) | |
| label = "Face detected" | |
| cv2.rectangle(img, (x, max(0, y - 38)), (x + 185, y), (0, 145, 175), -1) | |
| cv2.putText(img, label, (x + 10, max(22, y - 12)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) | |
| def process_frame(frame: Optional[np.ndarray]) -> Tuple[Optional[np.ndarray], str]: | |
| start = time.perf_counter() | |
| if frame is None: | |
| return None, "### Waiting for camera\nClick **Record** on the webcam panel to start live analysis." | |
| output = frame.copy() | |
| if output.ndim == 2: | |
| output = cv2.cvtColor(output, cv2.COLOR_GRAY2RGB) | |
| if output.shape[-1] == 4: | |
| output = cv2.cvtColor(output, cv2.COLOR_RGBA2RGB) | |
| gray = cv2.cvtColor(output, cv2.COLOR_RGB2GRAY) | |
| gray = cv2.equalizeHist(gray) | |
| faces = FACE_CASCADE.detectMultiScale( | |
| gray, | |
| scaleFactor=1.08, | |
| minNeighbors=5, | |
| minSize=(55, 55), | |
| flags=cv2.CASCADE_SCALE_IMAGE, | |
| ) | |
| for (x, y, w, h) in faces: | |
| draw_corner_box(output, int(x), int(y), int(w), int(h)) | |
| elapsed_ms = (time.perf_counter() - start) * 1000 | |
| fps = 1000 / elapsed_ms if elapsed_ms > 0 else 0 | |
| if len(faces) == 0: | |
| status = ( | |
| "### Live status\n" | |
| "No frontal face detected yet. Move your face toward the center, improve lighting, and keep the camera steady.\n\n" | |
| f"**Processing:** {elapsed_ms:.1f} ms \n" | |
| f"**Approx FPS:** {fps:.1f}" | |
| ) | |
| else: | |
| status = ( | |
| "### Live status\n" | |
| f"**Faces detected:** {len(faces)} \n" | |
| "**Phase 1:** bounding boxes only \n" | |
| "**Next phase:** facial expression + apparent age range + optional presentation estimate \n\n" | |
| f"**Processing:** {elapsed_ms:.1f} ms \n" | |
| f"**Approx FPS:** {fps:.1f}" | |
| ) | |
| return output, status | |
| demo = gr.Interface( | |
| fn=process_frame, | |
| inputs=gr.Image( | |
| label="Camera input", | |
| sources=["webcam"], | |
| type="numpy", | |
| streaming=True, | |
| mirror_webcam=True, | |
| height=520, | |
| ), | |
| outputs=[ | |
| gr.Image(label="Annotated output", type="numpy", height=520), | |
| gr.Markdown(label="Status"), | |
| ], | |
| title="FaceSense Live", | |
| description=HEADER, | |
| live=True, | |
| css=CUSTOM_CSS, | |
| allow_flagging="never", | |
| api_name="predict", | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(default_concurrency_limit=4).launch() | |