| import cv2 |
| import numpy as np |
| import gradio as gr |
| import tempfile |
| import tensorflow as tf |
| from tensorflow import keras |
|
|
| |
| |
| |
| physical_devices = tf.config.list_physical_devices("CPU") |
| tf.config.set_visible_devices(physical_devices, "CPU") |
|
|
| model = keras.models.load_model("emotion_detection_model.h5") |
|
|
| |
| |
| _dummy = np.zeros((1, 48, 48, 1), dtype=np.float32) |
| model.predict(_dummy, verbose=0) |
|
|
| |
| EMOTION_LABELS = { |
| 0: "Angry", |
| 1: "Happy", |
| 2: "Neutral", |
| 3: "Sad", |
| 4: "Surprised" |
| } |
|
|
| |
| COLORS = { |
| "Angry": (0, 0, 255), |
| "Happy": (0, 200, 0), |
| "Neutral": (200, 200, 0), |
| "Sad": (255, 100, 0), |
| "Surprised": (0, 140, 255) |
| } |
|
|
| |
| face_cascade = cv2.CascadeClassifier( |
| cv2.data.haarcascades + "haarcascade_frontalface_default.xml" |
| ) |
|
|
| |
| _snapshot_path = {"path": None} |
|
|
|
|
| |
| def detect(frame): |
| """ |
| Receives one webcam frame (RGB numpy array) from Gradio. |
| Returns annotated RGB frame immediately β no threads, no queues. |
| TF graph is pre-compiled so predict() takes ~20-50ms on CPU. |
| """ |
| if frame is None: |
| return None |
|
|
| |
| bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) |
|
|
| |
| h, w = bgr.shape[:2] |
| if w > 640: |
| scale = 640 / w |
| bgr = cv2.resize(bgr, (640, int(h * scale))) |
|
|
| gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) |
| gray = cv2.equalizeHist(gray) |
|
|
| |
| faces = face_cascade.detectMultiScale( |
| gray, |
| scaleFactor=1.3, |
| minNeighbors=5, |
| minSize=(40, 40), |
| flags=cv2.CASCADE_SCALE_IMAGE |
| ) |
|
|
| if len(faces) == 0: |
| cv2.putText(bgr, "No face detected", (20, 40), |
| cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2, cv2.LINE_AA) |
| else: |
| |
| batch = [] |
| regions = [] |
| for (x, y, w, h) in faces: |
| roi = gray[y:y+h, x:x+w] |
| roi = cv2.resize(roi, (48, 48)).astype(np.float32) / 255.0 |
| batch.append(roi.reshape(48, 48, 1)) |
| regions.append((x, y, w, h)) |
|
|
| |
| batch = np.array(batch, dtype=np.float32) |
| predictions = model.predict(batch, verbose=0) |
|
|
| for i, (x, y, w, h) in enumerate(regions): |
| emotion = EMOTION_LABELS[np.argmax(predictions[i])] |
| confidence = float(np.max(predictions[i])) * 100 |
| color = COLORS[emotion] |
|
|
| |
| cv2.rectangle(bgr, (x, y), (x+w, y+h), color, 2) |
|
|
| |
| label = f"{emotion} {confidence:.1f}%" |
| font = cv2.FONT_HERSHEY_SIMPLEX |
| (tw, th), bl = cv2.getTextSize(label, font, 0.7, 2) |
| ly = max(y - 8, th + 8) |
| cv2.rectangle(bgr, |
| (x, ly - th - bl - 4), |
| (x + tw + 6, ly + bl - 2), |
| color, -1) |
| cv2.putText(bgr, label, (x + 3, ly - bl), |
| font, 0.7, (0, 0, 0), 2, cv2.LINE_AA) |
|
|
| |
| result = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) |
|
|
| |
| _snapshot_path["frame"] = result.copy() |
|
|
| return result |
|
|
|
|
| def save_snapshot(): |
| """Saves the latest annotated frame as a PNG and returns path for download.""" |
| frame = _snapshot_path.get("frame") |
| if frame is None: |
| return None |
| tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False) |
| tmp.close() |
| cv2.imwrite(tmp.name, cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)) |
| return tmp.name |
|
|
|
|
| |
| with gr.Blocks(title="Real-Time Emotion Detection") as demo: |
|
|
| gr.Markdown(""" |
| # π Real-Time Emotion Detection |
| Click **βΆ Start** on the webcam feed to begin. |
| Press **π₯ Download Snapshot** anytime to save the current frame. |
| π Angry | π Happy | π Neutral | π’ Sad | π² Surprised |
| """) |
|
|
| with gr.Row(): |
| webcam = gr.Image(sources=["webcam"], streaming=True, |
| label="π· Live Webcam") |
| output_img = gr.Image(label="π― Detected Emotion") |
|
|
| with gr.Row(): |
| download_btn = gr.DownloadButton( |
| label="π₯ Download Snapshot", variant="primary" |
| ) |
|
|
| |
| webcam.stream( |
| fn=detect, |
| inputs=webcam, |
| outputs=output_img, |
| stream_every=0.1, |
| time_limit=600 |
| ) |
|
|
| download_btn.click(fn=save_snapshot, inputs=None, outputs=download_btn) |
|
|
| gr.Markdown(""" |
| --- |
| **Model:** CNN β Conv2D(45) β Conv2D(64) β Conv2D(128) β Dense(256) β Softmax(5) | |
| **Input:** 48Γ48 grayscale | **Detector:** Haar Cascade |
| """) |
|
|
| if __name__ == "__main__": |
| demo.launch() |