Spaces:
Sleeping
Sleeping
| """Face age/gender/emotion pipeline over ONNX models. | |
| Detection: RetinaFace (uniface, ONNX). Classifiers: the project's own models | |
| converted to ONNX (age regression, gender CNN, emotion CNN). Preprocessing | |
| mirrors the original PyTorch/Keras pipeline (output/tkinter_result.py) so the | |
| predictions match the trained models. | |
| """ | |
| import os | |
| import cv2 | |
| import numpy as np | |
| import onnxruntime as ort | |
| from uniface import RetinaFace | |
| # age/gender share the same crop + normalization (torchvision Normalize) | |
| _MEAN = np.array([0.6284, 0.4901, 0.4325], np.float32) | |
| _STD = np.array([0.1869, 0.1712, 0.1561], np.float32) | |
| # emotion model trained on FER-2013 minus the sparse 'disgust' class -> 6 classes | |
| EMOTION_LABELS = ["Angry", "Fear", "Happy", "Neutral", "Sad", "Surprise"] | |
| _HERE = os.path.dirname(os.path.abspath(__file__)) | |
| class FacePipeline: | |
| def __init__(self, models_dir=None): | |
| d = models_dir or os.path.join(_HERE, "models") | |
| prov = ["CPUExecutionProvider"] | |
| self.det = RetinaFace(confidence_threshold=0.6, providers=prov) | |
| self.age = ort.InferenceSession(os.path.join(d, "age.onnx"), providers=prov) | |
| self.gender = ort.InferenceSession(os.path.join(d, "gender.onnx"), providers=prov) | |
| self.emotion = ort.InferenceSession(os.path.join(d, "emotion.onnx"), providers=prov) | |
| self._age_in = self.age.get_inputs()[0].name | |
| self._gender_in = self.gender.get_inputs()[0].name | |
| self._emo_in = self.emotion.get_inputs()[0].name | |
| def _prep_rgb(self, rgb, box): | |
| x1, y1, x2, y2 = box | |
| crop = rgb[y1:y2, x1:x2] | |
| crop = cv2.resize(crop, (224, 224)).astype(np.float32) / 255.0 | |
| crop = (crop - _MEAN) / _STD | |
| return np.transpose(crop, (2, 0, 1))[None].astype(np.float32) | |
| def _prep_emotion(self, rgb, box): | |
| x1, y1, x2, y2 = box | |
| crop = rgb[y1:y2, x1:x2] | |
| # original applied COLOR_BGR2GRAY to an RGB array — replicate for parity | |
| gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) | |
| gray = cv2.resize(gray, (48, 48)).astype(np.float32) / 255.0 | |
| return gray[None, :, :, None] | |
| def predict(self, rgb): | |
| """rgb: HxWx3 uint8 RGB array. Returns list of dicts per face.""" | |
| faces = self.det.detect(rgb) | |
| h, w = rgb.shape[:2] | |
| out = [] | |
| for f in faces: | |
| x1, y1, x2, y2 = [int(round(v)) for v in f.bbox] | |
| x1, y1 = max(0, x1), max(0, y1) | |
| x2, y2 = min(w, x2), min(h, y2) | |
| if x2 - x1 < 5 or y2 - y1 < 5: | |
| continue | |
| box = (x1, y1, x2, y2) | |
| age = float(self.age.run(None, {self._age_in: self._prep_rgb(rgb, box)})[0].ravel()[0]) | |
| logit = float(self.gender.run(None, {self._gender_in: self._prep_rgb(rgb, box)})[0].ravel()[0]) | |
| female_prob = 1.0 / (1.0 + np.exp(-logit)) | |
| gender = "Male" if female_prob < 0.5 else "Female" | |
| emo = self.emotion.run(None, {self._emo_in: self._prep_emotion(rgb, box)})[0].ravel() | |
| emotion = EMOTION_LABELS[int(np.argmax(emo))] | |
| out.append({ | |
| "box": box, | |
| "age": round(age), | |
| "gender": gender, | |
| "female_prob": round(female_prob, 3), | |
| "emotion": emotion, | |
| }) | |
| return out | |
| # blue for male, red/pink for female (BGR for cv2 drawing) | |
| _MALE_BGR = (255, 120, 40) | |
| _FEMALE_BGR = (90, 90, 240) | |
| def draw(rgb, results): | |
| """Draw gender-colored boxes with a numbered badge per face (details go in | |
| the side panel). A badge avoids the label overlap that full text captions | |
| cause when faces are packed close together. Returns RGB uint8.""" | |
| img = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR).copy() | |
| for i, r in enumerate(results, 1): | |
| x1, y1, x2, y2 = r["box"] | |
| col = _MALE_BGR if r["gender"] == "Male" else _FEMALE_BGR | |
| cv2.rectangle(img, (x1, y1), (x2, y2), col, 2) | |
| # numbered badge just above the box (outside, so it never covers the face); | |
| # number matches the card list | |
| rad = int(max(9, min(18, (x2 - x1) / 7))) | |
| cx = x1 + rad | |
| cy = max(rad + 1, y1 - rad - 3) | |
| cv2.circle(img, (cx, cy), rad, col, -1, cv2.LINE_AA) | |
| cv2.circle(img, (cx, cy), rad, (255, 255, 255), 1, cv2.LINE_AA) | |
| label = str(i) | |
| fs = rad / 14.0 | |
| th = max(1, int(round(rad / 9))) | |
| (tw, tht), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, fs, th) | |
| cv2.putText(img, label, (cx - tw // 2, cy + tht // 2), | |
| cv2.FONT_HERSHEY_SIMPLEX, fs, (255, 255, 255), th, cv2.LINE_AA) | |
| return cv2.cvtColor(img, cv2.COLOR_BGR2RGB) | |