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.
Files changed (37) hide show
  1. frame_processors/__init__.py +37 -0
  2. frame_source/__init__.py +50 -0
  3. framesource/__init__.py +93 -0
  4. framesource/_msmf_config.py +26 -0
  5. framesource/_version.py +24 -0
  6. framesource/discovery.py +109 -0
  7. framesource/errors.py +82 -0
  8. framesource/factory.py +290 -0
  9. framesource/processors/__init__.py +34 -0
  10. framesource/processors/equirectangular360_processor.py +577 -0
  11. framesource/processors/fisheye2equirectangular_processor.py +328 -0
  12. framesource/processors/frame_processor.py +30 -0
  13. framesource/processors/hyperspectral_processor.py +23 -0
  14. framesource/processors/realsense_depth_processor.py +90 -0
  15. framesource/py.typed +0 -0
  16. framesource/sources/__init__.py +40 -0
  17. framesource/sources/audiospectrogram_capture.py +1068 -0
  18. framesource/sources/basler_capture.py +477 -0
  19. framesource/sources/folder_capture.py +920 -0
  20. framesource/sources/genicam_capture.py +681 -0
  21. framesource/sources/huateng_capture.py +245 -0
  22. framesource/sources/ipcamera_capture.py +254 -0
  23. framesource/sources/mvsdk.py +2454 -0
  24. framesource/sources/realsense_capture.py +565 -0
  25. framesource/sources/screen_capture.py +800 -0
  26. framesource/sources/video_capture_base.py +560 -0
  27. framesource/sources/video_file_capture.py +259 -0
  28. framesource/sources/webcam_capture.py +511 -0
  29. framesource/sources/ximea_capture.py +299 -0
  30. framesource/threading_utils.py +790 -0
  31. framesource-0.3.0.dist-info/METADATA +787 -0
  32. framesource-0.3.0.dist-info/RECORD +37 -0
  33. framesource-0.3.0.dist-info/WHEEL +5 -0
  34. framesource-0.3.0.dist-info/licenses/LICENSE +21 -0
  35. framesource-0.3.0.dist-info/scm_file_list.json +276 -0
  36. framesource-0.3.0.dist-info/scm_version.json +8 -0
  37. 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.")