File size: 4,870 Bytes
a02f1ff
7fd00f2
a02f1ff
 
 
 
 
7fd00f2
a02f1ff
7fd00f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a02f1ff
7fd00f2
 
 
 
 
a02f1ff
 
7fd00f2
 
 
 
 
a02f1ff
7fd00f2
 
 
 
 
 
 
 
 
 
 
a02f1ff
7fd00f2
 
 
a02f1ff
 
7fd00f2
 
 
 
a02f1ff
7fd00f2
 
 
 
 
a02f1ff
7fd00f2
a02f1ff
 
 
 
 
7fd00f2
a02f1ff
 
 
7fd00f2
 
a02f1ff
 
 
 
7fd00f2
 
 
 
 
 
 
a02f1ff
7fd00f2
 
 
 
 
 
 
 
a02f1ff
 
 
7fd00f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a02f1ff
 
7fd00f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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()