Files
Immich/machine-learning/immich_ml/models/facial_recognition/recognition.py
T

73 lines
2.9 KiB
Python

from typing import Any
import numpy as np
from numpy.typing import NDArray
from PIL import Image
from immich_ml.config import settings
from immich_ml.models.base import InferenceModel
from immich_ml.models.transforms import decode_pil, normalize, serialize_np_array
from immich_ml.schemas import (
FaceDetectionOutput,
FaceRecognitionOptions,
FacialRecognitionOutput,
ModelSource,
ModelTask,
ModelType,
Shape,
)
from immich_ml.sessions.policy import ShapePolicy, batches, runs
from ._ops import ALIGNED_SIZE, align_face
class FaceRecognizer(InferenceModel[FaceRecognitionOptions]):
depends = [(ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)]
identity = (ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION)
sources = (ModelSource.INSIGHTFACE,)
def __init__(self, model_name: str, **model_kwargs: Any) -> None:
super().__init__(model_name, **model_kwargs)
sizes = batches(settings.max_batch_size.facial_recognition)
self.shape_policy = ShapePolicy(dims=tuple(Shape(batch) for batch in sizes))
def _predict(
self,
inputs: NDArray[np.uint8] | bytes | Image.Image,
faces: FaceDetectionOutput,
options: FaceRecognitionOptions,
) -> FacialRecognitionOutput:
if faces["boxes"].shape[0] == 0:
return []
image = np.asarray(decode_pil(inputs), dtype=np.uint8)
landmarks = faces["landmarks"]
sizes = runs(len(landmarks), self.session.batches)
crops: NDArray[np.float32] | NDArray[np.uint8] = np.empty(
(len(landmarks), ALIGNED_SIZE, ALIGNED_SIZE, 3), dtype=np.uint8
)
for crop, kps in zip(crops, landmarks):
align_face(image, kps, crop)
if not self.session.for_shape(Shape(sizes[0])).normalizes_input:
crops = normalize(crops.transpose(0, 3, 1, 2).astype(np.float32, order="C"), mean=127.5, std=127.5)
embeddings = self._predict_batch(crops, sizes)
return self.postprocess(faces, embeddings)
def _predict_batch(self, crops: NDArray[np.float32] | NDArray[np.uint8], sizes: list[int]) -> NDArray[np.float32]:
embeddings = []
start = 0
for size in sizes:
session = self.session.for_shape(Shape(size))
embeddings.append(session.run(None, {session.get_inputs()[0].name: crops[start : start + size]})[0])
start += size
return embeddings[0] if len(embeddings) == 1 else np.concatenate(embeddings, axis=0)
def postprocess(self, faces: FaceDetectionOutput, embeddings: NDArray[np.float32]) -> FacialRecognitionOutput:
return [
{
"boundingBox": {"x1": x1, "y1": y1, "x2": x2, "y2": y2},
"embedding": serialize_np_array(embedding),
"score": score,
}
for (x1, y1, x2, y2), embedding, score in zip(faces["boxes"], embeddings, faces["scores"])
]