Skip to content

Commit 405b88f

Browse files
authored
Merge branch 'main' into disparity_shift
2 parents 5be75ed + cdbbc95 commit 405b88f

File tree

4 files changed

+247
-29
lines changed

4 files changed

+247
-29
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Frame syncing on OAK
2+
====================
3+
4+
This example showcases how you can use :ref:`Script` node to sync frames from multiple streams. It uses :ref:`ImgFrame`'s timestamps to achieve syncing precision.
5+
6+
Similar syncing demo scripts (python) can be found at our depthai-experiments repository in `gen2-syncing <https://github.com/luxonis/depthai-experiments/tree/master/gen2-syncing>`__
7+
folder.
8+
9+
Demo
10+
####
11+
12+
Terminal log after about 13 minutes. Color and disparity streams are perfectly in-sync.
13+
14+
.. code-block:: bash
15+
16+
[1662574807.8811488] Stream rgb, timestamp: 7:26:21.601595, sequence number: 21852
17+
[1662574807.8821492] Stream disp, timestamp: 7:26:21.601401, sequence number: 21852
18+
19+
[1662574807.913144] Stream rgb, timestamp: 7:26:21.634982, sequence number: 21853
20+
[1662574807.9141443] Stream disp, timestamp: 7:26:21.634730, sequence number: 21853
21+
22+
[1662574807.9451444] Stream rgb, timestamp: 7:26:21.668243, sequence number: 21854
23+
[1662574807.946151] Stream disp, timestamp: 7:26:21.668057, sequence number: 21854
24+
25+
Setup
26+
#####
27+
28+
.. include:: /includes/install_from_pypi.rst
29+
30+
.. include:: /includes/install_req.rst
31+
32+
Source code
33+
###########
34+
35+
.. tabs::
36+
37+
.. tab:: Python
38+
39+
Also `available on GitHub <https://github.com/luxonis/depthai-python/blob/main/examples/mixed/frame_sync.py>`__
40+
41+
.. literalinclude:: ../../../../examples/mixed/frame_sync.py
42+
:language: python
43+
:linenos:
44+
45+
.. tab:: C++
46+
47+
Also `available on GitHub <https://github.com/luxonis/depthai-core/blob/main/examples/mixed/frame_sync.cpp>`__
48+
49+
.. literalinclude:: ../../../../depthai-core/examples/mixed/frame_sync.cpp
50+
:language: cpp
51+
:linenos:
52+
53+
.. include:: /includes/footer-short.rst

