joyjonesmark commited on
Commit
e5abc2e
·
1 Parent(s): 53aa8b2

Initial deploy with models

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +38 -0
  2. frontend/app.py +485 -0
  3. models/custom_cnn.h5 +3 -0
  4. models/custom_cnn.history.json +262 -0
  5. models/custom_cnn.meta.json +15 -0
  6. models/logs/custom_cnn/train/events.out.tfevents.1769986631.JOSH_MARK.24880.0.v2 +3 -0
  7. models/logs/custom_cnn/validation/events.out.tfevents.1769987506.JOSH_MARK.24880.1.v2 +3 -0
  8. models/logs/mobilenet_v2/train/events.out.tfevents.1770019504.JOSH_MARK.24880.2.v2 +3 -0
  9. models/logs/mobilenet_v2/train/events.out.tfevents.1770020997.JOSH_MARK.24880.4.v2 +3 -0
  10. models/logs/mobilenet_v2/train/events.out.tfevents.1770060970.JOSH_MARK.1932.0.v2 +3 -0
  11. models/logs/mobilenet_v2/train/events.out.tfevents.1770062582.JOSH_MARK.1932.2.v2 +3 -0
  12. models/logs/mobilenet_v2/validation/events.out.tfevents.1770019615.JOSH_MARK.24880.3.v2 +3 -0
  13. models/logs/mobilenet_v2/validation/events.out.tfevents.1770021071.JOSH_MARK.24880.5.v2 +3 -0
  14. models/logs/mobilenet_v2/validation/events.out.tfevents.1770061342.JOSH_MARK.1932.1.v2 +3 -0
  15. models/logs/mobilenet_v2/validation/events.out.tfevents.1770062665.JOSH_MARK.1932.3.v2 +3 -0
  16. models/logs/vgg19/train/events.out.tfevents.1770023002.JOSH_MARK.24880.6.v2 +3 -0
  17. models/logs/vgg19/train/events.out.tfevents.1770029728.JOSH_MARK.24880.8.v2 +3 -0
  18. models/logs/vgg19/train/events.out.tfevents.1770063874.JOSH_MARK.14568.0.v2 +3 -0
  19. models/logs/vgg19/train/events.out.tfevents.1770068280.JOSH_MARK.14988.0.v2 +3 -0
  20. models/logs/vgg19/train/events.out.tfevents.1770082770.JOSH_MARK.14988.2.v2 +3 -0
  21. models/logs/vgg19/validation/events.out.tfevents.1770023476.JOSH_MARK.24880.7.v2 +3 -0
  22. models/logs/vgg19/validation/events.out.tfevents.1770030127.JOSH_MARK.24880.9.v2 +3 -0
  23. models/logs/vgg19/validation/events.out.tfevents.1770064525.JOSH_MARK.14568.1.v2 +3 -0
  24. models/logs/vgg19/validation/events.out.tfevents.1770068666.JOSH_MARK.14988.1.v2 +3 -0
  25. models/logs/vgg19/validation/events.out.tfevents.1770083165.JOSH_MARK.14988.3.v2 +3 -0
  26. models/mobilenet_v2.h5 +3 -0
  27. models/mobilenet_v2.history.json +67 -0
  28. models/mobilenet_v2.meta.json +15 -0
  29. models/vgg19.h5 +3 -0
  30. models/vgg19.history.json +112 -0
  31. models/vgg19.meta.json +15 -0
  32. requirements.txt +28 -0
  33. src/__init__.py +2 -0
  34. src/__pycache__/__init__.cpython-310.pyc +0 -0
  35. src/__pycache__/config.cpython-310.pyc +0 -0
  36. src/config.py +76 -0
  37. src/inference/__init__.py +3 -0
  38. src/inference/__pycache__/__init__.cpython-310.pyc +0 -0
  39. src/inference/__pycache__/predictor.cpython-310.pyc +0 -0
  40. src/inference/predictor.py +346 -0
  41. src/models/__init__.py +13 -0
  42. src/models/__pycache__/__init__.cpython-310.pyc +0 -0
  43. src/models/__pycache__/custom_cnn.cpython-310.pyc +0 -0
  44. src/models/__pycache__/mobilenet_model.cpython-310.pyc +0 -0
  45. src/models/__pycache__/model_utils.cpython-310.pyc +0 -0
  46. src/models/__pycache__/vgg_model.cpython-310.pyc +0 -0
  47. src/models/custom_cnn.py +183 -0
  48. src/models/mobilenet_model.py +203 -0
  49. src/models/model_utils.py +491 -0
  50. src/models/vgg_model.py +257 -0
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image with TensorFlow GPU support
2
+ # Use CPU version for Hugging Face Spaces free tier compatibility if needed
3
+ # But keeping GPU as the project is configured for it.
4
+ # HF Spaces offers CPU Basic (Free) and GPU upgrades.
5
+ # Using a lighter base image might be better for free tier, but TF is heavy anyway.
6
+ FROM tensorflow/tensorflow:2.13.0
7
+
8
+ # Set working directory
9
+ WORKDIR /app
10
+
11
+ # Install system dependencies including libGL for OpenCV
12
+ RUN apt-get update && apt-get install -y \
13
+ libgl1-mesa-glx \
14
+ libglib2.0-0 \
15
+ libsm6 \
16
+ libxext6 \
17
+ libxrender-dev \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Copy requirements first for caching
21
+ COPY requirements.txt .
22
+
23
+ # Install Python dependencies
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copy application code
27
+ COPY src/ ./src/
28
+ COPY frontend/ ./frontend/
29
+
30
+ # Create a models directory
31
+ # Note: You must upload your trained models here or use Git LFS
32
+ COPY models/ ./models/
33
+
34
+ # Expose the port Hugging Face Spaces expects
35
+ EXPOSE 7860
36
+
37
+ # Default command to run Streamlit on port 7860
38
+ CMD ["streamlit", "run", "frontend/app.py", "--server.port", "7860", "--server.address", "0.0.0.0"]
frontend/app.py ADDED
@@ -0,0 +1,485 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Streamlit Dashboard for Emotion Recognition System.
3
+ """
4
+ import io
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import streamlit as st
9
+ import numpy as np
10
+ import pandas as pd
11
+ import plotly.express as px
12
+ import plotly.graph_objects as go
13
+ from PIL import Image
14
+
15
+ # Add project root to path
16
+ sys.path.insert(0, str(Path(__file__).parent.parent))
17
+
18
+ from src.config import EMOTION_CLASSES, MODELS_DIR
19
+ from src.inference.predictor import EmotionPredictor
20
+
21
+ # Page configuration
22
+ st.set_page_config(
23
+ page_title="Emotion Recognition Dashboard",
24
+ page_icon="😊",
25
+ layout="wide",
26
+ initial_sidebar_state="expanded"
27
+ )
28
+
29
+ # Custom CSS
30
+ st.markdown("""
31
+ <style>
32
+ .main-header {
33
+ font-size: 2.5rem;
34
+ font-weight: bold;
35
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
36
+ -webkit-background-clip: text;
37
+ -webkit-text-fill-color: transparent;
38
+ text-align: center;
39
+ margin-bottom: 1rem;
40
+ }
41
+ .emotion-card {
42
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
43
+ padding: 1.5rem;
44
+ border-radius: 1rem;
45
+ color: white;
46
+ text-align: center;
47
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
48
+ }
49
+ .confidence-high {
50
+ color: #10b981;
51
+ font-weight: bold;
52
+ }
53
+ .confidence-medium {
54
+ color: #f59e0b;
55
+ font-weight: bold;
56
+ }
57
+ .confidence-low {
58
+ color: #ef4444;
59
+ font-weight: bold;
60
+ }
61
+ .stTabs [data-baseweb="tab-list"] {
62
+ gap: 2rem;
63
+ }
64
+ .stTabs [data-baseweb="tab"] {
65
+ height: 50px;
66
+ padding-left: 20px;
67
+ padding-right: 20px;
68
+ }
69
+ </style>
70
+ """, unsafe_allow_html=True)
71
+
72
+ # Emotion emoji mapping
73
+ EMOTION_EMOJIS = {
74
+ "angry": "😠",
75
+ "disgusted": "🤢",
76
+ "fearful": "😨",
77
+ "happy": "😊",
78
+ "neutral": "😐",
79
+ "sad": "😢",
80
+ "surprised": "😲"
81
+ }
82
+
83
+ # Color palette for emotions
84
+ EMOTION_COLORS = {
85
+ "angry": "#ef4444",
86
+ "disgusted": "#84cc16",
87
+ "fearful": "#a855f7",
88
+ "happy": "#22c55e",
89
+ "neutral": "#6b7280",
90
+ "sad": "#3b82f6",
91
+ "surprised": "#f59e0b"
92
+ }
93
+
94
+
95
+ @st.cache_resource
96
+ def load_predictor(model_name: str):
97
+ """Load and cache the emotion predictor."""
98
+ predictor = EmotionPredictor(model_name)
99
+ if predictor.load():
100
+ return predictor
101
+ return None
102
+
103
+
104
+ def get_intensity_class(intensity: str) -> str:
105
+ """Get CSS class for intensity."""
106
+ return f"confidence-{intensity}"
107
+
108
+
109
+ def create_probability_chart(probabilities: dict) -> go.Figure:
110
+ """Create a horizontal bar chart for probabilities."""
111
+ emotions = list(probabilities.keys())
112
+ values = list(probabilities.values())
113
+ colors = [EMOTION_COLORS.get(e, "#6b7280") for e in emotions]
114
+
115
+ fig = go.Figure(go.Bar(
116
+ x=values,
117
+ y=[f"{EMOTION_EMOJIS.get(e, '')} {e.capitalize()}" for e in emotions],
118
+ orientation='h',
119
+ marker_color=colors,
120
+ text=[f"{v:.1%}" for v in values],
121
+ textposition='outside'
122
+ ))
123
+
124
+ fig.update_layout(
125
+ title="Emotion Probabilities",
126
+ xaxis_title="Probability",
127
+ yaxis_title="Emotion",
128
+ height=350,
129
+ margin=dict(l=20, r=20, t=40, b=20),
130
+ xaxis=dict(range=[0, 1.1])
131
+ )
132
+
133
+ return fig
134
+
135
+
136
+ def create_emotion_distribution_pie(counts: dict) -> go.Figure:
137
+ """Create a pie chart for emotion distribution."""
138
+ emotions = [e for e, c in counts.items() if c > 0]
139
+ values = [c for c in counts.values() if c > 0]
140
+ colors = [EMOTION_COLORS.get(e, "#6b7280") for e in emotions]
141
+
142
+ fig = go.Figure(go.Pie(
143
+ labels=[f"{EMOTION_EMOJIS.get(e, '')} {e.capitalize()}" for e in emotions],
144
+ values=values,
145
+ marker_colors=colors,
146
+ hole=0.4,
147
+ textinfo='percent+label'
148
+ ))
149
+
150
+ fig.update_layout(
151
+ title="Emotion Distribution",
152
+ height=400,
153
+ margin=dict(l=20, r=20, t=40, b=20)
154
+ )
155
+
156
+ return fig
157
+
158
+
159
+ def main():
160
+ """Main dashboard application."""
161
+ # Header
162
+ st.markdown('<h1 class="main-header">🎭 Emotion Recognition Dashboard</h1>', unsafe_allow_html=True)
163
+ st.markdown("---")
164
+
165
+ # Sidebar
166
+ with st.sidebar:
167
+ st.image("https://img.icons8.com/clouds/200/brain.png", width=100)
168
+ st.title("⚙️ Settings")
169
+
170
+ # Model selection
171
+ available_models = EmotionPredictor.get_available_models()
172
+ model_options = [name for name, available in available_models.items() if available]
173
+
174
+ if not model_options:
175
+ st.error("No trained models found! Please train a model first.")
176
+ st.info("Run: `python scripts/train_models.py`")
177
+ model_name = None
178
+ else:
179
+ model_name = st.selectbox(
180
+ "🤖 Select Model",
181
+ model_options,
182
+ format_func=lambda x: {
183
+ "custom_cnn": "Custom CNN",
184
+ "mobilenet": "MobileNetV2",
185
+ "vgg19": "VGG-19"
186
+ }.get(x, x)
187
+ )
188
+
189
+ # Face detection toggle
190
+ detect_face = st.toggle("👤 Enable Face Detection", value=True)
191
+
192
+ # Confidence threshold
193
+ confidence_threshold = st.slider(
194
+ "📊 Confidence Threshold",
195
+ min_value=0.0,
196
+ max_value=1.0,
197
+ value=0.5,
198
+ step=0.05
199
+ )
200
+
201
+ st.markdown("---")
202
+
203
+ # Model info
204
+ st.subheader("📋 Model Status")
205
+ for name, available in available_models.items():
206
+ icon = "✅" if available else "❌"
207
+ display_name = {
208
+ "custom_cnn": "Custom CNN",
209
+ "mobilenet": "MobileNetV2",
210
+ "vgg19": "VGG-19"
211
+ }.get(name, name)
212
+ st.write(f"{icon} {display_name}")
213
+
214
+ # Main content
215
+ if model_name is None:
216
+ st.warning("Please train a model before using the dashboard.")
217
+ return
218
+
219
+ # Load predictor
220
+ predictor = load_predictor(model_name)
221
+ if predictor is None:
222
+ st.error(f"Failed to load model: {model_name}")
223
+ return
224
+
225
+ # Tabs
226
+ tab1, tab2, tab3 = st.tabs(["📷 Single Image", "📁 Batch Processing", "📊 Model Performance"])
227
+
228
+ # Tab 1: Single Image Analysis
229
+ with tab1:
230
+ st.subheader("Upload an Image for Emotion Analysis")
231
+
232
+ col1, col2 = st.columns([1, 1])
233
+
234
+ with col1:
235
+ uploaded_file = st.file_uploader(
236
+ "Choose an image...",
237
+ type=["jpg", "jpeg", "png", "bmp"],
238
+ key="single_upload"
239
+ )
240
+
241
+ if uploaded_file is not None:
242
+ image = Image.open(uploaded_file)
243
+ st.image(image, caption="Uploaded Image", width="stretch")
244
+
245
+ with col2:
246
+ if uploaded_file is not None:
247
+ with st.spinner("Analyzing emotion..."):
248
+ # Convert to numpy array
249
+ image_array = np.array(image.convert("RGB"))
250
+
251
+ # Predict
252
+ result = predictor.predict(image_array, detect_face=detect_face)
253
+
254
+ if "error" in result:
255
+ st.error(f"❌ {result['error']}")
256
+ else:
257
+ # Display result
258
+ emotion = result["emotion"]
259
+ confidence = result["confidence"]
260
+ intensity = result["intensity"]
261
+
262
+ # Emotion card
263
+ st.markdown(f"""
264
+ <div class="emotion-card">
265
+ <h1 style="font-size: 4rem; margin: 0;">{EMOTION_EMOJIS.get(emotion, '🎭')}</h1>
266
+ <h2 style="margin: 0.5rem 0;">{emotion.upper()}</h2>
267
+ <p style="font-size: 1.2rem;">Confidence: {confidence:.1%}</p>
268
+ <p>Intensity: {intensity.capitalize()}</p>
269
+ </div>
270
+ """, unsafe_allow_html=True)
271
+
272
+ # Probability chart
273
+ if "all_probabilities" in result:
274
+ fig = create_probability_chart(result["all_probabilities"])
275
+ st.plotly_chart(fig, use_container_width=True)
276
+
277
+ # Face detection info
278
+ if result["face_detected"]:
279
+ st.success("✅ Face detected successfully")
280
+ else:
281
+ st.warning("⚠️ No face detected - using full image")
282
+
283
+ # Tab 2: Batch Processing
284
+ with tab2:
285
+ st.subheader("Upload Multiple Images for Batch Analysis")
286
+
287
+ uploaded_files = st.file_uploader(
288
+ "Choose images...",
289
+ type=["jpg", "jpeg", "png", "bmp"],
290
+ accept_multiple_files=True,
291
+ key="batch_upload"
292
+ )
293
+
294
+ if uploaded_files:
295
+ st.write(f"📁 {len(uploaded_files)} files selected")
296
+
297
+ if st.button("🚀 Analyze All", type="primary"):
298
+ progress_bar = st.progress(0)
299
+ status_text = st.empty()
300
+
301
+ results = []
302
+ images = []
303
+
304
+ for i, file in enumerate(uploaded_files):
305
+ status_text.text(f"Processing image {i+1}/{len(uploaded_files)}...")
306
+ progress_bar.progress((i + 1) / len(uploaded_files))
307
+
308
+ try:
309
+ image = Image.open(file)
310
+ images.append(image)
311
+ image_array = np.array(image.convert("RGB"))
312
+ result = predictor.predict(image_array, detect_face=detect_face)
313
+ result["filename"] = file.name
314
+ results.append(result)
315
+ except Exception as e:
316
+ results.append({"error": str(e), "filename": file.name})
317
+
318
+ status_text.text("✅ Analysis complete!")
319
+
320
+ # Display results
321
+ col1, col2 = st.columns([1, 1])
322
+
323
+ with col1:
324
+ # Summary statistics
325
+ successful = [r for r in results if "error" not in r]
326
+
327
+ if successful:
328
+ emotion_counts = {}
329
+ for r in successful:
330
+ emotion = r["emotion"]
331
+ emotion_counts[emotion] = emotion_counts.get(emotion, 0) + 1
332
+
333
+ # Pie chart
334
+ fig = create_emotion_distribution_pie(emotion_counts)
335
+ st.plotly_chart(fig, use_container_width=True)
336
+
337
+ st.metric("Total Images", len(results))
338
+ st.metric("Successful", len(successful))
339
+ st.metric("Failed", len(results) - len(successful))
340
+
341
+ with col2:
342
+ # Results table
343
+ table_data = []
344
+ for r in results:
345
+ if "error" in r:
346
+ table_data.append({
347
+ "File": r.get("filename", "Unknown"),
348
+ "Emotion": "❌ Error",
349
+ "Confidence": "-",
350
+ "Intensity": "-"
351
+ })
352
+ else:
353
+ table_data.append({
354
+ "File": r.get("filename", "Unknown"),
355
+ "Emotion": f"{EMOTION_EMOJIS.get(r['emotion'], '')} {r['emotion'].capitalize()}",
356
+ "Confidence": f"{r['confidence']:.1%}",
357
+ "Intensity": r["intensity"].capitalize()
358
+ })
359
+
360
+ df = pd.DataFrame(table_data)
361
+ st.dataframe(df, use_container_width=True, height=400)
362
+
363
+ # Download button
364
+ csv = df.to_csv(index=False)
365
+ st.download_button(
366
+ "📥 Download Results (CSV)",
367
+ csv,
368
+ "emotion_results.csv",
369
+ "text/csv"
370
+ )
371
+
372
+ # Image gallery with predictions
373
+ st.subheader("📷 Analyzed Images")
374
+ cols = st.columns(4)
375
+ for i, (img, result) in enumerate(zip(images, results)):
376
+ with cols[i % 4]:
377
+ if "error" not in result:
378
+ emoji = EMOTION_EMOJIS.get(result["emotion"], "")
379
+ st.image(img, caption=f"{emoji} {result['emotion']}", width="stretch")
380
+ else:
381
+ st.image(img, caption="❌ Error", width="stretch")
382
+
383
+ # Tab 3: Model Performance
384
+ with tab3:
385
+ st.subheader("📊 Model Performance Metrics")
386
+
387
+ # Check for saved metrics
388
+ metrics_path = MODELS_DIR / f"{model_name}.meta.json"
389
+ history_path = MODELS_DIR / f"{model_name}.history.json"
390
+
391
+ if metrics_path.exists():
392
+ import json
393
+ with open(metrics_path, 'r') as f:
394
+ metadata = json.load(f)
395
+
396
+ col1, col2, col3 = st.columns(3)
397
+
398
+ with col1:
399
+ st.metric(
400
+ "Best Validation Accuracy",
401
+ f"{metadata.get('best_val_accuracy', 0):.1%}"
402
+ )
403
+
404
+ with col2:
405
+ st.metric(
406
+ "Training Duration",
407
+ f"{metadata.get('training_duration_seconds', 0)/60:.1f} min"
408
+ )
409
+
410
+ with col3:
411
+ st.metric(
412
+ "Epochs Completed",
413
+ metadata.get('epochs_completed', 0)
414
+ )
415
+
416
+ if history_path.exists():
417
+ with open(history_path, 'r') as f:
418
+ history = json.load(f)
419
+
420
+ # Training curves
421
+ fig = go.Figure()
422
+
423
+ epochs = list(range(1, len(history['accuracy']) + 1))
424
+
425
+ fig.add_trace(go.Scatter(
426
+ x=epochs, y=history['accuracy'],
427
+ mode='lines', name='Training Accuracy',
428
+ line=dict(color='#3b82f6')
429
+ ))
430
+
431
+ fig.add_trace(go.Scatter(
432
+ x=epochs, y=history['val_accuracy'],
433
+ mode='lines', name='Validation Accuracy',
434
+ line=dict(color='#ef4444')
435
+ ))
436
+
437
+ fig.update_layout(
438
+ title="Training History",
439
+ xaxis_title="Epoch",
440
+ yaxis_title="Accuracy",
441
+ height=400
442
+ )
443
+
444
+ st.plotly_chart(fig, use_container_width=True)
445
+
446
+ # Loss curves
447
+ fig2 = go.Figure()
448
+
449
+ fig2.add_trace(go.Scatter(
450
+ x=epochs, y=history['loss'],
451
+ mode='lines', name='Training Loss',
452
+ line=dict(color='#3b82f6')
453
+ ))
454
+
455
+ fig2.add_trace(go.Scatter(
456
+ x=epochs, y=history['val_loss'],
457
+ mode='lines', name='Validation Loss',
458
+ line=dict(color='#ef4444')
459
+ ))
460
+
461
+ fig2.update_layout(
462
+ title="Loss History",
463
+ xaxis_title="Epoch",
464
+ yaxis_title="Loss",
465
+ height=400
466
+ )
467
+
468
+ st.plotly_chart(fig2, use_container_width=True)
469
+ else:
470
+ st.info("No training metrics found for this model. Train the model to see performance data.")
471
+
472
+ # Show placeholder
473
+ st.markdown("""
474
+ ### Expected Metrics After Training
475
+
476
+ | Model | Expected Accuracy | Training Time |
477
+ |-------|------------------|---------------|
478
+ | Custom CNN | 60-68% | ~30 min |
479
+ | MobileNetV2 | 65-72% | ~45 min |
480
+ | VGG-19 | 68-75% | ~60 min |
481
+ """)
482
+
483
+
484
+ if __name__ == "__main__":
485
+ main()
models/custom_cnn.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:588b34caa2f1b8a8f7c29cdc51005ad244ffa3451dbff10de14b45a1c30f5ad6
3
+ size 86397296
models/custom_cnn.history.json ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "accuracy": [
3
+ 0.15269069373607635,
4
+ 0.15229885280132294,
5
+ 0.14925113320350647,
6
+ 0.16035354137420654,
7
+ 0.14650818705558777,
8
+ 0.1529519259929657,
9
+ 0.18447405099868774,
10
+ 0.20154127478599548,
11
+ 0.20162835717201233,
12
+ 0.2072448581457138,
13
+ 0.2176506370306015,
14
+ 0.23267154395580292,
15
+ 0.23820097744464874,
16
+ 0.23772205412387848,
17
+ 0.2543974220752716,
18
+ 0.2947579324245453,
19
+ 0.31957507133483887,
20
+ 0.3651602268218994,
21
+ 0.3738679885864258,
22
+ 0.3878439664840698,
23
+ 0.4040403962135315,
24
+ 0.40382272005081177,
25
+ 0.42293626070022583,
26
+ 0.4291187822818756,
27
+ 0.43521422147750854,
28
+ 0.4663880169391632,
29
+ 0.47574886679649353,
30
+ 0.480581670999527,
31
+ 0.49076977372169495,
32
+ 0.4974747598171234,
33
+ 0.5010885000228882,
34
+ 0.501349687576294,
35
+ 0.5120167136192322,
36
+ 0.5220306515693665,
37
+ 0.522944986820221,
38
+ 0.5263845324516296,
39
+ 0.5292145609855652,
40
+ 0.5357889533042908,
41
+ 0.5326105952262878,
42
+ 0.5370079874992371,
43
+ 0.5618686676025391,
44
+ 0.5710553526878357,
45
+ 0.5716649293899536,
46
+ 0.5865116715431213,
47
+ 0.5881226062774658,
48
+ 0.5922152400016785,
49
+ 0.5961337685585022,
50
+ 0.5938697457313538,
51
+ 0.6016196608543396,
52
+ 0.6010971665382385
53
+ ],
54
+ "loss": [
55
+ 20.70295524597168,
56
+ 6.6751275062561035,
57
+ 3.4141359329223633,
58
+ 2.6772806644439697,
59
+ 2.363776922225952,
60
+ 2.305654764175415,
61
+ 2.403165102005005,
62
+ 2.533033847808838,
63
+ 2.592125415802002,
64
+ 2.516871929168701,
65
+ 2.634507179260254,
66
+ 2.2788898944854736,
67
+ 2.248971700668335,
68
+ 2.259343147277832,
69
+ 2.2574307918548584,
70
+ 2.277164936065674,
71
+ 2.2215516567230225,
72
+ 2.0204102993011475,
73
+ 2.056856632232666,
74
+ 2.005807638168335,
75
+ 2.003291368484497,
76
+ 1.9824334383010864,
77
+ 1.954105257987976,
78
+ 1.9302726984024048,
79
+ 1.9290143251419067,
80
+ 1.7764708995819092,
81
+ 1.699206829071045,
82
+ 1.6769280433654785,
83
+ 1.6617697477340698,
84
+ 1.6487752199172974,
85
+ 1.6480680704116821,
86
+ 1.647431492805481,
87
+ 1.618944525718689,
88
+ 1.6199805736541748,
89
+ 1.6172568798065186,
90
+ 1.6095706224441528,
91
+ 1.60358726978302,
92
+ 1.5906955003738403,
93
+ 1.5961298942565918,
94
+ 1.5957502126693726,
95
+ 1.4978364706039429,
96
+ 1.4400925636291504,
97
+ 1.4301934242248535,
98
+ 1.3841499090194702,
99
+ 1.3796941041946411,
100
+ 1.3780864477157593,
101
+ 1.367702603340149,
102
+ 1.3718606233596802,
103
+ 1.3513654470443726,
104
+ 1.335976481437683
105
+ ],
106
+ "val_accuracy": [
107
+ 0.019334610551595688,
108
+ 0.03274690732359886,
109
+ 0.17749521136283875,
110
+ 0.09249259531497955,
111
+ 0.16617314517498016,
112
+ 0.1546768844127655,
113
+ 0.17035359144210815,
114
+ 0.14997386932373047,
115
+ 0.18359170854091644,
116
+ 0.12088486552238464,
117
+ 0.23880857229232788,
118
+ 0.28549033403396606,
119
+ 0.2426406592130661,
120
+ 0.14178714156150818,
121
+ 0.16094757616519928,
122
+ 0.21390001475811005,
123
+ 0.2203448861837387,
124
+ 0.352726012468338,
125
+ 0.34628114104270935,
126
+ 0.42222610116004944,
127
+ 0.32328861951828003,
128
+ 0.39296290278434753,
129
+ 0.38094407320022583,
130
+ 0.3227660655975342,
131
+ 0.35046160221099854,
132
+ 0.4741334319114685,
133
+ 0.49294549226760864,
134
+ 0.4274516701698303,
135
+ 0.5124542713165283,
136
+ 0.46873366832733154,
137
+ 0.5080996155738831,
138
+ 0.5359693169593811,
139
+ 0.4953840672969818,
140
+ 0.5192475318908691,
141
+ 0.5549556016921997,
142
+ 0.5014805793762207,
143
+ 0.5380595922470093,
144
+ 0.5481623411178589,
145
+ 0.5141961574554443,
146
+ 0.55303955078125,
147
+ 0.5638390779495239,
148
+ 0.5594844222068787,
149
+ 0.6054694056510925,
150
+ 0.5887476205825806,
151
+ 0.5864831805229187,
152
+ 0.5976310968399048,
153
+ 0.5755094885826111,
154
+ 0.5990245342254639,
155
+ 0.5859606266021729,
156
+ 0.5878766775131226
157
+ ],
158
+ "val_loss": [
159
+ 10.306150436401367,
160
+ 4.229166507720947,
161
+ 2.9087648391723633,
162
+ 2.51203989982605,
163
+ 2.2901315689086914,
164
+ 2.252924680709839,
165
+ 2.433279275894165,
166
+ 3.151798963546753,
167
+ 2.5496649742126465,
168
+ 2.702935218811035,
169
+ 2.5200254917144775,
170
+ 2.191758155822754,
171
+ 2.398240089416504,
172
+ 2.371654987335205,
173
+ 2.4415881633758545,
174
+ 2.3200838565826416,
175
+ 2.431246519088745,
176
+ 2.050586223602295,
177
+ 2.0909876823425293,
178
+ 1.967246413230896,
179
+ 2.160478115081787,
180
+ 2.0182101726531982,
181
+ 1.986769676208496,
182
+ 2.2352793216705322,
183
+ 2.157156467437744,
184
+ 1.7153019905090332,
185
+ 1.662605881690979,
186
+ 1.8105882406234741,
187
+ 1.6218799352645874,
188
+ 1.729047417640686,
189
+ 1.681536316871643,
190
+ 1.5821231603622437,
191
+ 1.6714152097702026,
192
+ 1.6351673603057861,
193
+ 1.5606050491333008,
194
+ 1.6746041774749756,
195
+ 1.6208828687667847,
196
+ 1.5802900791168213,
197
+ 1.672598958015442,
198
+ 1.58816397190094,
199
+ 1.512089729309082,
200
+ 1.5220553874969482,
201
+ 1.3933297395706177,
202
+ 1.4107120037078857,
203
+ 1.4645394086837769,
204
+ 1.416934609413147,
205
+ 1.4484670162200928,
206
+ 1.3919000625610352,
207
+ 1.4328689575195312,
208
+ 1.4462361335754395
209
+ ],
210
+ "learning_rate": [
211
+ 0.0010000000474974513,
212
+ 0.0010000000474974513,
213
+ 0.0010000000474974513,
214
+ 0.0010000000474974513,
215
+ 0.0010000000474974513,
216
+ 0.0010000000474974513,
217
+ 0.0010000000474974513,
218
+ 0.0010000000474974513,
219
+ 0.0010000000474974513,
220
+ 0.0010000000474974513,
221
+ 0.0010000000474974513,
222
+ 0.0005000000237487257,
223
+ 0.0005000000237487257,
224
+ 0.0005000000237487257,
225
+ 0.0005000000237487257,
226
+ 0.0005000000237487257,
227
+ 0.0005000000237487257,
228
+ 0.0002500000118743628,
229
+ 0.0002500000118743628,
230
+ 0.0002500000118743628,
231
+ 0.0002500000118743628,
232
+ 0.0002500000118743628,
233
+ 0.0002500000118743628,
234
+ 0.0002500000118743628,
235
+ 0.0002500000118743628,
236
+ 0.0001250000059371814,
237
+ 0.0001250000059371814,
238
+ 0.0001250000059371814,
239
+ 0.0001250000059371814,
240
+ 0.0001250000059371814,
241
+ 0.0001250000059371814,
242
+ 0.0001250000059371814,
243
+ 0.0001250000059371814,
244
+ 0.0001250000059371814,
245
+ 0.0001250000059371814,
246
+ 0.0001250000059371814,
247
+ 0.0001250000059371814,
248
+ 0.0001250000059371814,
249
+ 0.0001250000059371814,
250
+ 0.0001250000059371814,
251
+ 6.25000029685907e-05,
252
+ 6.25000029685907e-05,
253
+ 6.25000029685907e-05,
254
+ 6.25000029685907e-05,
255
+ 6.25000029685907e-05,
256
+ 6.25000029685907e-05,
257
+ 6.25000029685907e-05,
258
+ 6.25000029685907e-05,
259
+ 6.25000029685907e-05,
260
+ 6.25000029685907e-05
261
+ ]
262
+ }
models/custom_cnn.meta.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "learning_rate": 0.001,
3
+ "loss_function": "categorical_crossentropy",
4
+ "metrics": [
5
+ "accuracy"
6
+ ],
7
+ "training_started": "2026-02-02T04:27:09.945021",
8
+ "epochs_requested": 50,
9
+ "training_ended": "2026-02-02T13:34:58.163201",
10
+ "training_duration_seconds": 32868.21818,
11
+ "epochs_completed": 50,
12
+ "final_accuracy": 0.6010971665382385,
13
+ "final_val_accuracy": 0.5878766775131226,
14
+ "best_val_accuracy": 0.6054694056510925
15
+ }
models/logs/custom_cnn/train/events.out.tfevents.1769986631.JOSH_MARK.24880.0.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2cd35d42f7aee79730fef0a4045798070058d2917e0fe0f645ce912cd7bd6644
3
+ size 2535576
models/logs/custom_cnn/validation/events.out.tfevents.1769987506.JOSH_MARK.24880.1.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bd7d41f1dee732d8d180d5baa04ffda084995f29e0092703985c30b00648cb37
3
+ size 16084
models/logs/mobilenet_v2/train/events.out.tfevents.1770019504.JOSH_MARK.24880.2.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4098b3cc7c927c62fb9a294b8dbaf53b44a792f37aee5ea9ea78c9cb97d00c3a
3
+ size 3921756
models/logs/mobilenet_v2/train/events.out.tfevents.1770020997.JOSH_MARK.24880.4.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7845dd0eb4203fb6daff5b842829bd3deef54c6001fdcb1ad88e2f851edc6b3
3
+ size 4585881
models/logs/mobilenet_v2/train/events.out.tfevents.1770060970.JOSH_MARK.1932.0.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:42f1d7dee5c5ec1a8c6f30fadbe097cda75a4fc6065260e77480aafa696004e3
3
+ size 3036210
models/logs/mobilenet_v2/train/events.out.tfevents.1770062582.JOSH_MARK.1932.2.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7da8f5e7986b6ae22edf80a618a65a5ba606d9a9151597579a42802cdde4f215
3
+ size 2593460
models/logs/mobilenet_v2/validation/events.out.tfevents.1770019615.JOSH_MARK.24880.3.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:199544d9f955f5479f456aca9a2227ee975516753d990cc719285f22ea8b0b9b
3
+ size 5514
models/logs/mobilenet_v2/validation/events.out.tfevents.1770021071.JOSH_MARK.24880.5.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9118aa70fcaa07e680166b4e8048b69b846b9e709e58788e295d4f9d65c7bd65
3
+ size 6474
models/logs/mobilenet_v2/validation/events.out.tfevents.1770061342.JOSH_MARK.1932.1.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:118bbf8244c6d8db7d9e0065489c9b9f1dffc03caf99b98a0f7029d63077dd60
3
+ size 4234
models/logs/mobilenet_v2/validation/events.out.tfevents.1770062665.JOSH_MARK.1932.3.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c5ad6267d9dbfcc3ec01c803f8c1fa5822d259ba24659d3fe6a53cd0551c1404
3
+ size 3594
models/logs/vgg19/train/events.out.tfevents.1770023002.JOSH_MARK.24880.6.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dadbebc8dd36dfc128649c3175ee75300927a7f8821e6647ea47550b8c0bcc23
3
+ size 515013
models/logs/vgg19/train/events.out.tfevents.1770029728.JOSH_MARK.24880.8.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6ce34f9b9aa9c8f0948b66719f0c0f9eaebb1a4106869bf30985862e82312ccc
3
+ size 775630
models/logs/vgg19/train/events.out.tfevents.1770063874.JOSH_MARK.14568.0.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e20bdc0419e03f073770425f6dab7687c3c244febfb3256023db1b0c040fdba0
3
+ size 290086
models/logs/vgg19/train/events.out.tfevents.1770068280.JOSH_MARK.14988.0.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:26e239ce23e47c7fd42eae35402c1265d50d180260eafc77011e85ff38c04b26
3
+ size 1146399
models/logs/vgg19/train/events.out.tfevents.1770082770.JOSH_MARK.14988.2.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6f1737790b2df32c086e0443054fa7239a4a3f59abe0938fdeaa426055c41d5
3
+ size 774089
models/logs/vgg19/validation/events.out.tfevents.1770023476.JOSH_MARK.24880.7.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:79c078cbf2229a8a97568c940eb3a25bbbf6ad0a91d5a5084a7acfedef1a66f5
3
+ size 4234
models/logs/vgg19/validation/events.out.tfevents.1770030127.JOSH_MARK.24880.9.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c9398a51151e81b15eb0df2a09de090d5e26731f9bdf922aeef1b518f3020226
3
+ size 6474
models/logs/vgg19/validation/events.out.tfevents.1770064525.JOSH_MARK.14568.1.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d1a407c4f8c12b6d320a024e9c292edf58e5ce84d91ea824584cc3240dfcbb7
3
+ size 2314
models/logs/vgg19/validation/events.out.tfevents.1770068666.JOSH_MARK.14988.1.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f201dbfc9797a11794dbd64b866a881823ff7baa76acb47631e31ced8c2603e
3
+ size 9674
models/logs/vgg19/validation/events.out.tfevents.1770083165.JOSH_MARK.14988.3.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed2d98df90f98d6a03e33dbc7e68563d046fa282ab371d68c574ac8d2cb82663
3
+ size 6474
models/mobilenet_v2.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d2034e436498e4e419a49981d52892e9196283744a3f64b0c0151357d462c267
3
+ size 31157400
models/mobilenet_v2.history.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "accuracy": [
3
+ 0.2706809341907501,
4
+ 0.30677464604377747,
5
+ 0.315395325422287,
6
+ 0.3210989236831665,
7
+ 0.33437827229499817,
8
+ 0.3370341360569,
9
+ 0.3439567983150482,
10
+ 0.34918147325515747,
11
+ 0.3457418978214264,
12
+ 0.3518373370170593,
13
+ 0.3530564308166504
14
+ ],
15
+ "loss": [
16
+ 1.8339711427688599,
17
+ 1.7757574319839478,
18
+ 1.743255615234375,
19
+ 1.720676302909851,
20
+ 1.689762830734253,
21
+ 1.6830896139144897,
22
+ 1.670864462852478,
23
+ 1.6629376411437988,
24
+ 1.6575630903244019,
25
+ 1.650472640991211,
26
+ 1.643319010734558
27
+ ],
28
+ "val_accuracy": [
29
+ 0.25134992599487305,
30
+ 0.25134992599487305,
31
+ 0.14439992606639862,
32
+ 0.25134992599487305,
33
+ 0.25134992599487305,
34
+ 0.25134992599487305,
35
+ 0.26127851009368896,
36
+ 0.25622713565826416,
37
+ 0.11043372005224228,
38
+ 0.11757533252239227,
39
+ 0.11896882206201553
40
+ ],
41
+ "val_loss": [
42
+ 6.824132442474365,
43
+ 8.780102729797363,
44
+ 8.830862998962402,
45
+ 8.673893928527832,
46
+ 10.961349487304688,
47
+ 9.59477424621582,
48
+ 7.310698986053467,
49
+ 7.944781303405762,
50
+ 10.567312240600586,
51
+ 7.704894542694092,
52
+ 6.902732849121094
53
+ ],
54
+ "learning_rate": [
55
+ 9.999999747378752e-05,
56
+ 9.999999747378752e-05,
57
+ 9.999999747378752e-05,
58
+ 9.999999747378752e-05,
59
+ 9.999999747378752e-05,
60
+ 9.999999747378752e-05,
61
+ 4.999999873689376e-05,
62
+ 4.999999873689376e-05,
63
+ 4.999999873689376e-05,
64
+ 4.999999873689376e-05,
65
+ 4.999999873689376e-05
66
+ ]
67
+ }
models/mobilenet_v2.meta.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "learning_rate": 0.0001,
3
+ "loss_function": "categorical_crossentropy",
4
+ "metrics": [
5
+ "accuracy"
6
+ ],
7
+ "training_started": "2026-02-03T01:33:02.533762",
8
+ "epochs_requested": 20,
9
+ "training_ended": "2026-02-03T01:51:17.847554",
10
+ "training_duration_seconds": 1095.313792,
11
+ "epochs_completed": 11,
12
+ "final_accuracy": 0.3530564308166504,
13
+ "final_val_accuracy": 0.11896882206201553,
14
+ "best_val_accuracy": 0.26127851009368896
15
+ }
models/vgg19.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b7c1022f6a4ecc46f0de58d2f82dd783c03919ec783cc619a63f5073e359f1cc
3
+ size 141626776
models/vgg19.history.json ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "accuracy": [
3
+ 0.6472048163414001,
4
+ 0.6577411890029907,
5
+ 0.663314163684845,
6
+ 0.6661877632141113,
7
+ 0.6683211326599121,
8
+ 0.6746777892112732,
9
+ 0.678639829158783,
10
+ 0.6781609058380127,
11
+ 0.6804684996604919,
12
+ 0.6846917271614075,
13
+ 0.6875653266906738,
14
+ 0.6928770542144775,
15
+ 0.6930947303771973,
16
+ 0.6967955231666565,
17
+ 0.6986677050590515,
18
+ 0.7037182450294495,
19
+ 0.7013235688209534,
20
+ 0.705329179763794,
21
+ 0.7071577906608582,
22
+ 0.7095524072647095
23
+ ],
24
+ "loss": [
25
+ 0.9494282603263855,
26
+ 0.9269279837608337,
27
+ 0.9132461547851562,
28
+ 0.9060502648353577,
29
+ 0.8947601914405823,
30
+ 0.8831118941307068,
31
+ 0.8768442869186401,
32
+ 0.874686062335968,
33
+ 0.8647421002388,
34
+ 0.8572869896888733,
35
+ 0.8513924479484558,
36
+ 0.8356429934501648,
37
+ 0.8330938816070557,
38
+ 0.8248153924942017,
39
+ 0.827460765838623,
40
+ 0.8220475912094116,
41
+ 0.8190444111824036,
42
+ 0.8056929111480713,
43
+ 0.8031319379806519,
44
+ 0.7939973473548889
45
+ ],
46
+ "val_accuracy": [
47
+ 0.6099982857704163,
48
+ 0.6087789535522461,
49
+ 0.6148754358291626,
50
+ 0.6120885014533997,
51
+ 0.6239331364631653,
52
+ 0.6180108189582825,
53
+ 0.6162689328193665,
54
+ 0.6113917231559753,
55
+ 0.6174882650375366,
56
+ 0.6145271062850952,
57
+ 0.6225396394729614,
58
+ 0.6141787171363831,
59
+ 0.6279394030570984,
60
+ 0.6181849837303162,
61
+ 0.6190559267997742,
62
+ 0.6277651786804199,
63
+ 0.6309005618095398,
64
+ 0.6220170855522156,
65
+ 0.6244556903839111,
66
+ 0.6169657111167908
67
+ ],
68
+ "val_loss": [
69
+ 1.0432090759277344,
70
+ 1.0663282871246338,
71
+ 1.2026596069335938,
72
+ 1.053371787071228,
73
+ 1.1141172647476196,
74
+ 1.0393040180206299,
75
+ 1.0537439584732056,
76
+ 1.0666412115097046,
77
+ 1.0747647285461426,
78
+ 1.0494613647460938,
79
+ 1.0501089096069336,
80
+ 1.0589252710342407,
81
+ 1.0657376050949097,
82
+ 1.0464764833450317,
83
+ 1.0556851625442505,
84
+ 1.0362597703933716,
85
+ 1.051085352897644,
86
+ 1.071323037147522,
87
+ 1.0513226985931396,
88
+ 1.1507371664047241
89
+ ],
90
+ "learning_rate": [
91
+ 9.999999747378752e-05,
92
+ 9.999999747378752e-05,
93
+ 9.999999747378752e-05,
94
+ 9.999999747378752e-05,
95
+ 9.999999747378752e-05,
96
+ 9.999999747378752e-05,
97
+ 9.999999747378752e-05,
98
+ 9.999999747378752e-05,
99
+ 9.999999747378752e-05,
100
+ 9.999999747378752e-05,
101
+ 9.999999747378752e-05,
102
+ 4.999999873689376e-05,
103
+ 4.999999873689376e-05,
104
+ 4.999999873689376e-05,
105
+ 4.999999873689376e-05,
106
+ 4.999999873689376e-05,
107
+ 4.999999873689376e-05,
108
+ 4.999999873689376e-05,
109
+ 4.999999873689376e-05,
110
+ 4.999999873689376e-05
111
+ ]
112
+ }
models/vgg19.meta.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "learning_rate": 0.0001,
3
+ "loss_function": "categorical_crossentropy",
4
+ "metrics": [
5
+ "accuracy"
6
+ ],
7
+ "training_started": "2026-02-03T07:09:30.363125",
8
+ "epochs_requested": 20,
9
+ "training_ended": "2026-02-03T09:49:04.904804",
10
+ "training_duration_seconds": 9574.541679,
11
+ "epochs_completed": 20,
12
+ "final_accuracy": 0.7095524072647095,
13
+ "final_val_accuracy": 0.6169657111167908,
14
+ "best_val_accuracy": 0.6309005618095398
15
+ }
requirements.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core Deep Learning
2
+ tensorflow>=2.10.0
3
+ keras>=2.10.0
4
+ numpy>=1.21.0
5
+ pandas>=1.4.0
6
+ scikit-learn>=1.0.0
7
+
8
+ # Image Processing
9
+ opencv-python>=4.5.0
10
+ Pillow>=9.0.0
11
+ mtcnn>=0.1.1
12
+
13
+ # API
14
+ fastapi>=0.95.0
15
+ uvicorn>=0.21.0
16
+ python-multipart>=0.0.6
17
+
18
+ # Frontend
19
+ streamlit>=1.22.0
20
+ plotly>=5.13.0
21
+
22
+ # Visualization
23
+ matplotlib>=3.5.0
24
+ seaborn>=0.12.0
25
+
26
+ # Development
27
+ pytest>=7.0.0
28
+ httpx>=0.23.0
src/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Emotion Recognition System
2
+ __version__ = "1.0.0"
src/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (151 Bytes). View file
 
src/__pycache__/config.cpython-310.pyc ADDED
Binary file (1.71 kB). View file
 
src/config.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings for the Emotion Recognition System.
3
+ """
4
+ import os
5
+ from pathlib import Path
6
+
7
+ # Project paths
8
+ PROJECT_ROOT = Path(__file__).parent.parent
9
+ DATA_DIR = PROJECT_ROOT / "data"
10
+ TRAIN_DIR = DATA_DIR / "train"
11
+ TEST_DIR = DATA_DIR / "test"
12
+ MODELS_DIR = PROJECT_ROOT / "models"
13
+
14
+ # Create models directory if it doesn't exist
15
+ MODELS_DIR.mkdir(exist_ok=True)
16
+
17
+ # Image settings
18
+ IMAGE_SIZE = (48, 48)
19
+ IMAGE_SIZE_TRANSFER = (96, 96) # For transfer learning models
20
+ NUM_CHANNELS = 1 # Grayscale
21
+ NUM_CHANNELS_RGB = 3 # For transfer learning
22
+
23
+ # Emotion classes (7 classes from FER dataset)
24
+ EMOTION_CLASSES = [
25
+ "angry",
26
+ "disgusted",
27
+ "fearful",
28
+ "happy",
29
+ "neutral",
30
+ "sad",
31
+ "surprised"
32
+ ]
33
+ NUM_CLASSES = len(EMOTION_CLASSES)
34
+
35
+ # Emotion to index mapping
36
+ EMOTION_TO_IDX = {emotion: idx for idx, emotion in enumerate(EMOTION_CLASSES)}
37
+ IDX_TO_EMOTION = {idx: emotion for idx, emotion in enumerate(EMOTION_CLASSES)}
38
+
39
+ # Training hyperparameters
40
+ BATCH_SIZE = 64
41
+ EPOCHS = 50
42
+ LEARNING_RATE = 0.001
43
+ LEARNING_RATE_FINE_TUNE = 0.0001
44
+ VALIDATION_SPLIT = 0.2
45
+
46
+ # Data augmentation parameters
47
+ AUGMENTATION_CONFIG = {
48
+ "rotation_range": 15,
49
+ "width_shift_range": 0.1,
50
+ "height_shift_range": 0.1,
51
+ "horizontal_flip": True,
52
+ "zoom_range": 0.1,
53
+ "brightness_range": (0.9, 1.1),
54
+ "fill_mode": "nearest"
55
+ }
56
+
57
+ # Model save paths
58
+ CUSTOM_CNN_PATH = MODELS_DIR / "custom_cnn.h5"
59
+ MOBILENET_PATH = MODELS_DIR / "mobilenet_v2.h5"
60
+ VGG_PATH = MODELS_DIR / "vgg19.h5"
61
+
62
+ # Training callbacks
63
+ EARLY_STOPPING_PATIENCE = 10
64
+ REDUCE_LR_PATIENCE = 5
65
+ REDUCE_LR_FACTOR = 0.5
66
+
67
+ # Intensity thresholds
68
+ INTENSITY_HIGH_THRESHOLD = 0.8
69
+ INTENSITY_MEDIUM_THRESHOLD = 0.5
70
+
71
+ # API settings
72
+ API_HOST = "0.0.0.0"
73
+ API_PORT = 8000
74
+
75
+ # Streamlit settings
76
+ STREAMLIT_PORT = 8501
src/inference/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .predictor import EmotionPredictor
2
+
3
+ __all__ = ["EmotionPredictor"]
src/inference/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (212 Bytes). View file
 
