Spaces:
Sleeping
Sleeping
GitHub Action
commited on
Commit
·
bc745c3
1
Parent(s):
1ad4a22
Automated deployment from GitHub Actions
Browse files- README.md +2 -13
- app.py +72 -114
- huggingface-space/.dvc/.gitignore +2 -0
- huggingface-space/.dvc/config +0 -0
- huggingface-space/.dvcignore +0 -0
- huggingface-space/.gitignore +112 -0
- huggingface-space/Dockerfile +0 -0
- huggingface-space/LICENSE +21 -0
- huggingface-space/README.md +2 -2
- huggingface-space/app.py +195 -0
- huggingface-space/config/config.yaml +25 -0
- huggingface-space/dvc.lock +203 -0
- huggingface-space/dvc.yaml +39 -0
- huggingface-space/gpuCheck.py +42 -0
- huggingface-space/huggingface-space/.gitattributes +1 -0
- huggingface-space/huggingface-space/README.md +42 -0
- huggingface-space/main.py +70 -0
- huggingface-space/params.yaml +15 -0
- huggingface-space/requirements.txt +44 -0
- huggingface-space/research/01_data_exploration.ipynb +0 -0
- huggingface-space/setup.py +28 -0
- huggingface-space/src/EmotionRecognition/__init__.py +25 -0
- huggingface-space/src/EmotionRecognition/components/__init__.py +0 -0
- huggingface-space/src/EmotionRecognition/components/data_ingestion.py +27 -0
- huggingface-space/src/EmotionRecognition/components/data_preparation.py +118 -0
- huggingface-space/src/EmotionRecognition/components/data_preprocessing.py +86 -0
- huggingface-space/src/EmotionRecognition/components/data_validation.py +32 -0
- huggingface-space/src/EmotionRecognition/components/model_evaluation.py +63 -0
- huggingface-space/src/EmotionRecognition/components/model_trainer.py +99 -0
- huggingface-space/src/EmotionRecognition/config/__init__.py +0 -0
- huggingface-space/src/EmotionRecognition/config/configuration.py +64 -0
- huggingface-space/src/EmotionRecognition/entity/__init__.py +0 -0
- huggingface-space/src/EmotionRecognition/entity/config_entity.py +42 -0
- huggingface-space/src/EmotionRecognition/pipeline/__init__.py +0 -0
- huggingface-space/src/EmotionRecognition/pipeline/hf_predictor.py +109 -0
- huggingface-space/src/EmotionRecognition/pipeline/stage_01_data_preparation.py +27 -0
- huggingface-space/src/EmotionRecognition/pipeline/stage_02_model_training.py +28 -0
- huggingface-space/src/EmotionRecognition/pipeline/stage_03_model_evaluation.py +31 -0
- huggingface-space/src/EmotionRecognition/utils/__init__.py +0 -0
- huggingface-space/src/EmotionRecognition/utils/common.py +78 -0
- huggingface-space/temp.py +65 -0
- huggingface-space/templates/index.html +0 -0
- sota_model/config.json +42 -42
- sota_model/preprocessor_config.json +36 -36
- src/EmotionRecognition/pipeline/hf_predictor.py +31 -63
README.md
CHANGED
|
@@ -1,21 +1,10 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Emotion Detector
|
| 3 |
-
emoji: 🎭
|
| 4 |
-
colorFrom: purple
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: "3.50.2"
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
# 🎭 End-to-End Facial Emotion Recognition
|
| 13 |
|
| 14 |
<!-- Replace with a link to your final app screenshot -->
|
| 15 |
|
| 16 |
This repository contains a complete, end-to-end MLOps pipeline and a production-ready web application for real-time facial emotion recognition. The project leverages a state-of-the-art Vision Transformer model and is deployed as a user-friendly Gradio application on Hugging Face Spaces.
|
| 17 |
|
| 18 |
-
**Live Demo:** [🚀 Click here to try the application on Hugging Face Spaces!](https://huggingface.co/spaces/
|
| 19 |
|
| 20 |
---
|
| 21 |
|
|
@@ -50,4 +39,4 @@ Follow these steps to run the project locally.
|
|
| 50 |
|
| 51 |
```bash
|
| 52 |
git clone https://github.com/YOUR-USERNAME/Emotion-Recognition-MLOps.git
|
| 53 |
-
cd Emotion-Recognition-MLOps
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# 🎭 End-to-End Facial Emotion Recognition
|
| 2 |
|
| 3 |
<!-- Replace with a link to your final app screenshot -->
|
| 4 |
|
| 5 |
This repository contains a complete, end-to-end MLOps pipeline and a production-ready web application for real-time facial emotion recognition. The project leverages a state-of-the-art Vision Transformer model and is deployed as a user-friendly Gradio application on Hugging Face Spaces.
|
| 6 |
|
| 7 |
+
**Live Demo:** [🚀 Click here to try the application on Hugging Face Spaces!](https://huggingface.co/spaces/ALYYAN/Emotion-Recognition) <!-- Replace with your HF Space URL -->
|
| 8 |
|
| 9 |
---
|
| 10 |
|
|
|
|
| 39 |
|
| 40 |
```bash
|
| 41 |
git clone https://github.com/YOUR-USERNAME/Emotion-Recognition-MLOps.git
|
| 42 |
+
cd Emotion-Recognition-MLOps
|
app.py
CHANGED
|
@@ -3,7 +3,6 @@ import os
|
|
| 3 |
import cv2
|
| 4 |
import time
|
| 5 |
|
| 6 |
-
# Ensure the correct predictor class is imported
|
| 7 |
from src.EmotionRecognition.pipeline.hf_predictor import HFPredictor
|
| 8 |
|
| 9 |
# --- INITIALIZE THE MODEL ---
|
|
@@ -16,14 +15,21 @@ except Exception as e:
|
|
| 16 |
print(f"[FATAL ERROR] Failed to initialize predictor: {e}")
|
| 17 |
|
| 18 |
# --- UI CONTENT & STYLING ---
|
|
|
|
|
|
|
| 19 |
CSS = """
|
| 20 |
/* Animated Gradient Background */
|
| 21 |
body {
|
| 22 |
background: linear-gradient(-45deg, #0b0f19, #131a2d, #2a2a72, #522a72);
|
| 23 |
background-size: 400% 400%;
|
| 24 |
animation: gradient 15s ease infinite;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
-
@keyframes gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
|
| 27 |
|
| 28 |
/* General Layout & Typography */
|
| 29 |
.gradio-container { max-width: 1320px !important; margin: auto !important; }
|
|
@@ -31,90 +37,42 @@ body {
|
|
| 31 |
#subtitle { text-align: center; color: #bebebe; margin-top: 0; margin-bottom: 40px; font-size: 1.2rem; font-weight: 300; }
|
| 32 |
.gr-button { font-weight: bold !important; }
|
| 33 |
|
| 34 |
-
/*
|
| 35 |
#main-card {
|
| 36 |
-
background: rgba(22, 22, 34, 0.65);
|
| 37 |
border-radius: 16px;
|
| 38 |
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
| 39 |
-
backdrop-filter: blur(12px);
|
|
|
|
| 40 |
border: 1px solid rgba(255, 255, 255, 0.18);
|
| 41 |
padding: 1rem;
|
| 42 |
}
|
|
|
|
| 43 |
|
| 44 |
-
/* Prediction Bar Styling */
|
| 45 |
-
#predictions-column { background-color: transparent !important; padding: 1.5rem; }
|
| 46 |
-
#predictions-column > .gr-label { display: none; }
|
| 47 |
-
.prediction-list { list-style-type: none; padding: 0; margin-top:
|
| 48 |
.prediction-list li { display: flex; align-items: center; margin-bottom: 12px; font-size: 1.1rem; }
|
| 49 |
.prediction-list .label { width: 100px; text-transform: capitalize; color: #e0e0e0; }
|
| 50 |
.prediction-list .bar-container { flex-grow: 1; height: 24px; background-color: rgba(255,255,255,0.1); border-radius: 12px; margin: 0 15px; overflow: hidden; }
|
| 51 |
-
.prediction-list .bar { height: 100%; background: linear-gradient(90deg, #8A2BE2, #C71585); border-radius: 12px; transition: width
|
| 52 |
.prediction-list .percent { width: 60px; text-align: right; font-weight: bold; color: #FFF; }
|
| 53 |
footer { display: none !important; }
|
| 54 |
"""
|
| 55 |
|
| 56 |
ABOUT_MARKDOWN = """
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
### Project Team
|
| 66 |
-
|
| 67 |
-
- **💻 [Alyyan Ahmed](https://github.com/AlyyanAhmed21)**
|
| 68 |
-
- **💻 [Munim Akbar](https://github.com/MunimAkbar)**
|
| 69 |
-
|
| 70 |
-
---
|
| 71 |
-
|
| 72 |
-
### ✨ Key Technical Features
|
| 73 |
-
|
| 74 |
-
* **State-of-the-Art AI Model:** The core of this app is a **Swin Transformer**, a powerful Vision Transformer (ViT) architecture. It was pre-trained on the massive **AffectNet** dataset, ensuring high accuracy and robust generalization to real-world, "in the wild" facial expressions.
|
| 75 |
-
|
| 76 |
-
* **Full MLOps Lifecycle Demonstration:** This project wasn't a straight line. It involved a reproducible pipeline built with **DVC** that progressed through:
|
| 77 |
-
1. **Initial Model (MobileNetV2):** Achieved high accuracy (~96%) on a clean, posed dataset (CK+) but failed to generalize to real-world faces, demonstrating a key data science challenge.
|
| 78 |
-
2. **Data-Centric Iteration:** Experimented with combining and balancing multiple datasets (FER+, CK+) to improve robustness, highlighting the importance of data quality.
|
| 79 |
-
3. **Final SOTA Integration:** Strategically pivoted to a powerful, pre-trained model from the Hugging Face Hub to achieve superior real-world performance.
|
| 80 |
-
|
| 81 |
-
* **Full-Stack & Deployment:** The application architecture evolved from a Python-only script to a decoupled **FastAPI backend** and a **React frontend**, and was ultimately deployed as this streamlined and robust **Gradio** application.
|
| 82 |
-
|
| 83 |
-
* **Containerized & Automated:** The entire application is packaged with **Docker** and is set up for **CI/CD with GitHub Actions**, enabling automated testing and deployment to cloud platforms like Hugging Face Spaces.
|
| 84 |
-
|
| 85 |
-
---
|
| 86 |
-
|
| 87 |
-
### 🛠️ Architecture & Tech Stack
|
| 88 |
-
|
| 89 |
-
* **Machine Learning & CV:**
|
| 90 |
-
* Python, PyTorch, Hugging Face `transformers`
|
| 91 |
-
* `MTCNN` for robust face detection
|
| 92 |
-
* `OpenCV` for image processing
|
| 93 |
-
|
| 94 |
-
* **MLOps & DevOps:**
|
| 95 |
-
* **DVC:** For data versioning and building reproducible pipelines.
|
| 96 |
-
* **GitHub Actions:** For CI/CD and automated deployment.
|
| 97 |
-
* **Docker:** For containerizing the application for consistent environments.
|
| 98 |
-
* *(MLflow was used for experiment tracking during the training phase)*
|
| 99 |
-
|
| 100 |
-
* **Application & UI:**
|
| 101 |
-
* **Gradio:** For building and deploying this interactive UI.
|
| 102 |
-
* *(FastAPI and React were used in an alternate full-stack version of the application)*
|
| 103 |
-
|
| 104 |
-
### 💡 Skills Demonstrated
|
| 105 |
-
|
| 106 |
-
This project showcases a comprehensive skillset in building modern AI systems:
|
| 107 |
-
|
| 108 |
-
* **Data Science & Analysis:** Deeply analyzing dataset quality, identifying limitations (e.g., posed vs. "in the wild"), and making strategic, data-driven decisions to improve model performance.
|
| 109 |
-
* **Deep Learning & Computer Vision:** Implementing and fine-tuning multiple advanced architectures (CNNs, Vision Transformers) for a complex computer vision task.
|
| 110 |
-
* **Full-Stack Application Development:** Building both decoupled (FastAPI/React) and unified (Gradio) web applications to serve a live ML model.
|
| 111 |
-
* **MLOps & CI/CD Automation:** Engineering a complete, end-to-end pipeline that is version-controlled, reproducible, and automatically deployed, reflecting best practices in production machine learning.
|
| 112 |
"""
|
| 113 |
|
| 114 |
# --- BACKEND LOGIC ---
|
| 115 |
-
|
| 116 |
def create_prediction_html(probabilities):
|
| 117 |
-
"""Generates clean HTML for the prediction bars."""
|
| 118 |
if not probabilities:
|
| 119 |
return "<div style='padding: 2rem; text-align: center; color: #999;'>Waiting for prediction...</div>"
|
| 120 |
html = "<ul class='prediction-list'>"
|
|
@@ -130,22 +88,34 @@ def create_prediction_html(probabilities):
|
|
| 130 |
html += "</ul>"
|
| 131 |
return html
|
| 132 |
|
| 133 |
-
def
|
| 134 |
-
"""
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
return annotated_frame, create_prediction_html(probabilities)
|
| 144 |
|
| 145 |
def process_video(video_path, progress=gr.Progress(track_tqdm=True)):
|
| 146 |
-
|
| 147 |
-
if video_path is None:
|
| 148 |
-
return None
|
| 149 |
try:
|
| 150 |
cap = cv2.VideoCapture(video_path)
|
| 151 |
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
@@ -174,64 +144,52 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo:
|
|
| 174 |
gr.Markdown("# Facial Emotion Detector", elem_id="title")
|
| 175 |
gr.Markdown("A real-time AI application powered by Vision Transformers", elem_id="subtitle")
|
| 176 |
|
|
|
|
| 177 |
with gr.Box(elem_id="main-card"):
|
| 178 |
with gr.Tabs():
|
| 179 |
with gr.TabItem("Live Detection"):
|
| 180 |
-
with gr.Row(equal_height=
|
| 181 |
with gr.Column(scale=3):
|
| 182 |
-
|
| 183 |
-
# It acts as both input (from webcam) and output (displaying the result).
|
| 184 |
-
live_feed = gr.Image(source="webcam", streaming=True, type="numpy", label="Live Feed", height=550, mirror_webcam=True)
|
| 185 |
with gr.Column(scale=2, elem_id="predictions-column"):
|
| 186 |
-
gr.Markdown("### Emotion Probabilities")
|
| 187 |
live_predictions = gr.HTML()
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
with gr.TabItem("Upload Image"):
|
| 190 |
-
with gr.Row(equal_height=
|
| 191 |
with gr.Column(scale=3):
|
| 192 |
image_input = gr.Image(type="numpy", label="Upload an Image", height=550)
|
| 193 |
with gr.Column(scale=2, elem_id="predictions-column"):
|
|
|
|
| 194 |
image_predictions = gr.HTML()
|
| 195 |
image_button = gr.Button("Analyze Image", variant="primary")
|
| 196 |
|
| 197 |
with gr.TabItem("Upload Video"):
|
| 198 |
-
with gr.Row(equal_height=
|
| 199 |
video_input = gr.Video(label="Upload a Video File")
|
| 200 |
video_output = gr.Video(label="Processed Video")
|
| 201 |
video_button = gr.Button("Analyze Video", variant="primary")
|
| 202 |
|
| 203 |
with gr.TabItem("About"):
|
| 204 |
gr.Markdown(ABOUT_MARKDOWN)
|
|
|
|
| 205 |
|
| 206 |
-
# --- EVENT LISTENERS ---
|
|
|
|
|
|
|
| 207 |
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
live_feed.stream(
|
| 213 |
-
fn=unified_prediction_function,
|
| 214 |
-
inputs=[live_feed],
|
| 215 |
-
outputs=[live_feed, live_predictions]
|
| 216 |
-
)
|
| 217 |
-
|
| 218 |
-
# Image Upload Logic
|
| 219 |
-
image_button.click(
|
| 220 |
-
fn=unified_prediction_function,
|
| 221 |
-
inputs=[image_input],
|
| 222 |
-
outputs=[image_input, image_predictions]
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
# Video Upload Logic
|
| 226 |
-
video_button.click(
|
| 227 |
-
fn=process_video,
|
| 228 |
-
inputs=[video_input],
|
| 229 |
-
outputs=[video_output]
|
| 230 |
-
)
|
| 231 |
|
| 232 |
# --- LAUNCH THE APP ---
|
| 233 |
if predictor:
|
| 234 |
-
|
| 235 |
-
demo.queue().launch(debug=True)
|
| 236 |
else:
|
| 237 |
print("\n[FATAL ERROR] Could not start the application.")
|
|
|
|
| 3 |
import cv2
|
| 4 |
import time
|
| 5 |
|
|
|
|
| 6 |
from src.EmotionRecognition.pipeline.hf_predictor import HFPredictor
|
| 7 |
|
| 8 |
# --- INITIALIZE THE MODEL ---
|
|
|
|
| 15 |
print(f"[FATAL ERROR] Failed to initialize predictor: {e}")
|
| 16 |
|
| 17 |
# --- UI CONTENT & STYLING ---
|
| 18 |
+
# In app.py
|
| 19 |
+
|
| 20 |
CSS = """
|
| 21 |
/* Animated Gradient Background */
|
| 22 |
body {
|
| 23 |
background: linear-gradient(-45deg, #0b0f19, #131a2d, #2a2a72, #522a72);
|
| 24 |
background-size: 400% 400%;
|
| 25 |
animation: gradient 15s ease infinite;
|
| 26 |
+
color: #e0e0e0;
|
| 27 |
+
}
|
| 28 |
+
@keyframes gradient {
|
| 29 |
+
0% { background-position: 0% 50%; }
|
| 30 |
+
50% { background-position: 100% 50%; }
|
| 31 |
+
100% { background-position: 0% 50%; }
|
| 32 |
}
|
|
|
|
| 33 |
|
| 34 |
/* General Layout & Typography */
|
| 35 |
.gradio-container { max-width: 1320px !important; margin: auto !important; }
|
|
|
|
| 37 |
#subtitle { text-align: center; color: #bebebe; margin-top: 0; margin-bottom: 40px; font-size: 1.2rem; font-weight: 300; }
|
| 38 |
.gr-button { font-weight: bold !important; }
|
| 39 |
|
| 40 |
+
/* --- NEW: The "Glass Card" effect --- */
|
| 41 |
#main-card {
|
| 42 |
+
background: rgba(22, 22, 34, 0.65); /* Semi-transparent dark background */
|
| 43 |
border-radius: 16px;
|
| 44 |
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
| 45 |
+
backdrop-filter: blur(12px); /* The "frosted glass" effect */
|
| 46 |
+
-webkit-backdrop-filter: blur(12px); /* For Safari */
|
| 47 |
border: 1px solid rgba(255, 255, 255, 0.18);
|
| 48 |
padding: 1rem;
|
| 49 |
}
|
| 50 |
+
/* --- END NEW --- */
|
| 51 |
|
| 52 |
+
/* Prediction Bar Styling - now inside the card */
|
| 53 |
+
#predictions-column { background-color: transparent !important; border-radius: 12px; padding: 1.5rem; }
|
| 54 |
+
#predictions-column > .gr-label { display: none; }
|
| 55 |
+
.prediction-list { list-style-type: none; padding: 0; margin-top: 0; }
|
| 56 |
.prediction-list li { display: flex; align-items: center; margin-bottom: 12px; font-size: 1.1rem; }
|
| 57 |
.prediction-list .label { width: 100px; text-transform: capitalize; color: #e0e0e0; }
|
| 58 |
.prediction-list .bar-container { flex-grow: 1; height: 24px; background-color: rgba(255,255,255,0.1); border-radius: 12px; margin: 0 15px; overflow: hidden; }
|
| 59 |
+
.prediction-list .bar { height: 100%; background: linear-gradient(90deg, #8A2BE2, #C71585); border-radius: 12px; transition: width 0.2s ease-in-out; }
|
| 60 |
.prediction-list .percent { width: 60px; text-align: right; font-weight: bold; color: #FFF; }
|
| 61 |
footer { display: none !important; }
|
| 62 |
"""
|
| 63 |
|
| 64 |
ABOUT_MARKDOWN = """
|
| 65 |
+
### Model: Vision Transformer (ViT)
|
| 66 |
+
This application uses a Vision Transformer model, fine-tuned for facial emotion recognition.
|
| 67 |
+
### Dataset
|
| 68 |
+
The model was fine-tuned on the **Emotion Recognition Dataset** from Kaggle, a large, curated collection of labeled facial images. This diverse dataset allows the model to generalize to a wide variety of real-world faces and expressions.
|
| 69 |
+
*Dataset Link:* [https://www.kaggle.com/datasets/sujaykapadnis/emotion-recognition-dataset](https://www.kaggle.com/datasets/sujaykapadnis/emotion-recognition-dataset)
|
| 70 |
+
### MLOps Pipeline
|
| 71 |
+
This entire application, from data processing to training and deployment, was built using a reproducible MLOps pipeline, ensuring consistency and quality at every step.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
"""
|
| 73 |
|
| 74 |
# --- BACKEND LOGIC ---
|
|
|
|
| 75 |
def create_prediction_html(probabilities):
|
|
|
|
| 76 |
if not probabilities:
|
| 77 |
return "<div style='padding: 2rem; text-align: center; color: #999;'>Waiting for prediction...</div>"
|
| 78 |
html = "<ul class='prediction-list'>"
|
|
|
|
| 88 |
html += "</ul>"
|
| 89 |
return html
|
| 90 |
|
| 91 |
+
def live_detection_stream():
|
| 92 |
+
"""A generator function that runs the live feed loop. This is the definitive fix."""
|
| 93 |
+
cap = cv2.VideoCapture(0)
|
| 94 |
+
if not cap.isOpened():
|
| 95 |
+
print("[ERROR] Cannot open webcam")
|
| 96 |
+
return
|
| 97 |
+
try:
|
| 98 |
+
while True:
|
| 99 |
+
ret, frame = cap.read()
|
| 100 |
+
if not ret:
|
| 101 |
+
time.sleep(0.01)
|
| 102 |
+
continue
|
| 103 |
+
|
| 104 |
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 105 |
+
annotated_frame, probabilities = predictor.process_frame(frame_rgb)
|
| 106 |
+
yield annotated_frame, create_prediction_html(probabilities)
|
| 107 |
+
time.sleep(0.05) # Controls FPS. 0.05 = ~20 FPS target. The model inference will be the main bottleneck.
|
| 108 |
+
finally:
|
| 109 |
+
print("[INFO] Live feed stopped. Releasing webcam.")
|
| 110 |
+
cap.release()
|
| 111 |
+
|
| 112 |
+
def process_image(image):
|
| 113 |
+
if image is None: return None, create_prediction_html({})
|
| 114 |
+
annotated_frame, probabilities = predictor.process_frame(image)
|
| 115 |
return annotated_frame, create_prediction_html(probabilities)
|
| 116 |
|
| 117 |
def process_video(video_path, progress=gr.Progress(track_tqdm=True)):
|
| 118 |
+
if video_path is None: return None
|
|
|
|
|
|
|
| 119 |
try:
|
| 120 |
cap = cv2.VideoCapture(video_path)
|
| 121 |
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
|
|
| 144 |
gr.Markdown("# Facial Emotion Detector", elem_id="title")
|
| 145 |
gr.Markdown("A real-time AI application powered by Vision Transformers", elem_id="subtitle")
|
| 146 |
|
| 147 |
+
# --- NEW: Wrapper for the glass card effect ---
|
| 148 |
with gr.Box(elem_id="main-card"):
|
| 149 |
with gr.Tabs():
|
| 150 |
with gr.TabItem("Live Detection"):
|
| 151 |
+
with gr.Row(equal_height=True):
|
| 152 |
with gr.Column(scale=3):
|
| 153 |
+
live_output = gr.Image(label="Live Feed", interactive=False, height=550)
|
|
|
|
|
|
|
| 154 |
with gr.Column(scale=2, elem_id="predictions-column"):
|
| 155 |
+
gr.Markdown("### Emotion Probabilities") # Title for the panel
|
| 156 |
live_predictions = gr.HTML()
|
| 157 |
+
with gr.Row():
|
| 158 |
+
start_button = gr.Button("Start Webcam", variant="primary", scale=1)
|
| 159 |
+
stop_button = gr.Button("Stop Webcam", variant="secondary", scale=1)
|
| 160 |
+
|
| 161 |
+
stream_state = gr.State("Stop")
|
| 162 |
+
|
| 163 |
with gr.TabItem("Upload Image"):
|
| 164 |
+
with gr.Row(equal_height=True):
|
| 165 |
with gr.Column(scale=3):
|
| 166 |
image_input = gr.Image(type="numpy", label="Upload an Image", height=550)
|
| 167 |
with gr.Column(scale=2, elem_id="predictions-column"):
|
| 168 |
+
gr.Markdown("### Emotion Probabilities")
|
| 169 |
image_predictions = gr.HTML()
|
| 170 |
image_button = gr.Button("Analyze Image", variant="primary")
|
| 171 |
|
| 172 |
with gr.TabItem("Upload Video"):
|
| 173 |
+
with gr.Row(equal_height=True):
|
| 174 |
video_input = gr.Video(label="Upload a Video File")
|
| 175 |
video_output = gr.Video(label="Processed Video")
|
| 176 |
video_button = gr.Button("Analyze Video", variant="primary")
|
| 177 |
|
| 178 |
with gr.TabItem("About"):
|
| 179 |
gr.Markdown(ABOUT_MARKDOWN)
|
| 180 |
+
# --- END WRAPPER ---
|
| 181 |
|
| 182 |
+
# --- EVENT LISTENERS (No changes needed here) ---
|
| 183 |
+
start_event = start_button.click(lambda: "Start", None, stream_state, queue=False)
|
| 184 |
+
live_stream = start_event.then(live_detection_stream, stream_state, [live_output, live_predictions])
|
| 185 |
|
| 186 |
+
stop_button.click(fn=None, inputs=None, outputs=None, cancels=[live_stream])
|
| 187 |
+
|
| 188 |
+
image_button.click(process_image, [image_input], [image_input, image_predictions])
|
| 189 |
+
video_button.click(process_video, [video_input], [video_output])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
# --- LAUNCH THE APP ---
|
| 192 |
if predictor:
|
| 193 |
+
demo.queue().launch(debug=True, share=True)
|
|
|
|
| 194 |
else:
|
| 195 |
print("\n[FATAL ERROR] Could not start the application.")
|
huggingface-space/.dvc/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/config.local
|
| 2 |
+
/tmp
|
huggingface-space/.dvc/config
ADDED
|
File without changes
|
huggingface-space/.dvcignore
ADDED
|
File without changes
|
huggingface-space/.gitignore
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MLOps & Data Science Artifacts
|
| 2 |
+
# -------------------------------------------------------------------
|
| 3 |
+
# Ignore all data, models, and artifacts. These should be tracked by DVC.
|
| 4 |
+
/artifacts/
|
| 5 |
+
/data/
|
| 6 |
+
/sota_model/
|
| 7 |
+
# Ignore the DVC local cache. This is where the actual data files are stored.
|
| 8 |
+
.dvc/cache
|
| 9 |
+
|
| 10 |
+
# Ignore MLflow experiment tracking output
|
| 11 |
+
/mlruns/
|
| 12 |
+
|
| 13 |
+
# Ignore logs
|
| 14 |
+
/logs/
|
| 15 |
+
*.log
|
| 16 |
+
|
| 17 |
+
# Ignore common model file extensions, just in case
|
| 18 |
+
*.h5
|
| 19 |
+
*.pkl
|
| 20 |
+
*.model
|
| 21 |
+
*.onnx
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Python Virtual Environments
|
| 25 |
+
# -------------------------------------------------------------------
|
| 26 |
+
/venv/
|
| 27 |
+
/myenv/
|
| 28 |
+
/.venv/
|
| 29 |
+
/env/
|
| 30 |
+
/ENV/
|
| 31 |
+
*/.venv/
|
| 32 |
+
*/venv/
|
| 33 |
+
*/myenv/
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# Python Byte-code and Caches
|
| 37 |
+
# -------------------------------------------------------------------
|
| 38 |
+
__pycache__/
|
| 39 |
+
*.py[cod]
|
| 40 |
+
*$py.class
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# Python Packaging & Distribution
|
| 44 |
+
# -------------------------------------------------------------------
|
| 45 |
+
build/
|
| 46 |
+
develop-eggs/
|
| 47 |
+
dist/
|
| 48 |
+
downloads/
|
| 49 |
+
eggs/
|
| 50 |
+
.eggs/
|
| 51 |
+
lib/
|
| 52 |
+
lib64/
|
| 53 |
+
parts/
|
| 54 |
+
sdist/
|
| 55 |
+
var/
|
| 56 |
+
wheels/
|
| 57 |
+
*.egg-info/
|
| 58 |
+
.installed.cfg
|
| 59 |
+
*.egg
|
| 60 |
+
MANIFEST
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# IDE and Editor Configuration
|
| 64 |
+
# -------------------------------------------------------------------
|
| 65 |
+
# PyCharm
|
| 66 |
+
.idea/
|
| 67 |
+
|
| 68 |
+
# Visual Studio Code (allow sharing of recommended extensions)
|
| 69 |
+
.vscode/*
|
| 70 |
+
!.vscode/extensions.json
|
| 71 |
+
|
| 72 |
+
# Sublime Text
|
| 73 |
+
*.sublime-project
|
| 74 |
+
*.sublime-workspace
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# Secrets and Environment Variables
|
| 78 |
+
# -------------------------------------------------------------------
|
| 79 |
+
# NEVER commit secrets or environment variables
|
| 80 |
+
.env
|
| 81 |
+
*.env
|
| 82 |
+
secrets.yaml
|
| 83 |
+
secrets.json
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# Operating System Files
|
| 87 |
+
# -------------------------------------------------------------------
|
| 88 |
+
# macOS
|
| 89 |
+
.DS_Store
|
| 90 |
+
|
| 91 |
+
# Windows
|
| 92 |
+
Thumbs.db
|
| 93 |
+
desktop.ini
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# Jupyter Notebook Checkpoints
|
| 97 |
+
# -------------------------------------------------------------------
|
| 98 |
+
.ipynb_checkpoints/
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# Other
|
| 102 |
+
# -------------------------------------------------------------------
|
| 103 |
+
# Temporary files
|
| 104 |
+
*.tmp
|
| 105 |
+
*.bak
|
| 106 |
+
*.swp
|
| 107 |
+
|
| 108 |
+
.env
|
| 109 |
+
*.env
|
| 110 |
+
secrets.yaml
|
| 111 |
+
secrets.json
|
| 112 |
+
processed_video.mp4
|
huggingface-space/Dockerfile
ADDED
|
File without changes
|
huggingface-space/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 ALYYAN
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
huggingface-space/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
|
| 5 |
This repository contains a complete, end-to-end MLOps pipeline and a production-ready web application for real-time facial emotion recognition. The project leverages a state-of-the-art Vision Transformer model and is deployed as a user-friendly Gradio application on Hugging Face Spaces.
|
| 6 |
|
| 7 |
-
**Live Demo:** [🚀 Click here to try the application on Hugging Face Spaces!](https://huggingface.co/spaces/
|
| 8 |
|
| 9 |
---
|
| 10 |
|
|
@@ -39,4 +39,4 @@ Follow these steps to run the project locally.
|
|
| 39 |
|
| 40 |
```bash
|
| 41 |
git clone https://github.com/YOUR-USERNAME/Emotion-Recognition-MLOps.git
|
| 42 |
-
cd Emotion-Recognition-MLOps
|
|
|
|
| 4 |
|
| 5 |
This repository contains a complete, end-to-end MLOps pipeline and a production-ready web application for real-time facial emotion recognition. The project leverages a state-of-the-art Vision Transformer model and is deployed as a user-friendly Gradio application on Hugging Face Spaces.
|
| 6 |
|
| 7 |
+
**Live Demo:** [🚀 Click here to try the application on Hugging Face Spaces!](https://huggingface.co/spaces/ALYYAN/Emotion-Recognition) <!-- Replace with your HF Space URL -->
|
| 8 |
|
| 9 |
---
|
| 10 |
|
|
|
|
| 39 |
|
| 40 |
```bash
|
| 41 |
git clone https://github.com/YOUR-USERNAME/Emotion-Recognition-MLOps.git
|
| 42 |
+
cd Emotion-Recognition-MLOps
|
huggingface-space/app.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
import cv2
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
from src.EmotionRecognition.pipeline.hf_predictor import HFPredictor
|
| 7 |
+
|
| 8 |
+
# --- INITIALIZE THE MODEL ---
|
| 9 |
+
print("[INFO] Initializing predictor...")
|
| 10 |
+
try:
|
| 11 |
+
predictor = HFPredictor()
|
| 12 |
+
print("[INFO] Predictor initialized successfully.")
|
| 13 |
+
except Exception as e:
|
| 14 |
+
predictor = None
|
| 15 |
+
print(f"[FATAL ERROR] Failed to initialize predictor: {e}")
|
| 16 |
+
|
| 17 |
+
# --- UI CONTENT & STYLING ---
|
| 18 |
+
# In app.py
|
| 19 |
+
|
| 20 |
+
CSS = """
|
| 21 |
+
/* Animated Gradient Background */
|
| 22 |
+
body {
|
| 23 |
+
background: linear-gradient(-45deg, #0b0f19, #131a2d, #2a2a72, #522a72);
|
| 24 |
+
background-size: 400% 400%;
|
| 25 |
+
animation: gradient 15s ease infinite;
|
| 26 |
+
color: #e0e0e0;
|
| 27 |
+
}
|
| 28 |
+
@keyframes gradient {
|
| 29 |
+
0% { background-position: 0% 50%; }
|
| 30 |
+
50% { background-position: 100% 50%; }
|
| 31 |
+
100% { background-position: 0% 50%; }
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* General Layout & Typography */
|
| 35 |
+
.gradio-container { max-width: 1320px !important; margin: auto !important; }
|
| 36 |
+
#title { text-align: center; font-size: 3rem !important; font-weight: 700; color: #FFF; margin-bottom: 0.5rem; }
|
| 37 |
+
#subtitle { text-align: center; color: #bebebe; margin-top: 0; margin-bottom: 40px; font-size: 1.2rem; font-weight: 300; }
|
| 38 |
+
.gr-button { font-weight: bold !important; }
|
| 39 |
+
|
| 40 |
+
/* --- NEW: The "Glass Card" effect --- */
|
| 41 |
+
#main-card {
|
| 42 |
+
background: rgba(22, 22, 34, 0.65); /* Semi-transparent dark background */
|
| 43 |
+
border-radius: 16px;
|
| 44 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
| 45 |
+
backdrop-filter: blur(12px); /* The "frosted glass" effect */
|
| 46 |
+
-webkit-backdrop-filter: blur(12px); /* For Safari */
|
| 47 |
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
| 48 |
+
padding: 1rem;
|
| 49 |
+
}
|
| 50 |
+
/* --- END NEW --- */
|
| 51 |
+
|
| 52 |
+
/* Prediction Bar Styling - now inside the card */
|
| 53 |
+
#predictions-column { background-color: transparent !important; border-radius: 12px; padding: 1.5rem; }
|
| 54 |
+
#predictions-column > .gr-label { display: none; }
|
| 55 |
+
.prediction-list { list-style-type: none; padding: 0; margin-top: 0; }
|
| 56 |
+
.prediction-list li { display: flex; align-items: center; margin-bottom: 12px; font-size: 1.1rem; }
|
| 57 |
+
.prediction-list .label { width: 100px; text-transform: capitalize; color: #e0e0e0; }
|
| 58 |
+
.prediction-list .bar-container { flex-grow: 1; height: 24px; background-color: rgba(255,255,255,0.1); border-radius: 12px; margin: 0 15px; overflow: hidden; }
|
| 59 |
+
.prediction-list .bar { height: 100%; background: linear-gradient(90deg, #8A2BE2, #C71585); border-radius: 12px; transition: width 0.2s ease-in-out; }
|
| 60 |
+
.prediction-list .percent { width: 60px; text-align: right; font-weight: bold; color: #FFF; }
|
| 61 |
+
footer { display: none !important; }
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
ABOUT_MARKDOWN = """
|
| 65 |
+
### Model: Vision Transformer (ViT)
|
| 66 |
+
This application uses a Vision Transformer model, fine-tuned for facial emotion recognition.
|
| 67 |
+
### Dataset
|
| 68 |
+
The model was fine-tuned on the **Emotion Recognition Dataset** from Kaggle, a large, curated collection of labeled facial images. This diverse dataset allows the model to generalize to a wide variety of real-world faces and expressions.
|
| 69 |
+
*Dataset Link:* [https://www.kaggle.com/datasets/sujaykapadnis/emotion-recognition-dataset](https://www.kaggle.com/datasets/sujaykapadnis/emotion-recognition-dataset)
|
| 70 |
+
### MLOps Pipeline
|
| 71 |
+
This entire application, from data processing to training and deployment, was built using a reproducible MLOps pipeline, ensuring consistency and quality at every step.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
# --- BACKEND LOGIC ---
|
| 75 |
+
def create_prediction_html(probabilities):
|
| 76 |
+
if not probabilities:
|
| 77 |
+
return "<div style='padding: 2rem; text-align: center; color: #999;'>Waiting for prediction...</div>"
|
| 78 |
+
html = "<ul class='prediction-list'>"
|
| 79 |
+
sorted_preds = sorted(probabilities.items(), key=lambda item: item[1], reverse=True)
|
| 80 |
+
for emotion, prob in sorted_preds:
|
| 81 |
+
html += f"""
|
| 82 |
+
<li>
|
| 83 |
+
<strong class='label'>{emotion}</strong>
|
| 84 |
+
<div class='bar-container'><div class='bar' style='width: {prob*100:.1f}%;'></div></div>
|
| 85 |
+
<span class='percent'>{(prob*100):.1f}%</span>
|
| 86 |
+
</li>
|
| 87 |
+
"""
|
| 88 |
+
html += "</ul>"
|
| 89 |
+
return html
|
| 90 |
+
|
| 91 |
+
def live_detection_stream():
|
| 92 |
+
"""A generator function that runs the live feed loop. This is the definitive fix."""
|
| 93 |
+
cap = cv2.VideoCapture(0)
|
| 94 |
+
if not cap.isOpened():
|
| 95 |
+
print("[ERROR] Cannot open webcam")
|
| 96 |
+
return
|
| 97 |
+
try:
|
| 98 |
+
while True:
|
| 99 |
+
ret, frame = cap.read()
|
| 100 |
+
if not ret:
|
| 101 |
+
time.sleep(0.01)
|
| 102 |
+
continue
|
| 103 |
+
|
| 104 |
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 105 |
+
annotated_frame, probabilities = predictor.process_frame(frame_rgb)
|
| 106 |
+
yield annotated_frame, create_prediction_html(probabilities)
|
| 107 |
+
time.sleep(0.05) # Controls FPS. 0.05 = ~20 FPS target. The model inference will be the main bottleneck.
|
| 108 |
+
finally:
|
| 109 |
+
print("[INFO] Live feed stopped. Releasing webcam.")
|
| 110 |
+
cap.release()
|
| 111 |
+
|
| 112 |
+
def process_image(image):
|
| 113 |
+
if image is None: return None, create_prediction_html({})
|
| 114 |
+
annotated_frame, probabilities = predictor.process_frame(image)
|
| 115 |
+
return annotated_frame, create_prediction_html(probabilities)
|
| 116 |
+
|
| 117 |
+
def process_video(video_path, progress=gr.Progress(track_tqdm=True)):
|
| 118 |
+
if video_path is None: return None
|
| 119 |
+
try:
|
| 120 |
+
cap = cv2.VideoCapture(video_path)
|
| 121 |
+
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 122 |
+
output_path = "processed_video.mp4"
|
| 123 |
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
| 124 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 125 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 126 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 127 |
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
| 128 |
+
for _ in progress.tqdm(range(frame_count), desc="Processing Video"):
|
| 129 |
+
ret, frame = cap.read()
|
| 130 |
+
if not ret: break
|
| 131 |
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 132 |
+
annotated_frame, _ = predictor.process_frame(frame_rgb)
|
| 133 |
+
if annotated_frame is not None:
|
| 134 |
+
out.write(cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR))
|
| 135 |
+
cap.release()
|
| 136 |
+
out.release()
|
| 137 |
+
return output_path
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"[ERROR] Video processing failed: {e}")
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
# --- GRADIO UI ---
|
| 143 |
+
with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo:
|
| 144 |
+
gr.Markdown("# Facial Emotion Detector", elem_id="title")
|
| 145 |
+
gr.Markdown("A real-time AI application powered by Vision Transformers", elem_id="subtitle")
|
| 146 |
+
|
| 147 |
+
# --- NEW: Wrapper for the glass card effect ---
|
| 148 |
+
with gr.Box(elem_id="main-card"):
|
| 149 |
+
with gr.Tabs():
|
| 150 |
+
with gr.TabItem("Live Detection"):
|
| 151 |
+
with gr.Row(equal_height=True):
|
| 152 |
+
with gr.Column(scale=3):
|
| 153 |
+
live_output = gr.Image(label="Live Feed", interactive=False, height=550)
|
| 154 |
+
with gr.Column(scale=2, elem_id="predictions-column"):
|
| 155 |
+
gr.Markdown("### Emotion Probabilities") # Title for the panel
|
| 156 |
+
live_predictions = gr.HTML()
|
| 157 |
+
with gr.Row():
|
| 158 |
+
start_button = gr.Button("Start Webcam", variant="primary", scale=1)
|
| 159 |
+
stop_button = gr.Button("Stop Webcam", variant="secondary", scale=1)
|
| 160 |
+
|
| 161 |
+
stream_state = gr.State("Stop")
|
| 162 |
+
|
| 163 |
+
with gr.TabItem("Upload Image"):
|
| 164 |
+
with gr.Row(equal_height=True):
|
| 165 |
+
with gr.Column(scale=3):
|
| 166 |
+
image_input = gr.Image(type="numpy", label="Upload an Image", height=550)
|
| 167 |
+
with gr.Column(scale=2, elem_id="predictions-column"):
|
| 168 |
+
gr.Markdown("### Emotion Probabilities")
|
| 169 |
+
image_predictions = gr.HTML()
|
| 170 |
+
image_button = gr.Button("Analyze Image", variant="primary")
|
| 171 |
+
|
| 172 |
+
with gr.TabItem("Upload Video"):
|
| 173 |
+
with gr.Row(equal_height=True):
|
| 174 |
+
video_input = gr.Video(label="Upload a Video File")
|
| 175 |
+
video_output = gr.Video(label="Processed Video")
|
| 176 |
+
video_button = gr.Button("Analyze Video", variant="primary")
|
| 177 |
+
|
| 178 |
+
with gr.TabItem("About"):
|
| 179 |
+
gr.Markdown(ABOUT_MARKDOWN)
|
| 180 |
+
# --- END WRAPPER ---
|
| 181 |
+
|
| 182 |
+
# --- EVENT LISTENERS (No changes needed here) ---
|
| 183 |
+
start_event = start_button.click(lambda: "Start", None, stream_state, queue=False)
|
| 184 |
+
live_stream = start_event.then(live_detection_stream, stream_state, [live_output, live_predictions])
|
| 185 |
+
|
| 186 |
+
stop_button.click(fn=None, inputs=None, outputs=None, cancels=[live_stream])
|
| 187 |
+
|
| 188 |
+
image_button.click(process_image, [image_input], [image_input, image_predictions])
|
| 189 |
+
video_button.click(process_video, [video_input], [video_output])
|
| 190 |
+
|
| 191 |
+
# --- LAUNCH THE APP ---
|
| 192 |
+
if predictor:
|
| 193 |
+
demo.queue().launch(debug=True, share=True)
|
| 194 |
+
else:
|
| 195 |
+
print("\n[FATAL ERROR] Could not start the application.")
|
huggingface-space/config/config.yaml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
artifacts_root: artifacts
|
| 2 |
+
|
| 3 |
+
data_preparation: # This is our Stage 1
|
| 4 |
+
root_dir: artifacts/data_preparation
|
| 5 |
+
# Inputs from raw data
|
| 6 |
+
ferplus_pixels_csv: data/raw/fer2013.csv
|
| 7 |
+
ferplus_labels_csv: data/raw/fer2013new.csv
|
| 8 |
+
ckplus_dir: data/raw/CK+48
|
| 9 |
+
# Outputs
|
| 10 |
+
combined_train_dir: artifacts/data_preparation/train
|
| 11 |
+
ferplus_test_dir: artifacts/data_preparation/test
|
| 12 |
+
|
| 13 |
+
model_trainer:
|
| 14 |
+
root_dir: artifacts/training
|
| 15 |
+
# The trainer now takes its input directly from the preparation stage
|
| 16 |
+
train_data_dir: artifacts/data_preparation/train
|
| 17 |
+
test_data_dir: artifacts/data_preparation/test
|
| 18 |
+
trained_model_path: artifacts/training/model.keras
|
| 19 |
+
|
| 20 |
+
model_evaluation:
|
| 21 |
+
root_dir: artifacts/evaluation
|
| 22 |
+
test_data_dir: artifacts/data_preparation/test
|
| 23 |
+
trained_model_path: artifacts/training/model.keras
|
| 24 |
+
metrics_file_name: artifacts/evaluation/metrics.json
|
| 25 |
+
mlflow_uri: https://dagshub.com/AlyyanAhmed21/Emotion-Recognition-MLOps.mlflow # Example for DagsHub
|
huggingface-space/dvc.lock
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
schema: '2.0'
|
| 2 |
+
stages:
|
| 3 |
+
data_validation:
|
| 4 |
+
cmd: python src/EmotionRecognition/pipeline/stage_02_data_validation.py
|
| 5 |
+
deps:
|
| 6 |
+
- path: artifacts/data_ingestion
|
| 7 |
+
hash: md5
|
| 8 |
+
md5: 9208f64defb6697b78bab62e943d955d.dir
|
| 9 |
+
size: 302675528
|
| 10 |
+
nfiles: 2
|
| 11 |
+
- path: src/EmotionRecognition/config/configuration.py
|
| 12 |
+
hash: md5
|
| 13 |
+
md5: dacf4230e18681185b786aa280cdec5e
|
| 14 |
+
size: 4275
|
| 15 |
+
- path: src/EmotionRecognition/pipeline/stage_02_data_validation.py
|
| 16 |
+
hash: md5
|
| 17 |
+
md5: 18a3d78c83dc5b278e14523077035e41
|
| 18 |
+
size: 1141
|
| 19 |
+
outs:
|
| 20 |
+
- path: artifacts/data_validation/status.txt
|
| 21 |
+
hash: md5
|
| 22 |
+
md5: 86e6a2f694c57a675b3e2da6b95ff9ba
|
| 23 |
+
size: 23
|
| 24 |
+
data_preparation:
|
| 25 |
+
cmd: python src/EmotionRecognition/pipeline/stage_01_data_preparation.py
|
| 26 |
+
deps:
|
| 27 |
+
- path: data/raw/CK+48
|
| 28 |
+
hash: md5
|
| 29 |
+
md5: a1559eddfd0d86b541e5df18b4b8205e.dir
|
| 30 |
+
size: 1715162
|
| 31 |
+
nfiles: 981
|
| 32 |
+
- path: data/raw/fer2013.csv
|
| 33 |
+
hash: md5
|
| 34 |
+
md5: f8428a1edbd21e88f42c73edd2a14f95
|
| 35 |
+
size: 301072766
|
| 36 |
+
- path: data/raw/fer2013new.csv
|
| 37 |
+
hash: md5
|
| 38 |
+
md5: 413eba86d6e454536b99705b8c7fc5c5
|
| 39 |
+
size: 1602762
|
| 40 |
+
- path: src/EmotionRecognition/components/data_preparation.py
|
| 41 |
+
hash: md5
|
| 42 |
+
md5: 228140227aaedb9f07b4c00462f267c6
|
| 43 |
+
size: 5776
|
| 44 |
+
- path: src/EmotionRecognition/config/configuration.py
|
| 45 |
+
hash: md5
|
| 46 |
+
md5: 8786c8d41e2e50a49b4ca6d5bf59ad44
|
| 47 |
+
size: 2910
|
| 48 |
+
- path: src/EmotionRecognition/pipeline/stage_01_data_preparation.py
|
| 49 |
+
hash: md5
|
| 50 |
+
md5: 1a324b8f1cf01e4e60e0a8529b23b577
|
| 51 |
+
size: 1110
|
| 52 |
+
params:
|
| 53 |
+
params.yaml:
|
| 54 |
+
DATA_PARAMS.CLASSES:
|
| 55 |
+
- angry
|
| 56 |
+
- disgust
|
| 57 |
+
- fear
|
| 58 |
+
- happy
|
| 59 |
+
- neutral
|
| 60 |
+
- sad
|
| 61 |
+
- surprise
|
| 62 |
+
outs:
|
| 63 |
+
- path: artifacts/data_preparation/test
|
| 64 |
+
hash: md5
|
| 65 |
+
md5: 79c105a50ccbe2557fea9fab2c743fa5.dir
|
| 66 |
+
size: 6249935
|
| 67 |
+
nfiles: 3589
|
| 68 |
+
- path: artifacts/data_preparation/train
|
| 69 |
+
hash: md5
|
| 70 |
+
md5: 750c0a305d28467341396ab591ed2731.dir
|
| 71 |
+
size: 51232879
|
| 72 |
+
nfiles: 29471
|
| 73 |
+
model_training:
|
| 74 |
+
cmd: python src/EmotionRecognition/pipeline/stage_02_model_training.py
|
| 75 |
+
deps:
|
| 76 |
+
- path: artifacts/data_preparation/test
|
| 77 |
+
hash: md5
|
| 78 |
+
md5: 79c105a50ccbe2557fea9fab2c743fa5.dir
|
| 79 |
+
size: 6249935
|
| 80 |
+
nfiles: 3589
|
| 81 |
+
- path: artifacts/data_preparation/train
|
| 82 |
+
hash: md5
|
| 83 |
+
md5: 750c0a305d28467341396ab591ed2731.dir
|
| 84 |
+
size: 51232879
|
| 85 |
+
nfiles: 29471
|
| 86 |
+
- path: src/EmotionRecognition/components/model_trainer.py
|
| 87 |
+
hash: md5
|
| 88 |
+
md5: 5192acef195c9a9b03a88490476ead1c
|
| 89 |
+
size: 3916
|
| 90 |
+
- path: src/EmotionRecognition/pipeline/stage_02_model_training.py
|
| 91 |
+
hash: md5
|
| 92 |
+
md5: 2ee36d6e30a3a262e8327a26e71a37e9
|
| 93 |
+
size: 1076
|
| 94 |
+
params:
|
| 95 |
+
params.yaml:
|
| 96 |
+
DATA_PARAMS:
|
| 97 |
+
IMAGE_SIZE:
|
| 98 |
+
- 224
|
| 99 |
+
- 224
|
| 100 |
+
CHANNELS: 3
|
| 101 |
+
BATCH_SIZE: 32
|
| 102 |
+
CLASSES:
|
| 103 |
+
- angry
|
| 104 |
+
- disgust
|
| 105 |
+
- fear
|
| 106 |
+
- happy
|
| 107 |
+
- neutral
|
| 108 |
+
- sad
|
| 109 |
+
- surprise
|
| 110 |
+
NUM_CLASSES: 7
|
| 111 |
+
TRAINING_PARAMS:
|
| 112 |
+
EPOCHS: 50
|
| 113 |
+
LEARNING_RATE: 0.0001
|
| 114 |
+
OPTIMIZER: Adam
|
| 115 |
+
LOSS_FUNCTION: CategoricalCrossentropy
|
| 116 |
+
METRICS:
|
| 117 |
+
- accuracy
|
| 118 |
+
DROPOUT_RATE: 0.5
|
| 119 |
+
outs:
|
| 120 |
+
- path: artifacts/training/model.keras
|
| 121 |
+
hash: md5
|
| 122 |
+
md5: 2c632cb4cbf3f2944145a8da1927f2cf
|
| 123 |
+
size: 11331400
|
| 124 |
+
model_evaluation:
|
| 125 |
+
cmd: python src/EmotionRecognition/pipeline/stage_03_model_evaluation.py
|
| 126 |
+
deps:
|
| 127 |
+
- path: artifacts/data_preparation/test
|
| 128 |
+
hash: md5
|
| 129 |
+
md5: 79c105a50ccbe2557fea9fab2c743fa5.dir
|
| 130 |
+
size: 6249935
|
| 131 |
+
nfiles: 3589
|
| 132 |
+
- path: artifacts/training/model.keras
|
| 133 |
+
hash: md5
|
| 134 |
+
md5: 2c632cb4cbf3f2944145a8da1927f2cf
|
| 135 |
+
size: 11331400
|
| 136 |
+
- path: src/EmotionRecognition/components/model_evaluation.py
|
| 137 |
+
hash: md5
|
| 138 |
+
md5: 8b327667db406dd7c6489937747b8537
|
| 139 |
+
size: 2429
|
| 140 |
+
params:
|
| 141 |
+
params.yaml:
|
| 142 |
+
DATA_PARAMS:
|
| 143 |
+
IMAGE_SIZE:
|
| 144 |
+
- 224
|
| 145 |
+
- 224
|
| 146 |
+
CHANNELS: 3
|
| 147 |
+
BATCH_SIZE: 32
|
| 148 |
+
CLASSES:
|
| 149 |
+
- angry
|
| 150 |
+
- disgust
|
| 151 |
+
- fear
|
| 152 |
+
- happy
|
| 153 |
+
- neutral
|
| 154 |
+
- sad
|
| 155 |
+
- surprise
|
| 156 |
+
NUM_CLASSES: 7
|
| 157 |
+
outs:
|
| 158 |
+
- path: artifacts/evaluation/metrics.json
|
| 159 |
+
hash: md5
|
| 160 |
+
md5: 3e8f938b34095f56c597110c5d86064e
|
| 161 |
+
size: 72
|
| 162 |
+
data_preprocessing:
|
| 163 |
+
cmd: python src/EmotionRecognition/pipeline/stage_02_data_preprocessing.py
|
| 164 |
+
deps:
|
| 165 |
+
- path: artifacts/data_preparation/test
|
| 166 |
+
hash: md5
|
| 167 |
+
md5: 79c105a50ccbe2557fea9fab2c743fa5.dir
|
| 168 |
+
size: 6249935
|
| 169 |
+
nfiles: 3589
|
| 170 |
+
- path: artifacts/data_preparation/train
|
| 171 |
+
hash: md5
|
| 172 |
+
md5: 750c0a305d28467341396ab591ed2731.dir
|
| 173 |
+
size: 51232879
|
| 174 |
+
nfiles: 29471
|
| 175 |
+
- path: src/EmotionRecognition/components/data_preprocessing.py
|
| 176 |
+
hash: md5
|
| 177 |
+
md5: bc85964fdf86afb289051c2498037eb8
|
| 178 |
+
size: 3903
|
| 179 |
+
- path: src/EmotionRecognition/pipeline/stage_02_data_preprocessing.py
|
| 180 |
+
hash: md5
|
| 181 |
+
md5: 5631296a6b7bace5c2f6979eda5ca081
|
| 182 |
+
size: 971
|
| 183 |
+
params:
|
| 184 |
+
params.yaml:
|
| 185 |
+
DATA_PARAMS.CLASSES:
|
| 186 |
+
- angry
|
| 187 |
+
- disgust
|
| 188 |
+
- fear
|
| 189 |
+
- happy
|
| 190 |
+
- neutral
|
| 191 |
+
- sad
|
| 192 |
+
- surprise
|
| 193 |
+
outs:
|
| 194 |
+
- path: artifacts/data_preprocessing/test
|
| 195 |
+
hash: md5
|
| 196 |
+
md5: 79c105a50ccbe2557fea9fab2c743fa5.dir
|
| 197 |
+
size: 6249935
|
| 198 |
+
nfiles: 3589
|
| 199 |
+
- path: artifacts/data_preprocessing/train
|
| 200 |
+
hash: md5
|
| 201 |
+
md5: 3dc8382a4774d1a1f1d1e5dfe3ca4c1b.dir
|
| 202 |
+
size: 18389122
|
| 203 |
+
nfiles: 10500
|
huggingface-space/dvc.yaml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
stages:
|
| 2 |
+
data_preparation:
|
| 3 |
+
cmd: python src/EmotionRecognition/pipeline/stage_01_data_preparation.py
|
| 4 |
+
deps:
|
| 5 |
+
- src/EmotionRecognition/pipeline/stage_01_data_preparation.py
|
| 6 |
+
- src/EmotionRecognition/components/data_preparation.py
|
| 7 |
+
- data/raw/fer2013.csv
|
| 8 |
+
- data/raw/fer2013new.csv
|
| 9 |
+
- data/raw/CK+48
|
| 10 |
+
params:
|
| 11 |
+
- DATA_PARAMS.CLASSES
|
| 12 |
+
outs:
|
| 13 |
+
- artifacts/data_preparation/train
|
| 14 |
+
- artifacts/data_preparation/test
|
| 15 |
+
|
| 16 |
+
model_training:
|
| 17 |
+
cmd: python src/EmotionRecognition/pipeline/stage_02_model_training.py
|
| 18 |
+
deps:
|
| 19 |
+
- src/EmotionRecognition/pipeline/stage_02_model_training.py
|
| 20 |
+
- src/EmotionRecognition/components/model_trainer.py
|
| 21 |
+
- artifacts/data_preparation/train
|
| 22 |
+
- artifacts/data_preparation/test
|
| 23 |
+
params:
|
| 24 |
+
- DATA_PARAMS
|
| 25 |
+
- TRAINING_PARAMS
|
| 26 |
+
outs:
|
| 27 |
+
- artifacts/training/model.keras
|
| 28 |
+
|
| 29 |
+
model_evaluation:
|
| 30 |
+
cmd: python src/EmotionRecognition/pipeline/stage_03_model_evaluation.py
|
| 31 |
+
deps:
|
| 32 |
+
- src/EmotionRecognition/components/model_evaluation.py
|
| 33 |
+
- artifacts/data_preparation/test
|
| 34 |
+
- artifacts/training/model.keras
|
| 35 |
+
params:
|
| 36 |
+
- DATA_PARAMS
|
| 37 |
+
metrics:
|
| 38 |
+
- artifacts/evaluation/metrics.json:
|
| 39 |
+
cache: false
|
huggingface-space/gpuCheck.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tensorflow as tf
|
| 3 |
+
|
| 4 |
+
# --- THE WORKAROUND ---
|
| 5 |
+
# Define the full path to the CUDA bin directory
|
| 6 |
+
cuda_bin_path = r"E:\Nvidia\CUDA\v11.2\bin"
|
| 7 |
+
|
| 8 |
+
# Add this path to the OS environment's DLL search path
|
| 9 |
+
# This MUST be done BEFORE importing tensorflow
|
| 10 |
+
try:
|
| 11 |
+
os.add_dll_directory(cuda_bin_path)
|
| 12 |
+
print(f"Successfully added {cuda_bin_path} to DLL search path.")
|
| 13 |
+
except AttributeError:
|
| 14 |
+
# This function was added in Python 3.8. For older versions, you might need
|
| 15 |
+
# to add the path to the system PATH environment variable manually.
|
| 16 |
+
print("os.add_dll_directory not available. Ensure CUDA bin is in the system PATH.")
|
| 17 |
+
# --- END WORKAROUND ---
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
print(f"TensorFlow Version: {tf.__version__}")
|
| 21 |
+
print("-" * 30)
|
| 22 |
+
|
| 23 |
+
# Check for GPU devices
|
| 24 |
+
gpu_devices = tf.config.list_physical_devices('GPU')
|
| 25 |
+
print(f"Num GPUs Available: {len(gpu_devices)}")
|
| 26 |
+
print("-" * 30)
|
| 27 |
+
|
| 28 |
+
if gpu_devices:
|
| 29 |
+
print("GPU Device Details:")
|
| 30 |
+
for gpu in gpu_devices:
|
| 31 |
+
tf.config.experimental.set_memory_growth(gpu, True)
|
| 32 |
+
print(f"- {gpu.name}, Type: {gpu.device_type}")
|
| 33 |
+
print("\nSUCCESS: TensorFlow is configured to use the GPU!")
|
| 34 |
+
else:
|
| 35 |
+
print("\nFAILURE: TensorFlow did not detect a GPU.")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
import tensorflow as tf
|
| 39 |
+
from tensorflow.python.client import device_lib
|
| 40 |
+
|
| 41 |
+
print("Verbose device list:")
|
| 42 |
+
print(device_lib.list_local_devices())
|
huggingface-space/huggingface-space/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
sota_model/model.safetensors filter=lfs diff=lfs merge=lfs -text
|
huggingface-space/huggingface-space/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎭 End-to-End Facial Emotion Recognition
|
| 2 |
+
|
| 3 |
+
<!-- Replace with a link to your final app screenshot -->
|
| 4 |
+
|
| 5 |
+
This repository contains a complete, end-to-end MLOps pipeline and a production-ready web application for real-time facial emotion recognition. The project leverages a state-of-the-art Vision Transformer model and is deployed as a user-friendly Gradio application on Hugging Face Spaces.
|
| 6 |
+
|
| 7 |
+
**Live Demo:** [🚀 Click here to try the application on Hugging Face Spaces!](https://huggingface.co/spaces/YOUR-USERNAME/YOUR-SPACE-NAME) <!-- Replace with your HF Space URL -->
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## ✨ Features
|
| 12 |
+
|
| 13 |
+
- **Real-time Emotion Detection:** Analyzes your webcam feed to predict emotions in real-time.
|
| 14 |
+
- **High Accuracy:** Powered by a pre-trained Swin Transformer model fine-tuned on the massive AffectNet dataset for superior performance on "in the wild" faces.
|
| 15 |
+
- **Static Image & Video Analysis:** Upload your own images or videos for emotion prediction.
|
| 16 |
+
- **Polished UI:** A professional and responsive user interface with an animated background, built with Gradio.
|
| 17 |
+
- **Reproducible MLOps Pipeline:** The entire model training and data processing workflow is managed by DVC, ensuring 100% reproducibility.
|
| 18 |
+
- **Containerized for Deployment:** The application is packaged with Docker for easy and consistent deployment anywhere.
|
| 19 |
+
|
| 20 |
+
## 🛠️ Tech Stack
|
| 21 |
+
|
| 22 |
+
- **Model:** Swin Transformer (`PangPang/affectnet-swin-tiny-patch4-window7-224`)
|
| 23 |
+
- **ML/Ops:** Python, TensorFlow/Keras, DVC, MLflow, Hugging Face `transformers`
|
| 24 |
+
- **Backend & UI:** Gradio
|
| 25 |
+
- **Face Detection:** MTCNN
|
| 26 |
+
- **Deployment:** Hugging Face Spaces, Docker
|
| 27 |
+
|
| 28 |
+
## 🚀 Getting Started
|
| 29 |
+
|
| 30 |
+
Follow these steps to run the project locally.
|
| 31 |
+
|
| 32 |
+
### Prerequisites
|
| 33 |
+
|
| 34 |
+
- Python 3.10+
|
| 35 |
+
- Git and Git LFS ([installation guide](https://git-lfs.github.com))
|
| 36 |
+
- An NVIDIA GPU with CUDA drivers is recommended for the training pipeline, but the deployed app runs on CPU.
|
| 37 |
+
|
| 38 |
+
### 1. Clone the Repository
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
git clone https://github.com/YOUR-USERNAME/Emotion-Recognition-MLOps.git
|
| 42 |
+
cd Emotion-Recognition-MLOps
|
huggingface-space/main.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from EmotionRecognition import logger
|
| 2 |
+
from EmotionRecognition.pipeline.stage_01_data_ingestion import DataIngestionTrainingPipeline
|
| 3 |
+
from EmotionRecognition.pipeline.stage_02_data_validation import DataValidationTrainingPipeline
|
| 4 |
+
from EmotionRecognition.pipeline.stage_01_data_preparation import DataPreparationPipeline
|
| 5 |
+
from EmotionRecognition.pipeline.stage_02_model_training import ModelTrainingPipeline
|
| 6 |
+
from EmotionRecognition.pipeline.stage_03_model_evaluation import ModelEvaluationPipeline
|
| 7 |
+
|
| 8 |
+
# Data Ingestion Stage
|
| 9 |
+
STAGE_NAME = "Data Ingestion Stage"
|
| 10 |
+
try:
|
| 11 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 12 |
+
obj = DataIngestionTrainingPipeline()
|
| 13 |
+
obj.main()
|
| 14 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 15 |
+
except Exception as e:
|
| 16 |
+
logger.exception(e)
|
| 17 |
+
raise e
|
| 18 |
+
|
| 19 |
+
# Data Validation Stage
|
| 20 |
+
STAGE_NAME = "Data Validation Stage"
|
| 21 |
+
try:
|
| 22 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 23 |
+
obj = DataValidationTrainingPipeline()
|
| 24 |
+
obj.main()
|
| 25 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 26 |
+
except Exception as e:
|
| 27 |
+
logger.exception(e)
|
| 28 |
+
raise e
|
| 29 |
+
|
| 30 |
+
# Data Preprocessing Stage
|
| 31 |
+
#STAGE_NAME = "Data Preprocessing Stage"
|
| 32 |
+
#try:
|
| 33 |
+
# logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 34 |
+
# obj = DataPreprocessingTrainingPipeline()
|
| 35 |
+
# obj.main()
|
| 36 |
+
# logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 37 |
+
#except Exception as e:
|
| 38 |
+
# logger.exception(e)
|
| 39 |
+
# raise e
|
| 40 |
+
|
| 41 |
+
STAGE_NAME = "Data Preparation Stage"
|
| 42 |
+
try:
|
| 43 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 44 |
+
obj = DataPreparationPipeline()
|
| 45 |
+
obj.main()
|
| 46 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 47 |
+
except Exception as e:
|
| 48 |
+
logger.exception(e)
|
| 49 |
+
raise e
|
| 50 |
+
|
| 51 |
+
STAGE_NAME = "Model Training Stage"
|
| 52 |
+
try:
|
| 53 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 54 |
+
obj = ModelTrainingPipeline()
|
| 55 |
+
obj.main()
|
| 56 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 57 |
+
except Exception as e:
|
| 58 |
+
logger.exception(e)
|
| 59 |
+
raise e
|
| 60 |
+
|
| 61 |
+
# Model Evaluation Stage
|
| 62 |
+
STAGE_NAME = "Model Evaluation Stage"
|
| 63 |
+
try:
|
| 64 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 65 |
+
obj = ModelEvaluationPipeline()
|
| 66 |
+
obj.main()
|
| 67 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.exception(e)
|
| 70 |
+
raise e
|
huggingface-space/params.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DATA_PARAMS:
|
| 2 |
+
IMAGE_SIZE: [224, 224]
|
| 3 |
+
CHANNELS: 3
|
| 4 |
+
BATCH_SIZE: 32
|
| 5 |
+
# Our final 7 classes (Contempt is merged into Disgust)
|
| 6 |
+
CLASSES: ['angry', 'disgust', 'fear', 'happy', 'neutral', 'sad', 'surprise']
|
| 7 |
+
NUM_CLASSES: 7
|
| 8 |
+
|
| 9 |
+
TRAINING_PARAMS:
|
| 10 |
+
EPOCHS: 50 # A solid number for a baseline run
|
| 11 |
+
LEARNING_RATE: 0.0001 # A small, stable learning rate
|
| 12 |
+
OPTIMIZER: Adam
|
| 13 |
+
LOSS_FUNCTION: CategoricalCrossentropy
|
| 14 |
+
METRICS: ['accuracy']
|
| 15 |
+
DROPOUT_RATE: 0.5 # Strong regularization is good
|
huggingface-space/requirements.txt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -----------------------------------------------------------
|
| 2 |
+
# MLOps Pipeline & Data Versioning
|
| 3 |
+
# -----------------------------------------------------------
|
| 4 |
+
dvc[s3] # For data versioning. Change [s3] to your remote type or remove.
|
| 5 |
+
kaggle # For downloading datasets from Kaggle.
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# -----------------------------------------------------------
|
| 9 |
+
# Core Machine Learning & Deep Learning
|
| 10 |
+
# -----------------------------------------------------------
|
| 11 |
+
# Using the stable TensorFlow 2.10 for GPU support on native Windows
|
| 12 |
+
tensorflow==2.10.0
|
| 13 |
+
scikit-learn # For evaluation metrics.
|
| 14 |
+
|
| 15 |
+
# Hugging Face SOTA Model Dependencies
|
| 16 |
+
transformers # For loading models and processors from the Hub.
|
| 17 |
+
torch # PyTorch is a backend dependency for many HF vision models.
|
| 18 |
+
torchvision
|
| 19 |
+
timm # Another common dependency for HF vision transformers.
|
| 20 |
+
|
| 21 |
+
# Computer Vision
|
| 22 |
+
opencv-python # For image/video processing and drawing.
|
| 23 |
+
mtcnn # For fast and effective face detection.
|
| 24 |
+
Pillow # For basic image manipulation.
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# -----------------------------------------------------------
|
| 28 |
+
# Web Application & User Interface
|
| 29 |
+
# -----------------------------------------------------------
|
| 30 |
+
gradio==3.50.2 # Locked to a stable version for consistent UI behavior.
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# -----------------------------------------------------------
|
| 34 |
+
# Utilities
|
| 35 |
+
# -----------------------------------------------------------
|
| 36 |
+
numpy
|
| 37 |
+
pandas # For data manipulation in the preparation stage.
|
| 38 |
+
PyYAML # For reading .yaml configuration files.
|
| 39 |
+
python-box # For dot-notation access to config dictionaries.
|
| 40 |
+
tqdm # For progress bars in scripts.
|
| 41 |
+
ensure # For runtime type checking.
|
| 42 |
+
matplotlib # For plotting (useful in research).
|
| 43 |
+
seaborn # For advanced plotting (useful in research).
|
| 44 |
+
notebook # For running Jupyter notebooks.
|
huggingface-space/research/01_data_exploration.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
huggingface-space/setup.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import setuptools
|
| 2 |
+
|
| 3 |
+
with open("README.md", "r", encoding="utf-8") as f:
|
| 4 |
+
long_description = f.read()
|
| 5 |
+
|
| 6 |
+
__version__ = "0.0.1"
|
| 7 |
+
|
| 8 |
+
REPO_NAME = "Emotion-Recognition-MLOps"
|
| 9 |
+
AUTHOR_USER_NAME = "AlyyanAhmed21"
|
| 10 |
+
SRC_REPO = "EmotionRecognition"
|
| 11 |
+
AUTHOR_EMAIL = "alyyanawan19@gmail.com"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
setuptools.setup(
|
| 15 |
+
name=SRC_REPO,
|
| 16 |
+
version=__version__,
|
| 17 |
+
author=AUTHOR_USER_NAME,
|
| 18 |
+
author_email=AUTHOR_EMAIL,
|
| 19 |
+
description="A small python package for MLOps based facial emotion detection app",
|
| 20 |
+
long_description=long_description,
|
| 21 |
+
long_description_content_type="text/markdown",
|
| 22 |
+
url=f"https://github.com/{AUTHOR_USER_NAME}/{REPO_NAME}",
|
| 23 |
+
project_urls={
|
| 24 |
+
"Bug Tracker": f"https://github.com/{AUTHOR_USER_NAME}/{REPO_NAME}/issues",
|
| 25 |
+
},
|
| 26 |
+
package_dir={"": "src"},
|
| 27 |
+
packages=setuptools.find_packages(where="src")
|
| 28 |
+
)
|
huggingface-space/src/EmotionRecognition/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import logging
|
| 4 |
+
|
| 5 |
+
# Define the logging format
|
| 6 |
+
logging_str = "[%(asctime)s: %(levelname)s: %(module)s: %(message)s]"
|
| 7 |
+
|
| 8 |
+
# Define the directory for log files
|
| 9 |
+
log_dir = "logs"
|
| 10 |
+
log_filepath = os.path.join(log_dir, "running_logs.log")
|
| 11 |
+
os.makedirs(log_dir, exist_ok=True)
|
| 12 |
+
|
| 13 |
+
# Configure the logging
|
| 14 |
+
logging.basicConfig(
|
| 15 |
+
level=logging.INFO,
|
| 16 |
+
format=logging_str,
|
| 17 |
+
|
| 18 |
+
handlers=[
|
| 19 |
+
logging.FileHandler(log_filepath), # Log to a file
|
| 20 |
+
logging.StreamHandler(sys.stdout) # Log to the console
|
| 21 |
+
]
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Create a logger object that can be imported by other modules
|
| 25 |
+
logger = logging.getLogger("EmotionRecognitionLogger")
|
huggingface-space/src/EmotionRecognition/components/__init__.py
ADDED
|
File without changes
|
huggingface-space/src/EmotionRecognition/components/data_ingestion.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: src/EmotionRecognition/components/data_ingestion.py
|
| 2 |
+
import os
|
| 3 |
+
from EmotionRecognition import logger
|
| 4 |
+
from EmotionRecognition.entity.config_entity import DataIngestionConfig
|
| 5 |
+
|
| 6 |
+
class DataIngestion:
|
| 7 |
+
def __init__(self, config: DataIngestionConfig):
|
| 8 |
+
self.config = config
|
| 9 |
+
|
| 10 |
+
def validate_source_data(self):
|
| 11 |
+
"""
|
| 12 |
+
Validates the existence of all raw source data files and folders.
|
| 13 |
+
"""
|
| 14 |
+
logger.info("Validating source data files and folders...")
|
| 15 |
+
|
| 16 |
+
all_paths = [
|
| 17 |
+
self.config.root_dir,
|
| 18 |
+
self.config.ferplus_pixels_csv,
|
| 19 |
+
self.config.ferplus_labels_csv,
|
| 20 |
+
self.config.ckplus_dir
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
for path in all_paths:
|
| 24 |
+
if not os.path.exists(path):
|
| 25 |
+
raise FileNotFoundError(f"Missing required raw data source: {path}")
|
| 26 |
+
|
| 27 |
+
logger.info("All raw data sources found successfully.")
|
huggingface-space/src/EmotionRecognition/components/data_preparation.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: src/EmotionRecognition/components/data_preparation.py
|
| 2 |
+
import os
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
import shutil
|
| 8 |
+
from EmotionRecognition import logger
|
| 9 |
+
from EmotionRecognition.entity.config_entity import DataPreparationConfig
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import glob
|
| 12 |
+
|
| 13 |
+
class DataPreparation:
|
| 14 |
+
def __init__(self, config: DataPreparationConfig, params: dict):
|
| 15 |
+
self.config = config
|
| 16 |
+
self.params = params.DATA_PARAMS
|
| 17 |
+
|
| 18 |
+
def _process_and_save(self, best_emotion_name, usage, index, pixels, dataset_prefix):
|
| 19 |
+
"""Helper function to handle the merging logic and save images."""
|
| 20 |
+
|
| 21 |
+
# --- MERGING LOGIC ---
|
| 22 |
+
# If the emotion is 'contempt', we re-label it as 'disgust'.
|
| 23 |
+
if best_emotion_name == 'contempt':
|
| 24 |
+
final_emotion_name = 'disgust'
|
| 25 |
+
else:
|
| 26 |
+
final_emotion_name = best_emotion_name
|
| 27 |
+
# --- END MERGING LOGIC ---
|
| 28 |
+
|
| 29 |
+
# Check if this emotion is one of our final target classes
|
| 30 |
+
if final_emotion_name in self.params.CLASSES:
|
| 31 |
+
if usage == 'Training':
|
| 32 |
+
output_dir = self.config.combined_train_dir
|
| 33 |
+
elif usage == 'PublicTest':
|
| 34 |
+
output_dir = self.config.ferplus_test_dir
|
| 35 |
+
else:
|
| 36 |
+
return # Skip other usages like PrivateTest
|
| 37 |
+
|
| 38 |
+
image = Image.fromarray(pixels)
|
| 39 |
+
emotion_folder = Path(output_dir) / final_emotion_name
|
| 40 |
+
emotion_folder.mkdir(parents=True, exist_ok=True)
|
| 41 |
+
image.save(emotion_folder / f"{dataset_prefix}_{index}.png")
|
| 42 |
+
|
| 43 |
+
def _prepare_ferplus(self):
|
| 44 |
+
logger.info("Starting preparation of FER+ dataset...")
|
| 45 |
+
pixels_df = pd.read_csv(self.config.ferplus_pixels_csv)
|
| 46 |
+
labels_df = pd.read_csv(self.config.ferplus_labels_csv)
|
| 47 |
+
|
| 48 |
+
ferplus_emotion_columns = ['neutral', 'happiness', 'surprise', 'sadness', 'anger', 'disgust', 'fear', 'contempt']
|
| 49 |
+
|
| 50 |
+
for index, row in tqdm(pixels_df.iterrows(), total=len(pixels_df), desc="Processing FER+ Images"):
|
| 51 |
+
label_votes = labels_df.iloc[index][ferplus_emotion_columns].values
|
| 52 |
+
source_emotion_name = ferplus_emotion_columns[np.argmax(label_votes)]
|
| 53 |
+
|
| 54 |
+
# --- STANDARDIZE THE NAME ---
|
| 55 |
+
# Default to the source name
|
| 56 |
+
our_emotion_name = source_emotion_name
|
| 57 |
+
if source_emotion_name == 'happiness': our_emotion_name = 'happy'
|
| 58 |
+
if source_emotion_name == 'sadness': our_emotion_name = 'sad'
|
| 59 |
+
if source_emotion_name == 'anger': our_emotion_name = 'angry'
|
| 60 |
+
if source_emotion_name == 'contempt': our_emotion_name = 'disgust' # MERGE
|
| 61 |
+
|
| 62 |
+
if our_emotion_name in self.params.CLASSES:
|
| 63 |
+
usage = row['Usage']
|
| 64 |
+
if usage == 'Training': output_dir = self.config.combined_train_dir
|
| 65 |
+
elif usage == 'PublicTest': output_dir = self.config.ferplus_test_dir
|
| 66 |
+
else: continue
|
| 67 |
+
|
| 68 |
+
pixels = np.array(row['pixels'].split(), 'uint8').reshape((48, 48))
|
| 69 |
+
image = Image.fromarray(pixels)
|
| 70 |
+
emotion_folder = Path(output_dir) / our_emotion_name
|
| 71 |
+
emotion_folder.mkdir(parents=True, exist_ok=True)
|
| 72 |
+
image.save(emotion_folder / f"ferplus_{index}.png")
|
| 73 |
+
|
| 74 |
+
logger.info("FER+ dataset preparation complete.")
|
| 75 |
+
|
| 76 |
+
def _prepare_ckplus(self):
|
| 77 |
+
logger.info("Starting preparation of CK+ dataset...")
|
| 78 |
+
|
| 79 |
+
for ckplus_folder_name in tqdm(os.listdir(self.config.ckplus_dir), desc="Processing CK+ Folders"):
|
| 80 |
+
source_emotion_dir = Path(self.config.ckplus_dir) / ckplus_folder_name
|
| 81 |
+
|
| 82 |
+
# --- STANDARDIZE THE NAME ---
|
| 83 |
+
our_emotion_name = ckplus_folder_name # Default
|
| 84 |
+
if ckplus_folder_name == 'contempt': our_emotion_name = 'disgust' # MERGE
|
| 85 |
+
|
| 86 |
+
if our_emotion_name in self.params.CLASSES and source_emotion_dir.is_dir():
|
| 87 |
+
dest_emotion_dir = Path(self.config.combined_train_dir) / our_emotion_name
|
| 88 |
+
dest_emotion_dir.mkdir(parents=True, exist_ok=True)
|
| 89 |
+
|
| 90 |
+
for img_file in os.listdir(source_emotion_dir):
|
| 91 |
+
shutil.copy(source_emotion_dir / img_file, dest_emotion_dir / f"ckplus_{img_file}")
|
| 92 |
+
|
| 93 |
+
logger.info("CK+ dataset preparation complete.")
|
| 94 |
+
|
| 95 |
+
def _log_dataset_statistics(self):
|
| 96 |
+
logger.info("--- Final Dataset Statistics ---")
|
| 97 |
+
logger.info("Training Set:")
|
| 98 |
+
for emotion in sorted(self.params.CLASSES):
|
| 99 |
+
count = len(glob.glob(str(self.config.combined_train_dir / emotion / '*.png')))
|
| 100 |
+
logger.info(f"- {emotion}: {count} images")
|
| 101 |
+
|
| 102 |
+
logger.info("\nTest Set:")
|
| 103 |
+
for emotion in sorted(self.params.CLASSES):
|
| 104 |
+
count = len(glob.glob(str(self.config.ferplus_test_dir / emotion / '*.png')))
|
| 105 |
+
logger.info(f"- {emotion}: {count} images")
|
| 106 |
+
logger.info("---------------------------------")
|
| 107 |
+
|
| 108 |
+
def combine_and_prepare_data(self):
|
| 109 |
+
logger.info("--- Starting Data Preparation Stage ---")
|
| 110 |
+
if os.path.exists(self.config.combined_train_dir): shutil.rmtree(self.config.combined_train_dir)
|
| 111 |
+
if os.path.exists(self.config.ferplus_test_dir): shutil.rmtree(self.config.ferplus_test_dir)
|
| 112 |
+
os.makedirs(self.config.combined_train_dir, exist_ok=True)
|
| 113 |
+
os.makedirs(self.config.ferplus_test_dir, exist_ok=True)
|
| 114 |
+
|
| 115 |
+
self._prepare_ferplus()
|
| 116 |
+
self._prepare_ckplus()
|
| 117 |
+
self._log_dataset_statistics()
|
| 118 |
+
logger.info("--- Data Preparation Stage Complete ---")
|
huggingface-space/src/EmotionRecognition/components/data_preprocessing.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: src/EmotionRecognition/components/data_preprocessing.py
|
| 2 |
+
import os
|
| 3 |
+
import shutil
|
| 4 |
+
import random
|
| 5 |
+
import glob
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
from EmotionRecognition import logger
|
| 8 |
+
from EmotionRecognition.entity.config_entity import DataPreprocessingConfig
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
class DataPreprocessing:
|
| 12 |
+
def __init__(self, config: DataPreprocessingConfig, params: dict):
|
| 13 |
+
self.config = config
|
| 14 |
+
self.params = params.DATA_PARAMS
|
| 15 |
+
|
| 16 |
+
def _log_and_get_stats(self, directory):
|
| 17 |
+
"""Helper to get and log image counts for a directory."""
|
| 18 |
+
stats = {}
|
| 19 |
+
logger.info(f"Statistics for directory: {directory}")
|
| 20 |
+
for emotion in sorted(self.params.CLASSES):
|
| 21 |
+
path = Path(directory) / emotion
|
| 22 |
+
count = len(glob.glob(str(path / '*.png')))
|
| 23 |
+
stats[emotion] = count
|
| 24 |
+
logger.info(f"- {emotion}: {count} images")
|
| 25 |
+
return stats
|
| 26 |
+
|
| 27 |
+
def balance_dataset(self):
|
| 28 |
+
"""
|
| 29 |
+
Applies a hybrid oversampling and undersampling strategy to balance the training data.
|
| 30 |
+
"""
|
| 31 |
+
logger.info("--- Starting Hybrid Data Balancing Stage ---")
|
| 32 |
+
|
| 33 |
+
logger.info("Source Training Set Distribution:")
|
| 34 |
+
self._log_and_get_stats(self.config.source_train_dir)
|
| 35 |
+
|
| 36 |
+
if os.path.exists(self.config.balanced_train_dir): shutil.rmtree(self.config.balanced_train_dir)
|
| 37 |
+
os.makedirs(self.config.balanced_train_dir, exist_ok=True)
|
| 38 |
+
|
| 39 |
+
target_count = self.config.target_samples_per_class
|
| 40 |
+
logger.info(f"\nBalancing all training classes to {target_count} samples each...")
|
| 41 |
+
|
| 42 |
+
for emotion in tqdm(self.params.CLASSES, desc="Balancing Classes"):
|
| 43 |
+
source_emotion_dir = Path(self.config.source_train_dir) / emotion
|
| 44 |
+
dest_emotion_dir = Path(self.config.balanced_train_dir) / emotion
|
| 45 |
+
dest_emotion_dir.mkdir(parents=True, exist_ok=True)
|
| 46 |
+
|
| 47 |
+
image_files = os.listdir(source_emotion_dir)
|
| 48 |
+
|
| 49 |
+
if not image_files:
|
| 50 |
+
logger.warning(f"No images found for class '{emotion}'. Skipping.")
|
| 51 |
+
continue
|
| 52 |
+
|
| 53 |
+
current_count = len(image_files)
|
| 54 |
+
|
| 55 |
+
if current_count > target_count:
|
| 56 |
+
# Undersampling: Randomly select 'target_count' unique images
|
| 57 |
+
selected_files = random.sample(image_files, target_count)
|
| 58 |
+
else:
|
| 59 |
+
# Oversampling: Select with replacement to reach 'target_count'
|
| 60 |
+
selected_files = random.choices(image_files, k=target_count)
|
| 61 |
+
|
| 62 |
+
# --- THIS IS THE BUG FIX ---
|
| 63 |
+
# Copy the selected files, giving duplicates new names.
|
| 64 |
+
for i, filename in enumerate(selected_files):
|
| 65 |
+
# Get the original file's extension
|
| 66 |
+
base_name, extension = os.path.splitext(filename)
|
| 67 |
+
|
| 68 |
+
# If oversampling, create a unique name for each copy to prevent overwriting
|
| 69 |
+
if current_count < target_count:
|
| 70 |
+
dest_filename = f"{base_name}_copy{i}{extension}"
|
| 71 |
+
else:
|
| 72 |
+
dest_filename = filename # For undersampling, names are already unique
|
| 73 |
+
|
| 74 |
+
shutil.copy(source_emotion_dir / filename, dest_emotion_dir / dest_filename)
|
| 75 |
+
# --- END BUG FIX ---
|
| 76 |
+
|
| 77 |
+
# Copy the test set without changes
|
| 78 |
+
logger.info("\nCopying test set...")
|
| 79 |
+
if os.path.exists(self.config.balanced_test_dir): shutil.rmtree(self.config.balanced_test_dir)
|
| 80 |
+
shutil.copytree(self.config.source_test_dir, self.config.balanced_test_dir)
|
| 81 |
+
|
| 82 |
+
logger.info("\n--- Final Balanced Dataset Statistics ---")
|
| 83 |
+
self._log_and_get_stats(self.config.balanced_train_dir)
|
| 84 |
+
self._log_and_get_stats(self.config.balanced_test_dir)
|
| 85 |
+
|
| 86 |
+
logger.info("--- Data Balancing Stage Complete ---")
|
huggingface-space/src/EmotionRecognition/components/data_validation.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# In src/EmotionRecognition/components/data_validation.py
|
| 2 |
+
import os
|
| 3 |
+
from EmotionRecognition import logger
|
| 4 |
+
from EmotionRecognition.entity.config_entity import DataValidationConfig
|
| 5 |
+
|
| 6 |
+
class DataValidation:
|
| 7 |
+
def __init__(self, config: DataValidationConfig):
|
| 8 |
+
self.config = config
|
| 9 |
+
|
| 10 |
+
def validate_all_files_exist(self) -> bool:
|
| 11 |
+
try:
|
| 12 |
+
validation_status = True
|
| 13 |
+
|
| 14 |
+
# Check for all required files
|
| 15 |
+
for required_file in self.config.required_files:
|
| 16 |
+
if not os.path.exists(required_file):
|
| 17 |
+
validation_status = False
|
| 18 |
+
logger.error(f"Missing required file: {required_file}")
|
| 19 |
+
|
| 20 |
+
with open(self.config.status_file, 'w') as f:
|
| 21 |
+
f.write(f"Validation status: {validation_status}")
|
| 22 |
+
|
| 23 |
+
if validation_status:
|
| 24 |
+
logger.info("Data validation successful. All required files exist.")
|
| 25 |
+
else:
|
| 26 |
+
logger.error("Data validation failed. Please check the logs for missing files.")
|
| 27 |
+
|
| 28 |
+
return validation_status
|
| 29 |
+
|
| 30 |
+
except Exception as e:
|
| 31 |
+
logger.exception(e)
|
| 32 |
+
raise e
|
huggingface-space/src/EmotionRecognition/components/model_evaluation.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
from EmotionRecognition import logger
|
| 3 |
+
from EmotionRecognition.entity.config_entity import ModelEvaluationConfig
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import mlflow
|
| 6 |
+
import mlflow.keras
|
| 7 |
+
from EmotionRecognition.utils.common import save_json
|
| 8 |
+
|
| 9 |
+
class ModelEvaluation:
|
| 10 |
+
def __init__(self, config: ModelEvaluationConfig, params: dict):
|
| 11 |
+
self.config = config
|
| 12 |
+
self.params = params
|
| 13 |
+
|
| 14 |
+
def get_validation_dataset(self):
|
| 15 |
+
"""
|
| 16 |
+
Loads the prepared validation/test dataset from disk.
|
| 17 |
+
"""
|
| 18 |
+
data_params = self.params.DATA_PARAMS
|
| 19 |
+
|
| 20 |
+
val_ds = tf.keras.utils.image_dataset_from_directory(
|
| 21 |
+
self.config.test_data_dir,
|
| 22 |
+
labels='inferred',
|
| 23 |
+
label_mode='categorical',
|
| 24 |
+
class_names=data_params.CLASSES,
|
| 25 |
+
image_size=data_params.IMAGE_SIZE,
|
| 26 |
+
interpolation='nearest',
|
| 27 |
+
batch_size=data_params.BATCH_SIZE,
|
| 28 |
+
shuffle=False,
|
| 29 |
+
color_mode='grayscale' # <--- THIS IS THE FIX
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def preprocess(image, label):
|
| 33 |
+
image = tf.image.grayscale_to_rgb(image)
|
| 34 |
+
image = tf.cast(image, tf.float32) / 255.0
|
| 35 |
+
return image, label
|
| 36 |
+
|
| 37 |
+
return val_ds.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE).prefetch(tf.data.AUTOTUNE)
|
| 38 |
+
|
| 39 |
+
def evaluate_and_log(self):
|
| 40 |
+
logger.info("Preparing validation dataset for evaluation...")
|
| 41 |
+
val_ds = self.get_validation_dataset()
|
| 42 |
+
|
| 43 |
+
logger.info("Loading full trained model from disk...")
|
| 44 |
+
model = tf.keras.models.load_model(str(self.config.trained_model_path))
|
| 45 |
+
|
| 46 |
+
logger.info("Evaluating model...")
|
| 47 |
+
score = model.evaluate(val_ds)
|
| 48 |
+
|
| 49 |
+
scores = {"loss": score[0], "accuracy": score[1]}
|
| 50 |
+
logger.info(f"Evaluation scores: {scores}")
|
| 51 |
+
save_json(path=self.config.metrics_file_name, data=scores)
|
| 52 |
+
|
| 53 |
+
logger.info("Starting MLflow logging...")
|
| 54 |
+
mlflow.set_tracking_uri(self.config.mlflow_uri)
|
| 55 |
+
mlflow.set_experiment("Emotion Recognition Experiment")
|
| 56 |
+
|
| 57 |
+
with mlflow.start_run():
|
| 58 |
+
mlflow.log_params(self.params.DATA_PARAMS)
|
| 59 |
+
mlflow.log_params(self.params.TRAINING_PARAMS)
|
| 60 |
+
mlflow.log_metrics(scores)
|
| 61 |
+
mlflow.keras.log_model(model, "model")
|
| 62 |
+
|
| 63 |
+
logger.info("MLflow logging complete.")
|
huggingface-space/src/EmotionRecognition/components/model_trainer.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
from EmotionRecognition import logger
|
| 3 |
+
from EmotionRecognition.entity.config_entity import ModelTrainerConfig
|
| 4 |
+
from EmotionRecognition.utils.common import create_mobilenetv2_model
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
class ModelTrainer:
|
| 8 |
+
def __init__(self, config: ModelTrainerConfig, params: dict):
|
| 9 |
+
self.config = config
|
| 10 |
+
self.params = params
|
| 11 |
+
self.model = None
|
| 12 |
+
|
| 13 |
+
def get_datasets(self):
|
| 14 |
+
data_params = self.params.DATA_PARAMS
|
| 15 |
+
logger.info("Loading prepared train and test datasets...")
|
| 16 |
+
|
| 17 |
+
# Create a training dataset from the combined, imbalanced data
|
| 18 |
+
train_ds = tf.keras.utils.image_dataset_from_directory(
|
| 19 |
+
self.config.train_data_dir,
|
| 20 |
+
labels='inferred',
|
| 21 |
+
label_mode='categorical',
|
| 22 |
+
class_names=data_params.CLASSES,
|
| 23 |
+
image_size=data_params.IMAGE_SIZE,
|
| 24 |
+
interpolation='nearest',
|
| 25 |
+
batch_size=data_params.BATCH_SIZE,
|
| 26 |
+
shuffle=True,
|
| 27 |
+
color_mode='grayscale' # <--- ADD THIS LINE
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# Create a validation/test dataset
|
| 31 |
+
val_ds = tf.keras.utils.image_dataset_from_directory(
|
| 32 |
+
self.config.test_data_dir,
|
| 33 |
+
labels='inferred',
|
| 34 |
+
label_mode='categorical',
|
| 35 |
+
class_names=data_params.CLASSES,
|
| 36 |
+
image_size=data_params.IMAGE_SIZE,
|
| 37 |
+
interpolation='nearest',
|
| 38 |
+
batch_size=data_params.BATCH_SIZE,
|
| 39 |
+
shuffle=False,
|
| 40 |
+
color_mode='grayscale' # <--- AND ADD THIS LINE
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
def preprocess(image, label):
|
| 44 |
+
# This dataset is already in PNG format, so we decode PNG
|
| 45 |
+
# It's also already grayscale (1 channel)
|
| 46 |
+
image = tf.image.grayscale_to_rgb(image) # Models expect 3 channels
|
| 47 |
+
image = tf.cast(image, tf.float32) / 255.0
|
| 48 |
+
return image, label
|
| 49 |
+
|
| 50 |
+
data_augmentation = tf.keras.Sequential([
|
| 51 |
+
tf.keras.layers.RandomFlip("horizontal"),
|
| 52 |
+
tf.keras.layers.RandomRotation(0.1),
|
| 53 |
+
tf.keras.layers.RandomZoom(0.1)
|
| 54 |
+
])
|
| 55 |
+
|
| 56 |
+
train_ds = train_ds.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)
|
| 57 |
+
val_ds = val_ds.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)
|
| 58 |
+
train_ds = train_ds.map(lambda x, y: (data_augmentation(x, training=True), y), num_parallel_calls=tf.data.AUTOTUNE)
|
| 59 |
+
|
| 60 |
+
return train_ds.prefetch(tf.data.AUTOTUNE), val_ds.prefetch(tf.data.AUTOTUNE)
|
| 61 |
+
|
| 62 |
+
def build_and_train_model(self):
|
| 63 |
+
data_params = self.params.DATA_PARAMS
|
| 64 |
+
training_params = self.params.TRAINING_PARAMS
|
| 65 |
+
|
| 66 |
+
logger.info("Building model with a frozen MobileNetV2 base...")
|
| 67 |
+
input_shape = data_params.IMAGE_SIZE + [data_params.CHANNELS]
|
| 68 |
+
|
| 69 |
+
self.model = create_mobilenetv2_model(
|
| 70 |
+
input_shape=input_shape,
|
| 71 |
+
num_classes=data_params.NUM_CLASSES,
|
| 72 |
+
dropout_rate=training_params.DROPOUT_RATE
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
base_model = self.model.layers[1]
|
| 76 |
+
base_model.trainable = False
|
| 77 |
+
|
| 78 |
+
self.model.compile(
|
| 79 |
+
optimizer=tf.keras.optimizers.Adam(learning_rate=training_params.LEARNING_RATE),
|
| 80 |
+
loss=training_params.LOSS_FUNCTION,
|
| 81 |
+
metrics=training_params.METRICS
|
| 82 |
+
)
|
| 83 |
+
self.model.summary(print_fn=logger.info)
|
| 84 |
+
|
| 85 |
+
train_ds, val_ds = self.get_datasets()
|
| 86 |
+
|
| 87 |
+
logger.info(f"--- Starting training for {training_params.EPOCHS} epochs ---")
|
| 88 |
+
self.model.fit(
|
| 89 |
+
train_ds,
|
| 90 |
+
epochs=training_params.EPOCHS,
|
| 91 |
+
validation_data=val_ds
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
self.save_model()
|
| 95 |
+
|
| 96 |
+
def save_model(self):
|
| 97 |
+
model_path = str(self.config.trained_model_path)
|
| 98 |
+
self.model.save(model_path)
|
| 99 |
+
logger.info(f"Full model saved successfully to: {model_path}")
|
huggingface-space/src/EmotionRecognition/config/__init__.py
ADDED
|
File without changes
|
huggingface-space/src/EmotionRecognition/config/configuration.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from EmotionRecognition.utils.common import read_yaml, create_directories
|
| 2 |
+
from EmotionRecognition.entity.config_entity import (
|
| 3 |
+
DataPreparationConfig,
|
| 4 |
+
DataPreprocessingConfig,
|
| 5 |
+
ModelTrainerConfig,
|
| 6 |
+
ModelEvaluationConfig
|
| 7 |
+
)
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
class ConfigurationManager:
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
config_filepath = Path("config/config.yaml"),
|
| 14 |
+
params_filepath = Path("params.yaml")):
|
| 15 |
+
|
| 16 |
+
self.config = read_yaml(config_filepath)
|
| 17 |
+
self.params = read_yaml(params_filepath)
|
| 18 |
+
create_directories([self.config.artifacts_root])
|
| 19 |
+
|
| 20 |
+
def get_data_preparation_config(self) -> DataPreparationConfig:
|
| 21 |
+
prep_config = self.config.data_preparation
|
| 22 |
+
# Note: Raw data paths are now defined in data_preparation, not a separate ingestion config
|
| 23 |
+
create_directories([prep_config.root_dir])
|
| 24 |
+
return DataPreparationConfig(
|
| 25 |
+
root_dir=Path(prep_config.root_dir),
|
| 26 |
+
ferplus_pixels_csv=Path(prep_config.ferplus_pixels_csv),
|
| 27 |
+
ferplus_labels_csv=Path(prep_config.ferplus_labels_csv),
|
| 28 |
+
ckplus_dir=Path(prep_config.ckplus_dir),
|
| 29 |
+
combined_train_dir=Path(prep_config.combined_train_dir),
|
| 30 |
+
ferplus_test_dir=Path(prep_config.ferplus_test_dir)
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
def get_data_preprocessing_config(self) -> DataPreprocessingConfig:
|
| 34 |
+
preprocess_config = self.config.data_preprocessing
|
| 35 |
+
create_directories([preprocess_config.root_dir])
|
| 36 |
+
return DataPreprocessingConfig(
|
| 37 |
+
root_dir=Path(preprocess_config.root_dir),
|
| 38 |
+
source_train_dir=Path(preprocess_config.source_train_dir),
|
| 39 |
+
source_test_dir=Path(preprocess_config.source_test_dir),
|
| 40 |
+
balanced_train_dir=Path(preprocess_config.balanced_train_dir),
|
| 41 |
+
balanced_test_dir=Path(preprocess_config.balanced_test_dir),
|
| 42 |
+
target_samples_per_class=preprocess_config.target_samples_per_class
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
def get_model_trainer_config(self) -> ModelTrainerConfig:
|
| 46 |
+
config = self.config.model_trainer
|
| 47 |
+
create_directories([config.root_dir])
|
| 48 |
+
return ModelTrainerConfig(
|
| 49 |
+
root_dir=Path(config.root_dir),
|
| 50 |
+
train_data_dir=Path(config.train_data_dir),
|
| 51 |
+
test_data_dir=Path(config.test_data_dir),
|
| 52 |
+
trained_model_path=Path(config.trained_model_path)
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
def get_model_evaluation_config(self) -> ModelEvaluationConfig:
|
| 56 |
+
config = self.config.model_evaluation
|
| 57 |
+
create_directories([config.root_dir])
|
| 58 |
+
return ModelEvaluationConfig(
|
| 59 |
+
root_dir=Path(config.root_dir),
|
| 60 |
+
test_data_dir=Path(config.test_data_dir), # Corrected from data_dir
|
| 61 |
+
trained_model_path=Path(config.trained_model_path),
|
| 62 |
+
metrics_file_name=Path(config.metrics_file_name),
|
| 63 |
+
mlflow_uri=config.mlflow_uri
|
| 64 |
+
)
|
huggingface-space/src/EmotionRecognition/entity/__init__.py
ADDED
|
File without changes
|
huggingface-space/src/EmotionRecognition/entity/config_entity.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
@dataclass(frozen=True)
|
| 5 |
+
class DataPreparationConfig:
|
| 6 |
+
root_dir: Path
|
| 7 |
+
# Inputs from raw data
|
| 8 |
+
ferplus_pixels_csv: Path
|
| 9 |
+
ferplus_labels_csv: Path
|
| 10 |
+
ckplus_dir: Path
|
| 11 |
+
# Outputs of this stage
|
| 12 |
+
combined_train_dir: Path
|
| 13 |
+
ferplus_test_dir: Path
|
| 14 |
+
|
| 15 |
+
@dataclass(frozen=True)
|
| 16 |
+
class DataPreprocessingConfig:
|
| 17 |
+
root_dir: Path
|
| 18 |
+
# Inputs from the Data Preparation stage
|
| 19 |
+
source_train_dir: Path
|
| 20 |
+
source_test_dir: Path
|
| 21 |
+
# Outputs of this stage
|
| 22 |
+
balanced_train_dir: Path
|
| 23 |
+
balanced_test_dir: Path
|
| 24 |
+
# Parameter for the balancing strategy
|
| 25 |
+
target_samples_per_class: int
|
| 26 |
+
|
| 27 |
+
@dataclass(frozen=True)
|
| 28 |
+
class ModelTrainerConfig:
|
| 29 |
+
root_dir: Path
|
| 30 |
+
# Inputs from the Data Preprocessing stage
|
| 31 |
+
train_data_dir: Path
|
| 32 |
+
test_data_dir: Path
|
| 33 |
+
# Output of this stage
|
| 34 |
+
trained_model_path: Path
|
| 35 |
+
|
| 36 |
+
@dataclass(frozen=True)
|
| 37 |
+
class ModelEvaluationConfig:
|
| 38 |
+
root_dir: Path
|
| 39 |
+
test_data_dir: Path
|
| 40 |
+
trained_model_path: Path # <-- Make sure this is the name used
|
| 41 |
+
metrics_file_name: Path
|
| 42 |
+
mlflow_uri: str
|
huggingface-space/src/EmotionRecognition/pipeline/__init__.py
ADDED
|
File without changes
|
huggingface-space/src/EmotionRecognition/pipeline/hf_predictor.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import AutoImageProcessor, AutoModelForImageClassification
|
| 2 |
+
import torch
|
| 3 |
+
import numpy as np
|
| 4 |
+
import cv2
|
| 5 |
+
from mtcnn import MTCNN
|
| 6 |
+
from collections import deque, Counter
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
LOCAL_MODEL_PATH = "sota_model"
|
| 10 |
+
|
| 11 |
+
class HFPredictor:
|
| 12 |
+
def __init__(self, smoothing_window=10, confidence_threshold=0.3):
|
| 13 |
+
print(f"[PREDICTOR INFO] Loading model from local path: {LOCAL_MODEL_PATH}...")
|
| 14 |
+
self.processor = AutoImageProcessor.from_pretrained(LOCAL_MODEL_PATH)
|
| 15 |
+
self.model = AutoModelForImageClassification.from_pretrained(LOCAL_MODEL_PATH)
|
| 16 |
+
self.face_detector = MTCNN()
|
| 17 |
+
self.classes = list(self.model.config.id2label.values())
|
| 18 |
+
self.confidence_threshold = confidence_threshold
|
| 19 |
+
self.recent_predictions = deque(maxlen=smoothing_window)
|
| 20 |
+
self.stable_prediction = "---"
|
| 21 |
+
print("[PREDICTOR INFO] Predictor initialized successfully.")
|
| 22 |
+
|
| 23 |
+
def get_probabilities(self, frame):
|
| 24 |
+
"""
|
| 25 |
+
A lightweight function that takes a frame, runs inference,
|
| 26 |
+
updates the stable prediction, and returns ONLY the probability dictionary.
|
| 27 |
+
"""
|
| 28 |
+
if frame is None:
|
| 29 |
+
return {}
|
| 30 |
+
|
| 31 |
+
probabilities = {}
|
| 32 |
+
faces = self.face_detector.detect_faces(frame)
|
| 33 |
+
|
| 34 |
+
for face in faces:
|
| 35 |
+
x, y, width, height = face['box']
|
| 36 |
+
face_roi = frame[y:y+height, x:x+width]
|
| 37 |
+
|
| 38 |
+
if face_roi.size > 0:
|
| 39 |
+
pil_image = Image.fromarray(face_roi)
|
| 40 |
+
inputs = self.processor(images=pil_image, return_tensors="pt")
|
| 41 |
+
with torch.no_grad():
|
| 42 |
+
logits = self.model(**inputs).logits
|
| 43 |
+
|
| 44 |
+
probs = torch.nn.functional.softmax(logits, dim=-1)
|
| 45 |
+
predictions = probs[0].numpy()
|
| 46 |
+
pred_index = np.argmax(predictions)
|
| 47 |
+
confidence = predictions[pred_index]
|
| 48 |
+
|
| 49 |
+
if confidence > self.confidence_threshold:
|
| 50 |
+
self.recent_predictions.append(pred_index)
|
| 51 |
+
|
| 52 |
+
probabilities = {self.classes[i]: float(predictions[i]) for i in range(len(self.classes))}
|
| 53 |
+
|
| 54 |
+
return probabilities
|
| 55 |
+
|
| 56 |
+
def annotate_frame(self, frame):
|
| 57 |
+
"""
|
| 58 |
+
Takes a frame, detects faces, and returns the fully annotated version
|
| 59 |
+
using the latest stable prediction.
|
| 60 |
+
"""
|
| 61 |
+
if frame is None: return None
|
| 62 |
+
|
| 63 |
+
annotated_frame = frame.copy()
|
| 64 |
+
faces = self.face_detector.detect_faces(frame)
|
| 65 |
+
|
| 66 |
+
# We use the 'stable_prediction' which is updated by the high-fps get_probabilities call
|
| 67 |
+
# This ensures the box text is smooth and consistent.
|
| 68 |
+
for face in faces:
|
| 69 |
+
x, y, width, height = face['box']
|
| 70 |
+
GREEN = (0, 255, 0)
|
| 71 |
+
BLACK = (0, 0, 0)
|
| 72 |
+
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
| 73 |
+
text = self.stable_prediction # Use the smoothed prediction
|
| 74 |
+
|
| 75 |
+
(text_width, text_height), baseline = cv2.getTextSize(text, FONT, 0.8, 2)
|
| 76 |
+
cv2.rectangle(annotated_frame, (x, y - text_height - baseline - 10), (x + text_width + 10, y), GREEN, cv2.FILLED)
|
| 77 |
+
cv2.putText(annotated_frame, text, (x + 5, y - 5), FONT, 0.8, BLACK, 2)
|
| 78 |
+
cv2.rectangle(annotated_frame, (x, y), (x+width, y+height), GREEN, 3)
|
| 79 |
+
|
| 80 |
+
return annotated_frame
|
| 81 |
+
|
| 82 |
+
def process_frame_for_upload(self, frame):
|
| 83 |
+
"""A simple, all-in-one function for static images and videos."""
|
| 84 |
+
if frame is None: return None, {}
|
| 85 |
+
annotated_frame = frame.copy()
|
| 86 |
+
probabilities = {}
|
| 87 |
+
faces = self.face_detector.detect_faces(frame)
|
| 88 |
+
for face in faces:
|
| 89 |
+
x, y, width, height = face['box']
|
| 90 |
+
face_roi = frame[y:y+height, x:x+width]
|
| 91 |
+
if face_roi.size > 0:
|
| 92 |
+
pil_image = Image.fromarray(face_roi)
|
| 93 |
+
inputs = self.processor(images=pil_image, return_tensors="pt")
|
| 94 |
+
with torch.no_grad():
|
| 95 |
+
logits = self.model(**inputs).logits
|
| 96 |
+
probs = torch.nn.functional.softmax(logits, dim=-1)
|
| 97 |
+
predictions = probs[0].numpy()
|
| 98 |
+
pred_index = np.argmax(predictions)
|
| 99 |
+
emotion = self.classes[pred_index]
|
| 100 |
+
confidence = predictions[pred_index]
|
| 101 |
+
text = f"{emotion} ({confidence*100:.1f}%)"
|
| 102 |
+
# (Drawing logic is duplicated here for simplicity)
|
| 103 |
+
GREEN = (0, 255, 0); BLACK = (0, 0, 0); FONT = cv2.FONT_HERSHEY_SIMPLEX
|
| 104 |
+
(tw, th), bl = cv2.getTextSize(text, FONT, 0.8, 2)
|
| 105 |
+
cv2.rectangle(annotated_frame, (x, y-th-bl-10), (x+tw+10, y), GREEN, cv2.FILLED)
|
| 106 |
+
cv2.putText(annotated_frame, text, (x + 5, y - 5), FONT, 0.8, BLACK, 2)
|
| 107 |
+
cv2.rectangle(annotated_frame, (x, y), (x+width, y+height), GREEN, 3)
|
| 108 |
+
probabilities = {self.classes[i]: float(predictions[i]) for i in range(len(self.classes))}
|
| 109 |
+
return annotated_frame, probabilities
|
huggingface-space/src/EmotionRecognition/pipeline/stage_01_data_preparation.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: src/EmotionRecognition/pipeline/stage_01_data_preparation.py
|
| 2 |
+
from EmotionRecognition.config.configuration import ConfigurationManager
|
| 3 |
+
from EmotionRecognition.components.data_preparation import DataPreparation
|
| 4 |
+
from EmotionRecognition import logger
|
| 5 |
+
|
| 6 |
+
STAGE_NAME = "Data Preparation Stage"
|
| 7 |
+
|
| 8 |
+
class DataPreparationPipeline:
|
| 9 |
+
def main(self):
|
| 10 |
+
config_manager = ConfigurationManager()
|
| 11 |
+
data_prep_config = config_manager.get_data_preparation_config()
|
| 12 |
+
data_preparation = DataPreparation(config=data_prep_config, params=config_manager.params)
|
| 13 |
+
|
| 14 |
+
# --- THIS IS THE FIX ---
|
| 15 |
+
# Call the correct method name from the component
|
| 16 |
+
data_preparation.combine_and_prepare_data()
|
| 17 |
+
# --- END FIX ---
|
| 18 |
+
|
| 19 |
+
if __name__ == '__main__':
|
| 20 |
+
try:
|
| 21 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 22 |
+
pipeline = DataPreparationPipeline()
|
| 23 |
+
pipeline.main()
|
| 24 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 25 |
+
except Exception as e:
|
| 26 |
+
logger.exception(e)
|
| 27 |
+
raise e
|
huggingface-space/src/EmotionRecognition/pipeline/stage_02_model_training.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: src/EmotionRecognition/pipeline/stage_02_model_training.py
|
| 2 |
+
|
| 3 |
+
from EmotionRecognition.config.configuration import ConfigurationManager
|
| 4 |
+
from EmotionRecognition.components.model_trainer import ModelTrainer
|
| 5 |
+
from EmotionRecognition import logger
|
| 6 |
+
|
| 7 |
+
STAGE_NAME = "Model Training Stage"
|
| 8 |
+
|
| 9 |
+
class ModelTrainingPipeline:
|
| 10 |
+
def main(self):
|
| 11 |
+
try:
|
| 12 |
+
config_manager = ConfigurationManager()
|
| 13 |
+
model_trainer_config = config_manager.get_model_trainer_config()
|
| 14 |
+
model_trainer = ModelTrainer(config=model_trainer_config, params=config_manager.params)
|
| 15 |
+
model_trainer.build_and_train_model()
|
| 16 |
+
except Exception as e:
|
| 17 |
+
logger.exception(e)
|
| 18 |
+
raise e
|
| 19 |
+
|
| 20 |
+
if __name__ == '__main__':
|
| 21 |
+
try:
|
| 22 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 23 |
+
pipeline = ModelTrainingPipeline()
|
| 24 |
+
pipeline.main()
|
| 25 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 26 |
+
except Exception as e:
|
| 27 |
+
logger.exception(e)
|
| 28 |
+
raise e
|
huggingface-space/src/EmotionRecognition/pipeline/stage_03_model_evaluation.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from EmotionRecognition.config.configuration import ConfigurationManager
|
| 2 |
+
from EmotionRecognition.components.model_evaluation import ModelEvaluation
|
| 3 |
+
from EmotionRecognition import logger
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
STAGE_NAME = "Model Evaluation Stage"
|
| 8 |
+
|
| 9 |
+
class ModelEvaluationPipeline:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
pass
|
| 12 |
+
|
| 13 |
+
def main(self):
|
| 14 |
+
try:
|
| 15 |
+
config_manager = ConfigurationManager()
|
| 16 |
+
model_evaluation_config = config_manager.get_model_evaluation_config()
|
| 17 |
+
model_evaluation = ModelEvaluation(config=model_evaluation_config, params=config_manager.params)
|
| 18 |
+
model_evaluation.evaluate_and_log()
|
| 19 |
+
except Exception as e:
|
| 20 |
+
logger.exception(e)
|
| 21 |
+
raise e
|
| 22 |
+
|
| 23 |
+
if __name__ == '__main__':
|
| 24 |
+
try:
|
| 25 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' started <<<<<<")
|
| 26 |
+
obj = ModelEvaluationPipeline()
|
| 27 |
+
obj.main()
|
| 28 |
+
logger.info(f">>>>>> Stage '{STAGE_NAME}' completed successfully <<<<<<\n\nx==========x")
|
| 29 |
+
except Exception as e:
|
| 30 |
+
logger.exception(e)
|
| 31 |
+
raise e
|
huggingface-space/src/EmotionRecognition/utils/__init__.py
ADDED
|
File without changes
|
huggingface-space/src/EmotionRecognition/utils/common.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from box.exceptions import BoxValueError
|
| 3 |
+
import yaml
|
| 4 |
+
from EmotionRecognition import logger
|
| 5 |
+
import json
|
| 6 |
+
from ensure import ensure_annotations
|
| 7 |
+
from box import ConfigBox
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
import json
|
| 11 |
+
import tensorflow as tf
|
| 12 |
+
|
| 13 |
+
@ensure_annotations
|
| 14 |
+
def read_yaml(path_to_yaml: Path) -> ConfigBox:
|
| 15 |
+
"""reads yaml file and returns
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
path_to_yaml (str): path like input
|
| 19 |
+
|
| 20 |
+
Raises:
|
| 21 |
+
ValueError: if yaml file is empty
|
| 22 |
+
e: empty file
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
ConfigBox: ConfigBox type
|
| 26 |
+
"""
|
| 27 |
+
try:
|
| 28 |
+
with open(path_to_yaml) as yaml_file:
|
| 29 |
+
content = yaml.safe_load(yaml_file)
|
| 30 |
+
logger.info(f"yaml file: {path_to_yaml} loaded successfully")
|
| 31 |
+
return ConfigBox(content)
|
| 32 |
+
except BoxValueError:
|
| 33 |
+
raise ValueError("yaml file is empty")
|
| 34 |
+
except Exception as e:
|
| 35 |
+
raise e
|
| 36 |
+
|
| 37 |
+
@ensure_annotations
|
| 38 |
+
def create_directories(path_to_directories: list, verbose=True):
|
| 39 |
+
"""create list of directories
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
path_to_directories (list): list of path of directories
|
| 43 |
+
ignore_log (bool, optional): ignore if multiple dirs is to be created. Defaults to False.
|
| 44 |
+
"""
|
| 45 |
+
for path in path_to_directories:
|
| 46 |
+
os.makedirs(path, exist_ok=True)
|
| 47 |
+
if verbose:
|
| 48 |
+
logger.info(f"created directory at: {path}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def save_json(path: Path, data: dict):
|
| 52 |
+
with open(path, "w") as f:
|
| 53 |
+
json.dump(data, f, indent=4)
|
| 54 |
+
logger.info(f"json file saved at: {path}")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def create_mobilenetv2_model(input_shape, num_classes, dropout_rate, is_training=True): # <--- ADD ARGUMENT
|
| 59 |
+
"""
|
| 60 |
+
Builds the MobileNetV2 model with our custom head.
|
| 61 |
+
This centralized function ensures consistency.
|
| 62 |
+
"""
|
| 63 |
+
base_model = tf.keras.applications.MobileNetV2(
|
| 64 |
+
input_shape=input_shape, include_top=False, weights='imagenet'
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
inputs = tf.keras.Input(shape=input_shape)
|
| 68 |
+
# --- CRITICAL CHANGE ---
|
| 69 |
+
# Pass the is_training flag to the base model call
|
| 70 |
+
x = base_model(inputs, training=is_training)
|
| 71 |
+
# --- END CHANGE ---
|
| 72 |
+
x = tf.keras.layers.GlobalAveragePooling2D()(x)
|
| 73 |
+
x = tf.keras.layers.Dense(128, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(0.01))(x)
|
| 74 |
+
x = tf.keras.layers.Dropout(dropout_rate)(x)
|
| 75 |
+
outputs = tf.keras.layers.Dense(num_classes, activation='softmax')(x)
|
| 76 |
+
|
| 77 |
+
model = tf.keras.Model(inputs, outputs)
|
| 78 |
+
return model
|
huggingface-space/temp.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import logging
|
| 4 |
+
|
| 5 |
+
# Configure logging to provide feedback during script execution
|
| 6 |
+
logging.basicConfig(level=logging.INFO, format='[%(asctime)s]: %(message)s:')
|
| 7 |
+
|
| 8 |
+
# Define the project name
|
| 9 |
+
project_name = "EmotionRecognition"
|
| 10 |
+
|
| 11 |
+
# List of files and directories to be created
|
| 12 |
+
list_of_files = [
|
| 13 |
+
".github/workflows/.gitkeep",
|
| 14 |
+
f"src/{project_name}/__init__.py",
|
| 15 |
+
f"src/{project_name}/components/__init__.py",
|
| 16 |
+
f"src/{project_name}/components/data_ingestion.py",
|
| 17 |
+
f"src/{project_name}/components/data_validation.py",
|
| 18 |
+
f"src/{project_name}/components/data_preprocessing.py",
|
| 19 |
+
f"src/{project_name}/components/model_trainer.py",
|
| 20 |
+
f"src/{project_name}/components/model_evaluation.py",
|
| 21 |
+
f"src/{project_name}/utils/__init__.py",
|
| 22 |
+
f"src/{project_name}/utils/common.py",
|
| 23 |
+
f"src/{project_name}/config/__init__.py",
|
| 24 |
+
f"src/{project_name}/config/configuration.py",
|
| 25 |
+
f"src/{project_name}/pipeline/__init__.py",
|
| 26 |
+
f"src/{project_name}/pipeline/stage_01_data_ingestion.py",
|
| 27 |
+
f"src/{project_name}/pipeline/stage_02_data_validation.py",
|
| 28 |
+
f"src/{project_name}/pipeline/stage_03_data_preprocessing.py",
|
| 29 |
+
f"src/{project_name}/pipeline/stage_04_model_training.py",
|
| 30 |
+
f"src/{project_name}/pipeline/stage_05_model_evaluation.py",
|
| 31 |
+
f"src/{project_name}/pipeline/prediction.py",
|
| 32 |
+
f"src/{project_name}/entity/__init__.py",
|
| 33 |
+
f"src/{project_name}/entity/config_entity.py",
|
| 34 |
+
"config/config.yaml",
|
| 35 |
+
"params.yaml",
|
| 36 |
+
"app.py",
|
| 37 |
+
"main.py",
|
| 38 |
+
"Dockerfile",
|
| 39 |
+
"requirements.txt",
|
| 40 |
+
"setup.py",
|
| 41 |
+
"research/01_data_exploration.ipynb",
|
| 42 |
+
"templates/index.html", # For a simple web UI if needed
|
| 43 |
+
".dvcignore",
|
| 44 |
+
".gitignore"
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
# Loop through the list to create the files and directories
|
| 48 |
+
for filepath_str in list_of_files:
|
| 49 |
+
filepath = Path(filepath_str)
|
| 50 |
+
filedir, filename = os.path.split(filepath)
|
| 51 |
+
|
| 52 |
+
# 1. Create the directory if it doesn't exist
|
| 53 |
+
if filedir != "":
|
| 54 |
+
os.makedirs(filedir, exist_ok=True)
|
| 55 |
+
logging.info(f"Creating directory: {filedir} for the file: {filename}")
|
| 56 |
+
|
| 57 |
+
# 2. Create the file if it doesn't exist or is empty
|
| 58 |
+
if (not os.path.exists(filepath)) or (os.path.getsize(filepath) == 0):
|
| 59 |
+
with open(filepath, "w") as f:
|
| 60 |
+
pass # Create an empty file
|
| 61 |
+
logging.info(f"Creating empty file: {filepath}")
|
| 62 |
+
else:
|
| 63 |
+
logging.info(f"{filename} already exists")
|
| 64 |
+
|
| 65 |
+
logging.info("Project structure creation complete!")
|
huggingface-space/templates/index.html
ADDED
|
File without changes
|
sota_model/config.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
| 1 |
-
{
|
| 2 |
-
"_name_or_path": "dima806/facial_emotions_image_detection",
|
| 3 |
-
"architectures": [
|
| 4 |
-
"ViTForImageClassification"
|
| 5 |
-
],
|
| 6 |
-
"attention_probs_dropout_prob": 0.0,
|
| 7 |
-
"encoder_stride": 16,
|
| 8 |
-
"hidden_act": "gelu",
|
| 9 |
-
"hidden_dropout_prob": 0.0,
|
| 10 |
-
"hidden_size": 768,
|
| 11 |
-
"id2label": {
|
| 12 |
-
"0": "sad",
|
| 13 |
-
"1": "disgust",
|
| 14 |
-
"2": "angry",
|
| 15 |
-
"3": "neutral",
|
| 16 |
-
"4": "fear",
|
| 17 |
-
"5": "surprise",
|
| 18 |
-
"6": "happy"
|
| 19 |
-
},
|
| 20 |
-
"image_size": 224,
|
| 21 |
-
"initializer_range": 0.02,
|
| 22 |
-
"intermediate_size": 3072,
|
| 23 |
-
"label2id": {
|
| 24 |
-
"angry": 2,
|
| 25 |
-
"disgust": 1,
|
| 26 |
-
"fear": 4,
|
| 27 |
-
"happy": 6,
|
| 28 |
-
"neutral": 3,
|
| 29 |
-
"sad": 0,
|
| 30 |
-
"surprise": 5
|
| 31 |
-
},
|
| 32 |
-
"layer_norm_eps": 1e-12,
|
| 33 |
-
"model_type": "vit",
|
| 34 |
-
"num_attention_heads": 12,
|
| 35 |
-
"num_channels": 3,
|
| 36 |
-
"num_hidden_layers": 12,
|
| 37 |
-
"patch_size": 16,
|
| 38 |
-
"problem_type": "single_label_classification",
|
| 39 |
-
"qkv_bias": true,
|
| 40 |
-
"torch_dtype": "float32",
|
| 41 |
-
"transformers_version": "4.39.3"
|
| 42 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_name_or_path": "dima806/facial_emotions_image_detection",
|
| 3 |
+
"architectures": [
|
| 4 |
+
"ViTForImageClassification"
|
| 5 |
+
],
|
| 6 |
+
"attention_probs_dropout_prob": 0.0,
|
| 7 |
+
"encoder_stride": 16,
|
| 8 |
+
"hidden_act": "gelu",
|
| 9 |
+
"hidden_dropout_prob": 0.0,
|
| 10 |
+
"hidden_size": 768,
|
| 11 |
+
"id2label": {
|
| 12 |
+
"0": "sad",
|
| 13 |
+
"1": "disgust",
|
| 14 |
+
"2": "angry",
|
| 15 |
+
"3": "neutral",
|
| 16 |
+
"4": "fear",
|
| 17 |
+
"5": "surprise",
|
| 18 |
+
"6": "happy"
|
| 19 |
+
},
|
| 20 |
+
"image_size": 224,
|
| 21 |
+
"initializer_range": 0.02,
|
| 22 |
+
"intermediate_size": 3072,
|
| 23 |
+
"label2id": {
|
| 24 |
+
"angry": 2,
|
| 25 |
+
"disgust": 1,
|
| 26 |
+
"fear": 4,
|
| 27 |
+
"happy": 6,
|
| 28 |
+
"neutral": 3,
|
| 29 |
+
"sad": 0,
|
| 30 |
+
"surprise": 5
|
| 31 |
+
},
|
| 32 |
+
"layer_norm_eps": 1e-12,
|
| 33 |
+
"model_type": "vit",
|
| 34 |
+
"num_attention_heads": 12,
|
| 35 |
+
"num_channels": 3,
|
| 36 |
+
"num_hidden_layers": 12,
|
| 37 |
+
"patch_size": 16,
|
| 38 |
+
"problem_type": "single_label_classification",
|
| 39 |
+
"qkv_bias": true,
|
| 40 |
+
"torch_dtype": "float32",
|
| 41 |
+
"transformers_version": "4.39.3"
|
| 42 |
+
}
|
sota_model/preprocessor_config.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
| 1 |
-
{
|
| 2 |
-
"_valid_processor_keys": [
|
| 3 |
-
"images",
|
| 4 |
-
"do_resize",
|
| 5 |
-
"size",
|
| 6 |
-
"resample",
|
| 7 |
-
"do_rescale",
|
| 8 |
-
"rescale_factor",
|
| 9 |
-
"do_normalize",
|
| 10 |
-
"image_mean",
|
| 11 |
-
"image_std",
|
| 12 |
-
"return_tensors",
|
| 13 |
-
"data_format",
|
| 14 |
-
"input_data_format"
|
| 15 |
-
],
|
| 16 |
-
"do_normalize": true,
|
| 17 |
-
"do_rescale": true,
|
| 18 |
-
"do_resize": true,
|
| 19 |
-
"image_mean": [
|
| 20 |
-
0.5,
|
| 21 |
-
0.5,
|
| 22 |
-
0.5
|
| 23 |
-
],
|
| 24 |
-
"image_processor_type": "ViTImageProcessor",
|
| 25 |
-
"image_std": [
|
| 26 |
-
0.5,
|
| 27 |
-
0.5,
|
| 28 |
-
0.5
|
| 29 |
-
],
|
| 30 |
-
"resample": 2,
|
| 31 |
-
"rescale_factor": 0.00392156862745098,
|
| 32 |
-
"size": {
|
| 33 |
-
"height": 224,
|
| 34 |
-
"width": 224
|
| 35 |
-
}
|
| 36 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_valid_processor_keys": [
|
| 3 |
+
"images",
|
| 4 |
+
"do_resize",
|
| 5 |
+
"size",
|
| 6 |
+
"resample",
|
| 7 |
+
"do_rescale",
|
| 8 |
+
"rescale_factor",
|
| 9 |
+
"do_normalize",
|
| 10 |
+
"image_mean",
|
| 11 |
+
"image_std",
|
| 12 |
+
"return_tensors",
|
| 13 |
+
"data_format",
|
| 14 |
+
"input_data_format"
|
| 15 |
+
],
|
| 16 |
+
"do_normalize": true,
|
| 17 |
+
"do_rescale": true,
|
| 18 |
+
"do_resize": true,
|
| 19 |
+
"image_mean": [
|
| 20 |
+
0.5,
|
| 21 |
+
0.5,
|
| 22 |
+
0.5
|
| 23 |
+
],
|
| 24 |
+
"image_processor_type": "ViTImageProcessor",
|
| 25 |
+
"image_std": [
|
| 26 |
+
0.5,
|
| 27 |
+
0.5,
|
| 28 |
+
0.5
|
| 29 |
+
],
|
| 30 |
+
"resample": 2,
|
| 31 |
+
"rescale_factor": 0.00392156862745098,
|
| 32 |
+
"size": {
|
| 33 |
+
"height": 224,
|
| 34 |
+
"width": 224
|
| 35 |
+
}
|
| 36 |
+
}
|
src/EmotionRecognition/pipeline/hf_predictor.py
CHANGED
|
@@ -20,19 +20,26 @@ class HFPredictor:
|
|
| 20 |
self.stable_prediction = "---"
|
| 21 |
print("[PREDICTOR INFO] Predictor initialized successfully.")
|
| 22 |
|
| 23 |
-
|
|
|
|
| 24 |
"""
|
| 25 |
-
|
| 26 |
-
|
| 27 |
"""
|
| 28 |
-
if frame is None:
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
faces = self.face_detector.detect_faces(frame)
|
| 33 |
|
| 34 |
for face in faces:
|
| 35 |
x, y, width, height = face['box']
|
|
|
|
| 36 |
face_roi = frame[y:y+height, x:x+width]
|
| 37 |
|
| 38 |
if face_roi.size > 0:
|
|
@@ -44,66 +51,27 @@ class HFPredictor:
|
|
| 44 |
probs = torch.nn.functional.softmax(logits, dim=-1)
|
| 45 |
predictions = probs[0].numpy()
|
| 46 |
pred_index = np.argmax(predictions)
|
|
|
|
|
|
|
| 47 |
confidence = predictions[pred_index]
|
| 48 |
-
|
| 49 |
if confidence > self.confidence_threshold:
|
| 50 |
self.recent_predictions.append(pred_index)
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
if frame is None: return None
|
| 62 |
-
|
| 63 |
-
annotated_frame = frame.copy()
|
| 64 |
-
faces = self.face_detector.detect_faces(frame)
|
| 65 |
-
|
| 66 |
-
# We use the 'stable_prediction' which is updated by the high-fps get_probabilities call
|
| 67 |
-
# This ensures the box text is smooth and consistent.
|
| 68 |
-
for face in faces:
|
| 69 |
-
x, y, width, height = face['box']
|
| 70 |
-
GREEN = (0, 255, 0)
|
| 71 |
-
BLACK = (0, 0, 0)
|
| 72 |
-
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
| 73 |
-
text = self.stable_prediction # Use the smoothed prediction
|
| 74 |
-
|
| 75 |
-
(text_width, text_height), baseline = cv2.getTextSize(text, FONT, 0.8, 2)
|
| 76 |
-
cv2.rectangle(annotated_frame, (x, y - text_height - baseline - 10), (x + text_width + 10, y), GREEN, cv2.FILLED)
|
| 77 |
-
cv2.putText(annotated_frame, text, (x + 5, y - 5), FONT, 0.8, BLACK, 2)
|
| 78 |
-
cv2.rectangle(annotated_frame, (x, y), (x+width, y+height), GREEN, 3)
|
| 79 |
-
|
| 80 |
-
return annotated_frame
|
| 81 |
-
|
| 82 |
-
def process_frame_for_upload(self, frame):
|
| 83 |
-
"""A simple, all-in-one function for static images and videos."""
|
| 84 |
-
if frame is None: return None, {}
|
| 85 |
-
annotated_frame = frame.copy()
|
| 86 |
-
probabilities = {}
|
| 87 |
-
faces = self.face_detector.detect_faces(frame)
|
| 88 |
-
for face in faces:
|
| 89 |
-
x, y, width, height = face['box']
|
| 90 |
-
face_roi = frame[y:y+height, x:x+width]
|
| 91 |
-
if face_roi.size > 0:
|
| 92 |
-
pil_image = Image.fromarray(face_roi)
|
| 93 |
-
inputs = self.processor(images=pil_image, return_tensors="pt")
|
| 94 |
-
with torch.no_grad():
|
| 95 |
-
logits = self.model(**inputs).logits
|
| 96 |
-
probs = torch.nn.functional.softmax(logits, dim=-1)
|
| 97 |
-
predictions = probs[0].numpy()
|
| 98 |
-
pred_index = np.argmax(predictions)
|
| 99 |
-
emotion = self.classes[pred_index]
|
| 100 |
-
confidence = predictions[pred_index]
|
| 101 |
-
text = f"{emotion} ({confidence*100:.1f}%)"
|
| 102 |
-
# (Drawing logic is duplicated here for simplicity)
|
| 103 |
-
GREEN = (0, 255, 0); BLACK = (0, 0, 0); FONT = cv2.FONT_HERSHEY_SIMPLEX
|
| 104 |
-
(tw, th), bl = cv2.getTextSize(text, FONT, 0.8, 2)
|
| 105 |
-
cv2.rectangle(annotated_frame, (x, y-th-bl-10), (x+tw+10, y), GREEN, cv2.FILLED)
|
| 106 |
cv2.putText(annotated_frame, text, (x + 5, y - 5), FONT, 0.8, BLACK, 2)
|
| 107 |
cv2.rectangle(annotated_frame, (x, y), (x+width, y+height), GREEN, 3)
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
| 20 |
self.stable_prediction = "---"
|
| 21 |
print("[PREDICTOR INFO] Predictor initialized successfully.")
|
| 22 |
|
| 23 |
+
|
| 24 |
+
def process_frame(self, frame):
|
| 25 |
"""
|
| 26 |
+
Processes a single frame: flips it for a mirror effect, detects faces,
|
| 27 |
+
predicts emotions, and draws professional annotations.
|
| 28 |
"""
|
| 29 |
+
if frame is None: return frame, {}
|
| 30 |
+
|
| 31 |
+
# --- MIRROR FIX: Flip the frame FIRST! ---
|
| 32 |
+
# This ensures detection and drawing happen in the same coordinate space the user sees.
|
| 33 |
+
frame = cv2.flip(frame, 1)
|
| 34 |
+
annotated_frame = frame.copy()
|
| 35 |
+
# --- END FIX ---
|
| 36 |
|
| 37 |
+
all_probabilities = {}
|
| 38 |
faces = self.face_detector.detect_faces(frame)
|
| 39 |
|
| 40 |
for face in faces:
|
| 41 |
x, y, width, height = face['box']
|
| 42 |
+
x, y = max(0, x), max(0, y)
|
| 43 |
face_roi = frame[y:y+height, x:x+width]
|
| 44 |
|
| 45 |
if face_roi.size > 0:
|
|
|
|
| 51 |
probs = torch.nn.functional.softmax(logits, dim=-1)
|
| 52 |
predictions = probs[0].numpy()
|
| 53 |
pred_index = np.argmax(predictions)
|
| 54 |
+
|
| 55 |
+
# Use temporal smoothing for the displayed label
|
| 56 |
confidence = predictions[pred_index]
|
|
|
|
| 57 |
if confidence > self.confidence_threshold:
|
| 58 |
self.recent_predictions.append(pred_index)
|
| 59 |
+
if self.recent_predictions:
|
| 60 |
+
most_common_pred = Counter(self.recent_predictions).most_common(1)[0][0]
|
| 61 |
+
self.stable_prediction = self.classes[most_common_pred]
|
| 62 |
|
| 63 |
+
# --- PROFESSIONAL DRAWING LOGIC ---
|
| 64 |
+
GREEN = (0, 255, 0)
|
| 65 |
+
BLACK = (0, 0, 0)
|
| 66 |
+
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
| 67 |
+
text = f"{self.stable_prediction} ({confidence*100:.1f}%)"
|
| 68 |
+
|
| 69 |
+
(text_width, text_height), baseline = cv2.getTextSize(text, FONT, 0.8, 2)
|
| 70 |
+
|
| 71 |
+
cv2.rectangle(annotated_frame, (x, y - text_height - baseline - 10), (x + text_width + 10, y), GREEN, cv2.FILLED)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
cv2.putText(annotated_frame, text, (x + 5, y - 5), FONT, 0.8, BLACK, 2)
|
| 73 |
cv2.rectangle(annotated_frame, (x, y), (x+width, y+height), GREEN, 3)
|
| 74 |
+
|
| 75 |
+
all_probabilities = {self.classes[i]: float(predictions[i]) for i in range(len(self.classes))}
|
| 76 |
+
|
| 77 |
+
return annotated_frame, all_probabilities
|