examples/mixed/frame_sync.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import depthai as dai
2+
import time
3+
4+
FPS = 30
5+
6+
pipeline = dai.Pipeline()
7+
8+
# Define a source - color camera
9+
camRgb = pipeline.create(dai.node.ColorCamera)
10+
# Since we are saving RGB frames in Script node we need to make the
11+
# video pool size larger, otherwise the pipeline will freeze because
12+
# the ColorCamera won't be able to produce new video frames.
13+
camRgb.setVideoNumFramesPool(10)
14+
camRgb.setFps(FPS)
15+
16+
left = pipeline.create(dai.node.MonoCamera)
17+
left.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
18+
left.setBoardSocket(dai.CameraBoardSocket.LEFT)
19+
left.setFps(FPS)
20+
21+
right = pipeline.create(dai.node.MonoCamera)
22+
right.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
23+
right.setBoardSocket(dai.CameraBoardSocket.RIGHT)
24+
right.setFps(FPS)
25+
26+
stereo = pipeline.createStereoDepth()
27+
stereo.initialConfig.setMedianFilter(dai.MedianFilter.KERNEL_7x7)
28+
stereo.setLeftRightCheck(True)
29+
stereo.setExtendedDisparity(False)
30+
stereo.setSubpixel(False)
31+
left.out.link(stereo.left)
32+
right.out.link(stereo.right)
33+
34+
# Script node will sync high-res frames
35+
script = pipeline.create(dai.node.Script)
36+
37+
# Send both streams to the Script node so we can sync them
38+
stereo.disparity.link(script.inputs["disp_in"])
39+
camRgb.video.link(script.inputs["rgb_in"])
40+
41+
script.setScript("""
42+
FPS=30
43+
import time
44+
from datetime import timedelta
45+
import math
46+
47+
# Timestamp threshold (in miliseconds) under which frames will be considered synced.
48+
# Lower number means frames will have less delay between them, which can potentially
49+
# lead to dropped frames.
50+
MS_THRESHOL=math.ceil(500 / FPS)
51+
52+
def check_sync(queues, timestamp):
53+
matching_frames = []
54+
for name, list in queues.items(): # Go through each available stream
55+
# node.warn(f"List {name}, len {str(len(list))}")
56+
for i, msg in enumerate(list): # Go through each frame of this stream
57+
time_diff = abs(msg.getTimestamp() - timestamp)
58+
if time_diff <= timedelta(milliseconds=MS_THRESHOL): # If time diff is below threshold, this frame is considered in-sync
59+
matching_frames.append(i) # Append the position of the synced frame, so we can later remove all older frames
60+
break
61+
62+
if len(matching_frames) == len(queues):
63+
# We have all frames synced. Remove the excess ones
64+
i = 0
65+
for name, list in queues.items():
66+
queues[name] = queues[name][matching_frames[i]:] # Remove older (excess) frames
67+
i+=1
68+
return True
69+
else:
70+
return False # We don't have synced frames yet
71+
72+
names = ['disp', 'rgb']
73+
frames = dict() # Dict where we store all received frames
74+
for name in names:
75+
frames[name] = []
76+
77+
while True:
78+
for name in names:
79+
f = node.io[name+"_in"].tryGet()
80+
if f is not None:
81+
frames[name].append(f) # Save received frame
82+
83+
if check_sync(frames, f.getTimestamp()): # Check if we have any synced frames
84+
# Frames synced!
85+
node.info(f"Synced frame!")
86+
# node.warn(f"Queue size. Disp: {len(frames['disp'])}, rgb: {len(frames['rgb'])}")
87+
for name, list in frames.items():
88+
syncedF = list.pop(0) # We have removed older (excess) frames, so at positions 0 in dict we have synced frames
89+
node.info(f"{name}, ts: {str(syncedF.getTimestamp())}, seq {str(syncedF.getSequenceNum())}")
90+
node.io[name+'_out'].send(syncedF) # Send synced frames to the host
91+
92+
93+
time.sleep(0.001) # Avoid lazy looping
94+
""")
95+
96+
script_out = ['disp', 'rgb']
97+
98+
for name in script_out: # Create XLinkOut for disp/rgb streams
99+
xout = pipeline.create(dai.node.XLinkOut)
100+
xout.setStreamName(name)
101+
script.outputs[name+'_out'].link(xout.input)
102+
103+
with dai.Device(pipeline) as device:
104+
device.setLogLevel(dai.LogLevel.INFO)
105+
device.setLogOutputLevel(dai.LogLevel.INFO)
106+
names = ['rgb', 'disp']
107+
queues = [device.getOutputQueue(name) for name in names]
108+
109+
while True:
110+
for q in queues:
111+
img: dai.ImgFrame = q.get()
112+
# Display timestamp/sequence number of two synced frames
113+
print(f"Time: {time.time()}. Stream {q.getName()}, timestamp: {img.getTimestamp()}, sequence number: {img.getSequenceNum()}")

utilities/device_manager.py

100644100755
Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ def check_mac(s):
5454
return True
5555

5656
class Progress:
57-
def __init__(self):
57+
def __init__(self, txt = 'Flashing progress: 0.0%'):
5858
layout = [
59-
[sg.Text('Flashing progress: 0.0%', key='txt')],
59+
[sg.Text(txt, key='txt')],
6060
[sg.ProgressBar(1.0, orientation='h', size=(20,20), key='progress')],
6161
]
62-
self.self.window = sg.Window("Progress", layout, modal=True, finalize=True)
62+
self.window = sg.Window("Progress", layout, modal=True, finalize=True)
6363
def update(self, val):
6464
self.window['progress'].update(val)
6565
self.window['txt'].update(f'Flashing progress: {val*100:.1f}%')
@@ -83,8 +83,11 @@ def __init__(self, options, text):
8383
def wait(self):
8484
event, values = self.window.Read()
8585
self.window.close()
86-
type = getattr(dai.DeviceBootloader.Type, values['bootType'])
87-
return (str(event) == "Submit", type)
86+
if values is not None:
87+
type = getattr(dai.DeviceBootloader.Type, values['bootType'])
88+
return (str(event) == "Submit", type)
89+
else:
90+
return (False, None)
8891