src/inference/__pycache__/predictor.cpython-310.pyc ADDED
Binary file (8.6 kB). View file
 
src/inference/predictor.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference pipeline for emotion recognition.
3
+ """
4
+ import numpy as np
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Tuple, Union
7
+
8
+ import cv2
9
+ from PIL import Image
10
+ import tensorflow as tf
11
+ from tensorflow.keras.models import Model
12
+
13
+ import sys
14
+ sys.path.append(str(Path(__file__).parent.parent.parent))
15
+ from src.config import (
16
+ IMAGE_SIZE, IMAGE_SIZE_TRANSFER, EMOTION_CLASSES, IDX_TO_EMOTION,
17
+ INTENSITY_HIGH_THRESHOLD, INTENSITY_MEDIUM_THRESHOLD,
18
+ CUSTOM_CNN_PATH, MOBILENET_PATH, VGG_PATH
19
+ )
20
+ from src.preprocessing.face_detector import FaceDetector
21
+ from src.models.model_utils import load_model
22
+
23
+
24
+ class EmotionPredictor:
25
+ """
26
+ Unified prediction interface for emotion recognition.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ model_name: str = "custom_cnn",
32
+ model_path: Optional[Path] = None,
33
+ use_face_detection: bool = True
34
+ ):
35
+ """
36
+ Initialize the predictor.
37
+
38
+ Args:
39
+ model_name: Name of the model ('custom_cnn', 'mobilenet', 'vgg19')
40
+ model_path: Optional custom model path
41
+ use_face_detection: Whether to detect faces before prediction
42
+ """
43
+ self.model_name = model_name
44
+ self.model = None
45
+ self.face_detector = FaceDetector() if use_face_detection else None
46
+
47
+ # Determine model path
48
+ if model_path:
49
+ self.model_path = Path(model_path)
50
+ else:
51
+ paths = {
52
+ "custom_cnn": CUSTOM_CNN_PATH,
53
+ "mobilenet": MOBILENET_PATH,
54
+ "vgg19": VGG_PATH
55
+ }
56
+ self.model_path = paths.get(model_name)
57
+
58
+ # Set preprocessing based on model type
59
+ self.is_transfer_model = model_name in ["mobilenet", "vgg19"]
60
+ self.target_size = IMAGE_SIZE_TRANSFER if self.is_transfer_model else IMAGE_SIZE
61
+ self.use_rgb = self.is_transfer_model
62
+
63
+ def load(self) -> bool:
64
+ """
65
+ Load the model.
66
+
67
+ Returns:
68
+ True if model loaded successfully
69
+ """
70
+ try:
71
+ if self.model_path and self.model_path.exists():
72
+ self.model = load_model(self.model_path)
73
+ return True
74
+ else:
75
+ print(f"Model file not found: {self.model_path}")
76
+ return False
77
+ except Exception as e:
78
+ print(f"Error loading model: {e}")
79
+ return False
80
+
81
+ def preprocess_image(
82
+ self,
83
+ image: np.ndarray,
84
+ detect_face: bool = True
85
+ ) -> Tuple[Optional[np.ndarray], List[dict]]:
86
+ """
87
+ Preprocess an image for prediction.
88
+
89
+ Args:
90
+ image: Input image (BGR or RGB format)
91
+ detect_face: Whether to detect and extract face
92
+
93
+ Returns:
94
+ Tuple of (preprocessed image, face info)
95
+ """
96
+ faces_info = []
97
+
98
+ if detect_face and self.face_detector:
99
+ # Detect and extract face
100
+ face, faces_info = self.face_detector.detect_and_extract(
101
+ image,
102
+ target_size=self.target_size,
103
+ to_grayscale=not self.use_rgb
104
+ )
105
+
106
+ if face is None:
107
+ return None, faces_info
108
+
109
+ processed = face
110
+ else:
111
+ # Resize directly
112
+ processed = cv2.resize(image, self.target_size)
113
+
114
+ # Convert color if needed
115
+ if self.use_rgb:
116
+ if len(processed.shape) == 2:
117
+ processed = cv2.cvtColor(processed, cv2.COLOR_GRAY2RGB)
118
+ elif processed.shape[2] == 1:
119
+ processed = np.repeat(processed, 3, axis=2)
120
+ else:
121
+ if len(processed.shape) == 3 and processed.shape[2] == 3:
122
+ processed = cv2.cvtColor(processed, cv2.COLOR_BGR2GRAY)
123
+
124
+ # Normalize
125
+ processed = processed.astype(np.float32) / 255.0
126
+
127
+ # Add channel dimension if grayscale
128
+ if len(processed.shape) == 2:
129
+ processed = np.expand_dims(processed, axis=-1)
130
+
131
+ # Add batch dimension
132
+ processed = np.expand_dims(processed, axis=0)
133
+
134
+ return processed, faces_info
135
+
136
+ def predict(
137
+ self,
138
+ image: Union[np.ndarray, str, Path],
139
+ detect_face: bool = True,
140
+ return_all_scores: bool = True
141
+ ) -> Dict:
142
+ """
143
+ Predict emotion from an image.
144
+
145
+ Args:
146
+ image: Input image (array, file path, or PIL Image)
147
+ detect_face: Whether to detect face first
148
+ return_all_scores: Whether to return all class scores
149
+
150
+ Returns:
151
+ Prediction result dictionary
152
+ """
153
+ if self.model is None:
154
+ success = self.load()
155
+ if not success:
156
+ return {"error": "Model not loaded"}
157
+
158
+ # Load image if path provided
159
+ if isinstance(image, (str, Path)):
160
+ image = cv2.imread(str(image))
161
+ if image is None:
162
+ return {"error": f"Could not load image: {image}"}
163
+ elif isinstance(image, Image.Image):
164
+ image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
165
+
166
+ # Preprocess
167
+ processed, faces_info = self.preprocess_image(image, detect_face)
168
+
169
+ if processed is None:
170
+ return {
171
+ "error": "No face detected",
172
+ "face_detected": False,
173
+ "faces_info": faces_info
174
+ }
175
+
176
+ # Predict
177
+ predictions = self.model.predict(processed, verbose=0)
178
+
179
+ # Get top prediction
180
+ pred_idx = int(np.argmax(predictions[0]))
181
+ confidence = float(predictions[0][pred_idx])
182
+ emotion = IDX_TO_EMOTION[pred_idx]
183
+
184
+ # Calculate intensity
185
+ intensity = self._calculate_intensity(confidence)
186
+
187
+ result = {
188
+ "emotion": emotion,
189
+ "confidence": confidence,
190
+ "intensity": intensity,
191
+ "face_detected": len(faces_info) > 0,
192
+ "faces_info": faces_info,
193
+ "model_used": self.model_name
194
+ }
195
+
196
+ if return_all_scores:
197
+ result["all_probabilities"] = {
198
+ EMOTION_CLASSES[i]: float(predictions[0][i])
199
+ for i in range(len(EMOTION_CLASSES))
200
+ }
201
+
202
+ return result
203
+
204
+ def predict_batch(
205
+ self,
206
+ images: List[Union[np.ndarray, str, Path]],
207
+ detect_face: bool = True
208
+ ) -> Dict:
209
+ """
210
+ Predict emotions for multiple images.
211
+
212
+ Args:
213
+ images: List of images
214
+ detect_face: Whether to detect faces
215
+
216
+ Returns:
217
+ Batch prediction results
218
+ """
219
+ results = []
220
+ emotion_counts = {e: 0 for e in EMOTION_CLASSES}
221
+ successful_predictions = 0
222
+
223
+ for i, image in enumerate(images):
224
+ result = self.predict(image, detect_face)
225
+ result["image_index"] = i
226
+ results.append(result)
227
+
228
+ if "error" not in result:
229
+ emotion_counts[result["emotion"]] += 1
230
+ successful_predictions += 1
231
+
232
+ # Calculate distribution
233
+ if successful_predictions > 0:
234
+ emotion_distribution = {
235
+ e: count / successful_predictions
236
+ for e, count in emotion_counts.items()
237
+ }
238
+ else:
239
+ emotion_distribution = {e: 0.0 for e in EMOTION_CLASSES}
240
+
241
+ # Find dominant emotion
242
+ dominant_emotion = max(emotion_counts.items(), key=lambda x: x[1])
243
+
244
+ return {
245
+ "results": results,
246
+ "summary": {
247
+ "total_images": len(images),
248
+ "successful_predictions": successful_predictions,
249
+ "failed_predictions": len(images) - successful_predictions,
250
+ "emotion_counts": emotion_counts,
251
+ "emotion_distribution": emotion_distribution,
252
+ "dominant_emotion": dominant_emotion[0],
253
+ "dominant_emotion_count": dominant_emotion[1]
254
+ },
255
+ "model_used": self.model_name
256
+ }
257
+
258
+ def _calculate_intensity(self, confidence: float) -> str:
259
+ """
260
+ Calculate emotion intensity based on confidence.
261
+
262
+ Args:
263
+ confidence: Prediction confidence
264
+
265
+ Returns:
266
+ Intensity level ('high', 'medium', 'low')
267
+ """
268
+ if confidence >= INTENSITY_HIGH_THRESHOLD:
269
+ return "high"
270
+ elif confidence >= INTENSITY_MEDIUM_THRESHOLD:
271
+ return "medium"
272
+ else:
273
+ return "low"
274
+
275
+ def visualize_prediction(
276
+ self,
277
+ image: np.ndarray,
278
+ prediction: Dict
279
+ ) -> np.ndarray:
280
+ """
281
+ Visualize prediction on image.
282
+
283
+ Args:
284
+ image: Original image
285
+ prediction: Prediction result
286
+
287
+ Returns:
288
+ Image with visualizations
289
+ """
290
+ result = image.copy()
291
+
292
+ if self.face_detector and prediction.get("faces_info"):
293
+ # Draw face detection and emotion label
294
+ result = self.face_detector.draw_detections(
295
+ result,
296
+ prediction["faces_info"],
297
+ emotions=[prediction.get("emotion", "Unknown")],
298
+ confidences=[prediction.get("confidence", 0)]
299
+ )
300
+
301
+ return result
302
+
303
+ @staticmethod
304
+ def get_available_models() -> Dict[str, bool]:
305
+ """
306
+ Get available trained models.
307
+
308
+ Returns:
309
+ Dictionary of model name -> availability
310
+ """
311
+ return {
312
+ "custom_cnn": CUSTOM_CNN_PATH.exists(),
313
+ "mobilenet": MOBILENET_PATH.exists(),
314
+ "vgg19": VGG_PATH.exists()
315
+ }
316
+
317
+
318
+ def create_predictor(
319
+ model_name: str = "custom_cnn",
320
+ auto_load: bool = True
321
+ ) -> Optional[EmotionPredictor]:
322
+ """
323
+ Factory function to create a predictor.
324
+
325
+ Args:
326
+ model_name: Name of the model
327
+ auto_load: Whether to automatically load the model
328
+
329
+ Returns:
330
+ EmotionPredictor instance or None if loading fails
331
+ """
332
+ predictor = EmotionPredictor(model_name)
333
+
334
+ if auto_load:
335
+ if not predictor.load():
336
+ return None
337
+
338
+ return predictor
339
+
340
+
341
+ if __name__ == "__main__":
342
+ # Show available models
343
+ print("Available models:")
344
+ for name, available in EmotionPredictor.get_available_models().items():
345
+ status = "✓" if available else "✗"
346
+ print(f" {status} {name}")
src/models/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .custom_cnn import build_custom_cnn
2
+ from .mobilenet_model import build_mobilenet_model
3
+ from .vgg_model import build_vgg_model
4
+ from .model_utils import load_model, save_model, get_model_summary
5
+
6
+ __all__ = [
7
+ "build_custom_cnn",
8
+ "build_mobilenet_model",
9
+ "build_vgg_model",
10
+ "load_model",
11
+ "save_model",
12
+ "get_model_summary"
13
+ ]
src/models/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (444 Bytes). View file
 
