GitHub Action commited on
Commit
bc745c3
·
1 Parent(s): 1ad4a22

Automated deployment from GitHub Actions

Browse files
Files changed (45) hide show
  1. README.md +2 -13
  2. app.py +72 -114
  3. huggingface-space/.dvc/.gitignore +2 -0
  4. huggingface-space/.dvc/config +0 -0
  5. huggingface-space/.dvcignore +0 -0
  6. huggingface-space/.gitignore +112 -0
  7. huggingface-space/Dockerfile +0 -0
  8. huggingface-space/LICENSE +21 -0
  9. huggingface-space/README.md +2 -2
  10. huggingface-space/app.py +195 -0
  11. huggingface-space/config/config.yaml +25 -0
  12. huggingface-space/dvc.lock +203 -0
  13. huggingface-space/dvc.yaml +39 -0
  14. huggingface-space/gpuCheck.py +42 -0
  15. huggingface-space/huggingface-space/.gitattributes +1 -0
  16. huggingface-space/huggingface-space/README.md +42 -0
  17. huggingface-space/main.py +70 -0
  18. huggingface-space/params.yaml +15 -0
  19. huggingface-space/requirements.txt +44 -0
  20. huggingface-space/research/01_data_exploration.ipynb +0 -0
  21. huggingface-space/setup.py +28 -0
  22. huggingface-space/src/EmotionRecognition/__init__.py +25 -0
  23. huggingface-space/src/EmotionRecognition/components/__init__.py +0 -0
  24. huggingface-space/src/EmotionRecognition/components/data_ingestion.py +27 -0
  25. huggingface-space/src/EmotionRecognition/components/data_preparation.py +118 -0
  26. huggingface-space/src/EmotionRecognition/components/data_preprocessing.py +86 -0
  27. huggingface-space/src/EmotionRecognition/components/data_validation.py +32 -0
  28. huggingface-space/src/EmotionRecognition/components/model_evaluation.py +63 -0
  29. huggingface-space/src/EmotionRecognition/components/model_trainer.py +99 -0
  30. huggingface-space/src/EmotionRecognition/config/__init__.py +0 -0
  31. huggingface-space/src/EmotionRecognition/config/configuration.py +64 -0
  32. huggingface-space/src/EmotionRecognition/entity/__init__.py +0 -0
  33. huggingface-space/src/EmotionRecognition/entity/config_entity.py +42 -0
  34. huggingface-space/src/EmotionRecognition/pipeline/__init__.py +0 -0
  35. huggingface-space/src/EmotionRecognition/pipeline/hf_predictor.py +109 -0
  36. huggingface-space/src/EmotionRecognition/pipeline/stage_01_data_preparation.py +27 -0
  37. huggingface-space/src/EmotionRecognition/pipeline/stage_02_model_training.py +28 -0
  38. huggingface-space/src/EmotionRecognition/pipeline/stage_03_model_evaluation.py +31 -0
  39. huggingface-space/src/EmotionRecognition/utils/__init__.py +0 -0
  40. huggingface-space/src/EmotionRecognition/utils/common.py +78 -0
  41. huggingface-space/temp.py +65 -0
  42. huggingface-space/templates/index.html +0 -0
  43. sota_model/config.json +42 -42
  44. sota_model/preprocessor_config.json +36 -36
  45. 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/YOUR-USERNAME/YOUR-SPACE-NAME) <!-- Replace with your HF Space URL -->
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
- /* Main Content Card */
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); -webkit-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; } /* Hide the default Gradio label */
47
- .prediction-list { list-style-type: none; padding: 0; margin-top: 1.5rem; }
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: 0.1s linear; }
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
- ## 🚀 About This Project
58
-
59
- This application is the culmination of a complete, end-to-end MLOps project, demonstrating the full lifecycle from research and experimentation to a final, deployed, state-of-the-art solution.
60
-
61
- **💻 [View Project on GitHub](https://github.com/AlyyanAhmed21/Emotion-Recognition-MLOps)**
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 unified_prediction_function(frame):
134
- """
135
- A single, robust function that takes any frame (from webcam or upload)
136
- and returns the annotated frame and the prediction HTML.
137
- """
138
- if frame is None:
139
- return None, create_prediction_html({})
140
-
141
- annotated_frame, probabilities = predictor.process_frame_for_upload(frame)
142
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  return annotated_frame, create_prediction_html(probabilities)
144
 
145
  def process_video(video_path, progress=gr.Progress(track_tqdm=True)):
146
- """Processes an uploaded video file frame-by-frame."""
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=False):
181
  with gr.Column(scale=3):
182
- # The single, correct component for a live webcam feed.
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=False):
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=False):
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
- # Live Feed Logic: This is the simple, direct, and correct way.
209
- # The stream from the 'live_feed' component calls the prediction function.
210
- # The outputs are sent back to the 'live_feed' component (to update the image)
211
- # and the 'live_predictions' component.
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
- # Enabling the queue is essential for the video processing progress bar.
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/YOUR-USERNAME/YOUR-SPACE-NAME) <!-- Replace with your HF Space URL -->
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
- 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:
@@ -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
- 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
 
 
 
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