8992
class SelectIP:
9093
def __init__(self):
@@ -158,17 +161,13 @@ def wait(self) -> dai.DeviceInfo:
158161
self.window.close()
159162
return deviceSelected
160163

161-
def flashBootloader(device: dai.DeviceInfo):
164+
def flashBootloader(device: dai.DeviceInfo, type: dai.DeviceBootloader.Type):
162165
try:
163-
sel = SelectBootloader(['AUTO', 'USB', 'NETWORK'], "Select bootloader type to flash.")
164-
ok, type = sel.wait()
165-
if not ok:
166-
print("Flashing bootloader canceled.")
167-
return
166+
167+
pr = Progress('Connecting...')
168168

169169
bl = dai.DeviceBootloader(device, True)
170170

171-
pr = Progress()
172171
progress = lambda p : pr.update(p)
173172
if type == dai.DeviceBootloader.Type.AUTO:
174173
type = bl.getType()
@@ -178,22 +177,19 @@ def flashBootloader(device: dai.DeviceInfo):
178177
PrintException()
179178
sg.Popup(f'{ex}')
180179

181-
def factoryReset(bl: dai.DeviceBootloader):
182-
sel = SelectBootloader(['USB', 'NETWORK'], "Select bootloader type used for factory reset.")
183-
ok, type = sel.wait()
184-
if not ok:
185-
print("Factory reset canceled.")
186-
return
187-
180+
def factoryReset(device: dai.DeviceInfo, type: dai.DeviceBootloader.Type):
188181
try:
182+
pr = Progress('Preparing and connecting...')
183+
189184
blBinary = dai.DeviceBootloader.getEmbeddedBootloaderBinary(type)
190185
# Clear 1 MiB for USB BL and 8 MiB for NETWORK BL
191186
mib = 1 if type == dai.DeviceBootloader.Type.USB else 8
192187
blBinary = blBinary + ([0xFF] * ((mib * 1024 * 1024 + 512) - len(blBinary)))
193188
tmpBlFw = tempfile.NamedTemporaryFile(delete=False)
194189
tmpBlFw.write(bytes(blBinary))
195190

196-
pr = Progress()
191+
bl = dai.DeviceBootloader(device, True)
192+
197193
progress = lambda p : pr.update(p)
198194
success, msg = bl.flashBootloader(progress, tmpBlFw.name)
199195
msg = "Factory reset was successful." if success else f"Factory reset failed. Error: {msg}"
@@ -216,9 +212,11 @@ def flashFromFile(file, bl: dai.DeviceBootloader):
216212
def recoveryMode(bl: dai.DeviceBootloader):
217213
try:
218214
bl.bootUsbRomBootloader()
215+
return True
219216
except Exception as ex:
220217
PrintException()
221218
sg.Popup(f'{ex}')
219+
return False
222220

