PerceptAI / expression_model.py
karthikmulugu08's picture
feat: Real-Time Face & Body Analysis System v3.0
6e907b4
Raw
History Blame Contribute Delete
3.28 kB
"""
Custom PyTorch MLP trained on MediaPipe 468-point facial landmarks.
468 landmarks × 3 coords (x,y,z) = 1404-dimensional feature vector.
Used by the analyzer for fast, lightweight emotion classification
that runs in the main thread every frame (unlike DeepFace which is slow).
"""
import os
import numpy as np
MODEL_PATH = "models/expression_mlp.pth"
EMOTIONS = ["angry", "disgust", "fear", "happy", "neutral", "sad", "surprise"]
INPUT_DIM = 1404 # 468 × 3
def extract_landmark_features(face_landmarks) -> np.ndarray:
"""
Convert MediaPipe face landmarks to a scale- and position-invariant
1404-dim feature vector suitable for the MLP.
"""
coords = np.array(
[[lm.x, lm.y, lm.z] for lm in face_landmarks.landmark[:468]],
dtype=np.float32,
)
# Normalize x,y to [0,1] relative to the face bounding box
min_xy = coords[:, :2].min(axis=0)
max_xy = coords[:, :2].max(axis=0)
scale = (max_xy - min_xy).max() + 1e-6
coords[:, 0] = (coords[:, 0] - min_xy[0]) / scale
coords[:, 1] = (coords[:, 1] - min_xy[1]) / scale
# z is already relative in MediaPipe output
return coords.flatten()
class ExpressionMLP:
"""Lazy-loads torch only when the model file exists."""
def __init__(self, model_path: str = MODEL_PATH):
self._model = None
self._labels = EMOTIONS
self.available = False
if not os.path.exists(model_path):
print(f"[expression_model] no model at {model_path} — run train_expression_model.py first")
return
try:
import torch
import torch.nn as nn
class _MLP(nn.Module):
def __init__(self, n_in, n_cls):
super().__init__()
self.net = nn.Sequential(
nn.Linear(n_in, 512), nn.BatchNorm1d(512), nn.GELU(), nn.Dropout(0.30),
nn.Linear(512, 256), nn.BatchNorm1d(256), nn.GELU(), nn.Dropout(0.20),
nn.Linear(256, 128), nn.GELU(),
nn.Linear(128, n_cls),
)
def forward(self, x): return self.net(x)
ckpt = torch.load(model_path, map_location="cpu", weights_only=False)
self._labels = ckpt.get("labels", EMOTIONS)
net = _MLP(INPUT_DIM, len(self._labels))
net.load_state_dict(ckpt["model"])
net.eval()
self._model = net
self._torch = torch
self.available = True
print(f"[expression_model] loaded — classes: {self._labels}")
except Exception as e:
print(f"[expression_model] load failed: {e}")
def predict(self, face_landmarks) -> tuple[str, dict[str, float]]:
"""Returns (dominant_emotion, scores_dict). Call only if self.available."""
feats = extract_landmark_features(face_landmarks)
x = self._torch.tensor(feats, dtype=self._torch.float32).unsqueeze(0)
with self._torch.no_grad():
probs = self._torch.softmax(self._model(x), dim=1).squeeze().numpy()
scores = {lbl: float(p) for lbl, p in zip(self._labels, probs)}
dominant = max(scores, key=scores.get)
return dominant, scores