src/models/__pycache__/custom_cnn.cpython-310.pyc ADDED
Binary file (4.13 kB). View file
 
src/models/__pycache__/mobilenet_model.cpython-310.pyc ADDED
Binary file (5.01 kB). View file
 
src/models/__pycache__/model_utils.cpython-310.pyc ADDED
Binary file (6.2 kB). View file
 
src/models/__pycache__/vgg_model.cpython-310.pyc ADDED
Binary file (6.16 kB). View file
 
src/models/custom_cnn.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom CNN model architecture for emotion recognition.
3
+ Optimized for 48x48 grayscale images.
4
+ """
5
+ import tensorflow as tf
6
+ from tensorflow.keras.models import Sequential, Model
7
+ from tensorflow.keras.layers import (
8
+ Conv2D, MaxPooling2D, Dense, Dropout, Flatten,
9
+ BatchNormalization, Input, GlobalAveragePooling2D
10
+ )
11
+ from tensorflow.keras.regularizers import l2
12
+
13
+ import sys
14
+ from pathlib import Path
15
+ sys.path.append(str(Path(__file__).parent.parent.parent))
16
+ from src.config import IMAGE_SIZE, NUM_CLASSES, NUM_CHANNELS
17
+
18
+
19
+ def build_custom_cnn(
20
+ input_shape: tuple = (*IMAGE_SIZE, NUM_CHANNELS),
21
+ num_classes: int = NUM_CLASSES,
22
+ dropout_rate: float = 0.25,
23
+ dense_dropout: float = 0.5,
24
+ l2_reg: float = 0.01
25
+ ) -> Model:
26
+ """
27
+ Build a custom CNN architecture for emotion recognition.
28
+
29
+ Architecture:
30
+ - 4 Convolutional blocks with increasing filters (64 -> 128 -> 256 -> 512)
31
+ - Each block: Conv2D -> BatchNorm -> ReLU -> MaxPool -> Dropout
32
+ - Dense layers for classification
33
+
34
+ Args:
35
+ input_shape: Input image shape (height, width, channels)
36
+ num_classes: Number of emotion classes
37
+ dropout_rate: Dropout rate for conv blocks
38
+ dense_dropout: Dropout rate for dense layers
39
+ l2_reg: L2 regularization factor
40
+
41
+ Returns:
42
+ Compiled Keras model
43
+ """
44
+ model = Sequential([
45
+ # Input layer
46
+ Input(shape=input_shape),
47
+
48
+ # Block 1: 64 filters
49
+ Conv2D(64, (3, 3), padding='same', activation='relu',
50
+ kernel_regularizer=l2(l2_reg)),
51
+ BatchNormalization(),
52
+ Conv2D(64, (3, 3), padding='same', activation='relu',
53
+ kernel_regularizer=l2(l2_reg)),
54
+ BatchNormalization(),
55
+ MaxPooling2D(pool_size=(2, 2)),
56
+ Dropout(dropout_rate),
57
+
58
+ # Block 2: 128 filters
59
+ Conv2D(128, (3, 3), padding='same', activation='relu',
60
+ kernel_regularizer=l2(l2_reg)),
61
+ BatchNormalization(),
62
+ Conv2D(128, (3, 3), padding='same', activation='relu',
63
+ kernel_regularizer=l2(l2_reg)),
64
+ BatchNormalization(),
65
+ MaxPooling2D(pool_size=(2, 2)),
66
+ Dropout(dropout_rate),
67
+
68
+ # Block 3: 256 filters
69
+ Conv2D(256, (3, 3), padding='same', activation='relu',
70
+ kernel_regularizer=l2(l2_reg)),
71
+ BatchNormalization(),
72
+ Conv2D(256, (3, 3), padding='same', activation='relu',
73
+ kernel_regularizer=l2(l2_reg)),
74
+ BatchNormalization(),
75
+ MaxPooling2D(pool_size=(2, 2)),
76
+ Dropout(dropout_rate),
77
+
78
+ # Block 4: 512 filters
79
+ Conv2D(512, (3, 3), padding='same', activation='relu',
80
+ kernel_regularizer=l2(l2_reg)),
81
+ BatchNormalization(),
82
+ Conv2D(512, (3, 3), padding='same', activation='relu',
83
+ kernel_regularizer=l2(l2_reg)),
84
+ BatchNormalization(),
85
+ MaxPooling2D(pool_size=(2, 2)),
86
+ Dropout(dropout_rate),
87
+
88
+ # Classification head
89
+ Flatten(),
90
+ Dense(512, activation='relu', kernel_regularizer=l2(l2_reg)),
91
+ BatchNormalization(),
92
+ Dropout(dense_dropout),
93
+ Dense(256, activation='relu', kernel_regularizer=l2(l2_reg)),
94
+ BatchNormalization(),
95
+ Dropout(dense_dropout),
96
+ Dense(num_classes, activation='softmax')
97
+ ], name='custom_emotion_cnn')
98
+
99
+ return model
100
+
101
+
102
+ def build_custom_cnn_v2(
103
+ input_shape: tuple = (*IMAGE_SIZE, NUM_CHANNELS),
104
+ num_classes: int = NUM_CLASSES
105
+ ) -> Model:
106
+ """
107
+ Alternative CNN architecture with residual-like connections.
108
+
109
+ Args:
110
+ input_shape: Input image shape
111
+ num_classes: Number of emotion classes
112
+
113
+ Returns:
114
+ Keras model
115
+ """
116
+ inputs = Input(shape=input_shape)
117
+
118
+ # Initial convolution
119
+ x = Conv2D(32, (3, 3), padding='same', activation='relu')(inputs)
120
+ x = BatchNormalization()(x)
121
+
122
+ # Block 1
123
+ x = Conv2D(64, (3, 3), padding='same', activation='relu')(x)
124
+ x = BatchNormalization()(x)
125
+ x = Conv2D(64, (3, 3), padding='same', activation='relu')(x)
126
+ x = BatchNormalization()(x)
127
+ x = MaxPooling2D(pool_size=(2, 2))(x)
128
+ x = Dropout(0.25)(x)
129
+
130
+ # Block 2
131
+ x = Conv2D(128, (3, 3), padding='same', activation='relu')(x)
132
+ x = BatchNormalization()(x)
133
+ x = Conv2D(128, (3, 3), padding='same', activation='relu')(x)
134
+ x = BatchNormalization()(x)
135
+ x = MaxPooling2D(pool_size=(2, 2))(x)
136
+ x = Dropout(0.25)(x)
137
+
138
+ # Block 3
139
+ x = Conv2D(256, (3, 3), padding='same', activation='relu')(x)
140
+ x = BatchNormalization()(x)
141
+ x = Conv2D(256, (3, 3), padding='same', activation='relu')(x)
142
+ x = BatchNormalization()(x)
143
+ x = MaxPooling2D(pool_size=(2, 2))(x)
144
+ x = Dropout(0.25)(x)
145
+
146
+ # Global pooling and classification
147
+ x = GlobalAveragePooling2D()(x)
148
+ x = Dense(256, activation='relu')(x)
149
+ x = BatchNormalization()(x)
150
+ x = Dropout(0.5)(x)
151
+ outputs = Dense(num_classes, activation='softmax')(x)
152
+
153
+ model = Model(inputs=inputs, outputs=outputs, name='custom_emotion_cnn_v2')
154
+
155
+ return model
156
+
157
+
158
+ def get_model_config() -> dict:
159
+ """
160
+ Get the default model configuration.
161
+
162
+ Returns:
163
+ Dictionary with model configuration
164
+ """
165
+ return {
166
+ "name": "Custom CNN",
167
+ "input_shape": (*IMAGE_SIZE, NUM_CHANNELS),
168
+ "num_classes": NUM_CLASSES,
169
+ "expected_accuracy": "60-68%",
170
+ "training_time": "~30 minutes (GPU)",
171
+ "parameters": "~5M"
172
+ }
173
+
174
+
175
+ if __name__ == "__main__":
176
+ # Build and display model summary
177
+ model = build_custom_cnn()
178
+ model.summary()
179
+
180
+ print("\nModel configuration:")
181
+ config = get_model_config()
182
+ for key, value in config.items():
183
+ print(f" {key}: {value}")
src/models/mobilenet_model.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MobileNetV2 transfer learning model for emotion recognition.
3
+ """
4
+ import tensorflow as tf
5
+ from tensorflow.keras.models import Model
6
+ from tensorflow.keras.layers import (
7
+ Dense, Dropout, GlobalAveragePooling2D,
8
+ BatchNormalization, Input, Lambda
9
+ )
10
+ from tensorflow.keras.applications import MobileNetV2
11
+
12
+ import sys
13
+ from pathlib import Path
14
+ sys.path.append(str(Path(__file__).parent.parent.parent))
15
+ from src.config import IMAGE_SIZE_TRANSFER, NUM_CLASSES, NUM_CHANNELS_RGB
16
+
17
+
18
+ def build_mobilenet_model(
19
+ input_shape: tuple = (*IMAGE_SIZE_TRANSFER, NUM_CHANNELS_RGB),
20
+ num_classes: int = NUM_CLASSES,
21
+ trainable_layers: int = 30,
22
+ dropout_rate: float = 0.5
23
+ ) -> Model:
24
+ """
25
+ Build MobileNetV2 transfer learning model for emotion recognition.
26
+
27
+ Args:
28
+ input_shape: Input image shape (height, width, channels)
29
+ num_classes: Number of emotion classes
30
+ trainable_layers: Number of top layers to make trainable
31
+ dropout_rate: Dropout rate for dense layers
32
+
33
+ Returns:
34
+ Keras model
35
+ """
36
+ # Load pre-trained MobileNetV2
37
+ base_model = MobileNetV2(
38
+ weights='imagenet',
39
+ include_top=False,
40
+ input_shape=input_shape
41
+ )
42
+
43
+ # Freeze base layers
44
+ for layer in base_model.layers[:-trainable_layers]:
45
+ layer.trainable = False
46
+
47
+ # Make top layers trainable
48
+ for layer in base_model.layers[-trainable_layers:]:
49
+ layer.trainable = True
50
+
51
+ # Build the model
52
+ inputs = Input(shape=input_shape)
53
+
54
+ # Preprocess input for MobileNetV2 using Rescaling layer
55
+ # MobileNetV2 expects inputs in [-1, 1] range
56
+ x = tf.keras.layers.Rescaling(scale=1./127.5, offset=-1.0)(inputs)
57
+
58
+ # Pass through base model
59
+ x = base_model(x, training=True)
60
+
61
+ # Classification head
62
+ x = GlobalAveragePooling2D()(x)
63
+ x = Dense(512, activation='relu')(x)
64
+ x = BatchNormalization()(x)
65
+ x = Dropout(dropout_rate)(x)
66
+ x = Dense(256, activation='relu')(x)
67
+ x = BatchNormalization()(x)
68
+ x = Dropout(dropout_rate)(x)
69
+ outputs = Dense(num_classes, activation='softmax')(x)
70
+
71
+ model = Model(inputs=inputs, outputs=outputs, name='mobilenet_emotion')
72
+
73
+ return model
74
+
75
+
76
+ def build_mobilenet_from_grayscale(
77
+ input_shape: tuple = (*IMAGE_SIZE_TRANSFER, 1),
78
+ num_classes: int = NUM_CLASSES,
79
+ trainable_layers: int = 30,
80
+ dropout_rate: float = 0.5
81
+ ) -> Model:
82
+ """
83
+ Build MobileNetV2 model that accepts grayscale input.
84
+ Converts grayscale to RGB internally.
85
+
86
+ Args:
87
+ input_shape: Input shape for grayscale images
88
+ num_classes: Number of emotion classes
89
+ trainable_layers: Number of top layers to make trainable
90
+ dropout_rate: Dropout rate
91
+
92
+ Returns:
93
+ Keras model
94
+ """
95
+ # Load pre-trained MobileNetV2
96
+ base_model = MobileNetV2(
97
+ weights='imagenet',
98
+ include_top=False,
99
+ input_shape=(*IMAGE_SIZE_TRANSFER, 3)
100
+ )
101
+
102
+ # Freeze base layers
103
+ for layer in base_model.layers[:-trainable_layers]:
104
+ layer.trainable = False
105
+
106
+ # Input for grayscale image
107
+ inputs = Input(shape=input_shape)
108
+
109
+ # Convert grayscale to RGB by repeating channels
110
+ x = tf.keras.layers.Concatenate()([inputs, inputs, inputs])
111
+
112
+ # Preprocess for MobileNetV2 using Rescaling layer
113
+ x = tf.keras.layers.Rescaling(scale=1./127.5, offset=-1.0)(x)
114
+
115
+ # Base model
116
+ x = base_model(x, training=True)
117
+
118
+ # Classification head
119
+ x = GlobalAveragePooling2D()(x)
120
+ x = Dense(512, activation='relu')(x)
121
+ x = BatchNormalization()(x)
122
+ x = Dropout(dropout_rate)(x)
123
+ x = Dense(256, activation='relu')(x)
124
+ x = BatchNormalization()(x)
125
+ x = Dropout(dropout_rate)(x)
126
+ outputs = Dense(num_classes, activation='softmax')(x)
127
+
128
+ model = Model(inputs=inputs, outputs=outputs, name='mobilenet_emotion_grayscale')
129
+
130
+ return model
131
+
132
+
133
+ def freeze_base_model(model: Model) -> Model:
134
+ """
135
+ Freeze all layers in the base MobileNetV2 model.
136
+ Useful for initial training with frozen weights.
137
+
138
+ Args:
139
+ model: MobileNet emotion model
140
+
141
+ Returns:
142
+ Model with frozen base
143
+ """
144
+ for layer in model.layers:
145
+ if 'mobilenet' in layer.name.lower():
146
+ layer.trainable = False
147
+ return model
148
+
149
+
150
+ def unfreeze_top_layers(model: Model, num_layers: int = 30) -> Model:
151
+ """
152
+ Unfreeze top layers of the base model for fine-tuning.
153
+
154
+ Args:
155
+ model: MobileNet emotion model
156
+ num_layers: Number of top layers to unfreeze
157
+
158
+ Returns:
159
+ Model with partially unfrozen base
160
+ """
161
+ for layer in model.layers:
162
+ if 'mobilenet' in layer.name.lower():
163
+ # Get base model and unfreeze top layers
164
+ for base_layer in layer.layers[-num_layers:]:
165
+ base_layer.trainable = True
166
+ return model
167
+
168
+
169
+ def get_model_config() -> dict:
170
+ """
171
+ Get the default model configuration.
172
+
173
+ Returns:
174
+ Dictionary with model configuration
175
+ """
176
+ return {
177
+ "name": "MobileNetV2",
178
+ "input_shape": (*IMAGE_SIZE_TRANSFER, NUM_CHANNELS_RGB),
179
+ "num_classes": NUM_CLASSES,
180
+ "expected_accuracy": "65-72%",
181
+ "training_time": "~45 minutes (GPU)",
182
+ "parameters": "~3.5M",
183
+ "base_model": "MobileNetV2 (ImageNet)"
184
+ }
185
+
186
+
187
+ if __name__ == "__main__":
188
+ # Build and display model summary
189
+ print("Building MobileNetV2 model...")
190
+ model = build_mobilenet_model()
191
+
192
+ # Count trainable parameters
193
+ trainable = sum([tf.keras.backend.count_params(w) for w in model.trainable_weights])
194
+ non_trainable = sum([tf.keras.backend.count_params(w) for w in model.non_trainable_weights])
195
+
196
+ print(f"\nTotal parameters: {trainable + non_trainable:,}")
197
+ print(f"Trainable parameters: {trainable:,}")
198
+ print(f"Non-trainable parameters: {non_trainable:,}")
199
+
200
+ print("\nModel configuration:")
201
+ config = get_model_config()
202
+ for key, value in config.items():
203
+ print(f" {key}: {value}")
src/models/model_utils.py ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # """
2
+ # Model utility functions for saving, loading, and inspecting models.
3
+ # """
4
+ # import os
5
+ # import json
6
+ # from pathlib import Path
7
+ # from typing import Dict, Optional, Union
8
+
9
+ # import tensorflow as tf
10
+ # from tensorflow.keras.models import Model, load_model as keras_load_model
11
+
12
+ # import sys
13
+ # sys.path.append(str(Path(__file__).parent.parent.parent))
14
+ # from src.config import MODELS_DIR, CUSTOM_CNN_PATH, MOBILENET_PATH, VGG_PATH
15
+
16
+
17
+ # def save_model(
18
+ # model: Model,
19
+ # save_path: Union[str, Path],
20
+ # save_format: str = 'h5',
21
+ # include_optimizer: bool = True,
22
+ # save_metadata: bool = True,
23
+ # metadata: Optional[Dict] = None
24
+ # ) -> None:
25
+ # """
26
+ # Save a trained model to disk.
27
+
28
+ # Args:
29
+ # model: Keras model to save
30
+ # save_path: Path to save the model
31
+ # save_format: Format to save ('h5' or 'tf')
32
+ # include_optimizer: Whether to include optimizer state
33
+ # save_metadata: Whether to save training metadata
34
+ # metadata: Optional metadata dictionary
35
+ # """
36
+ # save_path = Path(save_path)
37
+
38
+ # # Create directory if needed
39
+ # save_path.parent.mkdir(parents=True, exist_ok=True)
40
+
41
+ # if save_format == 'h5':
42
+ # model.save(str(save_path), include_optimizer=include_optimizer)
43
+ # else:
44
+ # # SavedModel format
45
+ # model.save(str(save_path.with_suffix('')), save_format='tf')
46
+
47
+ # # Save metadata if requested
48
+ # if save_metadata and metadata:
49
+ # metadata_path = save_path.with_suffix('.json')
50
+ # with open(metadata_path, 'w') as f:
51
+ # json.dump(metadata, f, indent=2)
52
+
53
+ # print(f"Model saved to: {save_path}")
54
+
55
+
56
+ # def load_model(
57
+ # model_path: Union[str, Path],
58
+ # custom_objects: Optional[Dict] = None,
59
+ # compile_model: bool = True
60
+ # ) -> Model:
61
+ # """
62
+ # Load a saved model from disk.
63
+
64
+ # Args:
65
+ # model_path: Path to the saved model
66
+ # custom_objects: Optional custom objects for loading
67
+ # compile_model: Whether to compile the model
68
+
69
+ # Returns:
70
+ # Loaded Keras model
71
+ # """
72
+ # model_path = Path(model_path)
73
+
74
+ # if not model_path.exists():
75
+ # # Check if it's a SavedModel directory
76
+ # if model_path.with_suffix('').exists():
77
+ # model_path = model_path.with_suffix('')
78
+ # else:
79
+ # raise FileNotFoundError(f"Model not found: {model_path}")
80
+
81
+ # model = keras_load_model(
82
+ # str(model_path),
83
+ # custom_objects=custom_objects,
84
+ # compile=compile_model
85
+ # )
86
+
87
+ # print(f"Model loaded from: {model_path}")
88
+ # return model
89
+
90
+
91
+ # def load_model_metadata(model_path: Union[str, Path]) -> Optional[Dict]:
92
+ # """
93
+ # Load metadata for a saved model.
94
+
95
+ # Args:
96
+ # model_path: Path to the saved model
97
+
98
+ # Returns:
99
+ # Metadata dictionary or None
100
+ # """
101
+ # metadata_path = Path(model_path).with_suffix('.json')
102
+
103
+ # if metadata_path.exists():
104
+ # with open(metadata_path, 'r') as f:
105
+ # return json.load(f)
106
+ # return None
107
+
108
+
109
+ # def get_model_summary(model: Model, print_summary: bool = True) -> Dict:
110
+ # """
111
+ # Get a summary of the model architecture.
112
+
113
+ # Args:
114
+ # model: Keras model
115
+ # print_summary: Whether to print the summary
116
+
117
+ # Returns:
118
+ # Dictionary with model statistics
119
+ # """
120
+ # if print_summary:
121
+ # model.summary()
122
+
123
+ # # Calculate parameters
124
+ # trainable = sum([tf.keras.backend.count_params(w) for w in model.trainable_weights])
125
+ # non_trainable = sum([tf.keras.backend.count_params(w) for w in model.non_trainable_weights])
126
+
127
+ # summary = {
128
+ # "name": model.name,
129
+ # "total_params": trainable + non_trainable,
130
+ # "trainable_params": trainable,
131
+ # "non_trainable_params": non_trainable,
132
+ # "num_layers": len(model.layers),
133
+ # "input_shape": model.input_shape,
134
+ # "output_shape": model.output_shape
135
+ # }
136
+
137
+ # return summary
138
+
139
+
140
+ # def get_available_models() -> Dict[str, Dict]:
141
+ # """
142
+ # Get information about available pre-trained models.
143
+
144
+ # Returns:
145
+ # Dictionary with model information
146
+ # """
147
+ # models = {}
148
+
149
+ # model_paths = {
150
+ # "custom_cnn": CUSTOM_CNN_PATH,
151
+ # "mobilenet": MOBILENET_PATH,
152
+ # "vgg19": VGG_PATH
153
+ # }
154
+
155
+ # for name, path in model_paths.items():
156
+ # if Path(path).exists():
157
+ # metadata = load_model_metadata(path)
158
+ # models[name] = {
159
+ # "path": str(path),
160
+ # "exists": True,
161
+ # "metadata": metadata
162
+ # }
163
+ # else:
164
+ # models[name] = {
165
+ # "path": str(path),
166
+ # "exists": False,
167
+ # "metadata": None
168
+ # }
169
+
170
+ # return models
171
+
172
+
173
+ # def compare_models(models: Dict[str, Model]) -> Dict:
174
+ # """
175
+ # Compare multiple models.
176
+
177
+ # Args:
178
+ # models: Dictionary of model name -> model
179
+
180
+ # Returns:
181
+ # Comparison dictionary
182
+ # """
183
+ # comparison = {}
184
+
185
+ # for name, model in models.items():
186
+ # summary = get_model_summary(model, print_summary=False)
187
+ # comparison[name] = {
188
+ # "params": summary["total_params"],
189
+ # "trainable_params": summary["trainable_params"],
190
+ # "layers": summary["num_layers"]
191
+ # }
192
+
193
+ # return comparison
194
+
195
+
196
+ # def export_to_tflite(
197
+ # model: Model,
198
+ # save_path: Union[str, Path],
199
+ # quantize: bool = False
200
+ # ) -> None:
201
+ # """
202
+ # Export model to TensorFlow Lite format.
203
+
204
+ # Args:
205
+ # model: Keras model to export
206
+ # save_path: Path to save the TFLite model
207
+ # quantize: Whether to apply quantization
208
+ # """
209
+ # converter = tf.lite.TFLiteConverter.from_keras_model(model)
210
+
211
+ # if quantize:
212
+ # converter.optimizations = [tf.lite.Optimize.DEFAULT]
213
+
214
+ # tflite_model = converter.convert()
215
+
216
+ # save_path = Path(save_path)
217
+ # save_path.parent.mkdir(parents=True, exist_ok=True)
218
+
219
+ # with open(save_path, 'wb') as f:
220
+ # f.write(tflite_model)
221
+
222
+ # print(f"TFLite model saved to: {save_path}")
223
+
224
+
225
+ # if __name__ == "__main__":
226
+ # print("Available models:")
227
+ # models = get_available_models()
228
+ # for name, info in models.items():
229
+ # status = "✓ Trained" if info["exists"] else "✗ Not trained"
230
+ # print(f" {name}: {status}")
231
+
232
+ """
233
+ Model utility functions for saving, loading, and inspecting models.
234
+ """
235
+ import os
236
+ import json
237
+ from pathlib import Path
238
+ from typing import Dict, Optional, Union
239
+
240
+ import tensorflow as tf
241
+ from tensorflow.keras.models import Model, load_model as keras_load_model
242
+
243
+ import sys
244
+ sys.path.append(str(Path(__file__).parent.parent.parent))
245
+ from src.config import MODELS_DIR, CUSTOM_CNN_PATH, MOBILENET_PATH, VGG_PATH
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # Legacy preprocessing functions
250
+ # ---------------------------------------------------------------------------
251
+ # Older saved .h5 models used Lambda layers that baked these functions in.
252
+ # Current model code uses Rescaling layers instead, but these definitions
253
+ # must remain so keras_load_model() can deserialise the old .h5 files.
254
+ # ---------------------------------------------------------------------------
255
+
256
+ def preprocess_mobilenet(x):
257
+ """Legacy MobileNetV2 preprocessor — scales pixels to [-1, 1]."""
258
+ return x / 127.5 - 1.0
259
+
260
+
261
+ def preprocess_vgg(x):
262
+ """Legacy VGG-19 preprocessor — mean-subtracted scaling."""
263
+ return x * 255.0 - 127.5
264
+
265
+
266
+ _LEGACY_CUSTOM_OBJECTS: Dict = {
267
+ "preprocess_mobilenet": preprocess_mobilenet,
268
+ "preprocess_vgg": preprocess_vgg,
269
+ }
270
+
271
+
272
+ def save_model(
273
+ model: Model,
274
+ save_path: Union[str, Path],
275
+ save_format: str = 'h5',
276
+ include_optimizer: bool = True,
277
+ save_metadata: bool = True,
278
+ metadata: Optional[Dict] = None
279
+ ) -> None:
280
+ """
281
+ Save a trained model to disk.
282
+
283
+ Args:
284
+ model: Keras model to save
285
+ save_path: Path to save the model
286
+ save_format: Format to save ('h5' or 'tf')
287
+ include_optimizer: Whether to include optimizer state
288
+ save_metadata: Whether to save training metadata
289
+ metadata: Optional metadata dictionary
290
+ """
291
+ save_path = Path(save_path)
292
+
293
+ # Create directory if needed
294
+ save_path.parent.mkdir(parents=True, exist_ok=True)
295
+
296
+ if save_format == 'h5':
297
+ model.save(str(save_path), include_optimizer=include_optimizer)
298
+ else:
299
+ # SavedModel format
300
+ model.save(str(save_path.with_suffix('')), save_format='tf')
301
+
302
+ # Save metadata if requested
303
+ if save_metadata and metadata:
304
+ metadata_path = save_path.with_suffix('.json')
305
+ with open(metadata_path, 'w') as f:
306
+ json.dump(metadata, f, indent=2)
307
+
308
+ print(f"Model saved to: {save_path}")
309
+
310
+
311
+ def load_model(
312
+ model_path: Union[str, Path],
313
+ custom_objects: Optional[Dict] = None,
314
+ compile_model: bool = True
315
+ ) -> Model:
316
+ """
317
+ Load a saved model from disk.
318
+
319
+ Args:
320
+ model_path: Path to the saved model
321
+ custom_objects: Optional custom objects for loading
322
+ compile_model: Whether to compile the model
323
+
324
+ Returns:
325
+ Loaded Keras model
326
+ """
327
+ model_path = Path(model_path)
328
+
329
+ # Always include legacy preprocessing functions so that old .h5 models
330
+ # saved with Lambda layers can be loaded without extra steps.
331
+ merged_objects = dict(_LEGACY_CUSTOM_OBJECTS)
332
+ if custom_objects:
333
+ merged_objects.update(custom_objects)
334
+
335
+ if not model_path.exists():
336
+ # Check if it's a SavedModel directory
337
+ if model_path.with_suffix('').exists():
338
+ model_path = model_path.with_suffix('')
339
+ else:
340
+ raise FileNotFoundError(f"Model not found: {model_path}")
341
+
342
+ model = keras_load_model(
343
+ str(model_path),
344
+ custom_objects=merged_objects,
345
+ compile=compile_model
346
+ )
347
+
348
+ print(f"Model loaded from: {model_path}")
349
+ return model
350
+
351
+
352
+ def load_model_metadata(model_path: Union[str, Path]) -> Optional[Dict]:
353
+ """
354
+ Load metadata for a saved model.
355
+
356
+ Args:
357
+ model_path: Path to the saved model
358
+
359
+ Returns:
360
+ Metadata dictionary or None
361
+ """
362
+ metadata_path = Path(model_path).with_suffix('.json')
363
+
364
+ if metadata_path.exists():
365
+ with open(metadata_path, 'r') as f:
366
+ return json.load(f)
367
+ return None
368
+
369
+
370
+ def get_model_summary(model: Model, print_summary: bool = True) -> Dict:
371
+ """
372
+ Get a summary of the model architecture.
373
+
374
+ Args:
375
+ model: Keras model
376
+ print_summary: Whether to print the summary
377
+
378
+ Returns:
379
+ Dictionary with model statistics
380
+ """
381
+ if print_summary:
382
+ model.summary()
383
+
384
+ # Calculate parameters
385
+ trainable = sum([tf.keras.backend.count_params(w) for w in model.trainable_weights])
386
+ non_trainable = sum([tf.keras.backend.count_params(w) for w in model.non_trainable_weights])
387
+
388
+ summary = {
389
+ "name": model.name,
390
+ "total_params": trainable + non_trainable,
391
+ "trainable_params": trainable,
392
+ "non_trainable_params": non_trainable,
393
+ "num_layers": len(model.layers),
394
+ "input_shape": model.input_shape,
395
+ "output_shape": model.output_shape
396
+ }
397
+
398
+ return summary
399
+
400
+
401
+ def get_available_models() -> Dict[str, Dict]:
402
+ """
403
+ Get information about available pre-trained models.
404
+
405
+ Returns:
406
+ Dictionary with model information
407
+ """
408
+ models = {}
409
+
410
+ model_paths = {
411
+ "custom_cnn": CUSTOM_CNN_PATH,
412
+ "mobilenet": MOBILENET_PATH,
413
+ "vgg19": VGG_PATH
414
+ }
415
+
416
+ for name, path in model_paths.items():
417
+ if Path(path).exists():
418
+ metadata = load_model_metadata(path)
419
+ models[name] = {
420
+ "path": str(path),
421
+ "exists": True,
422
+ "metadata": metadata
423
+ }
424
+ else:
425
+ models[name] = {
426
+ "path": str(path),
427
+ "exists": False,
428
+ "metadata": None
429
+ }
430
+
431
+ return models
432
+
433
+
434
+ def compare_models(models: Dict[str, Model]) -> Dict:
435
+ """
436
+ Compare multiple models.
437
+
438
+ Args:
439
+ models: Dictionary of model name -> model
440
+
441
+ Returns:
442
+ Comparison dictionary
443
+ """
444
+ comparison = {}
445
+
446
+ for name, model in models.items():
447
+ summary = get_model_summary(model, print_summary=False)
448
+ comparison[name] = {
449
+ "params": summary["total_params"],
450
+ "trainable_params": summary["trainable_params"],
451
+ "layers": summary["num_layers"]
452
+ }
453
+
454
+ return comparison
455
+
456
+
457
+ def export_to_tflite(
458
+ model: Model,
459
+ save_path: Union[str, Path],
460
+ quantize: bool = False
461
+ ) -> None:
462
+ """
463
+ Export model to TensorFlow Lite format.
464
+
465
+ Args:
466
+ model: Keras model to export
467
+ save_path: Path to save the TFLite model
468
+ quantize: Whether to apply quantization
469
+ """
470
+ converter = tf.lite.TFLiteConverter.from_keras_model(model)
471
+
472
+ if quantize:
473
+ converter.optimizations = [tf.lite.Optimize.DEFAULT]
474
+
475
+ tflite_model = converter.convert()
476
+
477
+ save_path = Path(save_path)
478
+ save_path.parent.mkdir(parents=True, exist_ok=True)
479
+
480
+ with open(save_path, 'wb') as f:
481
+ f.write(tflite_model)
482
+
483
+ print(f"TFLite model saved to: {save_path}")
484
+
485
+
486
+ if __name__ == "__main__":
487
+ print("Available models:")
488
+ models = get_available_models()
489
+ for name, info in models.items():
490
+ status = "✓ Trained" if info["exists"] else "✗ Not trained"
491
+ print(f" {name}: {status}")
src/models/vgg_model.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VGG-19 transfer learning model for emotion recognition.
3
+ """
4
+ import tensorflow as tf
5
+ from tensorflow.keras.models import Model
6
+ from tensorflow.keras.layers import (
7
+ Dense, Dropout, GlobalAveragePooling2D, Flatten,
8
+ BatchNormalization, Input, Lambda
9
+ )
10
+ from tensorflow.keras.applications import VGG19
11
+
12
+ import sys
13
+ from pathlib import Path
14
+ sys.path.append(str(Path(__file__).parent.parent.parent))
15
+ from src.config import IMAGE_SIZE_TRANSFER, NUM_CLASSES, NUM_CHANNELS_RGB
16
+
17
+
18
+ def build_vgg_model(
19
+ input_shape: tuple = (*IMAGE_SIZE_TRANSFER, NUM_CHANNELS_RGB),
20
+ num_classes: int = NUM_CLASSES,
21
+ trainable_layers: int = 4,
22
+ dropout_rate: float = 0.5
23
+ ) -> Model:
24
+ """
25
+ Build VGG-19 transfer learning model for emotion recognition.
26
+
27
+ Args:
28
+ input_shape: Input image shape (height, width, channels)
29
+ num_classes: Number of emotion classes
30
+ trainable_layers: Number of top convolutional layers to make trainable
31
+ dropout_rate: Dropout rate for dense layers
32
+
33
+ Returns:
34
+ Keras model
35
+ """
36
+ # Load pre-trained VGG19
37
+ base_model = VGG19(
38
+ weights='imagenet',
39
+ include_top=False,
40
+ input_shape=input_shape
41
+ )
42
+
43
+ # Freeze all layers initially
44
+ for layer in base_model.layers:
45
+ layer.trainable = False
46
+
47
+ # Unfreeze top convolutional layers for fine-tuning
48
+ for layer in base_model.layers[-trainable_layers:]:
49
+ layer.trainable = True
50
+
51
+ # Build the model
52
+ inputs = Input(shape=input_shape)
53
+
54
+ # Preprocess input for VGG19 using Rescaling layer
55
+ # VGG19 expects inputs scaled to 0-255 range with mean subtraction
56
+ x = tf.keras.layers.Rescaling(scale=255.0, offset=-127.5)(inputs)
57
+
58
+ # Pass through base model
59
+ x = base_model(x, training=True)
60
+
61
+ # Classification head
62
+ x = GlobalAveragePooling2D()(x)
63
+ x = Dense(512, activation='relu')(x)
64
+ x = BatchNormalization()(x)
65
+ x = Dropout(dropout_rate)(x)
66
+ x = Dense(256, activation='relu')(x)
67
+ x = BatchNormalization()(x)
68
+ x = Dropout(dropout_rate)(x)
69
+ outputs = Dense(num_classes, activation='softmax')(x)
70
+
71
+ model = Model(inputs=inputs, outputs=outputs, name='vgg19_emotion')
72
+
73
+ return model
74
+
75
+
76
+ def build_vgg_from_grayscale(
77
+ input_shape: tuple = (*IMAGE_SIZE_TRANSFER, 1),
78
+ num_classes: int = NUM_CLASSES,
79
+ trainable_layers: int = 4,
80
+ dropout_rate: float = 0.5
81
+ ) -> Model:
82
+ """
83
+ Build VGG-19 model that accepts grayscale input.
84
+ Converts grayscale to RGB internally.
85
+
86
+ Args:
87
+ input_shape: Input shape for grayscale images
88
+ num_classes: Number of emotion classes
89
+ trainable_layers: Number of top layers to make trainable
90
+ dropout_rate: Dropout rate
91
+
92
+ Returns:
93
+ Keras model
94
+ """
95
+ # Load pre-trained VGG19
96
+ base_model = VGG19(
97
+ weights='imagenet',
98
+ include_top=False,
99
+ input_shape=(*IMAGE_SIZE_TRANSFER, 3)
100
+ )
101
+
102
+ # Freeze base layers
103
+ for layer in base_model.layers:
104
+ layer.trainable = False
105
+
106
+ # Unfreeze top layers
107
+ for layer in base_model.layers[-trainable_layers:]:
108
+ layer.trainable = True
109
+
110
+ # Input for grayscale image
111
+ inputs = Input(shape=input_shape)
112
+
113
+ # Convert grayscale to RGB by repeating channels
114
+ x = tf.keras.layers.Concatenate()([inputs, inputs, inputs])
115
+
116
+ # Preprocess for VGG19 using Rescaling layer
117
+ x = tf.keras.layers.Rescaling(scale=255.0, offset=-127.5)(x)
118
+
119
+ # Base model
120
+ x = base_model(x, training=True)
121
+
122
+ # Classification head
123
+ x = GlobalAveragePooling2D()(x)
124
+ x = Dense(512, activation='relu')(x)
125
+ x = BatchNormalization()(x)
126
+ x = Dropout(dropout_rate)(x)
127
+ x = Dense(256, activation='relu')(x)
128
+ x = BatchNormalization()(x)
129
+ x = Dropout(dropout_rate)(x)
130
+ outputs = Dense(num_classes, activation='softmax')(x)
131
+
132
+ model = Model(inputs=inputs, outputs=outputs, name='vgg19_emotion_grayscale')
133
+
134
+ return model
135
+
136
+
137
+ def build_vgg_with_flatten(
138
+ input_shape: tuple = (*IMAGE_SIZE_TRANSFER, NUM_CHANNELS_RGB),
139
+ num_classes: int = NUM_CLASSES,
140
+ dropout_rate: float = 0.5
141
+ ) -> Model:
142
+ """
143
+ Alternative VGG-19 architecture using Flatten instead of GAP.
144
+ This is closer to the original VGG architecture.
145
+
146
+ Args:
147
+ input_shape: Input image shape
148
+ num_classes: Number of emotion classes
149
+ dropout_rate: Dropout rate
150
+
151
+ Returns:
152
+ Keras model
153
+ """
154
+ base_model = VGG19(
155
+ weights='imagenet',
156
+ include_top=False,
157
+ input_shape=input_shape
158
+ )
159
+
160
+ # Freeze base model
161
+ for layer in base_model.layers:
162
+ layer.trainable = False
163
+
164
+ inputs = Input(shape=input_shape)
165
+ x = tf.keras.layers.Rescaling(scale=255.0, offset=-127.5)(inputs)
166
+ x = base_model(x, training=False)
167
+
168
+ # VGG-style classification head
169
+ x = Flatten()(x)
170
+ x = Dense(4096, activation='relu')(x)
171
+ x = Dropout(dropout_rate)(x)
172
+ x = Dense(4096, activation='relu')(x)
173
+ x = Dropout(dropout_rate)(x)
174
+ outputs = Dense(num_classes, activation='softmax')(x)
175
+
176
+ model = Model(inputs=inputs, outputs=outputs, name='vgg19_emotion_flatten')
177
+
178
+ return model
179
+
180
+
181
+ def freeze_base_model(model: Model) -> Model:
182
+ """
183
+ Freeze all layers in the base VGG model.
184
+
185
+ Args:
186
+ model: VGG emotion model
187
+
188
+ Returns:
189
+ Model with frozen base
190
+ """
191
+ for layer in model.layers:
192
+ if 'vgg' in layer.name.lower():
193
+ layer.trainable = False
194
+ return model
195
+
196
+
197
+ def unfreeze_top_blocks(model: Model, num_blocks: int = 1) -> Model:
198
+ """
199
+ Unfreeze top convolutional blocks of VGG for fine-tuning.
200
+ VGG19 has 5 blocks. Block 5 has 4 conv layers.
201
+
202
+ Args:
203
+ model: VGG emotion model
204
+ num_blocks: Number of blocks to unfreeze from top
205
+
206
+ Returns:
207
+ Model with partially unfrozen base
208
+ """
209
+ # Block layer counts: block1=2, block2=2, block3=4, block4=4, block5=4
210
+ block_layers = {5: 4, 4: 4, 3: 4, 2: 2, 1: 2}
211
+
212
+ layers_to_unfreeze = sum([block_layers[i] for i in range(6 - num_blocks, 6)])
213
+
214
+ for layer in model.layers:
215
+ if 'vgg' in layer.name.lower():
216
+ for vgg_layer in layer.layers[-layers_to_unfreeze:]:
217
+ if 'conv' in vgg_layer.name:
218
+ vgg_layer.trainable = True
219
+
220
+ return model
221
+
222
+
223
+ def get_model_config() -> dict:
224
+ """
225
+ Get the default model configuration.
226
+
227
+ Returns:
228
+ Dictionary with model configuration
229
+ """
230
+ return {
231
+ "name": "VGG-19",
232
+ "input_shape": (*IMAGE_SIZE_TRANSFER, NUM_CHANNELS_RGB),
233
+ "num_classes": NUM_CLASSES,
234
+ "expected_accuracy": "68-75%",
235
+ "training_time": "~60 minutes (GPU)",
236
+ "parameters": "~20M",
237
+ "base_model": "VGG-19 (ImageNet)"
238
+ }
239
+
240
+
241
+ if __name__ == "__main__":
242
+ # Build and display model summary
243
+ print("Building VGG-19 model...")
244
+ model = build_vgg_model()
245
+
246
+ # Count trainable parameters
247
+ trainable = sum([tf.keras.backend.count_params(w) for w in model.trainable_weights])
248
+ non_trainable = sum([tf.keras.backend.count_params(w) for w in model.non_trainable_weights])
249
+
250
+ print(f"\nTotal parameters: {trainable + non_trainable:,}")
251
+ print(f"Trainable parameters: {trainable:,}")
252
+ print(f"Non-trainable parameters: {non_trainable:,}")
253
+
254
+ print("\nModel configuration:")
255
+ config = get_model_config()
256
+ for key, value in config.items():
257
+ print(f" {key}: {value}")