223221
def connectToDevice(device: dai.DeviceInfo) -> dai.DeviceBootloader:
224222
try:
@@ -342,9 +340,11 @@ def deviceStateTxt(state: dai.XLinkDeviceState) -> str:
342340
],
343341
[sg.HSeparator()],
344342
[
345-
sg.Text("", size=(10, 2)),
343+
sg.Text("", size=(1, 2)),
346344
sg.Button("Flash configuration", size=(15, 2), font=('Arial', 10, 'bold'), disabled=True,
347345
button_color='#FFA500'),
346+
sg.Button("Clear configuration", size=(15, 2), font=('Arial', 10, 'bold'), disabled=True,
347+
button_color='#FFA500'),
348348
sg.Button("Clear flash", size=(15, 2), font=('Arial', 10, 'bold'), disabled=True,
349349
button_color='#FFA500'),
350350
sg.Button("Flash DAP", size=(15, 2), font=('Arial', 10, 'bold'), disabled=True,
@@ -443,9 +443,33 @@ def run(self) -> None:
443443
self.getConfigs()
444444
self.unlockConfig()
445445
elif event == "Flash newest Bootloader":
446-
self.closeDevice() # We will reconnect, as we need to set allowFlashingBootloader to True
447-
flashBootloader(self.device)
448-
self.window.Element('currBoot').update(self.bl.getVersion())
446+
sel = SelectBootloader(['AUTO', 'USB', 'NETWORK'], "Select bootloader type to flash.")
447+
ok, type = sel.wait()
448+
if ok:
449+
# We will reconnect, as we need to set allowFlashingBootloader to True
450+
self.closeDevice()
451+
flashBootloader(self.device, type)
452+
# Device will reboot, close previous and reset GUI
453+
self.closeDevice()
454+
self.resetGui()
455+
self.getDevices()
456+
else:
457+
print("Flashing bootloader cancelled.")
458+
459+
elif event == "Factory reset":
460+
sel = SelectBootloader(['NETWORK', 'USB'], "Select bootloader type used for factory reset.")
461+
ok, type = sel.wait()
462+
if ok:
463+
# We will reconnect, as we need to set allowFlashingBootloader to True
464+
self.closeDevice()
465+
factoryReset(self.device, type)
466+
# Device will reboot, close previous and reset GUI
467+
self.closeDevice()
468+
self.resetGui()
469+
self.getDevices()
470+
else:
471+
print("Factory reset cancelled.")
472+
449473
elif event == "Flash configuration":
450474
self.flashConfig()
451475
self.getConfigs()
@@ -455,8 +479,16 @@ def run(self) -> None:
455479
else:
456480
self.devices.clear()
457481
self.window.Element('devices').update("Search for devices", values=[])
458-
elif event == "Factory reset":
459-
factoryReset(self.bl)
482+
elif event == "Clear configuration":
483+
self.clearConfig()
484+
self.getConfigs()
485+
self.resetGui()
486+
if self.isUsb():
487+
self.unlockConfig()
488+
else:
489+
self.devices.clear()
490+
self.window.Element('devices').update("Search for devices", values=[])
491+
460492
elif event == "Flash DAP":
461493
file = sg.popup_get_file("Select .dap file", file_types=(('DepthAI Application Package', '*.dap'), ('All Files', '*.* *')))
462494
flashFromFile(file, self.bl)
@@ -467,7 +499,12 @@ def run(self) -> None:
467499
self.window['-COL2-'].update(visible=False)
468500
self.window['-COL1-'].update(visible=True)
469501
elif event == "recoveryMode":
470-
recoveryMode(self.bl)
502+
if recoveryMode(self.bl):
503+
sg.Popup(f'Device successfully put into USB recovery mode.')
504+
# Device will reboot, close previous and reset GUI
505+
self.closeDevice()
506+
self.resetGui()
507+
self.getDevices()
471508
self.window.close()
472509

473510
@property
@@ -540,6 +577,7 @@ def unlockConfig(self):
540577

541578
self.window['Flash newest Bootloader'].update(disabled=False)
542579
self.window['Flash configuration'].update(disabled=False)
580+
self.window['Clear configuration'].update(disabled=False)
543581
self.window['Factory reset'].update(disabled=False)
544582
# self.window['Clear flash'].update(disabled=False)
545583
self.window['Flash DAP'].update(disabled=False)
@@ -559,6 +597,7 @@ def resetGui(self):
559597

560598
self.window['Flash newest Bootloader'].update(disabled=True)
561599
self.window['Flash configuration'].update(disabled=True)
600+
self.window['Clear configuration'].update(disabled=True)
562601
self.window['Factory reset'].update(disabled=True)
563602
self.window['Clear flash'].update(disabled=True)
564603
self.window['Flash DAP'].update(disabled=True)
@@ -591,7 +630,9 @@ def getDevices(self):
591630
deviceTxt = deviceInfo.getMxId()
592631
listedDevices.append(deviceTxt)
593632
self.devices[deviceTxt] = deviceInfo
594-
self.window.Element('devices').update("Select device", values=listedDevices)
633+
634+
# Update the list regardless
635+
self.window.Element('devices').update("Select device", values=listedDevices)
595636
except Exception as ex:
596637
PrintException()
597638
sg.Popup(f'{ex}')
@@ -634,6 +675,17 @@ def flashConfig(self):
634675
except Exception as ex:
635676
PrintException()
636677
sg.Popup(f'{ex}')
678+
def clearConfig(self):
679+
try:
680+
success, error = self.bl.flashConfigClear()
681+
if not success:
682+
sg.Popup(f"Clearing configuration failed: {error}")
683+
else:
684+
sg.Popup("Successfully cleared configuration.")
685+
except Exception as ex:
686+
PrintException()
687+
sg.Popup(f'{ex}')
688+
637689

638690
app = DeviceManager()
639691
app.run()

0 commit comments

Comments
 (0)