ovkit.recognize.face의 소스 코드

"""Face-analysis adapter — typed decode for the OMZ face model family.

The ``face`` task covers several *different* output kinds (a detector, an
age+gender head, an emotion head, head pose angles, landmark regressors, and
re-identification embeddings). This adapter inspects the output signature and
decodes each into a human-readable :class:`~ovkit.Results` instead of dumping
raw tensors:

- SSD detection ``[1,1,N,7]``       -> ``Results.boxes``
- age (scalar) + gender ``[1,2]``   -> ``Results.text`` ("age 31 · male 98%")
- emotion probabilities ``[1,5]``   -> ``Results.probs`` + top emotion text
- yaw/pitch/roll scalar angles      -> ``Results.text``
- landmark coords ``[1,2K]``        -> ``Results.keypoints`` (pixels)
- embeddings ``[1,D]`` (D >= 64)    -> descriptive text + raw tensor
- anything else                     -> raw tensors (generic fallback)

Output orders (gender = [female, male]; emotions = neutral/happy/sad/
surprise/anger) follow the documented OMZ model interfaces.
"""

from __future__ import annotations

from typing import Any

import numpy as np

from ..core.backend import Backend
from ..core.results import Boxes, Keypoints, Probs, Results
from .base import BaseAdapter

_GENDERS = {0: "female", 1: "male"}
_EMOTIONS = {0: "neutral", 1: "happy", 2: "sad", 3: "surprise", 4: "anger"}
#: Head-pose output name -> human label (OMZ: angle_y=yaw, angle_p=pitch, angle_r=roll).
_ANGLES = {"angle_y_fc": "yaw", "angle_p_fc": "pitch", "angle_r_fc": "roll"}
#: Landmark regressors emit [1, 2K] normalized (x, y) pairs; K in {5, 35, 98}.
_LANDMARK_SIZES = frozenset({10, 70, 196})


[문서] class FaceAdapter(BaseAdapter): """Adapter for face-analysis models (detection, attributes, landmarks).""" task = "face"
[문서] def run(self, backend: Backend, image: np.ndarray, conf: float = 0.25, **_: Any) -> Results: size = self.model_input_hw(backend) rgb = bool(self.pre.get("rgb", False)) # OMZ face models: raw BGR feed = self.preprocess(image, size, rgb=rgb, scale=self.pre.get("scale", 1.0)) outputs = backend.infer(feed) return self._decode(image, outputs, conf)
# -- signature-based decode ---------------------------------------------- def _decode(self, image: np.ndarray, outputs: dict[str, Any], conf: float) -> Results: h, w = image.shape[:2] arrs = {n: np.asarray(a) for n, a in outputs.items()} # SSD-style detector: any [.., N, 7] output. for arr in arrs.values(): if arr.ndim >= 3 and arr.shape[-1] == 7: det = arr.reshape(-1, 7) keep = det[:, 2] >= conf det = det[keep] xyxy = det[:, 3:7] * np.array([w, h, w, h], dtype=np.float32) xyxy[:, 0::2] = xyxy[:, 0::2].clip(0, w) xyxy[:, 1::2] = xyxy[:, 1::2].clip(0, h) data = np.concatenate( [xyxy, det[:, 2:3], np.zeros_like(det[:, 1:2])], axis=1 ).astype(np.float32) return Results(image, task="detect", names={0: "face"}, boxes=Boxes(data)) # Age + gender: a scalar "age" output next to a 2-class "prob" output. age_name = next((n for n in arrs if "age" in n.lower() and arrs[n].size == 1), None) gen_name = next((n for n in arrs if n != age_name and arrs[n].size == 2), None) if age_name and gen_name: age = float(arrs[age_name].reshape(-1)[0]) * 100.0 g = arrs[gen_name].reshape(-1).astype(np.float32) gi = int(np.argmax(g)) r = Results(image, task=self.task, names=_GENDERS, probs=Probs(g)) r.text = f"age {age:.0f} · {_GENDERS[gi]} {g[gi] * 100:.0f}%" return r # Head pose: three scalar angle outputs. angles = {n: a for n, a in arrs.items() if a.size == 1 and "angle" in n.lower()} if len(angles) == 3: parts = [] for key, label in _ANGLES.items(): match = next((n for n in angles if n.startswith(key)), None) if match is not None: parts.append(f"{label} {float(angles[match].reshape(-1)[0]):+.1f}°") r = Results(image, task=self.task, names=self.names, tensors=outputs) r.text = " · ".join(parts) return r if len(arrs) == 1: flat = next(iter(arrs.values())).reshape(-1).astype(np.float32) # Emotion head: 5 probabilities. if flat.size == 5: ei = int(np.argmax(flat)) r = Results(image, task=self.task, names=_EMOTIONS, probs=Probs(flat)) r.text = f"{_EMOTIONS[ei]} {flat[ei] * 100:.0f}%" return r # Landmark regressor: 2K normalized coords (not a prob distribution). if flat.size in _LANDMARK_SIZES and np.all(np.isfinite(flat)): if float(flat.min()) >= -0.5 and float(flat.max()) <= 1.5: pts = flat.reshape(-1, 2) kpts = np.stack( [pts[:, 0] * w, pts[:, 1] * h, np.ones(len(pts), np.float32)], axis=1 ) return Results( image, task=self.task, names=self.names, keypoints=Keypoints(kpts[None]) ) # Two-class head with no age companion (e.g. anti-spoof: [real, spoof]). if flat.size == 2: names = {0: "real", 1: "spoof"} i = int(np.argmax(flat)) r = Results(image, task=self.task, names=names, probs=Probs(flat)) r.text = f"{names[i]} {flat[i] * 100:.0f}%" return r # Re-identification embedding. if flat.size >= 64: r = Results(image, task=self.task, names=self.names, tensors=outputs) r.text = f"{flat.size}-d face embedding (use for matching)" return r return Results(image, task=self.task, names=self.names, tensors=outputs)