face-rec-api / inference.py
ingyoun's picture
draw(): numbered badges instead of overlapping text labels
286ccf8 verified
Raw
History Blame Contribute Delete
4.65 kB
"""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)