-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathbase.py
More file actions
181 lines (139 loc) · 5.42 KB
/
Copy pathbase.py
File metadata and controls
181 lines (139 loc) · 5.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# dlclivegui/cameras/base.py
from __future__ import annotations
import logging
from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING, Any, ClassVar
import numpy as np
from ..config import CameraSettings
if TYPE_CHECKING:
from .factory import DetectedCamera
_BACKEND_REGISTRY: dict[str, type[CameraBackend]] = {}
logger = logging.getLogger(__name__)
def register_backend(name: str):
"""
Decorator to register a camera backend class.
Usage:
@register_backend("opencv")
class OpenCVCameraBackend(CameraBackend):
...
"""
def decorator(cls: type[CameraBackend]):
if not issubclass(cls, CameraBackend):
raise TypeError(f"Backend '{name}' must subclass CameraBackend")
_BACKEND_REGISTRY[name.lower()] = cls
logger.debug(f"Registered camera backend '{name}' -> {cls}")
return cls
return decorator
def register_backend_direct(name: str, cls: type[CameraBackend]):
"""Allow tests or dynamic plugins to register backends programmatically."""
if not issubclass(cls, CameraBackend):
raise TypeError(f"Backend '{name}' must subclass CameraBackend")
_BACKEND_REGISTRY[name.lower()] = cls
def unregister_backend(name: str):
"""Remove a backend from the registry. Useful for tests."""
_BACKEND_REGISTRY.pop(name.lower(), None)
def reset_backends():
"""Clear registry (useful for isolated unit tests)."""
_BACKEND_REGISTRY.clear()
class SupportLevel(str, Enum):
"""Allows definition of backend capabilities for UI"""
UNSUPPORTED = "unsupported"
BEST_EFFORT = "best_effort"
SUPPORTED = "supported"
DEFAULT_CAPABILITIES: dict[str, SupportLevel] = {
"set_resolution": SupportLevel.UNSUPPORTED,
"set_fps": SupportLevel.UNSUPPORTED,
"set_exposure": SupportLevel.UNSUPPORTED,
"set_gain": SupportLevel.UNSUPPORTED,
"preserve_mono": SupportLevel.UNSUPPORTED,
"device_discovery": SupportLevel.UNSUPPORTED,
"stable_identity": SupportLevel.UNSUPPORTED,
"hardware_trigger": SupportLevel.UNSUPPORTED,
}
class CameraBackend(ABC):
"""Abstract base class for camera backends."""
OPTIONS_KEY: ClassVar[str] = "" # override in subclasses if they want to support options
def __init__(self, settings: CameraSettings):
# Normalize to dataclass so all backends stay unchanged
self.settings: CameraSettings = settings
@classmethod
def name(cls) -> str:
"""Return the backend identifier."""
return cls.__name__.lower()
@classmethod
def is_available(cls) -> bool:
"""Return whether the backend can be used on this system."""
return True
@classmethod
def static_capabilities(cls) -> dict[str, SupportLevel]:
"""Return a dict describing supported features for UI purposes."""
return DEFAULT_CAPABILITIES
@property
def actual_pixel_format(self) -> str | None:
return None
@property
def recommended_preserve_mono(self) -> bool | None:
return None
@classmethod
def options_key(cls) -> str:
"""Return the key used to store this backend's options in CameraSettings."""
return cls.OPTIONS_KEY
@classmethod
def parse_options(cls, settings: CameraSettings) -> Any:
"""Return a typed options object for this backend (or None)."""
return None
@classmethod
def options_schema(cls) -> dict[str, Any] | None:
"""Optional: for UI/docs."""
return None
@classmethod
def sanitize_for_probe(cls, settings: CameraSettings) -> CameraSettings:
"""
Default: keep only the backend namespace and minimal safe toggles.
Backends may override.
"""
# shallow copy is fine if you deep-copy in factory already
dc = settings.model_copy(deep=True)
ns = (dc.properties or {}).get(cls.options_key(), {})
dc.properties = {cls.options_key(): dict(ns)}
return dc
@classmethod
def discover_devices(
cls,
*,
max_devices: int = 10,
should_cancel: callable[[], bool] | None = None,
progress_cb: callable[[str], None] | None = None,
) -> list[DetectedCamera] | None:
"""
Optional: return a rich list of devices without brute-force probing.
Return None to signal 'not implemented' (factory falls back to probing).
"""
return None
@classmethod
def rebind_settings(cls, settings: CameraSettings) -> CameraSettings:
"""
Optional: update settings in-place (or return a modified copy) by using stable identity,
e.g. device_id/VID/PID stored in settings.properties. Default: no-op.
"""
return settings
def stop(self) -> None: # noqa B027
"""Optional: Request a graceful stop. No-op by default."""
# Subclasses may override when they need to interrupt blocking reads.
pass
def device_name(self) -> str:
"""Return a human readable name for the device currently in use."""
return self.settings.name
@abstractmethod
def open(self) -> None:
"""Open the capture device."""
raise NotImplementedError
@abstractmethod
def read(self) -> tuple[np.ndarray, float]:
"""Read a frame and return the image with a timestamp."""
raise NotImplementedError
@abstractmethod
def close(self) -> None:
"""Release the capture device."""
raise NotImplementedError