framesource 0.3.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.
- frame_processors/__init__.py +37 -0
- frame_source/__init__.py +50 -0
- framesource/__init__.py +93 -0
- framesource/_msmf_config.py +26 -0
- framesource/_version.py +24 -0
- framesource/discovery.py +109 -0
- framesource/errors.py +82 -0
- framesource/factory.py +290 -0
- framesource/processors/__init__.py +34 -0
- framesource/processors/equirectangular360_processor.py +577 -0
- framesource/processors/fisheye2equirectangular_processor.py +328 -0
- framesource/processors/frame_processor.py +30 -0
- framesource/processors/hyperspectral_processor.py +23 -0
- framesource/processors/realsense_depth_processor.py +90 -0
- framesource/py.typed +0 -0
- framesource/sources/__init__.py +40 -0
- framesource/sources/audiospectrogram_capture.py +1068 -0
- framesource/sources/basler_capture.py +477 -0
- framesource/sources/folder_capture.py +920 -0
- framesource/sources/genicam_capture.py +681 -0
- framesource/sources/huateng_capture.py +245 -0
- framesource/sources/ipcamera_capture.py +254 -0
- framesource/sources/mvsdk.py +2454 -0
- framesource/sources/realsense_capture.py +565 -0
- framesource/sources/screen_capture.py +800 -0
- framesource/sources/video_capture_base.py +560 -0
- framesource/sources/video_file_capture.py +259 -0
- framesource/sources/webcam_capture.py +511 -0
- framesource/sources/ximea_capture.py +299 -0
- framesource/threading_utils.py +790 -0
- framesource-0.3.0.dist-info/METADATA +787 -0
- framesource-0.3.0.dist-info/RECORD +37 -0
- framesource-0.3.0.dist-info/WHEEL +5 -0
- framesource-0.3.0.dist-info/licenses/LICENSE +21 -0
- framesource-0.3.0.dist-info/scm_file_list.json +276 -0
- framesource-0.3.0.dist-info/scm_version.json +8 -0
- framesource-0.3.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ..errors import MissingDependencyError
|
|
9
|
+
from .video_capture_base import VideoCaptureBase
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GenicamCapture(VideoCaptureBase):
|
|
15
|
+
has_discovery = True
|
|
16
|
+
supports_exposure = True
|
|
17
|
+
supports_gain = True
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def get_config_schema(cls) -> dict[str, Any]:
|
|
21
|
+
"""Get configuration schema for GenICam capture"""
|
|
22
|
+
warnings.warn(
|
|
23
|
+
"get_config_schema() is deprecated and will be removed in a future release; "
|
|
24
|
+
"UI form schemas belong in the consuming application.",
|
|
25
|
+
DeprecationWarning,
|
|
26
|
+
stacklevel=2,
|
|
27
|
+
)
|
|
28
|
+
return {
|
|
29
|
+
"title": "GenICam Camera Configuration",
|
|
30
|
+
"description": "Configure GenICam compliant camera settings",
|
|
31
|
+
"fields": [
|
|
32
|
+
{
|
|
33
|
+
"name": "source",
|
|
34
|
+
"label": "Camera Source",
|
|
35
|
+
"type": "text",
|
|
36
|
+
"placeholder": "Serial number or device index (0, 1, 2...)",
|
|
37
|
+
"description": "Camera serial number or device index",
|
|
38
|
+
"required": False,
|
|
39
|
+
"default": "0",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "cti_files",
|
|
43
|
+
"label": "CTI Files",
|
|
44
|
+
"type": "text",
|
|
45
|
+
"placeholder": "/path/to/producer.cti",
|
|
46
|
+
"description": "GenTL producer files (comma-separated)",
|
|
47
|
+
"required": False,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "exposure",
|
|
51
|
+
"label": "Exposure Time (µs)",
|
|
52
|
+
"type": "number",
|
|
53
|
+
"min": 1,
|
|
54
|
+
"max": 1000000,
|
|
55
|
+
"placeholder": "10000",
|
|
56
|
+
"description": "Exposure time in microseconds",
|
|
57
|
+
"required": False,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "gain",
|
|
61
|
+
"label": "Gain (dB)",
|
|
62
|
+
"type": "number",
|
|
63
|
+
"min": 0,
|
|
64
|
+
"max": 40,
|
|
65
|
+
"step": 0.1,
|
|
66
|
+
"placeholder": "0.0",
|
|
67
|
+
"description": "Camera gain in decibels",
|
|
68
|
+
"required": False,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "width",
|
|
72
|
+
"label": "Width",
|
|
73
|
+
"type": "number",
|
|
74
|
+
"min": 1,
|
|
75
|
+
"max": 10000,
|
|
76
|
+
"placeholder": "1920",
|
|
77
|
+
"description": "Frame width in pixels",
|
|
78
|
+
"required": False,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "height",
|
|
82
|
+
"label": "Height",
|
|
83
|
+
"type": "number",
|
|
84
|
+
"min": 1,
|
|
85
|
+
"max": 10000,
|
|
86
|
+
"placeholder": "1080",
|
|
87
|
+
"description": "Frame height in pixels",
|
|
88
|
+
"required": False,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "fps",
|
|
92
|
+
"label": "Frame Rate (FPS)",
|
|
93
|
+
"type": "number",
|
|
94
|
+
"min": 1,
|
|
95
|
+
"max": 240,
|
|
96
|
+
"placeholder": "30",
|
|
97
|
+
"description": "Frames per second",
|
|
98
|
+
"required": False,
|
|
99
|
+
"default": 30,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "x",
|
|
103
|
+
"label": "X Offset",
|
|
104
|
+
"type": "number",
|
|
105
|
+
"min": 0,
|
|
106
|
+
"placeholder": "0",
|
|
107
|
+
"description": "Horizontal offset in pixels",
|
|
108
|
+
"required": False,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "y",
|
|
112
|
+
"label": "Y Offset",
|
|
113
|
+
"type": "number",
|
|
114
|
+
"min": 0,
|
|
115
|
+
"placeholder": "0",
|
|
116
|
+
"description": "Vertical offset in pixels",
|
|
117
|
+
"required": False,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def buffer_to_numpy(buffer):
|
|
124
|
+
import cv2
|
|
125
|
+
from harvesters.util.pfnc import (
|
|
126
|
+
bayer_location_formats,
|
|
127
|
+
bgr_formats,
|
|
128
|
+
bgra_formats,
|
|
129
|
+
lmn_411_location_formats,
|
|
130
|
+
lmn_422_location_formats,
|
|
131
|
+
lmn_422_packed_location_formats,
|
|
132
|
+
mono_location_formats,
|
|
133
|
+
rgb_formats,
|
|
134
|
+
rgba_formats,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
payload = buffer.payload
|
|
138
|
+
component = payload.components[0]
|
|
139
|
+
width = component.width
|
|
140
|
+
height = component.height
|
|
141
|
+
data_format = component.data_format
|
|
142
|
+
|
|
143
|
+
if data_format in mono_location_formats:
|
|
144
|
+
content = component.data.reshape(height, width)
|
|
145
|
+
else:
|
|
146
|
+
if (
|
|
147
|
+
data_format in rgb_formats
|
|
148
|
+
or data_format in rgba_formats
|
|
149
|
+
or data_format in bgr_formats
|
|
150
|
+
or data_format in bgra_formats
|
|
151
|
+
or data_format in bayer_location_formats
|
|
152
|
+
):
|
|
153
|
+
content = component.data.reshape(
|
|
154
|
+
height, width, int(component.num_components_per_pixel)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if data_format in bayer_location_formats:
|
|
158
|
+
content = cv2.cvtColor(content, cv2.COLOR_BayerGR2RGB)
|
|
159
|
+
|
|
160
|
+
if data_format in rgb_formats:
|
|
161
|
+
content = content[:, :, ::-1]
|
|
162
|
+
elif (
|
|
163
|
+
data_format in lmn_422_location_formats
|
|
164
|
+
or data_format in lmn_422_packed_location_formats
|
|
165
|
+
or data_format in lmn_411_location_formats
|
|
166
|
+
):
|
|
167
|
+
ycbcr422_data = component.data.reshape((-1, 4))
|
|
168
|
+
|
|
169
|
+
Y0 = ycbcr422_data[:, 0].astype(np.float32)
|
|
170
|
+
Cb = ycbcr422_data[:, 1].astype(np.float32)
|
|
171
|
+
Y1 = ycbcr422_data[:, 2].astype(np.float32)
|
|
172
|
+
Cr = ycbcr422_data[:, 3].astype(np.float32)
|
|
173
|
+
|
|
174
|
+
# Expand to per-pixel arrays
|
|
175
|
+
Y = np.empty((ycbcr422_data.shape[0] * 2,), dtype=np.float32)
|
|
176
|
+
Cb_full = np.empty_like(Y)
|
|
177
|
+
Cr_full = np.empty_like(Y)
|
|
178
|
+
|
|
179
|
+
Y[0::2] = Y0
|
|
180
|
+
Y[1::2] = Y1
|
|
181
|
+
Cb_full[0::2] = Cb
|
|
182
|
+
Cb_full[1::2] = Cb
|
|
183
|
+
Cr_full[0::2] = Cr
|
|
184
|
+
Cr_full[1::2] = Cr
|
|
185
|
+
# Limited range conversion (BT.601)
|
|
186
|
+
# Scale Y component
|
|
187
|
+
Y = Y.astype(np.float64) - 16
|
|
188
|
+
Y = np.clip(Y, 0, 255)
|
|
189
|
+
|
|
190
|
+
# Constants for limited-range YCbCr
|
|
191
|
+
R = 1.164 * Y + 1.596 * (Cr_full - 128)
|
|
192
|
+
G = 1.164 * Y - 0.392 * (Cb_full - 128) - 0.813 * (Cr_full - 128)
|
|
193
|
+
B = 1.164 * Y + 2.017 * (Cb_full - 128)
|
|
194
|
+
|
|
195
|
+
# Clip values to valid range [0, 255] and convert to uint8
|
|
196
|
+
R = np.clip(R, 0, 255).astype(np.uint8)
|
|
197
|
+
G = np.clip(G, 0, 255).astype(np.uint8)
|
|
198
|
+
B = np.clip(B, 0, 255).astype(np.uint8)
|
|
199
|
+
# Stack into final image
|
|
200
|
+
rgb_image = np.stack((B, G, R), axis=-1).reshape((height, width, 3))
|
|
201
|
+
return rgb_image
|
|
202
|
+
else:
|
|
203
|
+
raise NotImplementedError(f"Unsupported pixel data format `{data_format}`")
|
|
204
|
+
return content
|
|
205
|
+
|
|
206
|
+
def _read_implementation(self) -> tuple[bool, Optional[np.ndarray]]:
|
|
207
|
+
"""
|
|
208
|
+
Read a single frame from the Genicam compliant camera.
|
|
209
|
+
Returns:
|
|
210
|
+
Tuple[bool, Optional[np.ndarray]]: (success, frame)
|
|
211
|
+
"""
|
|
212
|
+
from genicam.gentl import TimeoutException
|
|
213
|
+
|
|
214
|
+
if not self.is_connected or self.camera is None:
|
|
215
|
+
return False, None
|
|
216
|
+
try:
|
|
217
|
+
n = self.camera.remote_device.node_map
|
|
218
|
+
n.TriggerSoftware.execute()
|
|
219
|
+
|
|
220
|
+
self.camera = self.camera
|
|
221
|
+
buffer = self.camera.fetch(timeout=3)
|
|
222
|
+
nump = self.buffer_to_numpy(buffer)
|
|
223
|
+
|
|
224
|
+
buffer.queue()
|
|
225
|
+
|
|
226
|
+
return True, nump
|
|
227
|
+
except TimeoutException:
|
|
228
|
+
# We have an ImageAcquirer object but nothing has
|
|
229
|
+
# been fetched, wait for the next round:
|
|
230
|
+
return False, None
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Error reading from Genicam camera: {e}")
|
|
233
|
+
return False, None
|
|
234
|
+
|
|
235
|
+
"""Genicam camera capture using genicam."""
|
|
236
|
+
|
|
237
|
+
def __init__(
|
|
238
|
+
self,
|
|
239
|
+
source: Any = None,
|
|
240
|
+
*,
|
|
241
|
+
is_mono: Optional[bool] = None,
|
|
242
|
+
cti_files: Optional[list[str]] = None,
|
|
243
|
+
exposure: Optional[float] = None,
|
|
244
|
+
gain: Optional[float] = None,
|
|
245
|
+
width: Optional[int] = None,
|
|
246
|
+
height: Optional[int] = None,
|
|
247
|
+
x: Optional[int] = None,
|
|
248
|
+
y: Optional[int] = None,
|
|
249
|
+
fps: Optional[float] = None,
|
|
250
|
+
acquisition_framerate: Optional[float] = None,
|
|
251
|
+
**kwargs,
|
|
252
|
+
):
|
|
253
|
+
"""Initialize the GenICam capture.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
source: Camera serial number (str) or device index (int).
|
|
257
|
+
is_mono: Stored for parity with other vendor sources (default:
|
|
258
|
+
False). GenICam pixel format is currently negotiated
|
|
259
|
+
automatically in :meth:`connect`, so this flag has no
|
|
260
|
+
effect yet.
|
|
261
|
+
cti_files: GenTL producer (``.cti``) file paths to load, in
|
|
262
|
+
addition to any discovered via the ``GENICAM_GENTL64_PATH``
|
|
263
|
+
environment variable. Applied on :meth:`connect` when
|
|
264
|
+
provided.
|
|
265
|
+
exposure: Initial exposure time in microseconds. Applied on
|
|
266
|
+
:meth:`connect` when provided.
|
|
267
|
+
gain: Initial gain in dB. Applied on :meth:`connect` when
|
|
268
|
+
provided.
|
|
269
|
+
width: Desired frame width in pixels. Applied on :meth:`connect`
|
|
270
|
+
(together with ``height``) when provided.
|
|
271
|
+
height: Desired frame height in pixels. Applied on
|
|
272
|
+
:meth:`connect` (together with ``width``) when provided.
|
|
273
|
+
x: Desired horizontal offset in pixels. Applied on
|
|
274
|
+
:meth:`connect` when ``x`` or ``y`` is provided.
|
|
275
|
+
y: Desired vertical offset in pixels. Applied on :meth:`connect`
|
|
276
|
+
when ``x`` or ``y`` is provided.
|
|
277
|
+
fps: Target frame rate, stored on ``self.fps`` (default: 60 when
|
|
278
|
+
not provided).
|
|
279
|
+
acquisition_framerate: Frame rate applied via :meth:`set_fps` on
|
|
280
|
+
:meth:`connect` when provided.
|
|
281
|
+
**kwargs: Additional passthrough options stored on ``self.config``.
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
MissingDependencyError: If the ``harvesters`` package is not
|
|
285
|
+
installed.
|
|
286
|
+
"""
|
|
287
|
+
# Preserve the historical ``self.config`` contents: only forward
|
|
288
|
+
# explicitly-provided values so the ``'width' in self.config`` style
|
|
289
|
+
# presence checks in connect() keep their old meaning.
|
|
290
|
+
for _key, _val in (
|
|
291
|
+
("is_mono", is_mono),
|
|
292
|
+
("cti_files", cti_files),
|
|
293
|
+
("exposure", exposure),
|
|
294
|
+
("gain", gain),
|
|
295
|
+
("width", width),
|
|
296
|
+
("height", height),
|
|
297
|
+
("x", x),
|
|
298
|
+
("y", y),
|
|
299
|
+
("fps", fps),
|
|
300
|
+
("acquisition_framerate", acquisition_framerate),
|
|
301
|
+
):
|
|
302
|
+
if _val is not None:
|
|
303
|
+
kwargs[_key] = _val
|
|
304
|
+
super().__init__(source, **kwargs)
|
|
305
|
+
self.camera = None
|
|
306
|
+
self.converter = None
|
|
307
|
+
try:
|
|
308
|
+
from harvesters.core import Harvester
|
|
309
|
+
|
|
310
|
+
self.h = Harvester()
|
|
311
|
+
except ImportError as e:
|
|
312
|
+
raise MissingDependencyError("harvesters", extra="genicam", details=str(e)) from e
|
|
313
|
+
|
|
314
|
+
self.is_mono = self.config.get("is_mono", False)
|
|
315
|
+
self.serial_number = source if isinstance(source, str) else None
|
|
316
|
+
self.device_index = source if isinstance(source, int) else 0
|
|
317
|
+
self.fps = self.config.get("fps", 60)
|
|
318
|
+
|
|
319
|
+
def try_set_node_param(self, container, param_name, attr_name, value):
|
|
320
|
+
try:
|
|
321
|
+
param = getattr(container, param_name)
|
|
322
|
+
setattr(param, attr_name, value)
|
|
323
|
+
print(f"Set {param_name}.{attr_name} to {value}")
|
|
324
|
+
except Exception as e:
|
|
325
|
+
print(f"Failed to set {param_name} to {value}: {e}")
|
|
326
|
+
|
|
327
|
+
def connect(self) -> bool:
|
|
328
|
+
"""Connect to Genicam camera."""
|
|
329
|
+
if self.h is None:
|
|
330
|
+
logger.error("Harvesters not available")
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
# self.h.add_file('/opt/pylon/lib/gentlproducer/gtl/ProducerU3V.cti')
|
|
335
|
+
|
|
336
|
+
cti_files = self.config.get("cti_files", [])
|
|
337
|
+
for cti_file in cti_files:
|
|
338
|
+
self.h.add_file(cti_file)
|
|
339
|
+
for file in GenicamCapture.find_cti_paths():
|
|
340
|
+
self.h.add_file(file)
|
|
341
|
+
|
|
342
|
+
self.h.update()
|
|
343
|
+
|
|
344
|
+
devices = self.h.device_info_list
|
|
345
|
+
|
|
346
|
+
if len(devices) == 0:
|
|
347
|
+
logger.error("No Genicam cameras found")
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
# Create camera object
|
|
351
|
+
if self.serial_number:
|
|
352
|
+
self.camera = self.h.create({"serial_number": self.serial_number})
|
|
353
|
+
else:
|
|
354
|
+
self.camera = self.h.create(self.device_index)
|
|
355
|
+
|
|
356
|
+
# Open camera
|
|
357
|
+
n = self.camera.remote_device.node_map
|
|
358
|
+
|
|
359
|
+
# Try to configure the camera in more suitable settings
|
|
360
|
+
self.try_set_node_param(n, "TriggerMode", "value", "On")
|
|
361
|
+
self.try_set_node_param(n, "TriggerActivation", "value", "RisingEdge")
|
|
362
|
+
self.try_set_node_param(n, "TriggerSource", "value", "Software")
|
|
363
|
+
self.try_set_node_param(n, "PixelFormat", "value", "BayerGR8")
|
|
364
|
+
self.try_set_node_param(n, "BinningHorizontal", "value", 1)
|
|
365
|
+
self.try_set_node_param(n, "BinningVertical", "value", 1)
|
|
366
|
+
|
|
367
|
+
# from genicam_tools import GenicamTools
|
|
368
|
+
# node_map = GenicamTools.print_node_map(n)
|
|
369
|
+
|
|
370
|
+
self.is_connected = True
|
|
371
|
+
|
|
372
|
+
# Apply config parameters
|
|
373
|
+
if "exposure" in self.config:
|
|
374
|
+
self.set_exposure(self.config["exposure"])
|
|
375
|
+
if "gain" in self.config:
|
|
376
|
+
self.set_gain(self.config["gain"])
|
|
377
|
+
if "width" in self.config and "height" in self.config:
|
|
378
|
+
self.set_frame_size(self.config["width"], self.config["height"])
|
|
379
|
+
if "x" in self.config or "y" in self.config:
|
|
380
|
+
self.set_offset(self.config.get("x", 0), self.config.get("y", 0))
|
|
381
|
+
if "fps" in self.config:
|
|
382
|
+
self.fps = self.config["fps"]
|
|
383
|
+
if "acquisition_framerate" in self.config:
|
|
384
|
+
self.set_fps(self.config["acquisition_framerate"])
|
|
385
|
+
|
|
386
|
+
self.camera.start()
|
|
387
|
+
|
|
388
|
+
logger.info(
|
|
389
|
+
f"Connected to Genicam camera {self.camera.device.module.vendor} "
|
|
390
|
+
f"{self.camera.device.module.model}"
|
|
391
|
+
)
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.error(f"Error connecting to Genicam camera: {e}")
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
def disconnect(self) -> bool:
|
|
399
|
+
"""Disconnect from Genicam camera."""
|
|
400
|
+
try:
|
|
401
|
+
if self.h is not None:
|
|
402
|
+
self.h.reset()
|
|
403
|
+
logger.info("Disconnected from Genicam camera")
|
|
404
|
+
return True
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logger.error(f"Error disconnecting from Genicam camera: {e}")
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
def get_exposure_range(self) -> tuple[float, float]:
|
|
410
|
+
"""Get exposure range in microseconds."""
|
|
411
|
+
if not self.is_connected or self.camera is None:
|
|
412
|
+
return (0.0, 0.0)
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
n = self.camera.remote_device.node_map
|
|
416
|
+
|
|
417
|
+
min_exposure = n.ExposureTime.min
|
|
418
|
+
max_exposure = n.ExposureTime.max
|
|
419
|
+
return (min_exposure, max_exposure)
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.error(f"Error getting exposure range: {e}")
|
|
422
|
+
return (0.0, 0.0)
|
|
423
|
+
|
|
424
|
+
def get_gain_range(self) -> tuple[float, float]:
|
|
425
|
+
"""Get gain range in dB."""
|
|
426
|
+
if not self.is_connected or self.camera is None:
|
|
427
|
+
return (0.0, 0.0)
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
n = self.camera.remote_device.node_map
|
|
431
|
+
|
|
432
|
+
min_gain = n.Gain.min
|
|
433
|
+
max_gain = n.Gain.max
|
|
434
|
+
return (min_gain, max_gain)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.error(f"Error getting gain range: {e}")
|
|
437
|
+
return (0.0, 0.0)
|
|
438
|
+
|
|
439
|
+
def set_exposure(self, value: float) -> bool:
|
|
440
|
+
"""Set exposure in microseconds."""
|
|
441
|
+
if not self.is_connected or self.camera is None:
|
|
442
|
+
return False
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
n = self.camera.remote_device.node_map
|
|
446
|
+
|
|
447
|
+
n.ExposureTime.value = value
|
|
448
|
+
self._exposure = value
|
|
449
|
+
return True
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error(f"Error setting exposure: {e}")
|
|
452
|
+
return False
|
|
453
|
+
|
|
454
|
+
def get_exposure(self) -> Optional[float]:
|
|
455
|
+
"""Get exposure in microseconds."""
|
|
456
|
+
if not self.is_connected or self.camera is None:
|
|
457
|
+
return self._exposure
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
n = self.camera.remote_device.node_map
|
|
461
|
+
|
|
462
|
+
return n.ExposureTime.value
|
|
463
|
+
except Exception:
|
|
464
|
+
return self._exposure
|
|
465
|
+
|
|
466
|
+
def set_gain(self, value: float) -> bool:
|
|
467
|
+
"""Set gain in dB."""
|
|
468
|
+
if not self.is_connected or self.camera is None:
|
|
469
|
+
return False
|
|
470
|
+
try:
|
|
471
|
+
n = self.camera.remote_device.node_map
|
|
472
|
+
n.Gain.value = value
|
|
473
|
+
|
|
474
|
+
self._gain = value
|
|
475
|
+
return True
|
|
476
|
+
except Exception as e:
|
|
477
|
+
logger.error(f"Error setting gain: {e}")
|
|
478
|
+
return False
|
|
479
|
+
|
|
480
|
+
def get_gain(self) -> Optional[float]:
|
|
481
|
+
"""Get gain in dB."""
|
|
482
|
+
if not self.is_connected or self.camera is None:
|
|
483
|
+
return self._gain
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
n = self.camera.remote_device.node_map
|
|
487
|
+
|
|
488
|
+
return n.Gain.value
|
|
489
|
+
except Exception:
|
|
490
|
+
return self._gain
|
|
491
|
+
|
|
492
|
+
def enable_auto_exposure(self, enable: bool = True) -> bool:
|
|
493
|
+
"""Enable or disable auto exposure for Genicam camera."""
|
|
494
|
+
if not self.is_connected or self.camera is None:
|
|
495
|
+
return False
|
|
496
|
+
try:
|
|
497
|
+
n = self.camera.remote_device.node_map
|
|
498
|
+
if enable:
|
|
499
|
+
n.ExposureAuto.value = "Continuous"
|
|
500
|
+
n.GainAuto.value = "Continuous"
|
|
501
|
+
else:
|
|
502
|
+
n.ExposureAuto.value = "Off"
|
|
503
|
+
n.GainAuto.value = "Off"
|
|
504
|
+
logger.info(f"Set Genicam auto exposure to {enable}")
|
|
505
|
+
return True
|
|
506
|
+
except Exception as e:
|
|
507
|
+
logger.error(f"Error setting Genicam auto exposure: {e}")
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
def set_frame_size(self, width: int, height: int) -> bool:
|
|
511
|
+
"""Set frame size for Genicam camera."""
|
|
512
|
+
if not self.is_connected or self.camera is None:
|
|
513
|
+
return False
|
|
514
|
+
try:
|
|
515
|
+
n = self.camera.remote_device.node_map
|
|
516
|
+
|
|
517
|
+
n.Width.value = width
|
|
518
|
+
n.Height.value = height
|
|
519
|
+
logger.info(f"Set Genicam camera resolution to {width}x{height}")
|
|
520
|
+
return True
|
|
521
|
+
except Exception as e:
|
|
522
|
+
logger.error(f"Error setting Genicam camera resolution: {e}")
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
def set_offset(self, x: int, y: int) -> bool:
|
|
526
|
+
"""Set offset for Genicam camera."""
|
|
527
|
+
if not self.is_connected or self.camera is None:
|
|
528
|
+
return False
|
|
529
|
+
try:
|
|
530
|
+
n = self.camera.remote_device.node_map
|
|
531
|
+
|
|
532
|
+
n.OffsetX.value = x
|
|
533
|
+
n.OffsetY.value = y
|
|
534
|
+
logger.info(f"Set Genicam offset to ({x},{y})")
|
|
535
|
+
return True
|
|
536
|
+
except Exception as e:
|
|
537
|
+
logger.error(f"Error setting Genicam camera offset: {e}")
|
|
538
|
+
return False
|
|
539
|
+
|
|
540
|
+
def get_frame_size(self) -> Optional[tuple[int, int]]:
|
|
541
|
+
"""Get frame size."""
|
|
542
|
+
if not self.is_connected or self.camera is None:
|
|
543
|
+
return None
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
n = self.camera.remote_device.node_map
|
|
547
|
+
|
|
548
|
+
width = n.Width.value
|
|
549
|
+
height = n.Height.value
|
|
550
|
+
return (width, height)
|
|
551
|
+
except Exception:
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
def set_fps(self, fps: float) -> bool:
|
|
555
|
+
"""Set FPS for Genicam camera."""
|
|
556
|
+
if not self.is_connected or self.camera is None:
|
|
557
|
+
return False
|
|
558
|
+
try:
|
|
559
|
+
n = self.camera.remote_device.node_map
|
|
560
|
+
|
|
561
|
+
n.AcquisitionFrameRateEnable.value = True
|
|
562
|
+
n.AcquisitionFrameRate.value = fps
|
|
563
|
+
logger.info(f"Set Genicam camera FPS to {fps}")
|
|
564
|
+
return True
|
|
565
|
+
except Exception as e:
|
|
566
|
+
logger.error(f"Error setting Genicam camera FPS: {e}")
|
|
567
|
+
return False
|
|
568
|
+
|
|
569
|
+
def get_fps(self) -> Optional[float]:
|
|
570
|
+
"""Get FPS."""
|
|
571
|
+
if not self.is_connected or self.camera is None:
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
n = self.camera.remote_device.node_map
|
|
576
|
+
return n.AcquisitionFrameRate.value
|
|
577
|
+
except Exception:
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
@staticmethod
|
|
581
|
+
def find_cti_paths():
|
|
582
|
+
res = []
|
|
583
|
+
cti_paths = os.getenv("GENICAM_GENTL64_PATH")
|
|
584
|
+
if cti_paths and len(cti_paths) > 0:
|
|
585
|
+
files = os.listdir(cti_paths)
|
|
586
|
+
for f in files:
|
|
587
|
+
if f.endswith(".cti"):
|
|
588
|
+
res.append(os.path.join(cti_paths, f))
|
|
589
|
+
return res
|
|
590
|
+
|
|
591
|
+
@classmethod
|
|
592
|
+
def discover(cls) -> list:
|
|
593
|
+
"""
|
|
594
|
+
Discover available GenICam compliant cameras.
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
list: List of dictionaries containing GenICam camera information.
|
|
598
|
+
Each dict contains: {'index': int, 'serial_number': str, 'name': str, 'vendor': str}
|
|
599
|
+
"""
|
|
600
|
+
devices = []
|
|
601
|
+
|
|
602
|
+
try:
|
|
603
|
+
from harvesters.core import Harvester
|
|
604
|
+
except ImportError:
|
|
605
|
+
logger.warning("Harvesters module not available. Cannot discover GenICam cameras.")
|
|
606
|
+
return []
|
|
607
|
+
|
|
608
|
+
harvester = None
|
|
609
|
+
try:
|
|
610
|
+
harvester = Harvester()
|
|
611
|
+
|
|
612
|
+
# Add common GenTL producer paths (this may need customization)
|
|
613
|
+
try:
|
|
614
|
+
for file in GenicamCapture.find_cti_paths():
|
|
615
|
+
harvester.add_file(file)
|
|
616
|
+
|
|
617
|
+
# Try to add some common GenTL producers
|
|
618
|
+
harvester.add_file("/opt/pylon5/lib64/pylon_TL_GenICam.cti") # Basler
|
|
619
|
+
harvester.add_file(
|
|
620
|
+
"/opt/mvIMPACT_acquire/lib/x86_64/mvGenTLProducer.cti"
|
|
621
|
+
) # MATRIX VISION
|
|
622
|
+
except Exception:
|
|
623
|
+
pass # If paths don't exist, that's fine
|
|
624
|
+
|
|
625
|
+
harvester.update()
|
|
626
|
+
|
|
627
|
+
for i, device_info in enumerate(harvester.device_info_list):
|
|
628
|
+
try:
|
|
629
|
+
serial = getattr(device_info, "serial_number", f"genicam_{i}")
|
|
630
|
+
device_data = {
|
|
631
|
+
"index": i,
|
|
632
|
+
"id": i,
|
|
633
|
+
"serial_number": serial,
|
|
634
|
+
"name": "Genicam " + getattr(device_info, "model", "GenICam Camera"),
|
|
635
|
+
"vendor": getattr(device_info, "vendor", "Unknown"),
|
|
636
|
+
}
|
|
637
|
+
devices.append(device_data)
|
|
638
|
+
logger.info(f"Found GenICam camera: {device_data}")
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.warning(f"Could not get info for GenICam device {i}: {e}")
|
|
642
|
+
continue
|
|
643
|
+
|
|
644
|
+
except Exception as e:
|
|
645
|
+
logger.error(f"Error discovering GenICam cameras: {e}")
|
|
646
|
+
finally:
|
|
647
|
+
if harvester:
|
|
648
|
+
try:
|
|
649
|
+
harvester.reset()
|
|
650
|
+
except Exception:
|
|
651
|
+
pass
|
|
652
|
+
|
|
653
|
+
return devices
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
if __name__ == "__main__":
|
|
657
|
+
# Example usage
|
|
658
|
+
import cv2
|
|
659
|
+
|
|
660
|
+
devices = GenicamCapture.discover()
|
|
661
|
+
print("Discovered GenICam cameras:")
|
|
662
|
+
for device in devices:
|
|
663
|
+
print(f" - {device['name']} (Serial: {device['serial_number']})")
|
|
664
|
+
|
|
665
|
+
camera = GenicamCapture() # Replace with actual serial number or index
|
|
666
|
+
if camera.connect():
|
|
667
|
+
print("Webcam connected successfully.")
|
|
668
|
+
print(f"Exposure: {camera.get_exposure()}")
|
|
669
|
+
print(f"Gain: {camera.get_gain()}")
|
|
670
|
+
print(f"Frame size: {camera.get_frame_size()}")
|
|
671
|
+
|
|
672
|
+
# Read a few frames
|
|
673
|
+
while camera.is_connected:
|
|
674
|
+
ret, frame = camera.read()
|
|
675
|
+
if ret and frame is not None:
|
|
676
|
+
cv2.imshow("Webcam", frame)
|
|
677
|
+
if cv2.waitKey(1) & 0xFF == ord("q"):
|
|
678
|
+
break
|
|
679
|
+
camera.disconnect()
|
|
680
|
+
else:
|
|
681
|
+
print("Failed to connect to webcam.")
|