pyorbbec 1.0.1.6__py3-none-any.whl → 1.0.1.8__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 (49) hide show
  1. {pyorbbec-1.0.1.6.dist-info → pyorbbec-1.0.1.8.dist-info}/METADATA +1 -1
  2. pyorbbec-1.0.1.8.dist-info/RECORD +59 -0
  3. pyorbbecsdk/OrbbecSDKConfig.xml +2332 -0
  4. pyorbbecsdk/examples/.gitkeep +0 -0
  5. pyorbbecsdk/examples/OrbbecSDK.dll +0 -0
  6. pyorbbecsdk/examples/OrbbecSDK.lib +0 -0
  7. pyorbbecsdk/examples/README.md +26 -0
  8. pyorbbecsdk/examples/__pycache__/utils.cpython-313.pyc +0 -0
  9. pyorbbecsdk/examples/callback.py +303 -0
  10. pyorbbecsdk/examples/color.py +64 -0
  11. pyorbbecsdk/examples/coordinate_transform.py +184 -0
  12. pyorbbecsdk/examples/depth.py +107 -0
  13. pyorbbecsdk/examples/depth_work_mode.py +50 -0
  14. pyorbbecsdk/examples/device_firmware_update.py +155 -0
  15. pyorbbecsdk/examples/device_optional_depth_presets_update.py +142 -0
  16. pyorbbecsdk/examples/enumerate.py +118 -0
  17. pyorbbecsdk/examples/extensions/depthengine/depthengine.dll +0 -0
  18. pyorbbecsdk/examples/extensions/depthengine/depthengine.lib +0 -0
  19. pyorbbecsdk/examples/extensions/filters/FilterProcessor.dll +0 -0
  20. pyorbbecsdk/examples/extensions/filters/ob_priv_filter.dll +0 -0
  21. pyorbbecsdk/examples/extensions/firmwareupdater/firmwareupdater.dll +0 -0
  22. pyorbbecsdk/examples/extensions/frameprocessor/ob_frame_processor.dll +0 -0
  23. pyorbbecsdk/examples/hdr.py +216 -0
  24. pyorbbecsdk/examples/hot_plug.py +160 -0
  25. pyorbbecsdk/examples/hw_d2c_align.py +135 -0
  26. pyorbbecsdk/examples/imu.py +60 -0
  27. pyorbbecsdk/examples/infrared.py +115 -0
  28. pyorbbecsdk/examples/logger.py +55 -0
  29. pyorbbecsdk/examples/metadata.py +64 -0
  30. pyorbbecsdk/examples/multi_device.py +169 -0
  31. pyorbbecsdk/examples/multi_streams.py +219 -0
  32. pyorbbecsdk/examples/net_device.py +158 -0
  33. pyorbbecsdk/examples/playback.py +277 -0
  34. pyorbbecsdk/examples/point_cloud.py +90 -0
  35. pyorbbecsdk/examples/post_processing.py +119 -0
  36. pyorbbecsdk/examples/preset.py +67 -0
  37. pyorbbecsdk/examples/pyorbbecsdk.cp313-win_amd64.pyd +0 -0
  38. pyorbbecsdk/examples/quick_start.py +90 -0
  39. pyorbbecsdk/examples/recorder.py +236 -0
  40. pyorbbecsdk/examples/requirements.txt +9 -0
  41. pyorbbecsdk/examples/save_image_to_disk.py +106 -0
  42. pyorbbecsdk/examples/sync_align.py +109 -0
  43. pyorbbecsdk/examples/two_devices_sync.py +233 -0
  44. pyorbbecsdk/examples/utils.py +127 -0
  45. pyorbbec-1.0.1.6.dist-info/RECORD +0 -17
  46. {pyorbbec-1.0.1.6.dist-info → pyorbbec-1.0.1.8.dist-info}/WHEEL +0 -0
  47. {pyorbbec-1.0.1.6.dist-info → pyorbbec-1.0.1.8.dist-info}/licenses/LICENSE +0 -0
  48. {pyorbbec-1.0.1.6.dist-info → pyorbbec-1.0.1.8.dist-info}/licenses/NOTICE +0 -0
  49. {pyorbbec-1.0.1.6.dist-info → pyorbbec-1.0.1.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,236 @@
1
+ # ******************************************************************************
2
+ # Copyright (c) 2024 Orbbec 3D Technology, Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http:# www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ******************************************************************************
16
+
17
+ import cv2
18
+ import numpy as np
19
+ from pyorbbecsdk import *
20
+ from utils import frame_to_bgr_image
21
+
22
+ is_paused = False
23
+ # cached frames for better visualization
24
+ cached_frames = {
25
+ 'color': None,
26
+ 'depth': None,
27
+ 'left_ir': None,
28
+ 'right_ir': None,
29
+ 'ir': None
30
+ }
31
+
32
+ def setup_camera():
33
+ """Setup camera and stream configuration"""
34
+ pipeline = Pipeline()
35
+ config = Config()
36
+ device = pipeline.get_device()
37
+
38
+ # Try to enable all possible sensors
39
+ video_sensors = [
40
+ OBSensorType.COLOR_SENSOR,
41
+ OBSensorType.DEPTH_SENSOR,
42
+ OBSensorType.IR_SENSOR,
43
+ OBSensorType.LEFT_IR_SENSOR,
44
+ OBSensorType.RIGHT_IR_SENSOR
45
+ ]
46
+ sensor_list = device.get_sensor_list()
47
+ for sensor in range(len(sensor_list)):
48
+ try:
49
+ sensor_type = sensor_list[sensor].get_type()
50
+ if sensor_type in video_sensors:
51
+ config.enable_stream(sensor_type)
52
+ except:
53
+ continue
54
+
55
+ pipeline.start(config)
56
+ return pipeline
57
+
58
+ def setup_imu():
59
+ """Setup IMU configuration"""
60
+ pipeline = Pipeline()
61
+ config = Config()
62
+ config.enable_accel_stream()
63
+ config.enable_gyro_stream()
64
+ pipeline.start(config)
65
+ return pipeline
66
+
67
+ def process_color(frame):
68
+ """Process color image"""
69
+ frame = frame if frame else cached_frames['color']
70
+ cached_frames['color'] = frame
71
+ return frame_to_bgr_image(frame) if frame else None
72
+
73
+
74
+ def process_depth(frame):
75
+ """Process depth image"""
76
+ frame = frame if frame else cached_frames['depth']
77
+ cached_frames['depth'] = frame
78
+ if not frame:
79
+ return None
80
+ try:
81
+ depth_data = np.frombuffer(frame.get_data(), dtype=np.uint16)
82
+ depth_data = depth_data.reshape(frame.get_height(), frame.get_width())
83
+ depth_image = cv2.normalize(depth_data, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
84
+ return cv2.applyColorMap(depth_image, cv2.COLORMAP_JET)
85
+ except ValueError:
86
+ return None
87
+
88
+
89
+ def process_ir(ir_frame):
90
+ """Process IR frame (left or right) to RGB image"""
91
+ if ir_frame is None:
92
+ return None
93
+ ir_frame = ir_frame.as_video_frame()
94
+ ir_data = np.asanyarray(ir_frame.get_data())
95
+ width = ir_frame.get_width()
96
+ height = ir_frame.get_height()
97
+ ir_format = ir_frame.get_format()
98
+
99
+ if ir_format == OBFormat.Y8:
100
+ ir_data = np.resize(ir_data, (height, width, 1))
101
+ data_type = np.uint8
102
+ image_dtype = cv2.CV_8UC1
103
+ max_data = 255
104
+ elif ir_format == OBFormat.MJPG:
105
+ ir_data = cv2.imdecode(ir_data, cv2.IMREAD_UNCHANGED)
106
+ data_type = np.uint8
107
+ image_dtype = cv2.CV_8UC1
108
+ max_data = 255
109
+ if ir_data is None:
110
+ print("decode mjpeg failed")
111
+ return None
112
+ ir_data = np.resize(ir_data, (height, width, 1))
113
+ else:
114
+ ir_data = np.frombuffer(ir_data, dtype=np.uint16)
115
+ data_type = np.uint16
116
+ image_dtype = cv2.CV_16UC1
117
+ max_data = 255
118
+ ir_data = np.resize(ir_data, (height, width, 1))
119
+
120
+ cv2.normalize(ir_data, ir_data, 0, max_data, cv2.NORM_MINMAX, dtype=image_dtype)
121
+ ir_data = ir_data.astype(data_type)
122
+ return cv2.cvtColor(ir_data, cv2.COLOR_GRAY2RGB)
123
+
124
+ def get_imu_text(frame, name):
125
+ """Format IMU data"""
126
+ if not frame:
127
+ return []
128
+ return [
129
+ f"{name} x: {frame.get_x():.2f}",
130
+ f"{name} y: {frame.get_y():.2f}",
131
+ f"{name} z: {frame.get_z():.2f}"
132
+ ]
133
+
134
+
135
+ def create_display(frames, width=1280, height=720):
136
+ """Create display window"""
137
+ display = np.zeros((height, width, 3), dtype=np.uint8)
138
+ h, w = height // 2, width // 2
139
+
140
+ # Process video frames
141
+ if 'color' in frames and frames['color'] is not None:
142
+ display[0:h, 0:w] = cv2.resize(frames['color'], (w, h))
143
+
144
+ if 'depth' in frames and frames['depth'] is not None:
145
+ display[0:h, w:] = cv2.resize(frames['depth'], (w, h))
146
+
147
+ if 'ir' in frames and frames['ir'] is not None:
148
+ display[h:, 0:w] = cv2.resize(frames['ir'], (w, h))
149
+
150
+ # Display IMU data
151
+ if 'imu' in frames:
152
+ y_offset = h + 20
153
+ for data_type in ['accel', 'gyro']:
154
+ text_lines = get_imu_text(frames['imu'].get(data_type), data_type.title())
155
+ for i, line in enumerate(text_lines):
156
+ cv2.putText(display, line, (w + 10, y_offset + i * 20),
157
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
158
+ y_offset += 80
159
+
160
+ return display
161
+
162
+
163
+ def main():
164
+ # Window settings
165
+ WINDOW_NAME = "MultiStream Record Viewer"
166
+ file_path = input("Enter output filename (.bag) and press Enter to start recording: ")
167
+
168
+ DISPLAY_WIDTH = 1280
169
+ DISPLAY_HEIGHT = 720
170
+
171
+ # Initialize camera
172
+ pipeline = setup_camera()
173
+ device = pipeline.get_device()
174
+ # initialize recording
175
+ recorder = RecordDevice(device, file_path)
176
+ imu_pipeline = setup_imu()
177
+ cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL)
178
+ cv2.resizeWindow(WINDOW_NAME, DISPLAY_WIDTH, DISPLAY_HEIGHT)
179
+ while True:
180
+ # Get all frames
181
+ frames = pipeline.wait_for_frames(100)
182
+ if not frames:
183
+ continue
184
+ # Process different frame types
185
+ processed_frames = {'color': process_color(frames.get_color_frame()),
186
+ 'depth': process_depth(frames.get_depth_frame())}
187
+
188
+ # Process IR image: try stereo IR first, fallback to mono if unavailable
189
+ try:
190
+ left = process_ir(frames.get_frame(OBFrameType.LEFT_IR_FRAME).as_video_frame())
191
+ right = process_ir(frames.get_frame(OBFrameType.RIGHT_IR_FRAME).as_video_frame())
192
+ if left is not None and right is not None:
193
+ processed_frames['ir'] = np.hstack((left, right))
194
+ except:
195
+ ir_frame = frames.get_ir_frame()
196
+ if ir_frame:
197
+ processed_frames['ir'] = process_ir(ir_frame.as_video_frame())
198
+
199
+ # Process IMU data
200
+ imu_frames = imu_pipeline.wait_for_frames(100)
201
+ if not imu_frames:
202
+ continue
203
+ accel = imu_frames.get_frame(OBFrameType.ACCEL_FRAME)
204
+ gyro = imu_frames.get_frame(OBFrameType.GYRO_FRAME)
205
+ if accel and gyro:
206
+ processed_frames['imu'] = {
207
+ 'accel': accel.as_accel_frame(),
208
+ 'gyro': gyro.as_gyro_frame()
209
+ }
210
+
211
+ # create display
212
+ display = create_display(processed_frames, DISPLAY_WIDTH, DISPLAY_HEIGHT)
213
+ cv2.imshow(WINDOW_NAME, display)
214
+
215
+ # check exit key
216
+ key = cv2.waitKey(1) & 0xFF
217
+ if key == ord('s'):
218
+ global is_paused
219
+ if not is_paused:
220
+ recorder.pause()
221
+ is_paused = True
222
+ print("[PAUSED] Recording paused")
223
+ else:
224
+ recorder.resume()
225
+ is_paused = False
226
+ print("[RESUMED] Recording resumed")
227
+ if key in (ord('q'), 27):
228
+ break
229
+
230
+ pipeline.stop()
231
+ recorder = None
232
+ cv2.destroyAllWindows()
233
+
234
+
235
+ if __name__ == "__main__":
236
+ main()
@@ -0,0 +1,9 @@
1
+ pybind11==2.11.0
2
+ pybind11-global==2.11.0
3
+ opencv-python
4
+ wheel
5
+ numpy<2.0 # see https://github.com/orbbec/pyorbbecsdk/issues/47
6
+ open3d # for visualization point cloud
7
+ av # for h264 decoding
8
+ pygame # for visualization
9
+ pynput # for keyboard input
@@ -0,0 +1,106 @@
1
+ # ******************************************************************************
2
+ # Copyright (c) 2024 Orbbec 3D Technology, Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http:# www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ******************************************************************************
16
+ import os
17
+
18
+ import cv2
19
+ import numpy as np
20
+
21
+ from pyorbbecsdk import *
22
+ from utils import frame_to_bgr_image
23
+
24
+
25
+ def save_depth_frame(frame: DepthFrame, index):
26
+ if frame is None:
27
+ return
28
+ width = frame.get_width()
29
+ height = frame.get_height()
30
+ timestamp = frame.get_timestamp()
31
+ scale = frame.get_depth_scale()
32
+ depth_format = frame.get_format()
33
+ if depth_format != OBFormat.Y16:
34
+ print("depth format is not Y16")
35
+ return
36
+ data = np.frombuffer(frame.get_data(), dtype=np.uint16)
37
+ data = data.reshape((height, width))
38
+ data = data.astype(np.float32) * scale
39
+ data = data.astype(np.uint16)
40
+ save_image_dir = os.path.join(os.getcwd(), "depth_images")
41
+ if not os.path.exists(save_image_dir):
42
+ os.mkdir(save_image_dir)
43
+ raw_filename = save_image_dir + "/depth_{}x{}_{}_{}.raw".format(width, height, index, timestamp)
44
+ data.tofile(raw_filename)
45
+
46
+
47
+ def save_color_frame(frame: ColorFrame, index):
48
+ if frame is None:
49
+ return
50
+ width = frame.get_width()
51
+ height = frame.get_height()
52
+ timestamp = frame.get_timestamp()
53
+ save_image_dir = os.path.join(os.getcwd(), "color_images")
54
+ if not os.path.exists(save_image_dir):
55
+ os.mkdir(save_image_dir)
56
+ filename = save_image_dir + "/color_{}x{}_{}_{}.png".format(width, height, index, timestamp)
57
+ image = frame_to_bgr_image(frame)
58
+ if image is None:
59
+ print("failed to convert frame to image")
60
+ return
61
+ cv2.imwrite(filename, image)
62
+
63
+
64
+ def main():
65
+ pipeline = Pipeline()
66
+ config = Config()
67
+ saved_color_cnt: int = 0
68
+ saved_depth_cnt: int = 0
69
+ has_color_sensor = False
70
+ try:
71
+ profile_list = pipeline.get_stream_profile_list(OBSensorType.COLOR_SENSOR)
72
+ if profile_list is not None:
73
+ color_profile: VideoStreamProfile = profile_list.get_default_video_stream_profile()
74
+ config.enable_stream(color_profile)
75
+ has_color_sensor = True
76
+ except OBError as e:
77
+ print(e)
78
+ depth_profile_list = pipeline.get_stream_profile_list(OBSensorType.DEPTH_SENSOR)
79
+ if depth_profile_list is not None:
80
+ depth_profile = depth_profile_list.get_default_video_stream_profile()
81
+ config.enable_stream(depth_profile)
82
+ pipeline.start(config)
83
+ while True:
84
+ try:
85
+ frames = pipeline.wait_for_frames(100)
86
+ if frames is None:
87
+ continue
88
+ if has_color_sensor:
89
+ if saved_color_cnt >= 5 and saved_depth_cnt >= 5:
90
+ break
91
+ elif saved_depth_cnt >= 5:
92
+ break
93
+ color_frame = frames.get_color_frame()
94
+ if color_frame is not None and saved_color_cnt < 5:
95
+ save_color_frame(color_frame, saved_color_cnt)
96
+ saved_color_cnt += 1
97
+ depth_frame = frames.get_depth_frame()
98
+ if depth_frame is not None and saved_depth_cnt < 5:
99
+ save_depth_frame(depth_frame, saved_depth_cnt)
100
+ saved_depth_cnt += 1
101
+ except KeyboardInterrupt:
102
+ break
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
@@ -0,0 +1,109 @@
1
+ # ******************************************************************************
2
+ # Copyright (c) 2024 Orbbec 3D Technology, Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http:# www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ******************************************************************************
16
+ import argparse
17
+ import sys
18
+
19
+ import cv2
20
+ import numpy as np
21
+
22
+ from pyorbbecsdk import *
23
+ from utils import frame_to_bgr_image
24
+
25
+ ESC_KEY = 27
26
+
27
+ MIN_DEPTH = 20 # 20mm
28
+ MAX_DEPTH = 10000 # 10000mm
29
+ # Temporal filter for smoothing depth data over time
30
+
31
+ def main(argv):
32
+ pipeline = Pipeline()
33
+ config = Config()
34
+ parser = argparse.ArgumentParser()
35
+ parser.add_argument("-s", "--enable_sync", help="enable sync", type=bool, default=True)
36
+ args = parser.parse_args(argv)
37
+
38
+ enable_sync = args.enable_sync
39
+ try:
40
+ profile_list = pipeline.get_stream_profile_list(OBSensorType.COLOR_SENSOR)
41
+ color_profile = profile_list.get_default_video_stream_profile()
42
+ config.enable_stream(color_profile)
43
+ profile_list = pipeline.get_stream_profile_list(OBSensorType.DEPTH_SENSOR)
44
+ depth_profile = profile_list.get_default_video_stream_profile()
45
+ config.enable_stream(depth_profile)
46
+ except Exception as e:
47
+ print(e)
48
+ return
49
+
50
+ if enable_sync:
51
+ try:
52
+ pipeline.enable_frame_sync()
53
+ except Exception as e:
54
+ print(e)
55
+
56
+ try:
57
+ pipeline.start(config)
58
+ except Exception as e:
59
+ print(e)
60
+ return
61
+
62
+ align_filter = AlignFilter(align_to_stream=OBStreamType.COLOR_STREAM)
63
+
64
+ while True:
65
+ try:
66
+ frames = pipeline.wait_for_frames(100)
67
+ if not frames:
68
+ continue
69
+ color_frame = frames.get_color_frame()
70
+ depth_frame = frames.get_depth_frame()
71
+ if not color_frame or not depth_frame:
72
+ continue
73
+ frames = align_filter.process(frames)
74
+ if not frames:
75
+ continue
76
+ frames = frames.as_frame_set()
77
+ color_frame = frames.get_color_frame()
78
+ depth_frame = frames.get_depth_frame()
79
+ if not color_frame or not depth_frame:
80
+ continue
81
+
82
+ color_image = frame_to_bgr_image(color_frame)
83
+ if color_image is None:
84
+ print("Failed to convert frame to image")
85
+ continue
86
+ try:
87
+ depth_data = np.frombuffer(depth_frame.get_data(), dtype=np.uint16).reshape(
88
+ (depth_frame.get_height(), depth_frame.get_width()))
89
+ except ValueError:
90
+ print("Failed to reshape depth data")
91
+ continue
92
+ depth_data = depth_data.astype(np.float32) * depth_frame.get_depth_scale()
93
+ depth_data = np.where((depth_data > MIN_DEPTH) & (depth_data < MAX_DEPTH), depth_data, 0)
94
+ depth_data = depth_data.astype(np.uint16)
95
+ depth_image = cv2.normalize(depth_data, None, 0, 255, cv2.NORM_MINMAX)
96
+ depth_image = cv2.applyColorMap(depth_image.astype(np.uint8), cv2.COLORMAP_JET)
97
+ depth_image = cv2.addWeighted(color_image, 0.5, depth_image, 0.5, 0)
98
+
99
+ cv2.imshow("SyncAlignViewer", depth_image)
100
+ if cv2.waitKey(1) in [ord('q'), ESC_KEY]:
101
+ break
102
+ except KeyboardInterrupt:
103
+ break
104
+ cv2.destroyAllWindows()
105
+ pipeline.stop()
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main(sys.argv[1:])
@@ -0,0 +1,233 @@
1
+ # ******************************************************************************
2
+ # Copyright (c) 2024 Orbbec 3D Technology, Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http:# www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ******************************************************************************
16
+ import json
17
+ import os
18
+ from queue import Queue
19
+ from typing import List
20
+
21
+ import cv2
22
+ import numpy as np
23
+
24
+ from pyorbbecsdk import *
25
+ from utils import frame_to_bgr_image
26
+
27
+ MAX_DEVICES = 2
28
+ curr_device_cnt = 0
29
+
30
+ MAX_QUEUE_SIZE = 5
31
+ ESC_KEY = 27
32
+
33
+ color_frames_queue: List[Queue] = [Queue() for _ in range(MAX_DEVICES)]
34
+ depth_frames_queue: List[Queue] = [Queue() for _ in range(MAX_DEVICES)]
35
+ serial_number_list: List[str] = ["" for _ in range(MAX_DEVICES)]
36
+ has_color_sensor: List[bool] = [False for _ in range(MAX_DEVICES)]
37
+ stop_rendering = False
38
+ multi_device_sync_config = {}
39
+ # config_file_path current file path
40
+ config_file_path = os.path.join(
41
+ os.path.abspath(os.path.dirname(__file__)),
42
+ "../config/multi_device_sync_config.json",
43
+ )
44
+
45
+
46
+ def sync_mode_from_str(sync_mode_str: str) -> OBMultiDeviceSyncMode:
47
+ # to lower case
48
+ sync_mode_str = sync_mode_str.upper()
49
+ if sync_mode_str == "FREE_RUN":
50
+ return OBMultiDeviceSyncMode.FREE_RUN
51
+ elif sync_mode_str == "STANDALONE":
52
+ return OBMultiDeviceSyncMode.STANDALONE
53
+ elif sync_mode_str == "PRIMARY":
54
+ return OBMultiDeviceSyncMode.PRIMARY
55
+ elif sync_mode_str == "SECONDARY":
56
+ return OBMultiDeviceSyncMode.SECONDARY
57
+ elif sync_mode_str == "SECONDARY_SYNCED":
58
+ return OBMultiDeviceSyncMode.SECONDARY_SYNCED
59
+ elif sync_mode_str == "SOFTWARE_TRIGGERING":
60
+ return OBMultiDeviceSyncMode.SOFTWARE_TRIGGERING
61
+ elif sync_mode_str == "HARDWARE_TRIGGERING":
62
+ return OBMultiDeviceSyncMode.HARDWARE_TRIGGERING
63
+ else:
64
+ raise ValueError(f"Invalid sync mode: {sync_mode_str}")
65
+
66
+
67
+ def on_new_frame_callback(frames: FrameSet, index: int):
68
+ global color_frames_queue, depth_frames_queue
69
+ global MAX_QUEUE_SIZE
70
+ assert index < MAX_DEVICES
71
+ color_frame = frames.get_color_frame()
72
+ depth_frame = frames.get_depth_frame()
73
+ if color_frame is not None:
74
+ if color_frames_queue[index].qsize() >= MAX_QUEUE_SIZE:
75
+ color_frames_queue[index].get()
76
+ color_frames_queue[index].put(color_frame)
77
+ if depth_frame is not None:
78
+ if depth_frames_queue[index].qsize() >= MAX_QUEUE_SIZE:
79
+ depth_frames_queue[index].get()
80
+ depth_frames_queue[index].put(depth_frame)
81
+
82
+
83
+ def rendering_frames():
84
+ global color_frames_queue, depth_frames_queue
85
+ global curr_device_cnt
86
+ global stop_rendering
87
+ global serial_number_list
88
+ while not stop_rendering:
89
+ for i in range(curr_device_cnt):
90
+ color_frame = None
91
+ depth_frame = None
92
+ if not color_frames_queue[i].empty():
93
+ color_frame = color_frames_queue[i].get()
94
+ if not depth_frames_queue[i].empty():
95
+ depth_frame = depth_frames_queue[i].get()
96
+ if color_frame is None and depth_frame is None:
97
+ continue
98
+
99
+ # print serial number, color timestamp, depth timestamp, break line
100
+ if color_frame is not None:
101
+ print(f"device#{i}, color frame timestamp: {color_frame.get_timestamp_us()} us , system timestamp: {color_frame.get_system_timestamp_us()} us")
102
+ if depth_frame is not None:
103
+ print(f"device#{i}, depth frame timestamp: {depth_frame.get_timestamp_us()} us , system timestamp: {depth_frame.get_system_timestamp_us()} us")
104
+ color_image = None
105
+ depth_image = None
106
+ color_width, color_height = 0, 0
107
+ if color_frame is not None:
108
+ color_width, color_height = (
109
+ color_frame.get_width(),
110
+ color_frame.get_height(),
111
+ )
112
+ color_image = frame_to_bgr_image(color_frame)
113
+ if depth_frame is not None:
114
+ width = depth_frame.get_width()
115
+ height = depth_frame.get_height()
116
+ scale = depth_frame.get_depth_scale()
117
+ depth_format = depth_frame.get_format()
118
+ if depth_format != OBFormat.Y16:
119
+ print("depth format is not Y16")
120
+ continue
121
+ depth_data = np.frombuffer(depth_frame.get_data(), dtype=np.uint16)
122
+ depth_data = depth_data.reshape((height, width))
123
+
124
+ depth_data = depth_data.astype(np.float32) * scale
125
+
126
+ depth_image = cv2.normalize(
127
+ depth_data, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U
128
+ )
129
+ depth_image = cv2.applyColorMap(depth_image, cv2.COLORMAP_JET)
130
+
131
+ if color_image is not None and depth_image is not None:
132
+ window_size = (color_width // 2, color_height // 2)
133
+ color_image = cv2.resize(color_image, window_size)
134
+ depth_image = cv2.resize(depth_image, window_size)
135
+ image = np.hstack((color_image, depth_image))
136
+ elif depth_image is not None and not has_color_sensor[i]:
137
+ image = depth_image
138
+ else:
139
+ continue
140
+ cv2.imshow("Device {}".format(i), image)
141
+ key = cv2.waitKey(1)
142
+ if key == ord("q") or key == ESC_KEY:
143
+ return
144
+
145
+
146
+ def start_streams(pipelines: List[Pipeline], configs: List[Config]):
147
+ index = 0
148
+ for pipeline, config in zip(pipelines, configs):
149
+ pipeline.start(
150
+ config,
151
+ lambda frame_set, curr_index=index: on_new_frame_callback(
152
+ frame_set, curr_index
153
+ ),
154
+ )
155
+ index += 1
156
+
157
+
158
+ def stop_streams(pipelines: List[Pipeline]):
159
+ for pipeline in pipelines:
160
+ pipeline.stop()
161
+
162
+
163
+ def read_config(config_file: str):
164
+ global multi_device_sync_config
165
+ with open(config_file, "r") as f:
166
+ config = json.load(f)
167
+ for device in config["devices"]:
168
+ multi_device_sync_config[device["serial_number"]] = device
169
+ print(f"Device {device['serial_number']}: {device['config']['mode']}")
170
+
171
+
172
+ def main():
173
+ global config_file_path
174
+ read_config(config_file_path)
175
+ ctx = Context()
176
+ device_list = ctx.query_devices()
177
+ global curr_device_cnt
178
+ curr_device_cnt = device_list.get_count()
179
+ if curr_device_cnt == 0:
180
+ print("No device connected")
181
+ return
182
+ if curr_device_cnt > MAX_DEVICES:
183
+ print("Too many devices connected")
184
+ return
185
+ pipelines: List[Pipeline] = []
186
+ configs: List[Config] = []
187
+ global has_color_sensor
188
+ for i in range(device_list.get_count()):
189
+ device = device_list.get_device_by_index(i)
190
+ pipeline = Pipeline(device)
191
+ config = Config()
192
+ serial_number = device.get_device_info().get_serial_number()
193
+ serial_number_list[i] = serial_number
194
+ sync_config_json = multi_device_sync_config[serial_number]
195
+ sync_config = device.get_multi_device_sync_config()
196
+ sync_config.mode = sync_mode_from_str(sync_config_json["config"]["mode"])
197
+ sync_config.color_delay_us = sync_config_json["config"]["color_delay_us"]
198
+ sync_config.depth_delay_us = sync_config_json["config"]["depth_delay_us"]
199
+ sync_config.trigger_out_enable = sync_config_json["config"]["trigger_out_enable"]
200
+ sync_config.trigger_out_delay_us = sync_config_json["config"]["trigger_out_delay_us"]
201
+ sync_config.frames_per_trigger = sync_config_json["config"]["frames_per_trigger"]
202
+ print(f"Device {serial_number} sync config: {sync_config}")
203
+ device.set_multi_device_sync_config(sync_config)
204
+ try:
205
+ profile_list = pipeline.get_stream_profile_list(OBSensorType.COLOR_SENSOR)
206
+ color_profile: VideoStreamProfile = (
207
+ profile_list.get_default_video_stream_profile()
208
+ )
209
+ config.enable_stream(color_profile)
210
+ has_color_sensor[i] = True
211
+ except OBError as e:
212
+ print(e)
213
+ has_color_sensor[i] = False
214
+ profile_list = pipeline.get_stream_profile_list(OBSensorType.DEPTH_SENSOR)
215
+ depth_profile = profile_list.get_default_video_stream_profile()
216
+ config.enable_stream(depth_profile)
217
+ config.enable_stream(depth_profile)
218
+ pipelines.append(pipeline)
219
+ configs.append(config)
220
+ global stop_rendering
221
+ start_streams(pipelines, configs)
222
+ ctx.enable_multi_device_sync(60000)
223
+ try:
224
+ rendering_frames()
225
+ stop_streams(pipelines)
226
+ except KeyboardInterrupt:
227
+ stop_rendering = True
228
+ stop_streams(pipelines)
229
+ cv2.destroyAllWindows()
230
+
231
+
232
+ if __name__ == "__main__":
233
+ main()