camera-ui-ml 1.2.0__tar.gz → 1.2.2__tar.gz

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.
Files changed (26) hide show
  1. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/PKG-INFO +1 -1
  2. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/__init__.py +1 -8
  3. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/backend.py +3 -12
  4. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/base.py +1 -2
  5. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/clip.py +21 -7
  6. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/ocr.py +2 -4
  7. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/geometry.py +1 -2
  8. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/model_manager.py +30 -2
  9. camera_ui_ml-1.2.2/camera_ui_ml/pipelines.py +134 -0
  10. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/preprocess.py +1 -1
  11. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/PKG-INFO +1 -1
  12. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/pyproject.toml +1 -1
  13. camera_ui_ml-1.2.0/camera_ui_ml/pipelines.py +0 -121
  14. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/LICENSE.md +0 -0
  15. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/README.md +0 -0
  16. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/__init__.py +0 -0
  17. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/box.py +0 -0
  18. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/embedder.py +0 -0
  19. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/parsing.py +0 -0
  20. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/pipeline.py +0 -0
  21. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/py.typed +0 -0
  22. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/SOURCES.txt +0 -0
  23. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/dependency_links.txt +0 -0
  24. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/requires.txt +0 -0
  25. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/top_level.txt +0 -0
  26. {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camera-ui-ml
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Shared ML inference framework for camera.ui detection plugins (onnx, openvino, coreml, ncnn)
5
5
  Author-email: seydx <hi@seydx.dev>
6
6
  Maintainer-email: seydx <hi@seydx.dev>
@@ -49,7 +49,6 @@ from .preprocess import (
49
49
  )
50
50
 
51
51
  __all__ = [
52
- # backend seam
53
52
  "InferenceBackend",
54
53
  "InputSpec",
55
54
  "Outputs",
@@ -57,9 +56,8 @@ __all__ = [
57
56
  "Layout",
58
57
  "Normalize",
59
58
  "DType",
60
- # model manager
61
59
  "BaseModelManager",
62
- # detectors (ClipEncoder is opt-in via camera_ui_ml.detectors.clip)
60
+ # ClipEncoder is opt-in via camera_ui_ml.detectors.clip
63
61
  "BaseDetector",
64
62
  "BoxDetector",
65
63
  "NormalizedDetection",
@@ -67,7 +65,6 @@ __all__ = [
67
65
  "Embedder",
68
66
  "PlateOcr",
69
67
  "OcrResult",
70
- # geometry
71
68
  "Box",
72
69
  "NormalizedBox",
73
70
  "cxcywh_to_xyxy",
@@ -75,7 +72,6 @@ __all__ = [
75
72
  "clamp_box",
76
73
  "normalize_box",
77
74
  "pad_box",
78
- # parsing
79
75
  "RawDetection",
80
76
  "channels_first",
81
77
  "parse_yolov9",
@@ -83,17 +79,14 @@ __all__ = [
83
79
  "decode_ocr",
84
80
  "l2_normalize",
85
81
  "nms",
86
- # preprocess
87
82
  "frame_to_rgb",
88
83
  "resize_rgb",
89
84
  "to_pil",
90
85
  "to_tensor",
91
86
  "crop_rgb",
92
87
  "decode_image",
93
- # pipeline
94
88
  "run_prepare",
95
89
  "prepare_executor",
96
- # orchestration
97
90
  "detect_objects",
98
91
  "detect_faces",
99
92
  "detect_plates",
@@ -15,8 +15,6 @@ Layout = Literal["nchw", "nhwc"]
15
15
  Normalize = Literal["unit", "facenet", "none"]
16
16
  DType = Literal["float32", "uint8"]
17
17
 
18
- # Inference outputs in model output order; detectors index `outputs[0]` for the
19
- # primary output (OCR scans all outputs, CLIP uses the single embedding output).
20
18
  Outputs = Sequence[NDArray]
21
19
 
22
20
 
@@ -25,9 +23,7 @@ class InputSpec:
25
23
  width: int
26
24
  height: int
27
25
  layout: Layout = "nchw"
28
- normalize: Normalize = (
29
- "unit" # "unit" = /255, "facenet" = (x-127.5)/128, "none" = raw
30
- )
26
+ normalize: Normalize = "unit"
31
27
  dtype: DType = "float32"
32
28
 
33
29
 
@@ -41,8 +37,7 @@ class InferenceBackend(ABC):
41
37
 
42
38
  @property
43
39
  def device(self) -> str:
44
- """Human-readable hardware this backend actually runs on (e.g. "CUDA:0",
45
- "GPU", "Neural Engine", "CPU")"""
40
+ """Human-readable hardware this backend runs on (e.g. "CUDA:0", "CPU")."""
46
41
  return "unknown"
47
42
 
48
43
  @abstractmethod
@@ -52,11 +47,7 @@ class InferenceBackend(ABC):
52
47
  def close(self) -> None: ...
53
48
 
54
49
  def adapt(self, image: NDArray, spec: InputSpec) -> Sequence[Any]:
55
- """Realize the canonical HWC uint8 RGB image into runtime model inputs.
56
-
57
- Default: one normalized tensor per ``spec`` (onnx/openvino/ncnn). CoreML
58
- image-input models override this to pass a PIL image instead. Runs on the
59
- shared prepare-executor (CPU)."""
50
+ """Realize the canonical HWC uint8 RGB image into runtime model inputs."""
60
51
  from .preprocess import to_tensor
61
52
 
62
53
  return [to_tensor(image, spec)]
@@ -50,8 +50,7 @@ class BaseDetector:
50
50
  self._init_task = None
51
51
 
52
52
  async def _configure(self, model_name: str) -> None:
53
- """Read model-derived config (input size, labels) from ``self.backend``.
54
- Overridden by subclasses that need it; the default is a no-op."""
53
+ """Read model-derived config (input size, labels) from ``self.backend``."""
55
54
 
56
55
  def _ready(self) -> bool:
57
56
  return self.initialized and self.backend is not None
@@ -20,7 +20,6 @@ class ClipEncoder:
20
20
  manager: BaseModelManager,
21
21
  logger: LoggerService,
22
22
  *,
23
- pretrained: str = "openai/clip-vit-base-patch32",
24
23
  embedding_model: str = "clip-vit-base-patch32",
25
24
  ) -> None:
26
25
  self.manager = manager
@@ -33,14 +32,15 @@ class ClipEncoder:
33
32
 
34
33
  self.initialized = False
35
34
  self.closed = False
36
- self._pretrained = pretrained
37
35
  self._init_task: asyncio.Task[None] | None = None
38
36
 
39
37
  async def initialize(self, vision_model: str, text_model: str) -> None:
40
38
  if self.initialized:
41
39
  return
42
40
  if self._init_task is None:
43
- self._init_task = asyncio.create_task(self._do_initialize(vision_model, text_model))
41
+ self._init_task = asyncio.create_task(
42
+ self._do_initialize(vision_model, text_model)
43
+ )
44
44
  await self._init_task
45
45
 
46
46
  async def close(self) -> None:
@@ -83,7 +83,10 @@ class ClipEncoder:
83
83
  async def embed_frames(self, frames: list[VideoFrameData]) -> list[list[float]]:
84
84
  if not self._ready():
85
85
  return [[] for _ in frames]
86
- return [await self.embed_frame(frame["width"], frame["height"], frame["data"]) for frame in frames]
86
+ return [
87
+ await self.embed_frame(frame["width"], frame["height"], frame["data"])
88
+ for frame in frames
89
+ ]
87
90
 
88
91
  def _ready(self) -> bool:
89
92
  return (
@@ -97,7 +100,14 @@ class ClipEncoder:
97
100
  try:
98
101
  self.vision = await self.manager.ensure_backend(vision_model)
99
102
  self.text = await self.manager.ensure_backend(text_model)
100
- self.processor = await asyncio.to_thread(CLIPProcessor.from_pretrained, self._pretrained)
103
+ processor_dir = await self.manager.ensure_clip_processor()
104
+ if processor_dir is None:
105
+ raise RuntimeError(
106
+ "CLIP processor files are not bundled (clip_processor_files is empty)"
107
+ )
108
+ self.processor = await asyncio.to_thread(
109
+ CLIPProcessor.from_pretrained, processor_dir
110
+ )
101
111
  if self.closed:
102
112
  return
103
113
  self.initialized = True
@@ -108,11 +118,15 @@ class ClipEncoder:
108
118
  self._init_task = None
109
119
 
110
120
  def _vision_input(self, pil: Any) -> NDArray:
111
- inputs = self.processor(images=pil, return_tensors="np", padding="max_length", truncation=True)
121
+ inputs = self.processor(
122
+ images=pil, return_tensors="np", padding="max_length", truncation=True
123
+ )
112
124
  return np.asarray(inputs["pixel_values"], dtype=np.float32)
113
125
 
114
126
  def _text_input(self, text: str) -> tuple[NDArray, NDArray]:
115
- inputs = self.processor(text=text, return_tensors="np", padding="max_length", truncation=True)
127
+ inputs = self.processor(
128
+ text=text, return_tensors="np", padding="max_length", truncation=True
129
+ )
116
130
  return (
117
131
  np.asarray(inputs["input_ids"], dtype=np.int64),
118
132
  np.asarray(inputs["attention_mask"], dtype=np.int64),
@@ -12,8 +12,7 @@ from ..parsing import decode_ocr
12
12
  from ..preprocess import frame_to_rgb
13
13
  from .base import BaseDetector
14
14
 
15
- #: Default plate alphabet: digits + uppercase letters + pad. Plugins pass the
16
- #: exact constant their model was trained with.
15
+ #: Default plate alphabet; pass the exact constant the model was trained with.
17
16
  DEFAULT_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_"
18
17
 
19
18
 
@@ -79,8 +78,7 @@ class PlateOcr(BaseDetector):
79
78
  def _decode(self, outputs: Outputs) -> OcrResult | None:
80
79
  logits: NDArray | None = None
81
80
  for value in outputs:
82
- # Batched [1, slots, classes] (onnx/openvino/coreml) or bare [slots,
83
- # classes] (ncnn) — squeeze to a 2D logits matrix either way.
81
+ # Output is [1, slots, classes] or bare [slots, classes]; squeeze to 2D.
84
82
  arr = np.squeeze(np.asarray(value))
85
83
  if arr.ndim == 2 and arr.shape[0] == self._slots:
86
84
  logits = arr
@@ -44,8 +44,7 @@ def normalize_box(box: Box, width: float, height: float) -> NormalizedBox:
44
44
 
45
45
 
46
46
  def pad_box(box: Box, width: float, height: float, padding: float, min_size: int = 0) -> Box:
47
- """Expand a box by ``padding`` (fraction of its size) and an optional minimum
48
- pixel size, clamped to the frame. Used to crop faces/plates with context."""
47
+ """Expand a box by ``padding`` (fraction of its size) and an optional minimum pixel size, clamped to the frame."""
49
48
  x1, y1, x2, y2 = box
50
49
  w = x2 - x1
51
50
  h = y2 - y1
@@ -14,6 +14,15 @@ _CHUNK = 1024 * 1024
14
14
 
15
15
 
16
16
  class BaseModelManager(ABC):
17
+ CLIP_PROCESSOR_FILENAMES = (
18
+ "preprocessor_config.json",
19
+ "tokenizer_config.json",
20
+ "vocab.json",
21
+ "merges.txt",
22
+ "special_tokens_map.json",
23
+ "tokenizer.json",
24
+ )
25
+
17
26
  def __init__(self, storage_path: str, logger: LoggerService, version: str) -> None:
18
27
  self.logger = logger
19
28
  self.model_path = os.path.join(storage_path, "models", version)
@@ -29,12 +38,28 @@ class BaseModelManager(ABC):
29
38
  def reset(self) -> None:
30
39
  self._load_tasks.clear()
31
40
 
41
+ def clip_processor_files(self) -> Mapping[str, tuple[str, str]]:
42
+ """CLIP processor (tokenizer + image preprocessor) files: key → (url, path relative to
43
+ ``model_path``). Empty (default) = plugin has no CLIP; CLIP plugins must override this."""
44
+ return {}
45
+
46
+ async def ensure_clip_processor(self) -> str | None:
47
+ files = self.clip_processor_files()
48
+ if not files:
49
+ return None
50
+ for url, rel in files.values():
51
+ await self._download(url, rel)
52
+ first_rel = next(iter(files.values()))[1]
53
+ return os.path.dirname(os.path.join(self.model_path, first_rel))
54
+
32
55
  @abstractmethod
33
56
  def model_files(self, model_name: str) -> Mapping[str, tuple[str, str]]:
34
57
  """Map of file key → (download_url, path relative to ``model_path``)."""
35
58
 
36
59
  @abstractmethod
37
- async def build_backend(self, model_name: str, paths: Mapping[str, str]) -> InferenceBackend:
60
+ async def build_backend(
61
+ self, model_name: str, paths: Mapping[str, str]
62
+ ) -> InferenceBackend:
38
63
  """Build the runtime backend from the (already downloaded) local paths."""
39
64
 
40
65
  async def _load(self, model_name: str) -> InferenceBackend:
@@ -42,7 +67,10 @@ class BaseModelManager(ABC):
42
67
  for url, rel in files.values():
43
68
  await self._download(url, rel)
44
69
 
45
- paths = {key: os.path.join(self.model_path, rel) for key, (_url, rel) in files.items()}
70
+ paths = {
71
+ key: os.path.join(self.model_path, rel)
72
+ for key, (_url, rel) in files.items()
73
+ }
46
74
  return await self.build_backend(model_name, paths)
47
75
 
48
76
  async def _download(self, url: str, rel: str) -> None:
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING, cast
5
+
6
+ from camera_ui_sdk import (
7
+ ClipEmbedding,
8
+ ClipResult,
9
+ DetectionLabel,
10
+ FaceDetection,
11
+ FaceResult,
12
+ LicensePlateDetection,
13
+ LicensePlateResult,
14
+ ObjectResult,
15
+ TrackedDetection,
16
+ VideoFrameData,
17
+ )
18
+
19
+ from .detectors.box import BoxDetector
20
+ from .detectors.embedder import Embedder
21
+ from .detectors.ocr import PlateOcr
22
+ from .geometry import normalize_box
23
+
24
+ if TYPE_CHECKING:
25
+ from .detectors.clip import ClipEncoder
26
+
27
+
28
+ async def detect_objects(
29
+ detector: BoxDetector, frame: VideoFrameData, threshold: float | None = None
30
+ ) -> ObjectResult:
31
+ raw = await detector.detect_frame(frame, threshold)
32
+ width, height = frame["width"], frame["height"]
33
+ detections: list[TrackedDetection] = [
34
+ {
35
+ "label": cast(DetectionLabel, detector.labels.get(cid, "unknown")),
36
+ "confidence": conf,
37
+ "box": normalize_box(box, width, height),
38
+ }
39
+ for cid, conf, box in raw
40
+ ]
41
+ return {"detected": len(detections) > 0, "detections": detections}
42
+
43
+
44
+ async def detect_faces(
45
+ detector: BoxDetector,
46
+ embedder: Embedder,
47
+ frames: list[VideoFrameData],
48
+ threshold: float | None = None,
49
+ ) -> list[FaceResult]:
50
+ tasks = [_detect_faces_one(detector, embedder, frame, threshold) for frame in frames]
51
+ return list(await asyncio.gather(*tasks))
52
+
53
+
54
+ async def _detect_faces_one(
55
+ detector: BoxDetector,
56
+ embedder: Embedder,
57
+ frame: VideoFrameData,
58
+ threshold: float | None,
59
+ ) -> FaceResult:
60
+ raw = await detector.detect_frame(frame, threshold)
61
+ if not raw:
62
+ return {"detected": False, "detections": []}
63
+
64
+ width, height = frame["width"], frame["height"]
65
+ data = bytes(frame["data"])
66
+ embeddings = await asyncio.gather(
67
+ *(embedder.embed_from_crop(data, width, height, box) for _cid, _conf, box in raw)
68
+ )
69
+ detections: list[FaceDetection] = [
70
+ {
71
+ "label": "person",
72
+ "attribute": "face",
73
+ "confidence": conf,
74
+ "box": normalize_box(box, width, height),
75
+ "embedding": embedding,
76
+ }
77
+ for (_cid, conf, box), embedding in zip(raw, embeddings, strict=False)
78
+ ]
79
+ return {"detected": len(detections) > 0, "detections": detections}
80
+
81
+
82
+ async def detect_plates(
83
+ detector: BoxDetector,
84
+ ocr: PlateOcr,
85
+ frames: list[VideoFrameData],
86
+ threshold: float | None = None,
87
+ ) -> list[LicensePlateResult]:
88
+ tasks = [_detect_plates_one(detector, ocr, frame, threshold) for frame in frames]
89
+ return list(await asyncio.gather(*tasks))
90
+
91
+
92
+ async def _detect_plates_one(
93
+ detector: BoxDetector, ocr: PlateOcr, frame: VideoFrameData, threshold: float | None
94
+ ) -> LicensePlateResult:
95
+ raw = await detector.detect_frame(frame, threshold)
96
+ if not raw:
97
+ return {"detected": False, "detections": []}
98
+
99
+ width, height = frame["width"], frame["height"]
100
+ data = bytes(frame["data"])
101
+ ocr_results = await asyncio.gather(
102
+ *(ocr.recognize_from_crop(data, width, height, box) for _cid, _conf, box in raw)
103
+ )
104
+ detections: list[LicensePlateDetection] = []
105
+ for (_cid, conf, box), result in zip(raw, ocr_results, strict=False):
106
+ if result is None or not result.text:
107
+ continue
108
+ detections.append(
109
+ {
110
+ "label": "vehicle",
111
+ "attribute": "license_plate",
112
+ "confidence": conf,
113
+ "plateText": result.text,
114
+ "box": normalize_box(box, width, height),
115
+ }
116
+ )
117
+ return {"detected": len(detections) > 0, "detections": detections}
118
+
119
+
120
+ async def detect_clip(encoder: ClipEncoder, frames: list[VideoFrameData]) -> list[ClipResult]:
121
+ embeddings = await encoder.embed_frames(frames)
122
+ results: list[ClipResult] = []
123
+ for frame, embedding in zip(frames, embeddings, strict=False):
124
+ items: list[ClipEmbedding] = []
125
+ if embedding:
126
+ items.append(
127
+ {
128
+ "label": frame.get("label") or "image",
129
+ "box": {"x": 0.0, "y": 0.0, "width": 1.0, "height": 1.0},
130
+ "embedding": embedding,
131
+ }
132
+ )
133
+ results.append({"embeddings": items, "embeddingModel": encoder.embedding_model})
134
+ return results
@@ -48,7 +48,7 @@ def to_tensor(rgb: NDArray, spec: InputSpec) -> NDArray:
48
48
  arr = resized.astype(np.float32) / 255.0
49
49
  elif spec.normalize == "facenet":
50
50
  arr = (resized.astype(np.float32) - 127.5) / 128.0
51
- else: # "none"
51
+ else:
52
52
  arr = resized.astype(np.uint8 if spec.dtype == "uint8" else np.float32)
53
53
 
54
54
  if spec.layout == "nchw":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camera-ui-ml
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Shared ML inference framework for camera.ui detection plugins (onnx, openvino, coreml, ncnn)
5
5
  Author-email: seydx <hi@seydx.dev>
6
6
  Maintainer-email: seydx <hi@seydx.dev>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "camera-ui-ml"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  description = "Shared ML inference framework for camera.ui detection plugins (onnx, openvino, coreml, ncnn)"
9
9
  keywords = ["camera.ui", "python", "ml", "inference", "detection"]
10
10
  readme = "README.md"
@@ -1,121 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, cast
4
-
5
- from camera_ui_sdk import (
6
- ClipEmbedding,
7
- ClipResult,
8
- DetectionLabel,
9
- FaceDetection,
10
- FaceResult,
11
- LicensePlateDetection,
12
- LicensePlateResult,
13
- ObjectResult,
14
- TrackedDetection,
15
- VideoFrameData,
16
- )
17
-
18
- from .detectors.box import BoxDetector
19
- from .detectors.embedder import Embedder
20
- from .detectors.ocr import PlateOcr
21
- from .geometry import normalize_box
22
-
23
- if TYPE_CHECKING:
24
- from .detectors.clip import ClipEncoder
25
-
26
-
27
- async def detect_objects(
28
- detector: BoxDetector, frame: VideoFrameData, threshold: float | None = None
29
- ) -> ObjectResult:
30
- raw = await detector.detect_frame(frame, threshold)
31
- width, height = frame["width"], frame["height"]
32
- detections: list[TrackedDetection] = [
33
- {
34
- "label": cast(DetectionLabel, detector.labels.get(cid, "unknown")),
35
- "confidence": conf,
36
- "box": normalize_box(box, width, height),
37
- }
38
- for cid, conf, box in raw
39
- ]
40
- return {"detected": len(detections) > 0, "detections": detections}
41
-
42
-
43
- async def detect_faces(
44
- detector: BoxDetector,
45
- embedder: Embedder,
46
- frames: list[VideoFrameData],
47
- threshold: float | None = None,
48
- ) -> list[FaceResult]:
49
- results: list[FaceResult] = []
50
- for frame in frames:
51
- raw = await detector.detect_frame(frame, threshold)
52
- if not raw:
53
- results.append({"detected": False, "detections": []})
54
- continue
55
-
56
- width, height = frame["width"], frame["height"]
57
- data = bytes(frame["data"])
58
- detections: list[FaceDetection] = []
59
- for _cid, conf, box in raw:
60
- embedding = await embedder.embed_from_crop(data, width, height, box)
61
- detections.append(
62
- {
63
- "label": "person",
64
- "attribute": "face",
65
- "confidence": conf,
66
- "box": normalize_box(box, width, height),
67
- "embedding": embedding,
68
- }
69
- )
70
- results.append({"detected": len(detections) > 0, "detections": detections})
71
- return results
72
-
73
-
74
- async def detect_plates(
75
- detector: BoxDetector,
76
- ocr: PlateOcr,
77
- frames: list[VideoFrameData],
78
- threshold: float | None = None,
79
- ) -> list[LicensePlateResult]:
80
- results: list[LicensePlateResult] = []
81
- for frame in frames:
82
- raw = await detector.detect_frame(frame, threshold)
83
- if not raw:
84
- results.append({"detected": False, "detections": []})
85
- continue
86
-
87
- width, height = frame["width"], frame["height"]
88
- data = bytes(frame["data"])
89
- detections: list[LicensePlateDetection] = []
90
- for _cid, conf, box in raw:
91
- result = await ocr.recognize_from_crop(data, width, height, box)
92
- if result is None or not result.text:
93
- continue
94
- detections.append(
95
- {
96
- "label": "vehicle",
97
- "attribute": "license_plate",
98
- "confidence": conf,
99
- "plateText": result.text,
100
- "box": normalize_box(box, width, height),
101
- }
102
- )
103
- results.append({"detected": len(detections) > 0, "detections": detections})
104
- return results
105
-
106
-
107
- async def detect_clip(encoder: ClipEncoder, frames: list[VideoFrameData]) -> list[ClipResult]:
108
- embeddings = await encoder.embed_frames(frames)
109
- results: list[ClipResult] = []
110
- for frame, embedding in zip(frames, embeddings, strict=False):
111
- items: list[ClipEmbedding] = []
112
- if embedding:
113
- items.append(
114
- {
115
- "label": frame.get("label") or "image",
116
- "box": {"x": 0.0, "y": 0.0, "width": 1.0, "height": 1.0},
117
- "embedding": embedding,
118
- }
119
- )
120
- results.append({"embeddings": items, "embeddingModel": encoder.embedding_model})
121
- return results
File without changes
File without changes
File without changes