cvflair 0.2.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.
- cvflair/__init__.py +33 -0
- cvflair/camera.py +266 -0
- cvflair/models.py +140 -0
- cvflair/py.typed +0 -0
- cvflair/themes.py +232 -0
- cvflair-0.2.0.dist-info/METADATA +249 -0
- cvflair-0.2.0.dist-info/RECORD +10 -0
- cvflair-0.2.0.dist-info/WHEEL +5 -0
- cvflair-0.2.0.dist-info/licenses/LICENSE +21 -0
- cvflair-0.2.0.dist-info/top_level.txt +1 -0
cvflair/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cvflair -- theme-based visualisation for computer vision detections.
|
|
3
|
+
|
|
4
|
+
The package draws nothing itself: it configures ``supervision`` annotators and
|
|
5
|
+
wraps the camera loop, so the everyday case fits in three lines::
|
|
6
|
+
|
|
7
|
+
from cvflair import Camera
|
|
8
|
+
|
|
9
|
+
cam = Camera(source=0, theme="neon")
|
|
10
|
+
for frame in cam.stream():
|
|
11
|
+
cam.show(frame)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from .camera import Camera, CameraError
|
|
17
|
+
from .models import Detector, UltralyticsDetector, load_ultralytics, resolve_detector
|
|
18
|
+
from .themes import Theme, available_themes, get_theme
|
|
19
|
+
|
|
20
|
+
__version__ = "0.2.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Camera",
|
|
24
|
+
"CameraError",
|
|
25
|
+
"Detector",
|
|
26
|
+
"Theme",
|
|
27
|
+
"UltralyticsDetector",
|
|
28
|
+
"available_themes",
|
|
29
|
+
"get_theme",
|
|
30
|
+
"load_ultralytics",
|
|
31
|
+
"resolve_detector",
|
|
32
|
+
"__version__",
|
|
33
|
+
]
|
cvflair/camera.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webcam capture on a background thread, with a one-slot latest-frame queue.
|
|
3
|
+
|
|
4
|
+
The reader thread never blocks on the consumer: before publishing a new frame
|
|
5
|
+
it drops the pending one, so ``read()`` always returns the most recent frame
|
|
6
|
+
and latency does not build up when annotation is slower than the camera.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import queue
|
|
12
|
+
import threading
|
|
13
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
14
|
+
from typing import Any, overload
|
|
15
|
+
|
|
16
|
+
import cv2
|
|
17
|
+
import numpy as np
|
|
18
|
+
import supervision as sv
|
|
19
|
+
|
|
20
|
+
from .models import ModelLike, resolve_detector
|
|
21
|
+
from .themes import Theme, get_theme
|
|
22
|
+
|
|
23
|
+
__all__ = ["Camera", "CameraError"]
|
|
24
|
+
|
|
25
|
+
QUIT_KEYS = (ord("q"), ord("Q"), 27) # 27 == ESC
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CameraError(RuntimeError):
|
|
29
|
+
"""The capture device could not be opened, or died while streaming."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Camera:
|
|
33
|
+
"""
|
|
34
|
+
A threaded video source with a theme attached.
|
|
35
|
+
|
|
36
|
+
::
|
|
37
|
+
|
|
38
|
+
cam = Camera(source=0, theme="neon")
|
|
39
|
+
for frame in cam.stream():
|
|
40
|
+
cam.show(frame, detections)
|
|
41
|
+
|
|
42
|
+
``stream()`` starts the reader thread on first use and releases the device
|
|
43
|
+
when the generator ends, so the loop above needs no explicit teardown.
|
|
44
|
+
Pressing ``q`` or ``ESC`` in the preview window ends the stream.
|
|
45
|
+
|
|
46
|
+
Any object with OpenCV's ``VideoCapture`` interface can be injected through
|
|
47
|
+
``capture_factory``; that is how the test suite runs without a camera.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
source: int | str = 0,
|
|
53
|
+
theme: str | Theme = "minimal",
|
|
54
|
+
*,
|
|
55
|
+
width: int | None = None,
|
|
56
|
+
height: int | None = None,
|
|
57
|
+
fps: int | None = None,
|
|
58
|
+
window_name: str = "cvflair",
|
|
59
|
+
capture_factory: Callable[[Any], Any] = cv2.VideoCapture,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.source = source
|
|
62
|
+
self.window_name = window_name
|
|
63
|
+
self.width = width
|
|
64
|
+
self.height = height
|
|
65
|
+
self.fps = fps
|
|
66
|
+
|
|
67
|
+
self._theme = get_theme(theme)
|
|
68
|
+
self._capture_factory = capture_factory
|
|
69
|
+
self._capture: Any | None = None
|
|
70
|
+
self._thread: threading.Thread | None = None
|
|
71
|
+
self._queue: queue.Queue[np.ndarray] = queue.Queue(maxsize=1)
|
|
72
|
+
self._stop_event = threading.Event()
|
|
73
|
+
self._window_open = False
|
|
74
|
+
self._frames_read = 0
|
|
75
|
+
self._frames_dropped = 0
|
|
76
|
+
|
|
77
|
+
# -- configuration ---------------------------------------------------
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def theme(self) -> Theme:
|
|
81
|
+
return self._theme
|
|
82
|
+
|
|
83
|
+
@theme.setter
|
|
84
|
+
def theme(self, theme: str | Theme) -> None:
|
|
85
|
+
self._theme = get_theme(theme)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def is_running(self) -> bool:
|
|
89
|
+
return self._thread is not None and self._thread.is_alive()
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def frames_read(self) -> int:
|
|
93
|
+
"""Frames pulled off the device since :meth:`start`."""
|
|
94
|
+
return self._frames_read
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def frames_dropped(self) -> int:
|
|
98
|
+
"""Frames discarded because the consumer was still busy."""
|
|
99
|
+
return self._frames_dropped
|
|
100
|
+
|
|
101
|
+
# -- lifecycle -------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def start(self) -> Camera:
|
|
104
|
+
"""Open the device and start the reader thread. Safe to call twice."""
|
|
105
|
+
if self.is_running:
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
capture = self._capture_factory(self.source)
|
|
109
|
+
if not capture.isOpened():
|
|
110
|
+
capture.release()
|
|
111
|
+
raise CameraError(
|
|
112
|
+
f"Could not open video source {self.source!r}. "
|
|
113
|
+
"Check that the device index is right and no other application is using it."
|
|
114
|
+
)
|
|
115
|
+
self._apply_properties(capture)
|
|
116
|
+
|
|
117
|
+
self._capture = capture
|
|
118
|
+
self._stop_event.clear()
|
|
119
|
+
self._thread = threading.Thread(
|
|
120
|
+
target=self._read_loop, name=f"cvflair-reader-{self.source}", daemon=True
|
|
121
|
+
)
|
|
122
|
+
self._thread.start()
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def _apply_properties(self, capture: Any) -> None:
|
|
126
|
+
if self.width is not None:
|
|
127
|
+
capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
|
|
128
|
+
if self.height is not None:
|
|
129
|
+
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
|
|
130
|
+
if self.fps is not None:
|
|
131
|
+
capture.set(cv2.CAP_PROP_FPS, self.fps)
|
|
132
|
+
|
|
133
|
+
def _read_loop(self) -> None:
|
|
134
|
+
capture = self._capture
|
|
135
|
+
while not self._stop_event.is_set():
|
|
136
|
+
ok, frame = capture.read()
|
|
137
|
+
if not ok or frame is None:
|
|
138
|
+
break # end of file, or the device went away
|
|
139
|
+
self._frames_read += 1
|
|
140
|
+
self._publish(frame)
|
|
141
|
+
self._stop_event.set()
|
|
142
|
+
|
|
143
|
+
def _publish(self, frame: np.ndarray) -> None:
|
|
144
|
+
"""Put ``frame`` in the one-slot queue, discarding whatever was there."""
|
|
145
|
+
try:
|
|
146
|
+
self._queue.get_nowait()
|
|
147
|
+
self._frames_dropped += 1
|
|
148
|
+
except queue.Empty:
|
|
149
|
+
pass
|
|
150
|
+
try:
|
|
151
|
+
self._queue.put_nowait(frame)
|
|
152
|
+
except queue.Full: # pragma: no cover - single producer, cannot happen
|
|
153
|
+
self._frames_dropped += 1
|
|
154
|
+
|
|
155
|
+
def close(self) -> None:
|
|
156
|
+
"""Stop the thread, release the device, close the preview window."""
|
|
157
|
+
self._stop_event.set()
|
|
158
|
+
if self._thread is not None:
|
|
159
|
+
self._thread.join(timeout=2.0)
|
|
160
|
+
self._thread = None
|
|
161
|
+
if self._capture is not None:
|
|
162
|
+
self._capture.release()
|
|
163
|
+
self._capture = None
|
|
164
|
+
if self._window_open:
|
|
165
|
+
cv2.destroyWindow(self.window_name)
|
|
166
|
+
self._window_open = False
|
|
167
|
+
|
|
168
|
+
def __enter__(self) -> Camera:
|
|
169
|
+
return self.start()
|
|
170
|
+
|
|
171
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
172
|
+
self.close()
|
|
173
|
+
|
|
174
|
+
def __repr__(self) -> str:
|
|
175
|
+
state = "running" if self.is_running else "stopped"
|
|
176
|
+
return f"Camera(source={self.source!r}, theme={self._theme.name!r}, {state})"
|
|
177
|
+
|
|
178
|
+
# -- reading ---------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def read(self, timeout: float = 5.0) -> np.ndarray | None:
|
|
181
|
+
"""
|
|
182
|
+
Return the latest frame, or ``None`` if the source ended or stalled.
|
|
183
|
+
|
|
184
|
+
A frame is never returned twice: the queue slot is consumed here.
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
return self._queue.get(timeout=timeout)
|
|
188
|
+
except queue.Empty:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
@overload
|
|
192
|
+
def stream(self, timeout: float = ..., *, model: None = ...) -> Iterator[np.ndarray]: ...
|
|
193
|
+
|
|
194
|
+
@overload
|
|
195
|
+
def stream(
|
|
196
|
+
self, timeout: float = ..., *, model: ModelLike
|
|
197
|
+
) -> Iterator[tuple[np.ndarray, sv.Detections]]: ...
|
|
198
|
+
|
|
199
|
+
def stream(self, timeout: float = 5.0, *, model: ModelLike | None = None):
|
|
200
|
+
"""
|
|
201
|
+
Yield frames until the source ends, ``q``/``ESC`` is pressed, or no
|
|
202
|
+
frame arrives within ``timeout`` seconds. Releases the device on exit.
|
|
203
|
+
|
|
204
|
+
Without ``model`` the frames come through bare. With one -- a weights
|
|
205
|
+
path, an Ultralytics model, or any callable returning ``sv.Detections``
|
|
206
|
+
-- each item is a ``(frame, detections)`` pair::
|
|
207
|
+
|
|
208
|
+
for frame, detections in cam.stream(model="yolov8n.pt"):
|
|
209
|
+
cam.show(frame, detections)
|
|
210
|
+
|
|
211
|
+
Inference runs in this loop, not in the reader thread: while a frame is
|
|
212
|
+
being processed the reader keeps replacing the queued frame, so the next
|
|
213
|
+
iteration gets the newest one rather than a backlog.
|
|
214
|
+
"""
|
|
215
|
+
detector = resolve_detector(model)
|
|
216
|
+
self.start()
|
|
217
|
+
try:
|
|
218
|
+
while not self._stop_event.is_set():
|
|
219
|
+
frame = self.read(timeout=timeout)
|
|
220
|
+
if frame is None:
|
|
221
|
+
break
|
|
222
|
+
yield frame if detector is None else (frame, detector(frame))
|
|
223
|
+
finally:
|
|
224
|
+
self.close()
|
|
225
|
+
|
|
226
|
+
# -- output ----------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
def annotate(
|
|
229
|
+
self,
|
|
230
|
+
frame: np.ndarray,
|
|
231
|
+
detections: sv.Detections | None = None,
|
|
232
|
+
labels: Sequence[str] | None = None,
|
|
233
|
+
) -> np.ndarray:
|
|
234
|
+
"""Apply the active theme in place. Useful when not using :meth:`show`."""
|
|
235
|
+
if detections is None:
|
|
236
|
+
return frame
|
|
237
|
+
return self._theme.annotate(frame, detections, labels=labels)
|
|
238
|
+
|
|
239
|
+
def show(
|
|
240
|
+
self,
|
|
241
|
+
frame: np.ndarray,
|
|
242
|
+
detections: sv.Detections | None = None,
|
|
243
|
+
labels: Sequence[str] | None = None,
|
|
244
|
+
*,
|
|
245
|
+
wait: int = 1,
|
|
246
|
+
) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Annotate and display ``frame``.
|
|
249
|
+
|
|
250
|
+
Returns ``False`` once the user asks to quit (``q``, ``ESC``, or the
|
|
251
|
+
window's close button), which also ends an active :meth:`stream`.
|
|
252
|
+
"""
|
|
253
|
+
self.annotate(frame, detections, labels=labels)
|
|
254
|
+
cv2.imshow(self.window_name, frame)
|
|
255
|
+
self._window_open = True
|
|
256
|
+
|
|
257
|
+
if (cv2.waitKey(wait) & 0xFF) in QUIT_KEYS or self._window_closed():
|
|
258
|
+
self._stop_event.set()
|
|
259
|
+
return False
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
def _window_closed(self) -> bool:
|
|
263
|
+
try:
|
|
264
|
+
return cv2.getWindowProperty(self.window_name, cv2.WND_PROP_VISIBLE) < 1
|
|
265
|
+
except cv2.error: # pragma: no cover - headless build without HighGUI
|
|
266
|
+
return False
|
cvflair/models.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional model binding.
|
|
3
|
+
|
|
4
|
+
cvflair ships no model and depends on none. This module only turns whatever was
|
|
5
|
+
passed as ``model`` into a callable that maps a frame to ``sv.Detections``:
|
|
6
|
+
|
|
7
|
+
* a weights path (``"yolov8n.pt"``) -- loaded through Ultralytics, which is an
|
|
8
|
+
extra (``pip install "cvflair[yolo]"``), never a hard dependency;
|
|
9
|
+
* an already constructed Ultralytics model -- its results are converted;
|
|
10
|
+
* any callable returning ``sv.Detections`` -- used as it is.
|
|
11
|
+
|
|
12
|
+
Keeping Ultralytics out of the dependency list is deliberate: it is AGPL-3.0,
|
|
13
|
+
and pulling it in would push that licence onto every cvflair user.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Protocol, runtime_checkable
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
import supervision as sv
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Detector",
|
|
27
|
+
"ModelLike",
|
|
28
|
+
"UltralyticsDetector",
|
|
29
|
+
"load_ultralytics",
|
|
30
|
+
"resolve_detector",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
#: Weight file suffixes Ultralytics can load.
|
|
34
|
+
WEIGHT_SUFFIXES = frozenset({".pt", ".onnx", ".engine", ".torchscript", ".mlpackage"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@runtime_checkable
|
|
38
|
+
class Detector(Protocol):
|
|
39
|
+
"""Anything that turns one frame into detections."""
|
|
40
|
+
|
|
41
|
+
def __call__(self, frame: np.ndarray) -> sv.Detections: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
ModelLike = str | Path | Detector | Any
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UltralyticsDetector:
|
|
48
|
+
"""
|
|
49
|
+
Runs an Ultralytics model and converts its output to ``sv.Detections``.
|
|
50
|
+
|
|
51
|
+
``predict_kwargs`` is forwarded to every call (``conf``, ``iou``, ``device``,
|
|
52
|
+
``classes`` ...). ``verbose`` defaults to ``False`` so the capture loop is
|
|
53
|
+
not drowned in per-frame logging.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
model: Any,
|
|
59
|
+
*,
|
|
60
|
+
convert: Callable[[Any], sv.Detections] | None = None,
|
|
61
|
+
**predict_kwargs: Any,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.model = model
|
|
64
|
+
self.predict_kwargs = {"verbose": False, **predict_kwargs}
|
|
65
|
+
self._convert = convert or sv.Detections.from_ultralytics
|
|
66
|
+
|
|
67
|
+
def __call__(self, frame: np.ndarray) -> sv.Detections:
|
|
68
|
+
results = self.model(frame, **self.predict_kwargs)
|
|
69
|
+
return self._convert(results[0])
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return f"UltralyticsDetector({type(self.model).__name__})"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _CallableDetector:
|
|
76
|
+
"""Wraps a user callable so a wrong return type fails with a clear message."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, function: Callable[[np.ndarray], sv.Detections]) -> None:
|
|
79
|
+
self.function = function
|
|
80
|
+
|
|
81
|
+
def __call__(self, frame: np.ndarray) -> sv.Detections:
|
|
82
|
+
detections = self.function(frame)
|
|
83
|
+
if not isinstance(detections, sv.Detections):
|
|
84
|
+
raise TypeError(
|
|
85
|
+
f"model returned {type(detections).__name__}, expected supervision.Detections. "
|
|
86
|
+
"Convert the model output first, e.g. sv.Detections.from_ultralytics(result)."
|
|
87
|
+
)
|
|
88
|
+
return detections
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
name = getattr(self.function, "__name__", type(self.function).__name__)
|
|
92
|
+
return f"_CallableDetector({name})"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def load_ultralytics(weights: str | Path, **predict_kwargs: Any) -> UltralyticsDetector:
|
|
96
|
+
"""Load Ultralytics weights. Raises ``ImportError`` when the extra is missing."""
|
|
97
|
+
try:
|
|
98
|
+
from ultralytics import YOLO
|
|
99
|
+
except ImportError as exc:
|
|
100
|
+
raise ImportError(
|
|
101
|
+
f"Loading {str(weights)!r} needs the ultralytics package: "
|
|
102
|
+
'pip install "cvflair[yolo]". It is an optional extra because Ultralytics '
|
|
103
|
+
"is AGPL-3.0 licensed; cvflair itself stays MIT."
|
|
104
|
+
) from exc
|
|
105
|
+
return UltralyticsDetector(YOLO(str(weights)), **predict_kwargs)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _is_ultralytics_model(model: Any) -> bool:
|
|
109
|
+
return type(model).__module__.split(".")[0] == "ultralytics"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolve_detector(model: ModelLike | None) -> Detector | None:
|
|
113
|
+
"""
|
|
114
|
+
Turn ``model`` into a detector callable, or ``None`` when nothing was given.
|
|
115
|
+
|
|
116
|
+
Weights are loaded here, so calling this is what triggers the (slow) model
|
|
117
|
+
load -- ``Camera.stream()`` does it on the first iteration, not before.
|
|
118
|
+
"""
|
|
119
|
+
if model is None:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
if isinstance(model, (str, Path)):
|
|
123
|
+
suffix = Path(model).suffix.lower()
|
|
124
|
+
if suffix not in WEIGHT_SUFFIXES:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Unrecognised weights file {str(model)!r}. "
|
|
127
|
+
f"Expected one of: {', '.join(sorted(WEIGHT_SUFFIXES))}."
|
|
128
|
+
)
|
|
129
|
+
return load_ultralytics(model)
|
|
130
|
+
|
|
131
|
+
if _is_ultralytics_model(model):
|
|
132
|
+
return UltralyticsDetector(model)
|
|
133
|
+
|
|
134
|
+
if callable(model):
|
|
135
|
+
return _CallableDetector(model)
|
|
136
|
+
|
|
137
|
+
raise TypeError(
|
|
138
|
+
f"model must be a weights path, an Ultralytics model, or a callable returning "
|
|
139
|
+
f"supervision.Detections; got {type(model).__name__}."
|
|
140
|
+
)
|
cvflair/py.typed
ADDED
|
File without changes
|
cvflair/themes.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Visual themes -- a thin configuration layer over ``supervision`` annotators.
|
|
3
|
+
|
|
4
|
+
A :class:`Theme` holds nothing but presentation settings plus the annotator
|
|
5
|
+
instances built from them. Annotators are created once, when the theme is
|
|
6
|
+
constructed, and reused for every frame; rebuilding them inside the capture
|
|
7
|
+
loop is the most common avoidable cost in this kind of pipeline.
|
|
8
|
+
|
|
9
|
+
No drawing maths lives here. Every pixel is still produced by ``supervision``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable, Sequence
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import supervision as sv
|
|
20
|
+
|
|
21
|
+
__all__ = ["Theme", "get_theme", "available_themes", "BoxStyle"]
|
|
22
|
+
|
|
23
|
+
BoxStyle = Literal["box", "round", "corner"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _dim(color: sv.Color, factor: float) -> sv.Color:
|
|
27
|
+
"""Return a darker copy of ``color`` (used for the neon halo pass)."""
|
|
28
|
+
return sv.Color(
|
|
29
|
+
r=int(color.r * factor),
|
|
30
|
+
g=int(color.g * factor),
|
|
31
|
+
b=int(color.b * factor),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _dim_palette(palette: sv.ColorPalette, factor: float) -> sv.ColorPalette:
|
|
36
|
+
return sv.ColorPalette([_dim(color, factor) for color in palette.colors])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Theme:
|
|
41
|
+
"""
|
|
42
|
+
A named bundle of annotator settings.
|
|
43
|
+
|
|
44
|
+
The defaults describe a plain white box with a plain label; the shipped
|
|
45
|
+
presets (see :func:`get_theme`) are just different field values, which is
|
|
46
|
+
also how a user-defined theme is written::
|
|
47
|
+
|
|
48
|
+
Theme(name="my-theme", palette=sv.ColorPalette.from_hex(["#39FF14"]),
|
|
49
|
+
box_style="round", thickness=3)
|
|
50
|
+
|
|
51
|
+
Attributes are read at construction time only. Changing a field afterwards
|
|
52
|
+
does not rebuild the annotators -- construct a new theme instead.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name: str = "custom"
|
|
56
|
+
palette: sv.ColorPalette = field(default_factory=lambda: sv.ColorPalette.DEFAULT)
|
|
57
|
+
box_style: BoxStyle = "box"
|
|
58
|
+
thickness: int = 2
|
|
59
|
+
#: Corner rounding for ``box_style="round"``, in (0, 1].
|
|
60
|
+
roundness: float = 0.5
|
|
61
|
+
#: Corner arm length in pixels for ``box_style="corner"``.
|
|
62
|
+
corner_length: int = 20
|
|
63
|
+
#: Draw a dimmed, thicker box behind the main one to fake a glow.
|
|
64
|
+
glow: bool = False
|
|
65
|
+
glow_thickness: int = 5
|
|
66
|
+
glow_dim: float = 0.45
|
|
67
|
+
labels: bool = True
|
|
68
|
+
text_color: sv.Color = field(default_factory=lambda: sv.Color.BLACK)
|
|
69
|
+
text_scale: float = 0.5
|
|
70
|
+
text_thickness: int = 1
|
|
71
|
+
text_padding: int = 6
|
|
72
|
+
label_radius: int = 0
|
|
73
|
+
color_lookup: sv.ColorLookup = sv.ColorLookup.CLASS
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
if self.box_style not in ("box", "round", "corner"):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Unknown box_style {self.box_style!r}. Use 'box', 'round' or 'corner'."
|
|
79
|
+
)
|
|
80
|
+
if self.thickness < 1:
|
|
81
|
+
raise ValueError(f"thickness must be >= 1, got {self.thickness}.")
|
|
82
|
+
if not 0.0 < self.roundness <= 1.0:
|
|
83
|
+
raise ValueError(f"roundness must be in (0, 1], got {self.roundness}.")
|
|
84
|
+
|
|
85
|
+
self._glow_annotator = (
|
|
86
|
+
self._build_box_annotator(
|
|
87
|
+
palette=_dim_palette(self.palette, self.glow_dim),
|
|
88
|
+
thickness=self.thickness + self.glow_thickness,
|
|
89
|
+
)
|
|
90
|
+
if self.glow
|
|
91
|
+
else None
|
|
92
|
+
)
|
|
93
|
+
self._box_annotator = self._build_box_annotator(
|
|
94
|
+
palette=self.palette,
|
|
95
|
+
thickness=self.thickness,
|
|
96
|
+
)
|
|
97
|
+
self._label_annotator = (
|
|
98
|
+
sv.LabelAnnotator(
|
|
99
|
+
color=self.palette,
|
|
100
|
+
text_color=self.text_color,
|
|
101
|
+
text_scale=self.text_scale,
|
|
102
|
+
text_thickness=self.text_thickness,
|
|
103
|
+
text_padding=self.text_padding,
|
|
104
|
+
border_radius=self.label_radius,
|
|
105
|
+
color_lookup=self.color_lookup,
|
|
106
|
+
)
|
|
107
|
+
if self.labels
|
|
108
|
+
else None
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _build_box_annotator(self, palette: sv.ColorPalette, thickness: int):
|
|
112
|
+
if self.box_style == "round":
|
|
113
|
+
return sv.RoundBoxAnnotator(
|
|
114
|
+
color=palette,
|
|
115
|
+
thickness=thickness,
|
|
116
|
+
roundness=self.roundness,
|
|
117
|
+
color_lookup=self.color_lookup,
|
|
118
|
+
)
|
|
119
|
+
if self.box_style == "corner":
|
|
120
|
+
return sv.BoxCornerAnnotator(
|
|
121
|
+
color=palette,
|
|
122
|
+
thickness=thickness,
|
|
123
|
+
corner_length=self.corner_length,
|
|
124
|
+
color_lookup=self.color_lookup,
|
|
125
|
+
)
|
|
126
|
+
return sv.BoxAnnotator(
|
|
127
|
+
color=palette,
|
|
128
|
+
thickness=thickness,
|
|
129
|
+
color_lookup=self.color_lookup,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def annotate(
|
|
133
|
+
self,
|
|
134
|
+
scene: np.ndarray,
|
|
135
|
+
detections: sv.Detections,
|
|
136
|
+
labels: Sequence[str] | None = None,
|
|
137
|
+
) -> np.ndarray:
|
|
138
|
+
"""
|
|
139
|
+
Draw ``detections`` onto ``scene`` in place and return the same array.
|
|
140
|
+
|
|
141
|
+
``labels`` is passed straight through to ``supervision``: when it is
|
|
142
|
+
``None`` the label text falls back to the detections' ``class_name``
|
|
143
|
+
data field, then to the class id.
|
|
144
|
+
"""
|
|
145
|
+
if len(detections) == 0:
|
|
146
|
+
return scene
|
|
147
|
+
if self._glow_annotator is not None:
|
|
148
|
+
self._glow_annotator.annotate(scene=scene, detections=detections)
|
|
149
|
+
self._box_annotator.annotate(scene=scene, detections=detections)
|
|
150
|
+
if self._label_annotator is not None:
|
|
151
|
+
self._label_annotator.annotate(scene=scene, detections=detections, labels=labels)
|
|
152
|
+
return scene
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _minimal() -> Theme:
|
|
156
|
+
"""Thin single-colour box, plain label. Reads well in a screen recording."""
|
|
157
|
+
return Theme(
|
|
158
|
+
name="minimal",
|
|
159
|
+
palette=sv.ColorPalette([sv.Color.WHITE]),
|
|
160
|
+
box_style="box",
|
|
161
|
+
thickness=1,
|
|
162
|
+
text_color=sv.Color.BLACK,
|
|
163
|
+
text_scale=0.45,
|
|
164
|
+
text_padding=4,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _neon() -> Theme:
|
|
169
|
+
"""Saturated per-class colours, rounded box, dimmed halo behind it."""
|
|
170
|
+
return Theme(
|
|
171
|
+
name="neon",
|
|
172
|
+
palette=sv.ColorPalette.from_hex(
|
|
173
|
+
["#39FF14", "#FF00E5", "#00E5FF", "#FFE600", "#FF2D55", "#7B5CFF"]
|
|
174
|
+
),
|
|
175
|
+
box_style="round",
|
|
176
|
+
thickness=3,
|
|
177
|
+
roundness=0.6,
|
|
178
|
+
glow=True,
|
|
179
|
+
glow_thickness=6,
|
|
180
|
+
glow_dim=0.4,
|
|
181
|
+
text_color=sv.Color.BLACK,
|
|
182
|
+
text_scale=0.5,
|
|
183
|
+
text_padding=8,
|
|
184
|
+
label_radius=6,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _pastel() -> Theme:
|
|
189
|
+
"""Soft tones, generous rounding, dark text. Easy on a projector."""
|
|
190
|
+
return Theme(
|
|
191
|
+
name="pastel",
|
|
192
|
+
palette=sv.ColorPalette.from_hex(
|
|
193
|
+
["#FFADAD", "#A0C4FF", "#B9FBC0", "#FFD6A5", "#CDB4DB", "#9BF6FF"]
|
|
194
|
+
),
|
|
195
|
+
box_style="round",
|
|
196
|
+
thickness=2,
|
|
197
|
+
roundness=0.8,
|
|
198
|
+
text_color=sv.Color.from_hex("#2E2E2E"),
|
|
199
|
+
text_scale=0.45,
|
|
200
|
+
text_padding=8,
|
|
201
|
+
label_radius=10,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_THEMES: dict[str, Callable[[], Theme]] = {
|
|
206
|
+
"minimal": _minimal,
|
|
207
|
+
"neon": _neon,
|
|
208
|
+
"pastel": _pastel,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def available_themes() -> list[str]:
|
|
213
|
+
"""Names accepted by :func:`get_theme` and by ``Camera(theme=...)``."""
|
|
214
|
+
return sorted(_THEMES)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_theme(theme: str | Theme) -> Theme:
|
|
218
|
+
"""
|
|
219
|
+
Resolve a theme name to a fresh :class:`Theme`; pass instances through.
|
|
220
|
+
|
|
221
|
+
Each call builds a new instance, so two cameras never share annotators.
|
|
222
|
+
"""
|
|
223
|
+
if isinstance(theme, Theme):
|
|
224
|
+
return theme
|
|
225
|
+
if not isinstance(theme, str):
|
|
226
|
+
raise TypeError(f"theme must be a name or a Theme instance, got {type(theme).__name__}.")
|
|
227
|
+
try:
|
|
228
|
+
return _THEMES[theme.strip().lower()]()
|
|
229
|
+
except KeyError:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Unknown theme {theme!r}. Available: {', '.join(available_themes())}."
|
|
232
|
+
) from None
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cvflair
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Theme-based, model-agnostic visualisation layer for computer vision detections, built on supervision.
|
|
5
|
+
Author: kbycode
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kbycode/cvflair
|
|
8
|
+
Project-URL: Documentation, https://github.com/kbycode/cvflair#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/kbycode/cvflair
|
|
10
|
+
Project-URL: Issues, https://github.com/kbycode/cvflair/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/kbycode/cvflair/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: computer-vision,opencv,supervision,annotation,visualization,webcam,yolo,bounding-box,themes
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
25
|
+
Classifier: Topic :: Multimedia :: Video :: Display
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.24
|
|
30
|
+
Requires-Dist: opencv-python>=4.8
|
|
31
|
+
Requires-Dist: supervision<0.30,>=0.28
|
|
32
|
+
Provides-Extra: yolo
|
|
33
|
+
Requires-Dist: ultralytics>=8.2; extra == "yolo"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
37
|
+
Requires-Dist: pillow>=10.0; extra == "dev"
|
|
38
|
+
Provides-Extra: release
|
|
39
|
+
Requires-Dist: build>=1.2; extra == "release"
|
|
40
|
+
Requires-Dist: twine>=5.0; extra == "release"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# cvflair
|
|
44
|
+
|
|
45
|
+
Bilgisayarlı görü tespitlerini üç satırda, hazır temalarla ekrana çizen ince bir katman.
|
|
46
|
+
|
|
47
|
+
Çizim işini [supervision](https://github.com/roboflow/supervision) yapar; cvflair kamera
|
|
48
|
+
döngüsünü ve tema ayarlarını üstlenir. Model bağımsızdır: `supervision`'ın `Detections`
|
|
49
|
+
nesnesini üreten her kaynak (YOLO, MediaPipe, InsightFace veya özel bir model) tema
|
|
50
|
+
tarafından çizilebilir.
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
*Aynı tespitler, üç tema. Animasyon `tools/make_demo_gif.py` ile üretildi — sentetik
|
|
55
|
+
sahne, kamera gerekmiyor.*
|
|
56
|
+
|
|
57
|
+
> **Durum:** Faz 1 tamam (kamera döngüsü, üç tema, testler), Faz 2 başladı (model
|
|
58
|
+
> bağlama). Paket henüz PyPI'da yayınlanmadı.
|
|
59
|
+
|
|
60
|
+
## Kurulum
|
|
61
|
+
|
|
62
|
+
Python 3.10 veya üzeri gerekir (bu alt sınır `supervision`'dan geliyor).
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/kbycode/cvflair.git
|
|
66
|
+
cd cvflair
|
|
67
|
+
pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Yayınlandıktan sonra: `pip install cvflair`
|
|
71
|
+
|
|
72
|
+
YOLO ile kullanmak için Ultralytics extra'sı: `pip install -e ".[yolo]"`
|
|
73
|
+
(ayrıntı ve lisans notu için aşağıdaki [Lisans](#lisans) bölümü).
|
|
74
|
+
|
|
75
|
+
## Hızlı başlangıç
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from cvflair import Camera
|
|
79
|
+
|
|
80
|
+
cam = Camera(source=0, theme="neon")
|
|
81
|
+
for frame in cam.stream():
|
|
82
|
+
cam.show(frame)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Kamera açılır, kareler ayrı bir thread'de okunur, pencere `q` veya ESC ile kapanır —
|
|
86
|
+
`release()` çağırmaya, `while True` kurmaya gerek yok.
|
|
87
|
+
|
|
88
|
+
> Model verilmeyen bu akışta ekranda ham kare görünür: tema ancak ortada tespit
|
|
89
|
+
> varken çizim yapar. Temayı modelsiz, canlı görmek için:
|
|
90
|
+
> `python examples/demo_fake_detections.py` — kamera görüntüsü üzerine hareketli sahte
|
|
91
|
+
> kutular çizer ve temaları 3 saniyede bir değiştirir.
|
|
92
|
+
|
|
93
|
+
## Modelle kullanım
|
|
94
|
+
|
|
95
|
+
`stream()`'e bir model verildiğinde her adım `(kare, tespitler)` çifti döndürür ve
|
|
96
|
+
tema otomatik uygulanır:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from cvflair import Camera
|
|
100
|
+
|
|
101
|
+
cam = Camera(source=0, theme="neon")
|
|
102
|
+
for frame, detections in cam.stream(model="yolov8n.pt"):
|
|
103
|
+
cam.show(frame, detections)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`model` üç şeyden biri olabilir:
|
|
107
|
+
|
|
108
|
+
| Değer | Anlamı |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `"yolov8n.pt"` (ağırlık yolu) | Ultralytics ile yüklenir — `cvflair[yolo]` gerekir |
|
|
111
|
+
| Hazır bir Ultralytics modeli | `YOLO(...)` nesnesi doğrudan verilebilir, çıktısı dönüştürülür |
|
|
112
|
+
| Herhangi bir çağrılabilir | `sv.Detections` döndüren kendi fonksiyonun — MediaPipe, InsightFace, özel model |
|
|
113
|
+
|
|
114
|
+
Son seçenek kütüphaneyi model-agnostik yapan yer:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import supervision as sv
|
|
118
|
+
from cvflair import Camera
|
|
119
|
+
|
|
120
|
+
def detect(frame) -> sv.Detections:
|
|
121
|
+
... # kendi modelin
|
|
122
|
+
return sv.Detections(xyxy=..., class_id=..., confidence=...)
|
|
123
|
+
|
|
124
|
+
cam = Camera(source=0, theme="pastel")
|
|
125
|
+
for frame, detections in cam.stream(model=detect):
|
|
126
|
+
cam.show(frame, detections)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Çıkarım bu döngüde çalışır, okuma thread'inde değil: bir kare işlenirken okuyucu
|
|
130
|
+
kuyruktaki kareyi tazelemeye devam eder, dolayısıyla bir sonraki tur birikmiş
|
|
131
|
+
kareyle değil en güncel kareyle başlar.
|
|
132
|
+
|
|
133
|
+
Ultralytics'e ek ayar geçirmek için `UltralyticsDetector` doğrudan kullanılabilir:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from cvflair import Camera, UltralyticsDetector
|
|
137
|
+
from ultralytics import YOLO
|
|
138
|
+
|
|
139
|
+
detector = UltralyticsDetector(YOLO("yolov8n.pt"), conf=0.4, device="cpu", classes=[0])
|
|
140
|
+
cam = Camera(source=0, theme="neon")
|
|
141
|
+
for frame, detections in cam.stream(model=detector):
|
|
142
|
+
cam.show(frame, detections)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Etiket metni doğrudan da verilebilir: `cam.show(frame, detections, labels=[...])`.
|
|
146
|
+
Pencere yönetimi uygulamaya aitse `cam.annotate(frame, detections)` yalnızca çizim yapar.
|
|
147
|
+
|
|
148
|
+
## Temalar
|
|
149
|
+
|
|
150
|
+
| Tema | Görünüm | |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `minimal` | ince beyaz çerçeve, sade etiket — ekran kaydı ve profesyonel demo için |  |
|
|
153
|
+
| `neon` | sınıf başına canlı renk, yuvarlak köşe, koyu hâle ile parlama hissi |  |
|
|
154
|
+
| `pastel` | yumuşak tonlar, geniş yuvarlama, koyu etiket yazısı — atölye/projeksiyon |  |
|
|
155
|
+
|
|
156
|
+
Yol haritasındaki `cyberpunk` ve `hud` temaları Faz 2'de gelecek.
|
|
157
|
+
|
|
158
|
+
Özel bir tema, `Theme` doğrudan kurulup `Camera`'ya verilerek tanımlanır:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import supervision as sv
|
|
162
|
+
from cvflair import Camera, Theme
|
|
163
|
+
|
|
164
|
+
my_theme = Theme(
|
|
165
|
+
name="my-theme",
|
|
166
|
+
palette=sv.ColorPalette.from_hex(["#39FF14", "#FF00E5"]),
|
|
167
|
+
box_style="corner", # "box" | "round" | "corner"
|
|
168
|
+
thickness=2,
|
|
169
|
+
glow=True,
|
|
170
|
+
text_scale=0.6,
|
|
171
|
+
)
|
|
172
|
+
cam = Camera(source=0, theme=my_theme)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Temaları görmenin iki yolu:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python examples/demo_fake_detections.py # canlı kamera + hareketli sahte tespitler
|
|
179
|
+
python examples/theme_preview.py # kamerasız, her temayı bir PNG'ye çizer
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## API özeti
|
|
183
|
+
|
|
184
|
+
| Üye | Ne yapar |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `Camera(source, theme, width, height, fps, window_name, capture_factory)` | Kaynağı ve temayı bağlar; kamerayı henüz açmaz |
|
|
187
|
+
| `cam.start()` / `cam.close()` | Cihazı açar ve okuma thread'ini başlatır / her şeyi bırakır |
|
|
188
|
+
| `cam.stream(timeout, model=None)` | Kareleri üretir; model verilirse `(kare, tespitler)` çifti. İlk kullanımda `start()`, bitince `close()` eder |
|
|
189
|
+
| `cam.read(timeout)` | En güncel tek kareyi döndürür, kaynak bittiyse `None` |
|
|
190
|
+
| `cam.show(frame, detections, labels)` | Temayı uygular, pencerede gösterir; çıkış istendiğinde `False` döner |
|
|
191
|
+
| `cam.annotate(frame, detections, labels)` | Sadece çizer, pencere açmaz |
|
|
192
|
+
| `cam.theme` | Okunur/yazılır; `cam.theme = "minimal"` çalışır |
|
|
193
|
+
| `cam.frames_read` / `cam.frames_dropped` | Okunan ve tüketici yetişemediği için atılan kare sayısı |
|
|
194
|
+
| `get_theme(ad)` / `available_themes()` | Tema adını çözer / mevcut adları listeler |
|
|
195
|
+
| `UltralyticsDetector(model, **kwargs)` | Ultralytics çıktısını `sv.Detections`'a çevirir; `conf`, `iou`, `device` gibi ayarları taşır |
|
|
196
|
+
| `resolve_detector(model)` | Ağırlık yolu / model / çağrılabilir → detektör; `stream()` bunu kullanır |
|
|
197
|
+
|
|
198
|
+
`Camera` bağlam yöneticisi olarak da kullanılabilir: `with Camera() as cam: ...`
|
|
199
|
+
|
|
200
|
+
## Nasıl çalışıyor
|
|
201
|
+
|
|
202
|
+
- **Kareler ayrı thread'de okunur.** Okuyucu, tüketiciyi beklemez.
|
|
203
|
+
- **Kuyruk tek slotlu.** Yeni kare gelince bekleyen eski kare düşürülür
|
|
204
|
+
(`frames_dropped` ile sayılır). Böylece işleme yavaşladığında gecikme birikmez;
|
|
205
|
+
ekranda hep en güncel kare olur.
|
|
206
|
+
- **Annotator'lar bir kere kurulur.** `Theme` nesnesi oluşturulurken `supervision`
|
|
207
|
+
annotator'ları hazırlanır ve her karede yeniden kullanılır — döngü içinde annotator
|
|
208
|
+
kurmak bu tür işlerde en sık görülen gereksiz maliyettir.
|
|
209
|
+
- **Çizim matematiği yeniden yazılmadı.** Her piksel `supervision` tarafından çiziliyor;
|
|
210
|
+
cvflair sadece yapılandırma ve akış katmanı.
|
|
211
|
+
- **Model paketin dışında.** `stream(model=...)` verilen şeyi bir çağrılabilire çevirir;
|
|
212
|
+
ağırlıklar ilk yinelemede yüklenir. Hiçbir model kodu veya ağırlığı pakete gömülü değil.
|
|
213
|
+
|
|
214
|
+
## Geliştirme
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
python -m venv .venv
|
|
218
|
+
.venv\Scripts\activate
|
|
219
|
+
pip install -e ".[dev]"
|
|
220
|
+
pytest
|
|
221
|
+
ruff check .
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Testler kamera gerektirmez: `Camera`'ya `capture_factory` üzerinden sahte bir
|
|
225
|
+
`VideoCapture` verilir, temalar da sentetik kareler üzerinde doğrulanır.
|
|
226
|
+
|
|
227
|
+
Dokümantasyon görselleri de kamerasız üretilir:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
python tools/make_demo_gif.py # docs/demo.gif
|
|
231
|
+
python examples/theme_preview.py # examples/output/theme-*.png
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Yol haritası
|
|
235
|
+
|
|
236
|
+
| Faz | İçerik | Durum |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| Faz 1 | Kamera döngüsü, `minimal`/`neon`/`pastel` temaları, README, demo GIF, testler | tamam |
|
|
239
|
+
| Faz 2 | Model bağlama (`stream(model=...)`) | tamam |
|
|
240
|
+
| Faz 2 | PyPI paketi, Türkçe dokümantasyon sitesi, tema playground, `cyberpunk`/`hud` | sırada |
|
|
241
|
+
| Faz 3 | GitHub Actions (lint + test), issue şablonları, örnek galerisi | planlandı |
|
|
242
|
+
|
|
243
|
+
## Lisans
|
|
244
|
+
|
|
245
|
+
MIT — bkz. [LICENSE](LICENSE). Bağımlılıkların hepsi izin verici lisanslı
|
|
246
|
+
(`supervision` MIT, `opencv-python` Apache 2.0, `numpy` BSD).
|
|
247
|
+
|
|
248
|
+
YOLO ağırlıkları veya Ultralytics kodu bu pakete gömülü değildir; Ultralytics'in
|
|
249
|
+
kullanılması hâlinde AGPL-3.0 koşulları onu kullanan projenin sorumluluğundadır.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
cvflair/__init__.py,sha256=JFOK3FPvkLiY-yM9De81mRCwdj_YYZho1ZPVHfQTbrM,805
|
|
2
|
+
cvflair/camera.py,sha256=Tpkwd98ecRFoBcsCKiomH-Z7n75GEVDenL60XN5oTbk,8984
|
|
3
|
+
cvflair/models.py,sha256=gfoUBFEsozY5l0NbJJ5XYo-GWhwX84CpLBbAHzF0x8Y,4731
|
|
4
|
+
cvflair/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
cvflair/themes.py,sha256=r4EmV1DneFP5-c5ioccXdPoqM4YnSMf-Yw9y_DSb5c8,7650
|
|
6
|
+
cvflair-0.2.0.dist-info/licenses/LICENSE,sha256=0Tnxs_G4BvM4usmyYt-fIEogX96duNa_QmLJelQzb0w,1064
|
|
7
|
+
cvflair-0.2.0.dist-info/METADATA,sha256=LgVHZilD4jgaP-B3U_345z-pTTbx7N3ROFI1A8SniuY,10382
|
|
8
|
+
cvflair-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
9
|
+
cvflair-0.2.0.dist-info/top_level.txt,sha256=lPrkOHgpDfjoyb4I-sPatny-icHdTZjAtpb81IGQLXM,8
|
|
10
|
+
cvflair-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kbycode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cvflair
|