plexus-python 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.
- plexus/__init__.py +31 -0
- plexus/__main__.py +4 -0
- plexus/adapters/__init__.py +122 -0
- plexus/adapters/base.py +409 -0
- plexus/adapters/ble.py +257 -0
- plexus/adapters/can.py +439 -0
- plexus/adapters/can_detect.py +174 -0
- plexus/adapters/mavlink.py +642 -0
- plexus/adapters/mavlink_detect.py +192 -0
- plexus/adapters/modbus.py +622 -0
- plexus/adapters/mqtt.py +350 -0
- plexus/adapters/opcua.py +607 -0
- plexus/adapters/registry.py +206 -0
- plexus/adapters/serial_adapter.py +547 -0
- plexus/buffer.py +257 -0
- plexus/cameras/__init__.py +57 -0
- plexus/cameras/auto.py +239 -0
- plexus/cameras/base.py +189 -0
- plexus/cameras/picamera.py +171 -0
- plexus/cameras/usb.py +143 -0
- plexus/cli.py +783 -0
- plexus/client.py +465 -0
- plexus/config.py +169 -0
- plexus/connector.py +666 -0
- plexus/deps.py +246 -0
- plexus/detect.py +1238 -0
- plexus/importers/__init__.py +25 -0
- plexus/importers/rosbag.py +778 -0
- plexus/sensors/__init__.py +118 -0
- plexus/sensors/ads1115.py +164 -0
- plexus/sensors/adxl345.py +179 -0
- plexus/sensors/auto.py +290 -0
- plexus/sensors/base.py +412 -0
- plexus/sensors/bh1750.py +102 -0
- plexus/sensors/bme280.py +241 -0
- plexus/sensors/gps.py +317 -0
- plexus/sensors/ina219.py +149 -0
- plexus/sensors/magnetometer.py +239 -0
- plexus/sensors/mpu6050.py +162 -0
- plexus/sensors/sht3x.py +139 -0
- plexus/sensors/spi_scan.py +164 -0
- plexus/sensors/system.py +261 -0
- plexus/sensors/vl53l0x.py +109 -0
- plexus/streaming.py +743 -0
- plexus/tui.py +642 -0
- plexus_python-0.1.0.dist-info/METADATA +470 -0
- plexus_python-0.1.0.dist-info/RECORD +50 -0
- plexus_python-0.1.0.dist-info/WHEEL +4 -0
- plexus_python-0.1.0.dist-info/entry_points.txt +2 -0
- plexus_python-0.1.0.dist-info/licenses/LICENSE +190 -0
plexus/cameras/base.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base camera class and utilities for Plexus camera drivers.
|
|
3
|
+
|
|
4
|
+
All camera drivers inherit from BaseCamera and implement the capture() method.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CameraFrame:
|
|
18
|
+
"""A single camera frame with JPEG-encoded image data."""
|
|
19
|
+
data: bytes
|
|
20
|
+
width: int
|
|
21
|
+
height: int
|
|
22
|
+
timestamp: float = field(default_factory=time.time)
|
|
23
|
+
camera_id: str = ""
|
|
24
|
+
tags: Dict[str, str] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseCamera(ABC):
|
|
28
|
+
"""
|
|
29
|
+
Base class for all camera drivers.
|
|
30
|
+
|
|
31
|
+
Subclasses must implement:
|
|
32
|
+
- capture() -> Optional[CameraFrame]: Capture a single frame
|
|
33
|
+
- name: Human-readable camera name
|
|
34
|
+
|
|
35
|
+
Optional overrides:
|
|
36
|
+
- setup(): Initialize the camera (called once)
|
|
37
|
+
- cleanup(): Clean up resources (called on stop)
|
|
38
|
+
- is_available(): Check if camera is connected
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name: str = "Unknown Camera"
|
|
42
|
+
description: str = ""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
frame_rate: float = 10.0,
|
|
47
|
+
resolution: Tuple[int, int] = (640, 480),
|
|
48
|
+
quality: int = 80,
|
|
49
|
+
camera_id: str = "",
|
|
50
|
+
tags: Optional[Dict[str, str]] = None,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Initialize the camera driver.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
frame_rate: Target frames per second. Default 10 fps.
|
|
57
|
+
resolution: (width, height) tuple. Default (640, 480).
|
|
58
|
+
quality: JPEG quality 1-100. Default 80.
|
|
59
|
+
camera_id: Unique identifier for this camera.
|
|
60
|
+
tags: Tags to add to all frames from this camera.
|
|
61
|
+
"""
|
|
62
|
+
self.frame_rate = frame_rate
|
|
63
|
+
self.resolution = resolution
|
|
64
|
+
self.quality = max(1, min(100, quality))
|
|
65
|
+
self.camera_id = camera_id
|
|
66
|
+
self.tags = tags or {}
|
|
67
|
+
self._running = False
|
|
68
|
+
self._error: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def capture(self) -> Optional[CameraFrame]:
|
|
72
|
+
"""
|
|
73
|
+
Capture a single frame from the camera.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
CameraFrame with JPEG-encoded image data, or None if capture failed.
|
|
77
|
+
"""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def setup(self) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Initialize the camera hardware.
|
|
83
|
+
Called once before capturing starts.
|
|
84
|
+
Override in subclass if needed.
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def cleanup(self) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Clean up camera resources.
|
|
91
|
+
Called when camera is stopped.
|
|
92
|
+
Override in subclass if needed.
|
|
93
|
+
"""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def is_available(self) -> bool:
|
|
97
|
+
"""
|
|
98
|
+
Check if the camera is connected and responding.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if camera is available.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
frame = self.capture()
|
|
105
|
+
return frame is not None
|
|
106
|
+
except Exception:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def get_info(self) -> Dict[str, Any]:
|
|
110
|
+
"""Get camera information for display."""
|
|
111
|
+
return {
|
|
112
|
+
"camera_id": self.camera_id,
|
|
113
|
+
"name": self.name,
|
|
114
|
+
"description": self.description,
|
|
115
|
+
"frame_rate": self.frame_rate,
|
|
116
|
+
"resolution": list(self.resolution),
|
|
117
|
+
"quality": self.quality,
|
|
118
|
+
"available": self.is_available(),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class CameraHub:
|
|
123
|
+
"""
|
|
124
|
+
Manages multiple cameras.
|
|
125
|
+
|
|
126
|
+
Usage:
|
|
127
|
+
from plexus.cameras import CameraHub, USBCamera
|
|
128
|
+
|
|
129
|
+
hub = CameraHub()
|
|
130
|
+
hub.add(USBCamera(device_index=0))
|
|
131
|
+
hub.add(USBCamera(device_index=1))
|
|
132
|
+
|
|
133
|
+
# Capture from all cameras
|
|
134
|
+
frames = hub.capture_all()
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self):
|
|
138
|
+
self.cameras: List[BaseCamera] = []
|
|
139
|
+
|
|
140
|
+
def add(self, camera: BaseCamera) -> "CameraHub":
|
|
141
|
+
"""Add a camera to the hub."""
|
|
142
|
+
self.cameras.append(camera)
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def remove(self, camera: BaseCamera) -> "CameraHub":
|
|
146
|
+
"""Remove a camera from the hub."""
|
|
147
|
+
self.cameras.remove(camera)
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def setup(self) -> None:
|
|
151
|
+
"""Initialize all cameras."""
|
|
152
|
+
for camera in self.cameras:
|
|
153
|
+
try:
|
|
154
|
+
camera.setup()
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.warning(f"Failed to setup {camera.name}: {e}")
|
|
157
|
+
camera._error = str(e)
|
|
158
|
+
|
|
159
|
+
def cleanup(self) -> None:
|
|
160
|
+
"""Clean up all cameras."""
|
|
161
|
+
for camera in self.cameras:
|
|
162
|
+
try:
|
|
163
|
+
camera.cleanup()
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def capture_all(self) -> List[CameraFrame]:
|
|
168
|
+
"""Capture from all cameras once."""
|
|
169
|
+
frames = []
|
|
170
|
+
for camera in self.cameras:
|
|
171
|
+
try:
|
|
172
|
+
frame = camera.capture()
|
|
173
|
+
if frame:
|
|
174
|
+
frames.append(frame)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
camera._error = str(e)
|
|
177
|
+
return frames
|
|
178
|
+
|
|
179
|
+
def get_camera(self, camera_id: str) -> Optional[BaseCamera]:
|
|
180
|
+
"""Get a camera by ID."""
|
|
181
|
+
for camera in self.cameras:
|
|
182
|
+
if camera.camera_id == camera_id:
|
|
183
|
+
return camera
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def get_info(self) -> List[Dict[str, Any]]:
|
|
187
|
+
"""Get info about all cameras."""
|
|
188
|
+
return [c.get_info() for c in self.cameras]
|
|
189
|
+
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Raspberry Pi Camera Module driver using picamera2.
|
|
3
|
+
|
|
4
|
+
Supports Pi Camera Module v1, v2, v3, and HQ Camera.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from typing import Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from plexus.cameras.base import BaseCamera, CameraFrame
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# picamera2 is optional - only imported when PiCamera is used
|
|
16
|
+
try:
|
|
17
|
+
from picamera2 import Picamera2
|
|
18
|
+
import io
|
|
19
|
+
PICAMERA_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
PICAMERA_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PiCamera(BaseCamera):
|
|
25
|
+
"""
|
|
26
|
+
Raspberry Pi Camera Module driver using picamera2.
|
|
27
|
+
|
|
28
|
+
Works with Pi Camera Module v1, v2, v3, and HQ Camera on
|
|
29
|
+
Raspberry Pi devices running Raspberry Pi OS.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
from plexus.cameras import PiCamera
|
|
33
|
+
|
|
34
|
+
camera = PiCamera(camera_num=0)
|
|
35
|
+
camera.setup()
|
|
36
|
+
frame = camera.capture()
|
|
37
|
+
camera.cleanup()
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name = "Pi Camera"
|
|
41
|
+
description = "Raspberry Pi Camera Module via picamera2"
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
camera_num: int = 0,
|
|
46
|
+
frame_rate: float = 30.0,
|
|
47
|
+
resolution: Tuple[int, int] = (1280, 720),
|
|
48
|
+
quality: int = 85,
|
|
49
|
+
camera_id: Optional[str] = None,
|
|
50
|
+
**kwargs,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Initialize Pi Camera driver.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
camera_num: Camera number (0 = first camera, 1 = second, etc.)
|
|
57
|
+
frame_rate: Target frames per second. Default 30 fps.
|
|
58
|
+
resolution: (width, height) tuple. Default (1280, 720).
|
|
59
|
+
quality: JPEG quality 1-100. Default 85.
|
|
60
|
+
camera_id: Unique identifier. Defaults to "picam:{camera_num}".
|
|
61
|
+
"""
|
|
62
|
+
if not PICAMERA_AVAILABLE:
|
|
63
|
+
raise ImportError(
|
|
64
|
+
"picamera2 is required for Pi Camera support. "
|
|
65
|
+
"Install with: pip install plexus-python[picamera]"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
super().__init__(
|
|
69
|
+
frame_rate=frame_rate,
|
|
70
|
+
resolution=resolution,
|
|
71
|
+
quality=quality,
|
|
72
|
+
camera_id=camera_id or f"picam:{camera_num}",
|
|
73
|
+
**kwargs,
|
|
74
|
+
)
|
|
75
|
+
self.camera_num = camera_num
|
|
76
|
+
self._picam: Optional[Picamera2] = None
|
|
77
|
+
|
|
78
|
+
def setup(self) -> None:
|
|
79
|
+
"""Initialize the camera."""
|
|
80
|
+
if self._picam is not None:
|
|
81
|
+
self.cleanup()
|
|
82
|
+
self._picam = Picamera2(camera_num=self.camera_num)
|
|
83
|
+
|
|
84
|
+
# Configure for still capture with specified resolution
|
|
85
|
+
config = self._picam.create_still_configuration(
|
|
86
|
+
main={"size": self.resolution, "format": "RGB888"},
|
|
87
|
+
)
|
|
88
|
+
self._picam.configure(config)
|
|
89
|
+
self._picam.start()
|
|
90
|
+
|
|
91
|
+
def cleanup(self) -> None:
|
|
92
|
+
"""Stop and close the camera."""
|
|
93
|
+
if self._picam is not None:
|
|
94
|
+
self._picam.stop()
|
|
95
|
+
self._picam.close()
|
|
96
|
+
self._picam = None
|
|
97
|
+
|
|
98
|
+
def capture(self) -> Optional[CameraFrame]:
|
|
99
|
+
"""
|
|
100
|
+
Capture a single frame.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
CameraFrame with JPEG-encoded image, or None if capture failed.
|
|
104
|
+
"""
|
|
105
|
+
if self._picam is None:
|
|
106
|
+
self.setup()
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Capture to numpy array
|
|
110
|
+
frame = self._picam.capture_array()
|
|
111
|
+
|
|
112
|
+
if frame is None:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
# Encode to JPEG using OpenCV if available, otherwise use PIL
|
|
116
|
+
try:
|
|
117
|
+
import cv2
|
|
118
|
+
encode_params = [cv2.IMWRITE_JPEG_QUALITY, self.quality]
|
|
119
|
+
# Convert RGB to BGR for OpenCV
|
|
120
|
+
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
|
121
|
+
success, jpeg_data = cv2.imencode('.jpg', frame_bgr, encode_params)
|
|
122
|
+
if not success:
|
|
123
|
+
return None
|
|
124
|
+
data = jpeg_data.tobytes()
|
|
125
|
+
except ImportError:
|
|
126
|
+
# Fallback to PIL
|
|
127
|
+
from PIL import Image
|
|
128
|
+
img = Image.fromarray(frame)
|
|
129
|
+
buffer = io.BytesIO()
|
|
130
|
+
img.save(buffer, format='JPEG', quality=self.quality)
|
|
131
|
+
data = buffer.getvalue()
|
|
132
|
+
|
|
133
|
+
return CameraFrame(
|
|
134
|
+
data=data,
|
|
135
|
+
width=frame.shape[1],
|
|
136
|
+
height=frame.shape[0],
|
|
137
|
+
timestamp=time.time(),
|
|
138
|
+
camera_id=self.camera_id,
|
|
139
|
+
tags=self.tags.copy(),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.debug(f"Pi camera capture failed: {e}")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def is_available(self) -> bool:
|
|
147
|
+
"""Check if camera is available."""
|
|
148
|
+
if not PICAMERA_AVAILABLE:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
camera_info = Picamera2.global_camera_info()
|
|
153
|
+
return len(camera_info) > self.camera_num
|
|
154
|
+
except Exception:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
def get_info(self) -> dict:
|
|
158
|
+
"""Get camera information."""
|
|
159
|
+
info = super().get_info()
|
|
160
|
+
info["camera_num"] = self.camera_num
|
|
161
|
+
|
|
162
|
+
# Get model info if available
|
|
163
|
+
if PICAMERA_AVAILABLE:
|
|
164
|
+
try:
|
|
165
|
+
camera_info = Picamera2.global_camera_info()
|
|
166
|
+
if len(camera_info) > self.camera_num:
|
|
167
|
+
info["model"] = camera_info[self.camera_num].get("Model", "Unknown")
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
return info
|
plexus/cameras/usb.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
USB webcam driver using OpenCV.
|
|
3
|
+
|
|
4
|
+
Supports any camera compatible with cv2.VideoCapture (USB webcams, built-in cameras).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from plexus.cameras.base import BaseCamera, CameraFrame
|
|
11
|
+
|
|
12
|
+
# OpenCV is optional - only imported when USBCamera is used
|
|
13
|
+
try:
|
|
14
|
+
import cv2
|
|
15
|
+
OPENCV_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
OPENCV_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class USBCamera(BaseCamera):
|
|
21
|
+
"""
|
|
22
|
+
USB webcam driver using OpenCV VideoCapture.
|
|
23
|
+
|
|
24
|
+
Works with USB webcams, built-in laptop cameras, and other
|
|
25
|
+
video capture devices supported by OpenCV.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
from plexus.cameras import USBCamera
|
|
29
|
+
|
|
30
|
+
camera = USBCamera(device_index=0)
|
|
31
|
+
camera.setup()
|
|
32
|
+
frame = camera.capture()
|
|
33
|
+
camera.cleanup()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
name = "USB Camera"
|
|
37
|
+
description = "USB webcam via OpenCV VideoCapture"
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
device_index: int = 0,
|
|
42
|
+
frame_rate: float = 15.0,
|
|
43
|
+
resolution: Tuple[int, int] = (640, 480),
|
|
44
|
+
quality: int = 80,
|
|
45
|
+
camera_id: Optional[str] = None,
|
|
46
|
+
**kwargs,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Initialize USB camera driver.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
device_index: Camera device index (0 = first camera, 1 = second, etc.)
|
|
53
|
+
frame_rate: Target frames per second. Default 15 fps.
|
|
54
|
+
resolution: (width, height) tuple. Default (640, 480).
|
|
55
|
+
quality: JPEG quality 1-100. Default 80.
|
|
56
|
+
camera_id: Unique identifier. Defaults to "usb:{device_index}".
|
|
57
|
+
"""
|
|
58
|
+
if not OPENCV_AVAILABLE:
|
|
59
|
+
raise ImportError(
|
|
60
|
+
"OpenCV is required for USB camera support. "
|
|
61
|
+
"Install with: pip install plexus-python[camera]"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
super().__init__(
|
|
65
|
+
frame_rate=frame_rate,
|
|
66
|
+
resolution=resolution,
|
|
67
|
+
quality=quality,
|
|
68
|
+
camera_id=camera_id or f"usb:{device_index}",
|
|
69
|
+
**kwargs,
|
|
70
|
+
)
|
|
71
|
+
self.device_index = device_index
|
|
72
|
+
self._cap: Optional[cv2.VideoCapture] = None
|
|
73
|
+
|
|
74
|
+
def setup(self) -> None:
|
|
75
|
+
"""Initialize the camera."""
|
|
76
|
+
self._cap = cv2.VideoCapture(self.device_index)
|
|
77
|
+
|
|
78
|
+
if not self._cap.isOpened():
|
|
79
|
+
raise RuntimeError(f"Failed to open camera at index {self.device_index}")
|
|
80
|
+
|
|
81
|
+
# Set resolution
|
|
82
|
+
self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.resolution[0])
|
|
83
|
+
self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.resolution[1])
|
|
84
|
+
|
|
85
|
+
# Set frame rate if supported
|
|
86
|
+
self._cap.set(cv2.CAP_PROP_FPS, self.frame_rate)
|
|
87
|
+
|
|
88
|
+
# Read actual values (camera may not support requested settings)
|
|
89
|
+
actual_width = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
90
|
+
actual_height = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
91
|
+
self.resolution = (actual_width, actual_height)
|
|
92
|
+
|
|
93
|
+
def cleanup(self) -> None:
|
|
94
|
+
"""Release the camera."""
|
|
95
|
+
if self._cap is not None:
|
|
96
|
+
self._cap.release()
|
|
97
|
+
self._cap = None
|
|
98
|
+
|
|
99
|
+
def capture(self) -> Optional[CameraFrame]:
|
|
100
|
+
"""
|
|
101
|
+
Capture a single frame.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
CameraFrame with JPEG-encoded image, or None if capture failed.
|
|
105
|
+
"""
|
|
106
|
+
if self._cap is None:
|
|
107
|
+
self.setup()
|
|
108
|
+
|
|
109
|
+
ret, frame = self._cap.read()
|
|
110
|
+
if not ret or frame is None:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Encode to JPEG
|
|
114
|
+
encode_params = [cv2.IMWRITE_JPEG_QUALITY, self.quality]
|
|
115
|
+
success, jpeg_data = cv2.imencode('.jpg', frame, encode_params)
|
|
116
|
+
|
|
117
|
+
if not success:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
return CameraFrame(
|
|
121
|
+
data=jpeg_data.tobytes(),
|
|
122
|
+
width=frame.shape[1],
|
|
123
|
+
height=frame.shape[0],
|
|
124
|
+
timestamp=time.time(),
|
|
125
|
+
camera_id=self.camera_id,
|
|
126
|
+
tags=self.tags.copy(),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def is_available(self) -> bool:
|
|
130
|
+
"""Check if camera is available without fully initializing."""
|
|
131
|
+
if not OPENCV_AVAILABLE:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
cap = cv2.VideoCapture(self.device_index)
|
|
135
|
+
available = cap.isOpened()
|
|
136
|
+
cap.release()
|
|
137
|
+
return available
|
|
138
|
+
|
|
139
|
+
def get_info(self) -> dict:
|
|
140
|
+
"""Get camera information."""
|
|
141
|
+
info = super().get_info()
|
|
142
|
+
info["device_index"] = self.device_index
|
|
143
|
+
return info
|