allantacuelwvsu commited on
Commit
52109c6
·
1 Parent(s): 33c6b5f

upload app and supporting files

Browse files
app.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import streamlit as st
3
+ from streamlit_webrtc import webrtc_streamer, VideoProcessorBase
4
+ import av
5
+ import cv2
6
+ import torch
7
+ import numpy as np
8
+ from torchvision import transforms
9
+ from utils.model_loader import load_model
10
+
11
+
12
+ st.title("Facial Expression Recognition")
13
+
14
+ # Load model
15
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
16
+ MODEL_PATH = "data/models/expression_predictor_cnn.pth"
17
+ CLASSES = ['Angry', 'Disgust', 'Scared', 'Happy', 'Neutral', 'Sad', 'Surprised']
18
+ model = load_model(MODEL_PATH, DEVICE)
19
+
20
+ # Face detection
21
+ face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
22
+
23
+ # Transform for inference
24
+ transform = transforms.Compose([
25
+ transforms.ToPILImage(),
26
+ transforms.Grayscale(),
27
+ transforms.Resize((48, 48)),
28
+ transforms.ToTensor(),
29
+ transforms.Normalize((0.5,), (0.5,))
30
+ ])
31
+
32
+ # Video processor
33
+ livestatus = st.empty()
34
+ class VideoProcessor(VideoProcessorBase):
35
+ def recv(self, frame):
36
+ global global_face_data
37
+ img = frame.to_ndarray(format="bgr24")
38
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
39
+ faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5)
40
+
41
+ face_data = []
42
+
43
+ for i, (x, y, w, h) in enumerate(faces):
44
+ face_crop = gray[y:y+h, x:x+w]
45
+ face_tensor = transform(face_crop).unsqueeze(0).to(DEVICE)
46
+
47
+ with torch.no_grad():
48
+ outputs = model(face_tensor)
49
+ probs = torch.nn.functional.softmax(outputs, dim=1)[0].cpu().numpy()
50
+ top_idx = np.argmax(probs)
51
+ label = CLASSES[top_idx]
52
+
53
+ # Draw face + label on video
54
+ face_id = f"Face {i+1}"
55
+ cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 1)
56
+ cv2.putText(img, f"{face_id}: {label}", (x, y - 10),
57
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
58
+
59
+ return av.VideoFrame.from_ndarray(img, format="bgr24")
60
+
61
+ ctx = webrtc_streamer(
62
+ key="emotion-detect",
63
+ video_processor_factory=VideoProcessor,
64
+ media_stream_constraints={"video": True, "audio": False},
65
+ async_processing=True,
66
+ )
67
+
68
+ if ctx.state.playing:
69
+ livestatus.success("🟢 Live")
70
+ else:
71
+ livestatus.error("🔴 Offline")
data/models/expression_predictor_cnn.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dbe74499e1659461a187bd820854520513fb94b2e9c094da7c2adf2915038d32
3
+ size 1576685
src/streamlit_app.py DELETED
@@ -1,40 +0,0 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
training_loss_plot.png ADDED
utils/model_loader.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ class ExpressionCNN(nn.Module):
5
+ def __init__(self, num_classes=7):
6
+ super(ExpressionCNN, self).__init__()
7
+ self.conv = nn.Sequential(
8
+ nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(32), nn.MaxPool2d(2),
9
+ nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(64), nn.MaxPool2d(2),
10
+ nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(128), nn.MaxPool2d(2),
11
+ nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(256), nn.AdaptiveAvgPool2d((1, 1))
12
+ )
13
+ self.fc = nn.Sequential(
14
+ nn.Flatten(),
15
+ nn.Linear(256, num_classes)
16
+ )
17
+
18
+ def forward(self, x):
19
+ x = self.conv(x)
20
+ x = self.fc(x)
21
+ return x
22
+
23
+ def load_model(model_path, device):
24
+ model = ExpressionCNN()
25
+ model.load_state_dict(torch.load(model_path, map_location=device))
26
+ model.to(device)
27
+ model.eval()
28
+ return model
utils/train.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.optim as optim
4
+ from torchvision import datasets, transforms
5
+ from torch.utils.data import DataLoader
6
+ import matplotlib.pyplot as plt
7
+ from PIL import ImageFile
8
+
9
+ # Configuration
10
+ BATCH_SIZE = 64
11
+ EPOCHS = 10
12
+ IMG_SIZE = 48
13
+ MODEL_PATH = "data/models/expression_predictor_cnn.pth"
14
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
15
+ print(f"Using device: {DEVICE}")
16
+
17
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
18
+
19
+ # Data transforms
20
+ transform = transforms.Compose([
21
+ transforms.Grayscale(),
22
+ transforms.Resize((48, 48)),
23
+ transforms.RandomHorizontalFlip(),
24
+ transforms.RandomRotation(10),
25
+ transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
26
+ transforms.ToTensor(),
27
+ transforms.Normalize((0.5,), (0.5,))
28
+ ])
29
+
30
+ # Datasets and loaders
31
+ train_dataset = datasets.ImageFolder("data/train", transform=transform)
32
+ val_dataset = datasets.ImageFolder("data/validation", transform=transform)
33
+
34
+ train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=0)
35
+ val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, num_workers=0)
36
+
37
+ # Class names
38
+ CLASSES = train_dataset.classes
39
+ NUM_CLASSES = len(CLASSES)
40
+ print(f"Classes: {CLASSES}")
41
+
42
+ # CNN Model
43
+ class ExpressionCNN(nn.Module):
44
+ def __init__(self):
45
+ super(ExpressionCNN, self).__init__()
46
+ self.conv = nn.Sequential(
47
+ nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(32), nn.MaxPool2d(2),
48
+ nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(64), nn.MaxPool2d(2),
49
+ nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(128), nn.MaxPool2d(2),
50
+ nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(), nn.BatchNorm2d(256), nn.AdaptiveAvgPool2d((1, 1))
51
+ )
52
+ self.fc = nn.Sequential(
53
+ nn.Flatten(),
54
+ nn.Linear(256, NUM_CLASSES)
55
+ )
56
+
57
+ def forward(self, x):
58
+ x = self.conv(x)
59
+ x = self.fc(x)
60
+ return x
61
+
62
+ model = ExpressionCNN().to(DEVICE)
63
+ criterion = nn.CrossEntropyLoss()
64
+ optimizer = optim.Adam(model.parameters(), lr=0.001)
65
+ scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)
66
+
67
+ # Training loop
68
+ train_loss_log = []
69
+ val_loss_log = []
70
+
71
+ for epoch in range(EPOCHS):
72
+ print(f"\nStarting Epoch {epoch+1}/{EPOCHS}")
73
+ model.train()
74
+ running_loss = 0.0
75
+ for images, labels in train_loader:
76
+ images, labels = images.to(DEVICE), labels.to(DEVICE)
77
+ optimizer.zero_grad()
78
+ outputs = model(images)
79
+ loss = criterion(outputs, labels)
80
+ loss.backward()
81
+ optimizer.step()
82
+ running_loss += loss.item()
83
+ scheduler.step()
84
+
85
+ train_loss = running_loss / len(train_loader)
86
+ train_loss_log.append(train_loss)
87
+
88
+ # Validation
89
+ model.eval()
90
+ val_loss = 0.0
91
+ correct = 0
92
+ total = 0
93
+ with torch.no_grad():
94
+ for images, labels in val_loader:
95
+ images, labels = images.to(DEVICE), labels.to(DEVICE)
96
+ outputs = model(images)
97
+ loss = criterion(outputs, labels)
98
+ val_loss += loss.item()
99
+ _, predicted = torch.max(outputs, 1)
100
+ correct += (predicted == labels).sum().item()
101
+ total += labels.size(0)
102
+
103
+ val_loss /= len(val_loader)
104
+ val_loss_log.append(val_loss)
105
+ accuracy = correct / total * 100
106
+
107
+ print(f"[{epoch+1}/{EPOCHS}] Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val Acc: {accuracy:.2f}%")
108
+
109
+ # Save model
110
+ torch.save(model.state_dict(), MODEL_PATH)
111
+ print(f"✅ Model saved to {MODEL_PATH}")
112
+
113
+ # Plot loss
114
+ plt.plot(train_loss_log, label="Train")
115
+ plt.plot(val_loss_log, label="Validation")
116
+ plt.title("Loss Curve")
117
+ plt.xlabel("Epoch")
118
+ plt.ylabel("Loss")
119
+ plt.legend()
120
+ plt.grid()
121
+ plt.savefig("training_loss_plot.png")