File size: 3,164 Bytes
d2885a7
 
 
ba73462
 
 
 
 
d2885a7
 
ba73462
d2885a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ba73462
 
 
 
 
 
 
 
 
 
d2885a7
 
 
 
 
ba73462
d2885a7
 
ba73462
d2885a7
ba73462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2885a7
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
"""
Embedding and similarity utilities for face recognition.

Uses OpenCV HSV color histograms for face embeddings (114-dimensional vectors).
No external deep learning libraries required β€” cv2 and numpy only.

Note: facenet-pytorch has been removed. Face cropping is handled by the ML model
      (runtime_detector.py). Embeddings here are used only for deduplication.
"""

import cv2
import numpy as np


def cosine_similarity(embedding1: list[float], embedding2: list[float]) -> float:
    """
    Compute cosine similarity between two embeddings.

    Args:
        embedding1: First embedding vector
        embedding2: Second embedding vector

    Returns:
        Similarity score (0.0 to 1.0, higher = more similar)
    """
    e1 = np.array(embedding1, dtype=np.float32)
    e2 = np.array(embedding2, dtype=np.float32)

    e1_norm = e1 / (np.linalg.norm(e1) + 1e-8)
    e2_norm = e2 / (np.linalg.norm(e2) + 1e-8)

    similarity = float(np.dot(e1_norm, e2_norm))

    return max(0.0, min(1.0, similarity))


def euclidean_distance(embedding1: list[float], embedding2: list[float]) -> float:
    """
    Compute Euclidean distance between two embeddings.

    Args:
        embedding1: First embedding vector
        embedding2: Second embedding vector

    Returns:
        Distance score (lower = more similar)
    """
    e1 = np.array(embedding1, dtype=np.float32)
    e2 = np.array(embedding2, dtype=np.float32)

    return float(np.linalg.norm(e1 - e2))


def generate_face_embedding(face_crop: np.ndarray) -> list[float]:
    """
    Generate a 114-D embedding from a face crop using HSV color histograms.

    Uses OpenCV HSV histograms β€” no external deep learning libraries needed.
    Sufficient for face deduplication within a session.

    Embedding breakdown:
        - H channel: 50 bins
        - S channel: 32 bins
        - V channel: 32 bins
        - Total: 114 dimensions, L2-normalized

    Args:
        face_crop: Face image as numpy array (BGR, any size β€” resized internally)

    Returns:
        List of 114 floats representing the face embedding

    Raises:
        ValueError: If face_crop is None or invalid
    """
    if face_crop is None or face_crop.size == 0:
        raise ValueError("generate_face_embedding: face_crop is None or empty")

    # Resize to fixed size for consistent histograms
    face_resized = cv2.resize(face_crop, (64, 64), interpolation=cv2.INTER_LINEAR)

    # Convert BGR β†’ HSV
    # HSV is more robust to lighting changes than raw RGB
    face_hsv = cv2.cvtColor(face_resized, cv2.COLOR_BGR2HSV)

    # Compute per-channel histograms
    # H: 0–179 in OpenCV,  S: 0–255,  V: 0–255
    h_hist = cv2.calcHist([face_hsv], [0], None, [50], [0, 180]).flatten()
    s_hist = cv2.calcHist([face_hsv], [1], None, [32], [0, 256]).flatten()
    v_hist = cv2.calcHist([face_hsv], [2], None, [32], [0, 256]).flatten()

    # Concatenate into one vector
    embedding = np.concatenate([h_hist, s_hist, v_hist]).astype(np.float32)

    # L2-normalize so cosine similarity works correctly
    norm = np.linalg.norm(embedding) + 1e-8
    embedding = embedding / norm

    return embedding.tolist()