ovkit 0.1.0__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.
- ovkit/__init__.py +53 -0
- ovkit/__main__.py +166 -0
- ovkit/core/__init__.py +8 -0
- ovkit/core/backend.py +166 -0
- ovkit/core/constants.py +158 -0
- ovkit/core/convert.py +64 -0
- ovkit/core/download.py +261 -0
- ovkit/core/errors.py +43 -0
- ovkit/core/model.py +358 -0
- ovkit/core/registry.py +153 -0
- ovkit/core/results.py +322 -0
- ovkit/core/tasks.py +105 -0
- ovkit/face/__init__.py +19 -0
- ovkit/face/analyzer.py +26 -0
- ovkit/genai/__init__.py +25 -0
- ovkit/genai/pipelines.py +143 -0
- ovkit/image/__init__.py +7 -0
- ovkit/image/ops.py +99 -0
- ovkit/manifests/aliases.yaml +54 -0
- ovkit/manifests/genai.yaml +38 -0
- ovkit/manifests/models.yaml +47 -0
- ovkit/manifests/omz.yaml +442 -0
- ovkit/py.typed +0 -0
- ovkit/recognize/__init__.py +60 -0
- ovkit/recognize/base.py +103 -0
- ovkit/recognize/classify.py +107 -0
- ovkit/recognize/detect.py +328 -0
- ovkit/recognize/face.py +129 -0
- ovkit/recognize/generic.py +87 -0
- ovkit/recognize/ocr.py +65 -0
- ovkit/recognize/pose.py +85 -0
- ovkit/recognize/segment.py +125 -0
- ovkit/solutions/__init__.py +9 -0
- ovkit/solutions/anomaly.py +19 -0
- ovkit/solutions/ocr.py +24 -0
- ovkit/solutions/reid.py +26 -0
- ovkit/solutions/tracking.py +23 -0
- ovkit-0.1.0.dist-info/METADATA +243 -0
- ovkit-0.1.0.dist-info/RECORD +42 -0
- ovkit-0.1.0.dist-info/WHEEL +4 -0
- ovkit-0.1.0.dist-info/entry_points.txt +2 -0
- ovkit-0.1.0.dist-info/licenses/LICENSE +202 -0
ovkit/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""ovkit — a simple Python inference API for OpenVINO.
|
|
2
|
+
|
|
3
|
+
One import, one :class:`Model` class, a callable object, and clean
|
|
4
|
+
:class:`Results` — plus OpenVINO's strengths (AUTO/NPU devices, async, INT8).
|
|
5
|
+
|
|
6
|
+
Example
|
|
7
|
+
-------
|
|
8
|
+
>>> from ovkit import Model
|
|
9
|
+
>>> model = Model("rtdetr_r50") # name -> auto download/convert/cache
|
|
10
|
+
>>> results = model("img.jpg", conf=0.25)
|
|
11
|
+
>>> for r in results:
|
|
12
|
+
... print(r.boxes.xyxy, r.boxes.conf, r.boxes.cls)
|
|
13
|
+
... r.save("out.jpg")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .core.errors import (
|
|
19
|
+
ConversionError,
|
|
20
|
+
DownloadError,
|
|
21
|
+
GatedModelError,
|
|
22
|
+
LicenseError,
|
|
23
|
+
MirrorMissingError,
|
|
24
|
+
ModelNotFoundError,
|
|
25
|
+
OfflineError,
|
|
26
|
+
OVKitError,
|
|
27
|
+
TaskDetectionError,
|
|
28
|
+
)
|
|
29
|
+
from .core.model import Model
|
|
30
|
+
from .core.registry import list_models
|
|
31
|
+
from .core.results import Boxes, Keypoints, Masks, Probs, Results
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Model",
|
|
37
|
+
"Results",
|
|
38
|
+
"Boxes",
|
|
39
|
+
"Masks",
|
|
40
|
+
"Keypoints",
|
|
41
|
+
"Probs",
|
|
42
|
+
"list_models",
|
|
43
|
+
"OVKitError",
|
|
44
|
+
"ModelNotFoundError",
|
|
45
|
+
"OfflineError",
|
|
46
|
+
"DownloadError",
|
|
47
|
+
"GatedModelError",
|
|
48
|
+
"MirrorMissingError",
|
|
49
|
+
"ConversionError",
|
|
50
|
+
"TaskDetectionError",
|
|
51
|
+
"LicenseError",
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|
ovkit/__main__.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""``ovkit`` command-line interface: ``run``, ``list``, ``info``, ``download``, ``devices``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .core.convert import to_ir
|
|
10
|
+
from .core.download import fetch
|
|
11
|
+
from .core.errors import OVKitError
|
|
12
|
+
from .core.registry import list_models, resolve
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _cmd_list(_: argparse.Namespace) -> int:
|
|
16
|
+
names = list_models()
|
|
17
|
+
if not names:
|
|
18
|
+
print("No models registered.")
|
|
19
|
+
return 0
|
|
20
|
+
aliases: list[tuple[str, str]] = []
|
|
21
|
+
models: list[tuple[str, str, str]] = []
|
|
22
|
+
for name in names:
|
|
23
|
+
entry = resolve(name)
|
|
24
|
+
if entry is None:
|
|
25
|
+
continue
|
|
26
|
+
if entry.name != name: # capability alias -> its target
|
|
27
|
+
aliases.append((name, entry.name))
|
|
28
|
+
continue
|
|
29
|
+
desc = entry.description or ""
|
|
30
|
+
if len(desc) > 60:
|
|
31
|
+
desc = desc[:57] + "..."
|
|
32
|
+
models.append((name, str(entry.task), desc))
|
|
33
|
+
if aliases:
|
|
34
|
+
print("aliases (capability -> model):")
|
|
35
|
+
for alias, target in aliases:
|
|
36
|
+
print(f" {alias:24s} -> {target}")
|
|
37
|
+
print()
|
|
38
|
+
print(f"models ({len(models)}):")
|
|
39
|
+
for name, task, desc in models:
|
|
40
|
+
print(f" {name:44s} {task:18s} {desc}")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _cmd_info(args: argparse.Namespace) -> int:
|
|
45
|
+
entry = resolve(args.name)
|
|
46
|
+
if entry is None:
|
|
47
|
+
print(f"'{args.name}' is not a registered model.", file=sys.stderr)
|
|
48
|
+
return 1
|
|
49
|
+
print(f"name : {entry.name}")
|
|
50
|
+
print(f"task : {entry.task}")
|
|
51
|
+
if entry.description:
|
|
52
|
+
print(f"description: {entry.description}")
|
|
53
|
+
print(f"license : {entry.license}")
|
|
54
|
+
print(f"source : {entry.src} ({entry.repo or entry.url})")
|
|
55
|
+
print(f"precision : {entry.precision}")
|
|
56
|
+
if entry.filename:
|
|
57
|
+
print(f"filename : {entry.filename}")
|
|
58
|
+
if entry.imgsz:
|
|
59
|
+
print(f"imgsz : {entry.imgsz}")
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _cmd_download(args: argparse.Namespace) -> int:
|
|
64
|
+
entry = resolve(args.name)
|
|
65
|
+
if entry is None:
|
|
66
|
+
print(f"'{args.name}' is not a registered model.", file=sys.stderr)
|
|
67
|
+
return 1
|
|
68
|
+
print(f"Fetching {entry.name} from {entry.src}...")
|
|
69
|
+
source = fetch(entry)
|
|
70
|
+
print(f"Downloaded source: {source}")
|
|
71
|
+
if not args.no_convert:
|
|
72
|
+
ir = to_ir(source, entry.name, entry.precision)
|
|
73
|
+
print(f"IR ready: {ir}")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _cmd_devices(_: argparse.Namespace) -> int:
|
|
78
|
+
from .core.backend import available_devices
|
|
79
|
+
|
|
80
|
+
for dev in available_devices():
|
|
81
|
+
print(dev)
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
86
|
+
"""One-shot inference from the shell: ``ovkit run detect img.jpg``."""
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
|
|
89
|
+
from .core.model import Model
|
|
90
|
+
|
|
91
|
+
model = Model(args.model, device=args.device)
|
|
92
|
+
results = model(args.source, conf=args.conf)
|
|
93
|
+
if not isinstance(results, list): # raw (.npy/.wav) input -> tensor dict
|
|
94
|
+
for name, arr in results.items():
|
|
95
|
+
print(f"{name}: shape={tuple(arr.shape)} dtype={arr.dtype}")
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
for r in results:
|
|
99
|
+
parts = [f"task={r.task}"]
|
|
100
|
+
if r.text:
|
|
101
|
+
parts.append(f'text="{r.text}"')
|
|
102
|
+
if r.boxes is not None:
|
|
103
|
+
parts.append(f"{len(r.boxes)} boxes")
|
|
104
|
+
for x1, y1, x2, y2, c, cl in r.boxes.data[:20]:
|
|
105
|
+
print(
|
|
106
|
+
f" {r.name_for(int(cl)):16s} {c:.2f} [{int(x1)},{int(y1)},{int(x2)},{int(y2)}]"
|
|
107
|
+
)
|
|
108
|
+
if r.probs is not None:
|
|
109
|
+
top = ", ".join(
|
|
110
|
+
f"{r.name_for(int(i))} {r.probs.data[int(i)]:.2f}" for i in r.probs.top5
|
|
111
|
+
)
|
|
112
|
+
parts.append(f"top-5: {top}")
|
|
113
|
+
if r.masks is not None:
|
|
114
|
+
parts.append(f"masks {tuple(r.masks.data.shape)}")
|
|
115
|
+
if r.keypoints is not None:
|
|
116
|
+
parts.append(f"keypoints {tuple(r.keypoints.data.shape)}")
|
|
117
|
+
print(" | ".join(parts))
|
|
118
|
+
|
|
119
|
+
save = args.save
|
|
120
|
+
if save is None and results and Path(str(args.source)).is_file():
|
|
121
|
+
save = f"{Path(str(args.source)).stem}_out.jpg"
|
|
122
|
+
if save and results:
|
|
123
|
+
results[0].save(save)
|
|
124
|
+
print(f"saved -> {save}")
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main(argv: list[str] | None = None) -> int:
|
|
129
|
+
"""CLI entry point. Returns a process exit code."""
|
|
130
|
+
parser = argparse.ArgumentParser(prog="ovkit", description="ovkit model utilities")
|
|
131
|
+
parser.add_argument("--version", action="version", version=f"ovkit {__version__}")
|
|
132
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
133
|
+
|
|
134
|
+
p_list = sub.add_parser("list", help="list registered models")
|
|
135
|
+
p_list.set_defaults(func=_cmd_list)
|
|
136
|
+
|
|
137
|
+
p_info = sub.add_parser("info", help="show details for a model")
|
|
138
|
+
p_info.add_argument("name")
|
|
139
|
+
p_info.set_defaults(func=_cmd_info)
|
|
140
|
+
|
|
141
|
+
p_dl = sub.add_parser("download", help="download (and convert) a model")
|
|
142
|
+
p_dl.add_argument("name")
|
|
143
|
+
p_dl.add_argument("--no-convert", action="store_true", help="skip IR conversion")
|
|
144
|
+
p_dl.set_defaults(func=_cmd_download)
|
|
145
|
+
|
|
146
|
+
p_dev = sub.add_parser("devices", help="list OpenVINO devices")
|
|
147
|
+
p_dev.set_defaults(func=_cmd_devices)
|
|
148
|
+
|
|
149
|
+
p_run = sub.add_parser("run", help="run a model on an image/folder/video from the shell")
|
|
150
|
+
p_run.add_argument("model", help="alias, registered name, or model path")
|
|
151
|
+
p_run.add_argument("source", help="image / folder / video path (or .npy/.wav)")
|
|
152
|
+
p_run.add_argument("--conf", type=float, default=0.25, help="confidence threshold")
|
|
153
|
+
p_run.add_argument("--device", default="AUTO", help="AUTO | CPU | GPU | NPU")
|
|
154
|
+
p_run.add_argument("--save", metavar="PATH", help="annotated output (default: <src>_out.jpg)")
|
|
155
|
+
p_run.set_defaults(func=_cmd_run)
|
|
156
|
+
|
|
157
|
+
args = parser.parse_args(argv)
|
|
158
|
+
try:
|
|
159
|
+
return args.func(args)
|
|
160
|
+
except OVKitError as exc:
|
|
161
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
162
|
+
return 2
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
raise SystemExit(main())
|
ovkit/core/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Core runtime: backend, model, results, registry, download, convert, tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .model import Model
|
|
6
|
+
from .results import Boxes, Keypoints, Masks, Probs, Results
|
|
7
|
+
|
|
8
|
+
__all__ = ["Model", "Results", "Boxes", "Masks", "Keypoints", "Probs"]
|
ovkit/core/backend.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Thin OpenVINO runtime wrapper: device abstraction, sync + async inference.
|
|
2
|
+
|
|
3
|
+
A :class:`Backend` owns a compiled model for a chosen device and exposes both a
|
|
4
|
+
single-shot :meth:`infer` (synchronous) and a throughput-oriented
|
|
5
|
+
:meth:`infer_batch` built on ``ov.AsyncInferQueue`` for streams/folders/video.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
#: A single OpenVINO Core, shared process-wide (creating many is wasteful).
|
|
17
|
+
_CORE = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def core() -> Any:
|
|
21
|
+
"""Return the shared :class:`openvino.Core`, creating it on first use."""
|
|
22
|
+
global _CORE
|
|
23
|
+
if _CORE is None:
|
|
24
|
+
import openvino as ov
|
|
25
|
+
|
|
26
|
+
_CORE = ov.Core()
|
|
27
|
+
return _CORE
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def available_devices() -> list[str]:
|
|
31
|
+
"""Return device names visible to OpenVINO (e.g. ``["CPU", "GPU", "NPU"]``)."""
|
|
32
|
+
return list(core().available_devices)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Backend:
|
|
36
|
+
"""A compiled model bound to a device, with sync and async inference.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
model:
|
|
41
|
+
Path to an IR ``.xml`` / ONNX file, or an already-built ``ov.Model``.
|
|
42
|
+
device:
|
|
43
|
+
OpenVINO device string. ``"AUTO"`` (default) lets OpenVINO pick.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, model: str | Path | Any, device: str = "AUTO") -> None:
|
|
47
|
+
self.device = device
|
|
48
|
+
c = core()
|
|
49
|
+
src = str(model) if isinstance(model, (str, Path)) else model
|
|
50
|
+
self.compiled = c.compile_model(src, device)
|
|
51
|
+
self.inputs = self.compiled.inputs
|
|
52
|
+
self.outputs = self.compiled.outputs
|
|
53
|
+
|
|
54
|
+
# -- introspection ------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def input_shape(self) -> tuple[int, ...]:
|
|
58
|
+
"""Partial shape of the first input as a tuple (``-1`` for dynamic)."""
|
|
59
|
+
ps = self.compiled.inputs[0].get_partial_shape()
|
|
60
|
+
dims: list[int] = []
|
|
61
|
+
for d in ps:
|
|
62
|
+
dims.append(int(d.get_length()) if d.is_static else -1)
|
|
63
|
+
return tuple(dims)
|
|
64
|
+
|
|
65
|
+
def _adapt_image_channels(self, arr: np.ndarray) -> np.ndarray:
|
|
66
|
+
"""Match a single NCHW image tensor to the model's channel count.
|
|
67
|
+
|
|
68
|
+
Many OMZ models (OCR, some classifiers) take 1-channel grayscale; the
|
|
69
|
+
preprocessor always produces 3 channels, so reconcile the two here —
|
|
70
|
+
every image adapter funnels through :meth:`infer`. Only the 3<->1 image
|
|
71
|
+
case is touched; anything else passes through unchanged.
|
|
72
|
+
"""
|
|
73
|
+
shape = self.input_shape
|
|
74
|
+
if arr.ndim != 4 or len(shape) != 4 or shape[1] not in (1, 3):
|
|
75
|
+
return arr
|
|
76
|
+
exp, got = shape[1], arr.shape[1]
|
|
77
|
+
if got == exp:
|
|
78
|
+
return arr
|
|
79
|
+
if exp == 1 and got == 3: # to grayscale (channel-order agnostic)
|
|
80
|
+
return arr.mean(axis=1, keepdims=True).astype(arr.dtype)
|
|
81
|
+
if exp == 3 and got == 1: # broadcast gray -> 3 channels
|
|
82
|
+
return np.repeat(arr, 3, axis=1)
|
|
83
|
+
return arr
|
|
84
|
+
|
|
85
|
+
def output_signatures(self) -> list[tuple[str, tuple[int, ...]]]:
|
|
86
|
+
"""Return ``(name, shape)`` for each output (``-1`` for dynamic dims)."""
|
|
87
|
+
sigs: list[tuple[str, tuple[int, ...]]] = []
|
|
88
|
+
for out in self.compiled.outputs:
|
|
89
|
+
ps = out.get_partial_shape()
|
|
90
|
+
shape = tuple(int(d.get_length()) if d.is_static else -1 for d in ps)
|
|
91
|
+
try:
|
|
92
|
+
name = out.get_any_name()
|
|
93
|
+
except RuntimeError:
|
|
94
|
+
name = ""
|
|
95
|
+
sigs.append((name, shape))
|
|
96
|
+
return sigs
|
|
97
|
+
|
|
98
|
+
def rt_info(self, *keys: str) -> str | None:
|
|
99
|
+
"""Read a runtime-info value from the underlying model, or ``None``."""
|
|
100
|
+
try:
|
|
101
|
+
model = self.compiled.get_runtime_model()
|
|
102
|
+
info = model.get_rt_info(list(keys))
|
|
103
|
+
return str(info)
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
# -- inference ----------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def infer(self, inputs: np.ndarray | dict[Any, np.ndarray]) -> dict[str, np.ndarray]:
|
|
110
|
+
"""Run one synchronous inference and return ``{output_name: ndarray}``."""
|
|
111
|
+
if isinstance(inputs, np.ndarray):
|
|
112
|
+
inputs = self._adapt_image_channels(inputs)
|
|
113
|
+
result = self.compiled(inputs)
|
|
114
|
+
return self._named(result)
|
|
115
|
+
|
|
116
|
+
def infer_batch(
|
|
117
|
+
self,
|
|
118
|
+
feeds: Iterable[np.ndarray | dict[Any, np.ndarray]],
|
|
119
|
+
callback: Callable[[int, dict[str, np.ndarray]], None] | None = None,
|
|
120
|
+
jobs: int = 0,
|
|
121
|
+
) -> Iterator[dict[str, np.ndarray]]:
|
|
122
|
+
"""Run inference over ``feeds`` using an async queue (throughput mode).
|
|
123
|
+
|
|
124
|
+
Yields result dicts in completion order. When ``callback`` is given it
|
|
125
|
+
is invoked as ``callback(index, result)``; otherwise results are
|
|
126
|
+
collected and yielded. ``jobs`` sets the number of in-flight requests
|
|
127
|
+
(``0`` lets OpenVINO choose the optimal number).
|
|
128
|
+
"""
|
|
129
|
+
import openvino as ov
|
|
130
|
+
|
|
131
|
+
queue = ov.AsyncInferQueue(self.compiled, jobs)
|
|
132
|
+
collected: dict[int, dict[str, np.ndarray]] = {}
|
|
133
|
+
|
|
134
|
+
def _on_done(request: Any, userdata: int) -> None:
|
|
135
|
+
named = self._named({out: request.get_tensor(out).data for out in self.outputs})
|
|
136
|
+
if callback is not None:
|
|
137
|
+
callback(userdata, named)
|
|
138
|
+
else:
|
|
139
|
+
collected[userdata] = named
|
|
140
|
+
|
|
141
|
+
queue.set_callback(_on_done)
|
|
142
|
+
count = 0
|
|
143
|
+
for i, feed in enumerate(feeds):
|
|
144
|
+
if isinstance(feed, np.ndarray):
|
|
145
|
+
feed = self._adapt_image_channels(feed)
|
|
146
|
+
queue.start_async(feed, userdata=i)
|
|
147
|
+
count += 1
|
|
148
|
+
queue.wait_all()
|
|
149
|
+
|
|
150
|
+
if callback is None:
|
|
151
|
+
for i in range(count):
|
|
152
|
+
if i in collected:
|
|
153
|
+
yield collected[i]
|
|
154
|
+
|
|
155
|
+
def _named(self, result: Any) -> dict[str, np.ndarray]:
|
|
156
|
+
named: dict[str, np.ndarray] = {}
|
|
157
|
+
for idx, out in enumerate(self.compiled.outputs):
|
|
158
|
+
try:
|
|
159
|
+
name = out.get_any_name()
|
|
160
|
+
except RuntimeError:
|
|
161
|
+
name = f"output_{idx}"
|
|
162
|
+
try:
|
|
163
|
+
named[name] = np.asarray(result[out])
|
|
164
|
+
except (KeyError, TypeError):
|
|
165
|
+
named[name] = np.asarray(result[idx])
|
|
166
|
+
return named
|
ovkit/core/constants.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Shared constants: cache locations, license policy, well-known class lists."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# --- cache / environment ---------------------------------------------------
|
|
9
|
+
|
|
10
|
+
#: Environment variable pointing at the ovkit cache root.
|
|
11
|
+
ENV_HOME = "OVKIT_HOME"
|
|
12
|
+
#: Environment variable; when ``"1"`` no network access is attempted.
|
|
13
|
+
ENV_OFFLINE = "OVKIT_OFFLINE"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cache_root() -> Path:
|
|
17
|
+
"""Return the ovkit cache root, honoring ``$OVKIT_HOME``.
|
|
18
|
+
|
|
19
|
+
Defaults to ``~/.cache/ovkit``. The directory is created on demand.
|
|
20
|
+
"""
|
|
21
|
+
root = os.environ.get(ENV_HOME)
|
|
22
|
+
base = Path(root).expanduser() if root else Path.home() / ".cache" / "ovkit"
|
|
23
|
+
return base
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_offline() -> bool:
|
|
27
|
+
"""Return ``True`` when offline mode is requested via ``$OVKIT_OFFLINE``."""
|
|
28
|
+
return os.environ.get(ENV_OFFLINE, "").strip() in {"1", "true", "True", "yes"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --- license policy --------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
#: SPDX ids accepted for models registered in the manifest. Anything outside
|
|
34
|
+
#: this set (AGPL, non-commercial weights, ...) must not ship with ovkit.
|
|
35
|
+
PERMISSIVE_LICENSES: frozenset[str] = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"apache-2.0",
|
|
38
|
+
"mit",
|
|
39
|
+
"bsd-2-clause",
|
|
40
|
+
"bsd-3-clause",
|
|
41
|
+
"bsd",
|
|
42
|
+
"isc",
|
|
43
|
+
"unlicense",
|
|
44
|
+
"cc0-1.0",
|
|
45
|
+
"mpl-2.0",
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_permissive(license_id: str | None) -> bool:
|
|
51
|
+
"""Return ``True`` if ``license_id`` is a known permissive SPDX id."""
|
|
52
|
+
if not license_id:
|
|
53
|
+
return False
|
|
54
|
+
return license_id.strip().lower() in PERMISSIVE_LICENSES
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --- class name tables -----------------------------------------------------
|
|
58
|
+
|
|
59
|
+
#: 80 COCO class names (detection/segmentation), indexed by class id.
|
|
60
|
+
COCO80: tuple[str, ...] = (
|
|
61
|
+
"person",
|
|
62
|
+
"bicycle",
|
|
63
|
+
"car",
|
|
64
|
+
"motorcycle",
|
|
65
|
+
"airplane",
|
|
66
|
+
"bus",
|
|
67
|
+
"train",
|
|
68
|
+
"truck",
|
|
69
|
+
"boat",
|
|
70
|
+
"traffic light",
|
|
71
|
+
"fire hydrant",
|
|
72
|
+
"stop sign",
|
|
73
|
+
"parking meter",
|
|
74
|
+
"bench",
|
|
75
|
+
"bird",
|
|
76
|
+
"cat",
|
|
77
|
+
"dog",
|
|
78
|
+
"horse",
|
|
79
|
+
"sheep",
|
|
80
|
+
"cow",
|
|
81
|
+
"elephant",
|
|
82
|
+
"bear",
|
|
83
|
+
"zebra",
|
|
84
|
+
"giraffe",
|
|
85
|
+
"backpack",
|
|
86
|
+
"umbrella",
|
|
87
|
+
"handbag",
|
|
88
|
+
"tie",
|
|
89
|
+
"suitcase",
|
|
90
|
+
"frisbee",
|
|
91
|
+
"skis",
|
|
92
|
+
"snowboard",
|
|
93
|
+
"sports ball",
|
|
94
|
+
"kite",
|
|
95
|
+
"baseball bat",
|
|
96
|
+
"baseball glove",
|
|
97
|
+
"skateboard",
|
|
98
|
+
"surfboard",
|
|
99
|
+
"tennis racket",
|
|
100
|
+
"bottle",
|
|
101
|
+
"wine glass",
|
|
102
|
+
"cup",
|
|
103
|
+
"fork",
|
|
104
|
+
"knife",
|
|
105
|
+
"spoon",
|
|
106
|
+
"bowl",
|
|
107
|
+
"banana",
|
|
108
|
+
"apple",
|
|
109
|
+
"sandwich",
|
|
110
|
+
"orange",
|
|
111
|
+
"broccoli",
|
|
112
|
+
"carrot",
|
|
113
|
+
"hot dog",
|
|
114
|
+
"pizza",
|
|
115
|
+
"donut",
|
|
116
|
+
"cake",
|
|
117
|
+
"chair",
|
|
118
|
+
"couch",
|
|
119
|
+
"potted plant",
|
|
120
|
+
"bed",
|
|
121
|
+
"dining table",
|
|
122
|
+
"toilet",
|
|
123
|
+
"tv",
|
|
124
|
+
"laptop",
|
|
125
|
+
"mouse",
|
|
126
|
+
"remote",
|
|
127
|
+
"keyboard",
|
|
128
|
+
"cell phone",
|
|
129
|
+
"microwave",
|
|
130
|
+
"oven",
|
|
131
|
+
"toaster",
|
|
132
|
+
"sink",
|
|
133
|
+
"refrigerator",
|
|
134
|
+
"book",
|
|
135
|
+
"clock",
|
|
136
|
+
"vase",
|
|
137
|
+
"scissors",
|
|
138
|
+
"teddy bear",
|
|
139
|
+
"hair drier",
|
|
140
|
+
"toothbrush",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
#: Registry of named class tables referenced from the manifest ``classes`` key.
|
|
144
|
+
CLASS_TABLES: dict[str, tuple[str, ...]] = {
|
|
145
|
+
"coco80": COCO80,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def class_names(key: str | None, num_classes: int | None = None) -> dict[int, str]:
|
|
150
|
+
"""Resolve a manifest ``classes`` key to an ``{id: name}`` mapping.
|
|
151
|
+
|
|
152
|
+
Falls back to ``class_<i>`` names when ``key`` is unknown. ``num_classes``,
|
|
153
|
+
when given, sizes the fallback table.
|
|
154
|
+
"""
|
|
155
|
+
if key and key in CLASS_TABLES:
|
|
156
|
+
return {i: n for i, n in enumerate(CLASS_TABLES[key])}
|
|
157
|
+
n = num_classes if num_classes is not None else 0
|
|
158
|
+
return {i: f"class_{i}" for i in range(n)}
|
ovkit/core/convert.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Convert source models (ONNX / IR) to OpenVINO IR, with a conversion cache.
|
|
2
|
+
|
|
3
|
+
Conversion runs at most once per ``(name, precision)``: the resulting IR is
|
|
4
|
+
written to the model cache and reused on subsequent loads.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .download import model_cache_dir
|
|
12
|
+
from .errors import ConversionError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _ir_paths(name: str, precision: str) -> tuple[Path, Path]:
|
|
16
|
+
base = model_cache_dir(name) / "ir" / precision
|
|
17
|
+
return base / "model.xml", base / "model.bin"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cached_ir(name: str, precision: str) -> Path | None:
|
|
21
|
+
"""Return the cached IR ``.xml`` for ``(name, precision)`` if it exists."""
|
|
22
|
+
xml, _ = _ir_paths(name, precision)
|
|
23
|
+
return xml if xml.is_file() else None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_ir(source: Path, name: str, precision: str = "fp16") -> Path:
|
|
27
|
+
"""Convert ``source`` to OpenVINO IR and return the cached ``.xml`` path.
|
|
28
|
+
|
|
29
|
+
``source`` may already be IR (``.xml``) — in that case it is passed through
|
|
30
|
+
unchanged. ONNX sources are converted with ``openvino.convert_model`` and
|
|
31
|
+
serialized (compressing weights to fp16 when ``precision == "fp16"``).
|
|
32
|
+
The result is cached so conversion happens only once.
|
|
33
|
+
"""
|
|
34
|
+
source = Path(source)
|
|
35
|
+
|
|
36
|
+
# Already IR: load directly, no conversion/caching needed.
|
|
37
|
+
if source.suffix == ".xml":
|
|
38
|
+
return source
|
|
39
|
+
|
|
40
|
+
cached = cached_ir(name, precision)
|
|
41
|
+
if cached is not None:
|
|
42
|
+
return cached
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
import openvino as ov
|
|
46
|
+
except ImportError as exc: # pragma: no cover - dependency missing
|
|
47
|
+
raise ConversionError("openvino is required to convert models to IR.") from exc
|
|
48
|
+
|
|
49
|
+
if source.suffix not in {".onnx", ".pb", ".pdmodel"} and not source.is_file():
|
|
50
|
+
raise ConversionError(f"Cannot convert '{source}': unsupported or missing source.")
|
|
51
|
+
|
|
52
|
+
xml_path, _ = _ir_paths(name, precision)
|
|
53
|
+
xml_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
ov_model = ov.convert_model(str(source))
|
|
57
|
+
compress = precision == "fp16"
|
|
58
|
+
ov.save_model(ov_model, str(xml_path), compress_to_fp16=compress)
|
|
59
|
+
except Exception as exc: # pragma: no cover - conversion variety
|
|
60
|
+
raise ConversionError(f"Failed to convert '{source.name}' to IR: {exc}") from exc
|
|
61
|
+
|
|
62
|
+
if not xml_path.is_file(): # pragma: no cover - defensive
|
|
63
|
+
raise ConversionError(f"Conversion produced no IR at {xml_path}.")
|
|
64
|
+
return xml_path
|