camera-ui-ml 1.2.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from .backend import (
4
+ DType,
5
+ InferenceBackend,
6
+ InputSpec,
7
+ Layout,
8
+ NDArray,
9
+ Normalize,
10
+ Outputs,
11
+ )
12
+ from .detectors import (
13
+ BaseDetector,
14
+ BoxDetector,
15
+ Embedder,
16
+ LandmarkDetector,
17
+ NormalizedDetection,
18
+ OcrResult,
19
+ ParseKind,
20
+ PersonEmbedder,
21
+ PlateOcr,
22
+ Segmenter,
23
+ )
24
+ from .geometry import (
25
+ Box,
26
+ NormalizedBox,
27
+ clamp_box,
28
+ cxcywh_to_xyxy,
29
+ normalize_box,
30
+ pad_box,
31
+ scale_box,
32
+ )
33
+ from .model_manager import BaseModelManager
34
+ from .parsing import (
35
+ RawDetection,
36
+ channels_first,
37
+ decode_ocr,
38
+ l2_normalize,
39
+ nms,
40
+ parse_end2end,
41
+ parse_yolov9,
42
+ )
43
+ from .pipeline import prepare_executor, run_prepare
44
+ from .pipelines import (
45
+ DETECTION_FLOOR,
46
+ detect_clip,
47
+ detect_faces,
48
+ detect_faces_in_image,
49
+ detect_objects,
50
+ detect_objects_in_image,
51
+ detect_plates,
52
+ detect_plates_in_image,
53
+ embed_face_images,
54
+ embed_faces,
55
+ embed_person_images,
56
+ embed_persons,
57
+ requested_threshold,
58
+ segment_images,
59
+ segment_objects,
60
+ )
61
+ from .preprocess import (
62
+ crop_rgb,
63
+ decode_image,
64
+ frame_to_rgb,
65
+ resize_rgb,
66
+ to_pil,
67
+ to_tensor,
68
+ )
69
+ from .runtime import model_runtime
70
+ from .settings import reset_stored_settings
71
+
72
+ __all__ = [
73
+ "model_runtime",
74
+ "InferenceBackend",
75
+ "InputSpec",
76
+ "Outputs",
77
+ "NDArray",
78
+ "Layout",
79
+ "Normalize",
80
+ "DType",
81
+ "BaseModelManager",
82
+ # ClipEncoder is opt-in via camera_ui_ml.detectors.clip
83
+ "BaseDetector",
84
+ "BoxDetector",
85
+ "NormalizedDetection",
86
+ "ParseKind",
87
+ "Embedder",
88
+ "LandmarkDetector",
89
+ "PersonEmbedder",
90
+ "Segmenter",
91
+ "PlateOcr",
92
+ "OcrResult",
93
+ "Box",
94
+ "NormalizedBox",
95
+ "cxcywh_to_xyxy",
96
+ "scale_box",
97
+ "clamp_box",
98
+ "normalize_box",
99
+ "pad_box",
100
+ "RawDetection",
101
+ "channels_first",
102
+ "parse_yolov9",
103
+ "parse_end2end",
104
+ "decode_ocr",
105
+ "l2_normalize",
106
+ "nms",
107
+ "frame_to_rgb",
108
+ "resize_rgb",
109
+ "to_pil",
110
+ "to_tensor",
111
+ "crop_rgb",
112
+ "decode_image",
113
+ "run_prepare",
114
+ "prepare_executor",
115
+ "DETECTION_FLOOR",
116
+ "requested_threshold",
117
+ "detect_objects",
118
+ "detect_faces",
119
+ "detect_plates",
120
+ "detect_objects_in_image",
121
+ "detect_faces_in_image",
122
+ "detect_plates_in_image",
123
+ "reset_stored_settings",
124
+ "detect_clip",
125
+ "embed_faces",
126
+ "embed_face_images",
127
+ "embed_persons",
128
+ "embed_person_images",
129
+ "segment_objects",
130
+ "segment_images",
131
+ ]
camera_ui_ml/align.py ADDED
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ from PIL import Image
7
+
8
+ NDArray = np.ndarray[Any, Any]
9
+
10
+ ARCFACE_DST_112 = np.array(
11
+ [
12
+ [38.2946, 51.6963],
13
+ [73.5318, 51.5014],
14
+ [56.0252, 71.7366],
15
+ [41.5493, 92.3655],
16
+ [70.7299, 92.2041],
17
+ ],
18
+ dtype=np.float64,
19
+ )
20
+
21
+
22
+ def umeyama_similarity(src: NDArray, dst: NDArray) -> NDArray:
23
+ src = np.asarray(src, dtype=np.float64)
24
+ dst = np.asarray(dst, dtype=np.float64)
25
+ n, dim = src.shape
26
+
27
+ src_mean, dst_mean = src.mean(axis=0), dst.mean(axis=0)
28
+ src_demean, dst_demean = src - src_mean, dst - dst_mean
29
+
30
+ cov = dst_demean.T @ src_demean / n
31
+ u, s, vt = np.linalg.svd(cov)
32
+
33
+ d = np.ones(dim)
34
+ if np.linalg.det(u) * np.linalg.det(vt) < 0:
35
+ d[-1] = -1.0
36
+ rotation = u @ np.diag(d) @ vt
37
+
38
+ variance = (src_demean**2).sum() / n
39
+ scale = 1.0 if variance == 0 else float((s * d).sum() / variance)
40
+
41
+ matrix = np.zeros((2, 3), dtype=np.float64)
42
+ matrix[:, :2] = scale * rotation
43
+ matrix[:, 2] = dst_mean - scale * rotation @ src_mean
44
+ return matrix
45
+
46
+
47
+ def warp_face(rgb: NDArray, points: NDArray, size: int = 112) -> NDArray:
48
+ scale = size / 112.0
49
+ matrix = umeyama_similarity(np.asarray(points, dtype=np.float64), ARCFACE_DST_112 * scale)
50
+
51
+ # PIL maps destination pixels back to the source, so it wants the inverse
52
+ full = np.vstack([matrix, [0.0, 0.0, 1.0]])
53
+ inverse = np.linalg.inv(full)[:2]
54
+
55
+ warped = Image.fromarray(rgb).transform(
56
+ (size, size), Image.Transform.AFFINE, tuple(inverse.flatten()), resample=Image.Resampling.BILINEAR
57
+ )
58
+ return np.asarray(warped)
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Mapping, Sequence
5
+ from dataclasses import dataclass
6
+ from typing import Any, Literal
7
+
8
+ import numpy as np
9
+
10
+ from .pipeline import run_prepare
11
+
12
+ NDArray = np.ndarray[Any, Any]
13
+
14
+ Layout = Literal["nchw", "nhwc"]
15
+ Normalize = Literal["unit", "facenet", "arcface", "none"]
16
+ Channels = Literal["rgb", "bgr"]
17
+ DType = Literal["float32", "uint8"]
18
+
19
+ Outputs = Sequence[NDArray]
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class InputSpec:
24
+ width: int
25
+ height: int
26
+ layout: Layout = "nchw"
27
+ normalize: Normalize = "unit"
28
+ channels: Channels = "rgb"
29
+ dtype: DType = "float32"
30
+
31
+
32
+ class InferenceBackend(ABC):
33
+ @property
34
+ @abstractmethod
35
+ def input_size(self) -> tuple[int, int]: ...
36
+
37
+ @abstractmethod
38
+ def metadata(self) -> Mapping[str, str]: ...
39
+
40
+ @property
41
+ def device(self) -> str:
42
+ return "unknown"
43
+
44
+ @property
45
+ def runtime(self) -> str | None:
46
+ return None
47
+
48
+ @property
49
+ def precision(self) -> str | None:
50
+ return None
51
+
52
+ @abstractmethod
53
+ async def infer(self, inputs: Sequence[Any]) -> Outputs: ...
54
+
55
+ @abstractmethod
56
+ def close(self) -> None: ...
57
+
58
+ def adapt(self, image: NDArray, spec: InputSpec) -> Sequence[Any]:
59
+ from .preprocess import to_tensor
60
+
61
+ return [to_tensor(image, spec)]
62
+
63
+ async def run(self, image: NDArray, spec: InputSpec) -> Outputs:
64
+ prepared = await run_prepare(lambda: list(self.adapt(image, spec)))
65
+ return await self.infer(prepared)
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import BaseDetector
4
+ from .box import BoxDetector, NormalizedDetection, ParseKind
5
+ from .embedder import Embedder
6
+ from .landmarks import LandmarkDetector
7
+ from .ocr import DEFAULT_ALPHABET, OcrResult, PlateOcr
8
+ from .person import PersonEmbedder
9
+ from .segmenter import Segmenter
10
+
11
+ __all__ = [
12
+ "BaseDetector",
13
+ "BoxDetector",
14
+ "NormalizedDetection",
15
+ "ParseKind",
16
+ "Embedder",
17
+ "LandmarkDetector",
18
+ "PersonEmbedder",
19
+ "Segmenter",
20
+ "PlateOcr",
21
+ "OcrResult",
22
+ "DEFAULT_ALPHABET",
23
+ ]
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import TYPE_CHECKING
6
+
7
+ from camera_ui_sdk import LoggerService
8
+
9
+ from ..backend import InferenceBackend
10
+ from ..model_manager import BaseModelManager
11
+
12
+ if TYPE_CHECKING:
13
+ from camera_ui_sdk import LoadedModel
14
+
15
+
16
+ class BaseDetector:
17
+ name: str = "detector"
18
+
19
+ def __init__(self, manager: BaseModelManager, logger: LoggerService) -> None:
20
+ self.manager = manager
21
+ self.logger = logger
22
+ self.backend: InferenceBackend | None = None
23
+ self.initialized = False
24
+ self.closed = False
25
+ self.model_name: str | None = None
26
+ self.load_ms = 0
27
+ self._init_task: asyncio.Task[None] | None = None
28
+
29
+ async def initialize(self, model_name: str) -> None:
30
+ if self.initialized:
31
+ return
32
+ if self._init_task is None:
33
+ self._init_task = asyncio.create_task(self._do_initialize(model_name))
34
+ await self._init_task
35
+
36
+ async def close(self) -> None:
37
+ self.closed = True
38
+ if self._init_task is not None:
39
+ self._init_task.cancel()
40
+ self._init_task = None
41
+ self.initialized = False
42
+ if self.backend is not None:
43
+ self.backend.close()
44
+ self.backend = None
45
+
46
+ async def _do_initialize(self, model_name: str) -> None:
47
+ try:
48
+ self.logger.log(f"Loading {self.name}: {model_name}...")
49
+ started = time.monotonic()
50
+ self.backend = await self.manager.ensure_backend(model_name)
51
+ if self.closed:
52
+ return
53
+ await self._configure(model_name)
54
+ self.load_ms = round((time.monotonic() - started) * 1000)
55
+ self.model_name = model_name
56
+ self.initialized = True
57
+ self.logger.success(f"Loaded {self.name}: {model_name}")
58
+ except Exception as error:
59
+ self.logger.error(f"Failed to initialize {self.name}: {error}")
60
+ raise
61
+ finally:
62
+ self._init_task = None
63
+
64
+ async def _configure(self, model_name: str) -> None: ...
65
+
66
+ def loaded_model(self, role: str | None = None) -> LoadedModel | None:
67
+ if not self._ready() or not self.model_name or self.backend is None:
68
+ return None
69
+
70
+ entry: LoadedModel = {"name": self.model_name}
71
+ if role:
72
+ entry["role"] = role
73
+ if self.backend.device and self.backend.device != "unknown":
74
+ entry["device"] = self.backend.device
75
+ if self.backend.precision:
76
+ entry["precision"] = self.backend.precision
77
+ if self.load_ms:
78
+ entry["loadMs"] = self.load_ms
79
+ return entry
80
+
81
+ def _ready(self) -> bool:
82
+ return self.initialized and self.backend is not None
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import re
5
+ from collections.abc import Mapping
6
+ from typing import Any, Literal
7
+
8
+ import numpy as np
9
+ from camera_ui_sdk import ImageMetadata, LoggerService, VideoFrameData
10
+
11
+ from ..backend import InputSpec, NDArray, Normalize
12
+ from ..geometry import NormalizedBox, normalize_box, scale_box
13
+ from ..model_manager import BaseModelManager
14
+ from ..parsing import RawDetection, channels_first, nms, parse_end2end, parse_yolov9
15
+ from ..preprocess import decode_image, frame_to_rgb
16
+ from .base import BaseDetector
17
+
18
+ ParseKind = Literal["yolov9", "end2end"]
19
+ NormalizedDetection = tuple[int, float, NormalizedBox]
20
+
21
+
22
+ class BoxDetector(BaseDetector):
23
+ def __init__(
24
+ self,
25
+ manager: BaseModelManager,
26
+ logger: LoggerService,
27
+ *,
28
+ name: str = "detector",
29
+ parse: ParseKind = "yolov9",
30
+ size: tuple[int, int] = (320, 320),
31
+ normalize: Normalize = "unit",
32
+ multiclass: bool = False,
33
+ apply_nms: bool = True,
34
+ threshold: float = 0.5,
35
+ ) -> None:
36
+ super().__init__(manager, logger)
37
+ self.name = name
38
+ self.threshold = threshold
39
+ self.input_size = size
40
+ self.labels: dict[int, str] = {}
41
+
42
+ self._parse = parse
43
+ self._normalize = normalize
44
+ self._multiclass = multiclass
45
+ self._apply_nms = apply_nms
46
+
47
+ async def _configure(self, model_name: str) -> None:
48
+ assert self.backend is not None
49
+ self.input_size = self.backend.input_size
50
+ if self._multiclass:
51
+ self.labels = _parse_labels(self.backend.metadata())
52
+
53
+ @property
54
+ def _spec(self) -> InputSpec:
55
+ return InputSpec(
56
+ self.input_size[0],
57
+ self.input_size[1],
58
+ layout="nchw",
59
+ normalize=self._normalize,
60
+ )
61
+
62
+ async def detect(self, image: NDArray, threshold: float | None = None) -> list[RawDetection]:
63
+ if not self._ready():
64
+ return []
65
+ assert self.backend is not None
66
+
67
+ outputs = await self.backend.run(image, self._spec)
68
+ primary = np.asarray(outputs[0])
69
+ limit = self.threshold if threshold is None else threshold
70
+
71
+ if self._parse == "yolov9":
72
+ detections = parse_yolov9(channels_first(primary), limit)
73
+ else:
74
+ detections = parse_end2end(np.squeeze(primary), limit)
75
+
76
+ return nms(detections) if self._apply_nms else detections
77
+
78
+ async def detect_frame(self, frame: VideoFrameData, threshold: float | None = None) -> list[RawDetection]:
79
+ if not self._ready():
80
+ return []
81
+
82
+ rgb = frame_to_rgb(frame["data"], frame["width"], frame["height"], frame["format"])
83
+ raw = await self.detect(rgb, threshold)
84
+
85
+ scale_x = frame["width"] / self.input_size[0]
86
+ scale_y = frame["height"] / self.input_size[1]
87
+ return [(cid, conf, scale_box(box, scale_x, scale_y)) for cid, conf, box in raw]
88
+
89
+ async def detect_single(
90
+ self, image_data: bytes, metadata: ImageMetadata, threshold: float | None = None
91
+ ) -> list[NormalizedDetection]:
92
+ if not self._ready():
93
+ return []
94
+
95
+ raw = await self.detect(decode_image(image_data), threshold)
96
+ return [
97
+ (cid, conf, normalize_box(box, self.input_size[0], self.input_size[1])) for cid, conf, box in raw
98
+ ]
99
+
100
+
101
+ def _parse_labels(metadata: Mapping[str, str]) -> dict[int, str]:
102
+ names = metadata.get("names") or metadata.get("yolo.names")
103
+ if names:
104
+ parsed: dict[Any, Any] = ast.literal_eval(names)
105
+ return {int(key): str(value) for key, value in parsed.items()}
106
+
107
+ classes = metadata.get("classes")
108
+ if not classes:
109
+ return {}
110
+
111
+ labels: dict[int, str] = {}
112
+ for row, line in enumerate(classes.split(",")):
113
+ pair = re.split(r"[:\s]+", line.strip(), maxsplit=1)
114
+ if len(pair) == 2 and pair[0].strip().isdigit():
115
+ labels[int(pair[0])] = pair[1].strip()
116
+ else:
117
+ labels[row] = line.strip()
118
+ return labels
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import numpy as np
8
+ from camera_ui_sdk import LoggerService, VideoFrameData
9
+ from transformers import CLIPProcessor
10
+
11
+ from ..backend import InferenceBackend, NDArray
12
+ from ..model_manager import BaseModelManager
13
+ from ..parsing import l2_normalize
14
+ from ..pipeline import run_prepare
15
+ from ..preprocess import frame_to_rgb, to_pil
16
+
17
+ if TYPE_CHECKING:
18
+ from camera_ui_sdk import LoadedModel
19
+
20
+
21
+ class ClipEncoder:
22
+ def __init__(
23
+ self,
24
+ manager: BaseModelManager,
25
+ logger: LoggerService,
26
+ *,
27
+ embedding_model: str = "clip-vit-base-patch32",
28
+ ) -> None:
29
+ self.manager = manager
30
+ self.logger = logger
31
+ self.embedding_model = embedding_model
32
+
33
+ self.vision: InferenceBackend | None = None
34
+ self.text: InferenceBackend | None = None
35
+ self.processor: Any = None
36
+
37
+ self.initialized = False
38
+ self.closed = False
39
+ self.vision_model: str | None = None
40
+ self.text_model: str | None = None
41
+ self.load_ms = 0
42
+ self._init_task: asyncio.Task[None] | None = None
43
+
44
+ async def initialize(self, vision_model: str, text_model: str) -> None:
45
+ if self.initialized:
46
+ return
47
+ if self._init_task is None:
48
+ self._init_task = asyncio.create_task(self._do_initialize(vision_model, text_model))
49
+ await self._init_task
50
+
51
+ async def close(self) -> None:
52
+ self.closed = True
53
+ if self._init_task is not None:
54
+ self._init_task.cancel()
55
+ self._init_task = None
56
+ self.initialized = False
57
+ if self.vision is not None:
58
+ self.vision.close()
59
+ self.vision = None
60
+ if self.text is not None:
61
+ self.text.close()
62
+ self.text = None
63
+ self.processor = None
64
+
65
+ async def embed_image(self, image: NDArray) -> list[float]:
66
+ if not self._ready():
67
+ return []
68
+ assert self.vision is not None
69
+
70
+ pil = to_pil(image)
71
+ pixel_values = await run_prepare(lambda: self._vision_input(pil))
72
+ outputs = await self.vision.infer([pixel_values])
73
+ return [float(value) for value in l2_normalize(outputs[0])]
74
+
75
+ async def embed_text(self, text: str) -> list[float]:
76
+ if not self._ready():
77
+ return []
78
+ assert self.text is not None
79
+
80
+ input_ids, attention_mask = await run_prepare(lambda: self._text_input(text))
81
+ outputs = await self.text.infer([input_ids, attention_mask])
82
+ return [float(value) for value in l2_normalize(outputs[0])]
83
+
84
+ async def embed_frame(self, width: int, height: int, data: bytes) -> list[float]:
85
+ return await self.embed_image(frame_to_rgb(data, width, height))
86
+
87
+ async def embed_frames(self, frames: list[VideoFrameData]) -> list[list[float]]:
88
+ if not self._ready():
89
+ return [[] for _ in frames]
90
+ return [await self.embed_frame(frame["width"], frame["height"], frame["data"]) for frame in frames]
91
+
92
+ def loaded_models(self, _role: str | None = None) -> list[LoadedModel]:
93
+ if not self._ready():
94
+ return []
95
+
96
+ entries: list[LoadedModel] = []
97
+ for backend, name, role in (
98
+ (self.vision, self.vision_model, "vision"),
99
+ (self.text, self.text_model, "text"),
100
+ ):
101
+ if backend is None or not name:
102
+ continue
103
+ entry: LoadedModel = {"name": name, "role": role}
104
+ if backend.device and backend.device != "unknown":
105
+ entry["device"] = backend.device
106
+ if backend.precision:
107
+ entry["precision"] = backend.precision
108
+ if self.load_ms:
109
+ entry["loadMs"] = self.load_ms
110
+ entries.append(entry)
111
+ return entries
112
+
113
+ def _ready(self) -> bool:
114
+ return (
115
+ self.initialized
116
+ and self.vision is not None
117
+ and self.text is not None
118
+ and self.processor is not None
119
+ )
120
+
121
+ async def _do_initialize(self, vision_model: str, text_model: str) -> None:
122
+ try:
123
+ self.logger.log(f"Loading CLIP: {vision_model} + {text_model}...")
124
+ started = time.monotonic()
125
+ self.vision = await self.manager.ensure_backend(vision_model)
126
+ self.text = await self.manager.ensure_backend(text_model)
127
+ processor_dir = await self.manager.ensure_clip_processor()
128
+ if processor_dir is None:
129
+ raise RuntimeError("CLIP processor files are not bundled (clip_processor_files is empty)")
130
+ self.processor = await asyncio.to_thread(CLIPProcessor.from_pretrained, processor_dir)
131
+ if self.closed:
132
+ return
133
+ self.load_ms = round((time.monotonic() - started) * 1000)
134
+ self.vision_model = vision_model
135
+ self.text_model = text_model
136
+ self.initialized = True
137
+ self.logger.success(f"Loaded CLIP: {vision_model} + {text_model}")
138
+ except Exception as error:
139
+ self.logger.error(f"Failed to initialize CLIP encoder: {error}")
140
+ raise
141
+ finally:
142
+ self._init_task = None
143
+
144
+ def _vision_input(self, pil: Any) -> NDArray:
145
+ inputs = self.processor(images=pil, return_tensors="np", padding="max_length", truncation=True)
146
+ return np.asarray(inputs["pixel_values"], dtype=np.float32)
147
+
148
+ def _text_input(self, text: str) -> tuple[NDArray, NDArray]:
149
+ inputs = self.processor(text=text, return_tensors="np", padding="max_length", truncation=True)
150
+ return (
151
+ np.asarray(inputs["input_ids"], dtype=np.int64),
152
+ np.asarray(inputs["attention_mask"], dtype=np.int64),
153
+ )
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from camera_ui_sdk import LoggerService
4
+
5
+ from ..align import warp_face
6
+ from ..backend import InputSpec, NDArray, Normalize
7
+ from ..geometry import Box, square_box
8
+ from ..model_manager import BaseModelManager
9
+ from ..parsing import FaceLandmarks, l2_normalize
10
+ from ..preprocess import crop_rgb, frame_to_rgb
11
+ from .base import BaseDetector
12
+
13
+ # whole-picture enrolment against a live crop: cosine 0.14, located and cut like this: 1.00
14
+ FACE_PADDING = 0.25
15
+
16
+
17
+ class Embedder(BaseDetector):
18
+ def __init__(
19
+ self,
20
+ manager: BaseModelManager,
21
+ logger: LoggerService,
22
+ *,
23
+ size: int = 160,
24
+ normalize: Normalize = "facenet",
25
+ aligned: bool = False,
26
+ name: str = "face embedder",
27
+ ) -> None:
28
+ super().__init__(manager, logger)
29
+ self.name = name
30
+ self.input_size = (size, size)
31
+ self.normalize = normalize
32
+ # ArcFace-style heads need the 112x112 template, a plain box crop drops them from 51% to near zero
33
+ self.aligned = aligned
34
+
35
+ @property
36
+ def _spec(self) -> InputSpec:
37
+ return InputSpec(self.input_size[0], self.input_size[1], layout="nchw", normalize=self.normalize)
38
+
39
+ async def embed(self, image: NDArray) -> list[float]:
40
+ if not self._ready():
41
+ return []
42
+ assert self.backend is not None
43
+ outputs = await self.backend.run(image, self._spec)
44
+ return [float(value) for value in l2_normalize(outputs[0])]
45
+
46
+ async def embed_from_crop(self, frame_data: bytes, width: int, height: int, box: Box) -> list[float]:
47
+ if not self._ready():
48
+ return []
49
+
50
+ x1 = max(0, int(box[0]))
51
+ y1 = max(0, int(box[1]))
52
+ x2 = min(width, int(box[2]))
53
+ y2 = min(height, int(box[3]))
54
+ if x2 <= x1 or y2 <= y1:
55
+ return []
56
+
57
+ rgb = frame_to_rgb(frame_data, width, height)
58
+ return await self.embed(rgb[y1:y2, x1:x2])
59
+
60
+ async def embed_face(self, crop: NDArray, landmarks: FaceLandmarks | None) -> list[float]:
61
+ if not self._ready() or crop.size == 0 or landmarks is None:
62
+ return []
63
+ if self.aligned:
64
+ return await self.embed(warp_face(crop, landmarks.points, self.input_size[0]))
65
+ box = square_box(landmarks.box, crop.shape[1], crop.shape[0], FACE_PADDING)
66
+ return await self.embed(crop_rgb(crop, box))