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,565 @@
1
+ import logging
2
+ import warnings
3
+ from typing import Any, Optional
4
+
5
+ import cv2
6
+ import numpy as np
7
+
8
+ from ..errors import MissingDependencyError
9
+ from ..processors.realsense_depth_processor import (
10
+ RealsenseDepthProcessor,
11
+ RealsenseProcessingOutput,
12
+ )
13
+ from .video_capture_base import VideoCaptureBase
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class RealsenseCapture(VideoCaptureBase):
19
+ has_discovery = True
20
+ supports_exposure = True
21
+ supports_gain = True
22
+ supports_depth = True
23
+
24
+ def _read_implementation(self) -> tuple[bool, Optional[np.ndarray]]:
25
+ """
26
+ Read a single frame from the realsense camera.
27
+ Returns:
28
+ Tuple[bool, Optional[np.ndarray]]: (success, frame)
29
+ """
30
+ if not self.is_connected or self.pipeline is None:
31
+ return False, None
32
+
33
+ try:
34
+ # Wait for a coherent pair of frames: depth and color
35
+ frames = self.pipeline.wait_for_frames()
36
+ aligned = self._align.process(frames)
37
+
38
+ depth_frame = frames.get_depth_frame()
39
+ color_frame = frames.get_color_frame()
40
+ aligned_depth_frame = aligned.get_depth_frame()
41
+ aligned_color_frame = aligned.get_color_frame()
42
+
43
+ if not depth_frame or not color_frame:
44
+ return False, None
45
+ except RuntimeError as e:
46
+ # Handle case where pipeline has been stopped
47
+ if "wait_for_frames cannot be called before start()" in str(e):
48
+ return False, None
49
+ else:
50
+ raise # Re-raise other runtime errors
51
+
52
+ # if depth_frame and color_frame:
53
+ # print("Depth Intrinsics:", depth_frame.profile.as_video_stream_profile().get_intrinsics()) # noqa: E501
54
+ # print("Color Intrinsics:", color_frame.profile.as_video_stream_profile().get_intrinsics()) # noqa: E501
55
+
56
+ frame_dict = {
57
+ "raw_color": color_frame,
58
+ "raw_depth": depth_frame,
59
+ "aligned_depth": aligned_depth_frame,
60
+ "aligned_color": aligned_color_frame,
61
+ }
62
+
63
+ return True, frame_dict
64
+
65
+ """Realsense camera capture using Realsense lib."""
66
+
67
+ def __init__(
68
+ self,
69
+ source: int = 0,
70
+ *,
71
+ width: Optional[int] = None,
72
+ height: Optional[int] = None,
73
+ fps: Optional[float] = None,
74
+ processor: Optional[Any] = None,
75
+ **kwargs,
76
+ ):
77
+ """Initialize the RealSense capture.
78
+
79
+ Args:
80
+ source: Camera serial number (str) or device index (int,
81
+ default: 0).
82
+ width: Desired color-stream width in pixels. Falls back to the
83
+ device's maximum supported width when omitted (default:
84
+ None). Applied on :meth:`connect`.
85
+ height: Desired color-stream height in pixels. Falls back to the
86
+ device's maximum supported height when omitted (default:
87
+ None). Applied on :meth:`connect`.
88
+ fps: Desired stream frame rate. Falls back to the device's
89
+ maximum supported frame rate when omitted (default: None).
90
+ Applied on :meth:`connect`.
91
+ processor: Frame processor attached at construction time.
92
+ Defaults to a
93
+ :class:`~framesource.processors.realsense_depth_processor.RealsenseDepthProcessor`
94
+ configured for RGB output when not provided.
95
+ **kwargs: Additional passthrough options stored on ``self.config``.
96
+ """
97
+ # Preserve the historical ``self.config`` contents: only forward
98
+ # explicitly-provided values so the ``self.config.get(...)`` defaults
99
+ # below keep their old meaning.
100
+ for _key, _val in (
101
+ ("width", width),
102
+ ("height", height),
103
+ ("fps", fps),
104
+ ("processor", processor),
105
+ ):
106
+ if _val is not None:
107
+ kwargs[_key] = _val
108
+ super().__init__(source, **kwargs)
109
+ self.pipeline = None
110
+ self.device = None
111
+ self.profile = None
112
+ self._align = None
113
+
114
+ self.w = self.config.get("width", 0)
115
+ self.h = self.config.get("height", 0)
116
+ self.fps = self.config.get("fps", 0)
117
+
118
+ self._max_width = 0
119
+ self._max_height = 0
120
+ self._max_fps = 0
121
+
122
+ self._default_processor = self.config.get("processor", None)
123
+ if self._default_processor is None:
124
+ self._default_processor = RealsenseDepthProcessor(
125
+ output_format=RealsenseProcessingOutput.RGB
126
+ )
127
+ self.attach_processor(self._default_processor)
128
+
129
+ self.source = source if isinstance(source, int) else 0
130
+
131
+ if "is_mono" in kwargs:
132
+ logger.warning(
133
+ "'is_mono' argument is only used for certain industrial cameras "
134
+ "and has no effect for realsense camera."
135
+ )
136
+
137
+ def connect(self) -> bool:
138
+ """Connect to realsense camera."""
139
+ try:
140
+ import pyrealsense2 as rs
141
+ except ImportError as e:
142
+ raise MissingDependencyError("pyrealsense2", extra="realsense", details=str(e)) from e
143
+
144
+ try:
145
+ # Configure depth and color streams
146
+ self.pipeline = rs.pipeline()
147
+ self._align = rs.align(rs.stream.color)
148
+ config = rs.config()
149
+
150
+ serial_number = self.source if isinstance(self.source, str) else None
151
+ device_index = self.source if isinstance(self.source, int) else 0
152
+
153
+ if serial_number:
154
+ config.enable_device(serial_number)
155
+ elif device_index is not None:
156
+ ctx = rs.context()
157
+ devices = ctx.query_devices()
158
+
159
+ if device_index < 0 or device_index >= len(devices):
160
+ raise ValueError(f"Invalid device index: {device_index}")
161
+ selected_serial = devices[device_index].get_info(rs.camera_info.serial_number)
162
+ config.enable_device(selected_serial)
163
+
164
+ # Get device product line for setting a supporting resolution
165
+ pipeline_wrapper = rs.pipeline_wrapper(self.pipeline)
166
+ pipeline_profile = config.resolve(pipeline_wrapper)
167
+ self.device: rs.device = pipeline_profile.get_device()
168
+
169
+ serial_number = self.device.get_info(rs.camera_info.serial_number)
170
+ device_name = self.device.get_info(rs.camera_info.name)
171
+ device_product_line = str(self.device.get_info(rs.camera_info.product_line))
172
+
173
+ found_rgb = False
174
+ max_res = (0, 0, 0)
175
+ for s in self.device.query_sensors():
176
+ if any(p.stream_type() == rs.stream.color for p in s.get_stream_profiles()):
177
+ found_rgb = True
178
+ for profile in s.get_stream_profiles():
179
+ if profile.stream_type() == rs.stream.color:
180
+ vp = profile.as_video_stream_profile()
181
+ res = (vp.width(), vp.height(), vp.fps())
182
+ if res > max_res:
183
+ max_res = res
184
+ if not found_rgb:
185
+ print("The demo requires Depth camera with Color sensor")
186
+ exit(0)
187
+
188
+ self._max_width = max_res[0]
189
+ self._max_height = max_res[1]
190
+ self._max_fps = max_res[2]
191
+
192
+ width = self.w if self.w > 0 else self._max_width
193
+ height = self.h if self.h > 0 else self._max_height
194
+ fps = self.fps if self.fps > 0 else self._max_fps
195
+
196
+ # Enable depth stream without specifying resolution - let it auto-select
197
+ # (depth sensor often has different max resolution than color sensor)
198
+ config.enable_stream(rs.stream.depth, rs.format.z16, int(fps))
199
+ config.enable_stream(rs.stream.color, width, height, rs.format.bgr8, int(fps))
200
+
201
+ # Start streaming
202
+ self.profile = self.pipeline.start(config)
203
+
204
+ self.is_connected = True
205
+ logger.info(
206
+ f"Connected to realsense camera {self.source}, {device_name} "
207
+ f"({device_product_line} line) (Serial: {serial_number})"
208
+ )
209
+ return True
210
+ except Exception as e:
211
+ logger.error(f"Error connecting to realsense camera: {e}")
212
+ return False
213
+
214
+ def disconnect(self) -> bool:
215
+ """Disconnect from realsense camera."""
216
+ try:
217
+ if self.pipeline is not None:
218
+ self.pipeline.stop()
219
+ self.is_connected = False
220
+ logger.info("Disconnected from realsense camera")
221
+ return True
222
+ except Exception as e:
223
+ logger.error(f"Error disconnecting from realsense camera: {e}")
224
+ return False
225
+
226
+ def set_exposure(self, value: float) -> bool:
227
+ """Set exposure (1.0 to 165000 for most realsense cameras)."""
228
+ if not self.is_connected or self.pipeline is None:
229
+ return False
230
+
231
+ try:
232
+ self._exposure = value
233
+ import pyrealsense2 as rs
234
+
235
+ for s in self.device.query_sensors():
236
+ if s.supports(rs.option.exposure):
237
+ s.set_option(rs.option.exposure, int(value))
238
+ logger.info(f"Set exposure to {int(value)}")
239
+ else:
240
+ logger.info(f"Not setting exposure on sensor {s}, it is not supported.")
241
+
242
+ return True
243
+ except Exception as e:
244
+ logger.error(f"Error setting exposure: {e}")
245
+ return False
246
+
247
+ def get_exposure(self) -> Optional[float]:
248
+ """Get current exposure."""
249
+ if not self.is_connected or self.pipeline is None:
250
+ return None
251
+
252
+ try:
253
+ import pyrealsense2 as rs
254
+
255
+ for s in self.device.query_sensors():
256
+ if s.supports(rs.option.exposure):
257
+ exposure = s.get_option(rs.option.exposure)
258
+ return float(exposure)
259
+ except Exception:
260
+ return self._exposure
261
+
262
+ def set_gain(self, value: float) -> bool:
263
+ """Set gain (16-248 for most realsense cameras)."""
264
+ if not self.is_connected or self.pipeline is None:
265
+ return False
266
+
267
+ try:
268
+ self._gain = value
269
+ import pyrealsense2 as rs
270
+
271
+ for s in self.device.query_sensors():
272
+ if s.supports(rs.option.gain):
273
+ s.set_option(rs.option.gain, int(value))
274
+ logger.info(f"Set gain to {int(value)}")
275
+ else:
276
+ logger.info(f"Not setting gain on sensor {s}, it is not supported.")
277
+
278
+ return True
279
+ except Exception as e:
280
+ logger.error(f"Error setting gain: {e}")
281
+ return False
282
+
283
+ def get_gain(self) -> Optional[float]:
284
+ """Get current gain."""
285
+ if not self.is_connected or self.pipeline is None:
286
+ return None
287
+
288
+ try:
289
+ import pyrealsense2 as rs
290
+
291
+ for s in self.device.query_sensors():
292
+ if s.supports(rs.option.gain):
293
+ gain = s.get_option(rs.option.gain)
294
+ return float(gain)
295
+ except Exception:
296
+ return self._gain
297
+
298
+ def get_exposure_range(self) -> Optional[tuple[float, float]]:
299
+ """Get current exposure."""
300
+ if not self.is_connected or self.pipeline is None:
301
+ return None
302
+
303
+ try:
304
+ import pyrealsense2 as rs
305
+
306
+ for s in self.device.query_sensors():
307
+ if s.supports(rs.option.exposure):
308
+ exposure = s.get_option_range(rs.option.exposure)
309
+ return float(exposure.min), float(exposure.max)
310
+ except Exception:
311
+ return 0.0, 0.0
312
+
313
+ def get_gain_range(self) -> Optional[tuple[float, float]]:
314
+ """Get current gain."""
315
+ if not self.is_connected or self.pipeline is None:
316
+ return None
317
+
318
+ try:
319
+ import pyrealsense2 as rs
320
+
321
+ for s in self.device.query_sensors():
322
+ if s.supports(rs.option.gain):
323
+ gain = s.get_option_range(rs.option.gain)
324
+ return float(gain.min), float(gain.max)
325
+ except Exception:
326
+ return 0.0, 0.0
327
+
328
+ def _get_active_profile(self):
329
+ import pyrealsense2 as rs
330
+
331
+ color_stream = self.profile.get_stream(rs.stream.color)
332
+ video_profile = color_stream.as_video_stream_profile()
333
+ return video_profile
334
+
335
+ def get_frame_size(self) -> Optional[tuple[int, int]]:
336
+ """Get frame size."""
337
+ if not self.is_connected or self.profile is None:
338
+ return None
339
+
340
+ # Get current resolution
341
+ video_profile = self._get_active_profile()
342
+ width = video_profile.width()
343
+ height = video_profile.height()
344
+
345
+ return width, height
346
+
347
+ def set_frame_size(self, width: int, height: int) -> bool:
348
+ """Set frame size."""
349
+ self.w = width
350
+ self.h = height
351
+
352
+ return True
353
+
354
+ def get_fps(self) -> Optional[float]:
355
+ """Get FPS."""
356
+ if not self.is_connected or self.profile is None:
357
+ return None
358
+
359
+ video_profile = self._get_active_profile()
360
+ fps = video_profile.fps()
361
+ return fps
362
+
363
+ def set_fps(self, fps: float) -> bool:
364
+ self.fps = fps
365
+ return True
366
+
367
+ def enable_auto_exposure(self, enable: bool = True) -> bool:
368
+ """
369
+ Enable or disable auto exposure for realsense camera.
370
+ """
371
+ if not self.is_connected or self.pipeline is None:
372
+ return False
373
+ try:
374
+ import pyrealsense2 as rs
375
+
376
+ for s in self.device.query_sensors():
377
+ # Check if the sensor supports auto-exposure
378
+ if s.supports(rs.option.enable_auto_exposure):
379
+ s.set_option(rs.option.enable_auto_exposure, int(enable))
380
+ logger.info(f"Set auto exposure to {enable}")
381
+ else:
382
+ logger.info(f"Not setting auto exposure on sensor {s}, it is not supported.")
383
+
384
+ return True
385
+ except Exception as e:
386
+ logger.error(f"Error setting auto exposure: {e}")
387
+ return False
388
+
389
+ @classmethod
390
+ def discover(cls) -> list:
391
+ """
392
+ Discover available RealSense cameras.
393
+
394
+ Returns:
395
+ list: List of dictionaries containing RealSense camera information.
396
+ Each dict contains: {'index': int, 'serial_number': str, 'name': str,
397
+ 'product_line': str}
398
+ """
399
+ devices = []
400
+
401
+ try:
402
+ import pyrealsense2 as rs
403
+ except ImportError:
404
+ logger.warning("pyrealsense2 module not available. Cannot discover RealSense cameras.")
405
+ return []
406
+
407
+ try:
408
+ # Get RealSense context
409
+ ctx = rs.context()
410
+
411
+ # Query all connected devices
412
+ device_list = ctx.query_devices()
413
+
414
+ for index in range(len(device_list)):
415
+ try:
416
+ device = device_list[index]
417
+ device_data = {
418
+ "index": index,
419
+ "serial_number": device.get_info(rs.camera_info.serial_number),
420
+ "name": device.get_info(rs.camera_info.name),
421
+ "product_line": device.get_info(rs.camera_info.product_line),
422
+ }
423
+ devices.append(device_data)
424
+ logger.info(f"Found RealSense camera: {device_data}")
425
+
426
+ except Exception as e:
427
+ logger.warning(f"Could not get info for RealSense device {index}: {e}")
428
+ continue
429
+
430
+ except Exception as e:
431
+ logger.error(f"Error discovering RealSense cameras: {e}")
432
+
433
+ return devices
434
+
435
+ @classmethod
436
+ def get_config_schema(cls) -> dict[str, Any]:
437
+ """Get configuration schema for RealSense camera capture"""
438
+ warnings.warn(
439
+ "get_config_schema() is deprecated and will be removed in a future release; "
440
+ "UI form schemas belong in the consuming application.",
441
+ DeprecationWarning,
442
+ stacklevel=2,
443
+ )
444
+ return {
445
+ "title": "RealSense Camera Configuration",
446
+ "description": "Configure Intel RealSense depth camera settings",
447
+ "fields": [
448
+ {
449
+ "name": "source",
450
+ "label": "Camera Index",
451
+ "type": "number",
452
+ "min": 0,
453
+ "max": 10,
454
+ "placeholder": "0",
455
+ "description": "Camera device index (0 for first RealSense camera)",
456
+ "required": False,
457
+ "default": 0,
458
+ },
459
+ {
460
+ "name": "width",
461
+ "label": "Width",
462
+ "type": "number",
463
+ "min": 424,
464
+ "max": 1920,
465
+ "placeholder": "640",
466
+ "description": "Frame width in pixels",
467
+ "required": False,
468
+ "default": 640,
469
+ },
470
+ {
471
+ "name": "height",
472
+ "label": "Height",
473
+ "type": "number",
474
+ "min": 240,
475
+ "max": 1080,
476
+ "placeholder": "480",
477
+ "description": "Frame height in pixels",
478
+ "required": False,
479
+ "default": 480,
480
+ },
481
+ {
482
+ "name": "fps",
483
+ "label": "Frame Rate (FPS)",
484
+ "type": "number",
485
+ "min": 6,
486
+ "max": 90,
487
+ "placeholder": "30",
488
+ "description": "Frames per second",
489
+ "required": False,
490
+ "default": 30,
491
+ },
492
+ {
493
+ "name": "depth_range_min",
494
+ "label": "Min Depth Range (m)",
495
+ "type": "number",
496
+ "min": 0.1,
497
+ "max": 5.0,
498
+ "step": 0.1,
499
+ "placeholder": "0.3",
500
+ "description": "Minimum depth detection range in meters",
501
+ "required": False,
502
+ "default": 0.3,
503
+ },
504
+ {
505
+ "name": "depth_range_max",
506
+ "label": "Max Depth Range (m)",
507
+ "type": "number",
508
+ "min": 1.0,
509
+ "max": 10.0,
510
+ "step": 0.1,
511
+ "placeholder": "3.0",
512
+ "description": "Maximum depth detection range in meters",
513
+ "required": False,
514
+ "default": 3.0,
515
+ },
516
+ {
517
+ "name": "output_type",
518
+ "label": "Output Type",
519
+ "type": "select",
520
+ "options": [
521
+ {"value": "color", "label": "Color Only"},
522
+ {"value": "depth", "label": "Depth Only"},
523
+ {"value": "both", "label": "Color + Depth"},
524
+ ],
525
+ "description": "Type of output frames",
526
+ "required": False,
527
+ "default": "color",
528
+ },
529
+ ],
530
+ }
531
+
532
+
533
+ if __name__ == "__main__":
534
+ # Example usage
535
+ camera = RealsenseCapture(
536
+ width=640,
537
+ height=480,
538
+ processor=RealsenseDepthProcessor(
539
+ output_format=RealsenseProcessingOutput.ALIGNED_SIDE_BY_SIDE
540
+ ),
541
+ )
542
+
543
+ # processor = RealsenseDepthProcessor(
544
+ # output_format=RealsenseProcessingOutput.ALIGNED_SIDE_BY_SIDE
545
+ # )
546
+ # camera.attach_processor(processor)
547
+
548
+ if camera.connect():
549
+ print("Realsense camera connected successfully.")
550
+ print(f"Exposure: {camera.get_exposure()}")
551
+ print(f"Gain: {camera.get_gain()}")
552
+ print(f"Frame size: {camera.get_frame_size()}")
553
+
554
+ # Read a few frames
555
+ while camera.is_connected:
556
+ ret, frame = camera.read()
557
+ if ret:
558
+ cv2.imshow("Realsense camera", frame) # type: ignore
559
+ if cv2.waitKey(1) & 0xFF == ord("q"):
560
+ break
561
+
562
+ camera.stop()
563
+ camera.disconnect()
564
+ else:
565
+ print("Failed to connect to realsense camera.")