File size: 5,495 Bytes
63c5f12
 
efeddde
 
 
 
 
 
63c5f12
 
efeddde
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63c5f12
efeddde
 
 
 
 
 
 
 
 
 
 
63c5f12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
efeddde
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import os
import logging
from pathlib import Path
from typing import Union, Optional, Tuple, List
import cv2
import numpy as np
from PIL import Image

logger = logging.getLogger(__name__)

def cosine_similarity(vec1: Union[np.ndarray, List[float]], vec2: Union[np.ndarray, List[float]]) -> float:
    """Compute cosine similarity between two feature vectors."""
    a = np.asarray(vec1, dtype=np.float32).flatten()
    b = np.asarray(vec2, dtype=np.float32).flatten()
    norm_a = np.linalg.norm(a)
    norm_b = np.linalg.norm(b)
    if norm_a == 0 or norm_b == 0:
        return 0.0
    return float(np.dot(a, b) / (norm_a * norm_b))

class FaceEncoder:
    """
    Local face detection + 512-dimensional ArcFace embedding generation using InsightFace.
    Biometric vectors are retained strictly in volatile RAM for similarity ranking
    and are never committed to public logs, reports, or the blockchain.
    """

    def __init__(self):
        self._app = None
        self.model_name = os.getenv("FACE_MODEL", "buffalo_s")

    def _load(self):
        if self._app is not None:
            return
        try:
            from insightface.app import FaceAnalysis
        except ImportError as exc:
            raise RuntimeError(
                "InsightFace is not installed. Run: pip install -r requirements.txt"
            ) from exc

        try:
            self._app = FaceAnalysis(
                name=self.model_name,
                providers=["CPUExecutionProvider"]
            )
            self._app.prepare(ctx_id=0)
        except Exception as exc:
            if self.model_name != "buffalo_s":
                logger.warning("Could not load %s, falling back to buffalo_s: %s", self.model_name, exc)
                self.model_name = "buffalo_s"
                self._app = FaceAnalysis(
                    name="buffalo_s",
                    providers=["CPUExecutionProvider"]
                )
                self._app.prepare(ctx_id=0)
            else:
                raise

    def _to_cv2(self, image_input: Union[Path, str, Image.Image, np.ndarray]) -> np.ndarray:
        if isinstance(image_input, (str, Path)):
            img_path = Path(image_input)
            try:
                pil_img = Image.open(img_path).convert("RGB")
                return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
            except Exception:
                img = cv2.imread(str(img_path))
                if img is None:
                    raise ValueError(f"Unable to read image from path: {image_input}")
                return img
        elif isinstance(image_input, Image.Image):
            rgb = image_input.convert("RGB")
            return cv2.cvtColor(np.array(rgb), cv2.COLOR_RGB2BGR)
        elif isinstance(image_input, np.ndarray):
            return image_input
        else:
            raise TypeError(f"Unsupported image input type: {type(image_input)}")

    def get_embedding(
        self, image_input: Union[Path, str, Image.Image, np.ndarray]
    ) -> Tuple[Optional[np.ndarray], dict]:
        """
        Extract the primary 512-D ArcFace facial embedding and detection telemetry.
        """
        self._load()
        img = self._to_cv2(image_input)
        faces = self._app.get(img)
        if not faces:
            return None, {"detected": False, "count": 0, "det_score": 0.0, "norm": 0.0}

        # Sort faces by detection score descending
        faces = sorted(faces, key=lambda f: getattr(f, "det_score", 0.0), reverse=True)
        primary = faces[0]
        raw_emb = getattr(primary, "embedding", None)
        det_score = float(getattr(primary, "det_score", 0.0))

        if raw_emb is not None:
            embedding = np.asarray(raw_emb, dtype=np.float32).flatten()
            norm = float(np.linalg.norm(embedding))
            return embedding, {
                "detected": True,
                "count": len(faces),
                "det_score": det_score,
                "norm": norm,
            }

        return None, {"detected": True, "count": len(faces), "det_score": det_score, "norm": 0.0}

    def analyze(self, image_path: Path):
        """
        Full facial topology analysis for forensic reporting.
        """
        self._load()
        img = self._to_cv2(image_path)
        faces = self._app.get(img)

        embedding_generated = False
        det_score = 0.0
        norm = 0.0
        face_boxes = []
        landmarks = []

        if faces:
            faces = sorted(faces, key=lambda f: getattr(f, "det_score", 0.0), reverse=True)
            primary = faces[0]
            if getattr(primary, "embedding", None) is not None:
                embedding_generated = True
                emb = np.asarray(primary.embedding, dtype=np.float32).flatten()
                norm = float(np.linalg.norm(emb))
                det_score = float(getattr(primary, "det_score", 0.0))

            for face in faces:
                if hasattr(face, "bbox") and face.bbox is not None:
                    face_boxes.append([float(x) for x in face.bbox])
                if hasattr(face, "kps") and face.kps is not None:
                    landmarks.append([[float(x), float(y)] for x, y in face.kps])

        return {
            "detected": len(faces) > 0,
            "count": len(faces),
            "embedding_generated": embedding_generated,
            "det_score": det_score,
            "norm": norm,
            "face_boxes": face_boxes,
            "landmarks": landmarks,
        }