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.
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/PKG-INFO +1 -1
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/__init__.py +1 -8
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/backend.py +3 -12
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/base.py +1 -2
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/clip.py +21 -7
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/ocr.py +2 -4
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/geometry.py +1 -2
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/model_manager.py +30 -2
- camera_ui_ml-1.2.2/camera_ui_ml/pipelines.py +134 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/preprocess.py +1 -1
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/PKG-INFO +1 -1
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/pyproject.toml +1 -1
- camera_ui_ml-1.2.0/camera_ui_ml/pipelines.py +0 -121
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/LICENSE.md +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/README.md +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/__init__.py +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/box.py +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/detectors/embedder.py +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/parsing.py +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/pipeline.py +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml/py.typed +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/SOURCES.txt +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/dependency_links.txt +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/requires.txt +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/camera_ui_ml.egg-info/top_level.txt +0 -0
- {camera_ui_ml-1.2.0 → camera_ui_ml-1.2.2}/setup.cfg +0 -0
|
@@ -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
|
-
#
|
|
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
|
|
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(
|
|
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 [
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
#
|
|
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(
|
|
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 = {
|
|
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:
|
|
51
|
+
else:
|
|
52
52
|
arr = resized.astype(np.uint8 if spec.dtype == "uint8" else np.float32)
|
|
53
53
|
|
|
54
54
|
if spec.layout == "nchw":
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "camera-ui-ml"
|
|
7
|
-
version = "1.2.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|