-
Notifications
You must be signed in to change notification settings - Fork 190
Examples/minimal fsync and ptp examples #1797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sb-lxn
wants to merge
4
commits into
develop
Choose a base branch
from
examples/minimal_fsync_and_ptp_examples
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
examples/python/Misc/MultiDevice/external_sync_frame_sync_minimal.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| import depthai as dai | ||
| import contextlib | ||
| from typing import Optional, Dict | ||
| from datetime import timedelta, datetime | ||
| import signal | ||
| import time | ||
| import threading | ||
| import cv2 | ||
|
|
||
| # This example only works on devices that are connected with M8 cables with FSYNC Y splitters | ||
|
|
||
| deviceInfos = dai.Device.getAllAvailableDevices() | ||
| targetFps = 30 | ||
| resolution = (640, 480) | ||
| syncThresholdSec = 1e-3 # 1ms | ||
| running = True | ||
|
|
||
| def interruptHandler(sig, frame): | ||
| global running | ||
| if running: | ||
| print("Interrupted! Exiting...") | ||
| running = False | ||
| else: | ||
| print("Exiting now!") | ||
| exit(0) | ||
|
|
||
| signal.signal(signal.SIGINT, interruptHandler) | ||
|
|
||
| def getDeviceName(device : dai.Device) -> str: | ||
| info = device.getDeviceInfo() | ||
| name = info.deviceId | ||
| if info.name is not None and info.name != "": | ||
| name += "[" + info.name + "]" | ||
| return name | ||
|
|
||
| with contextlib.ExitStack() as stack: | ||
| # Variables to keep track of master and slave pipelines and outputs | ||
| masterPipeline: Optional[dai.Pipeline] = None | ||
| masterNode: Optional[Dict[str, dai.Node.Output]] = None | ||
| masterName: Optional[str] = None | ||
|
|
||
| slavePipelines: Dict[str, dai.Pipeline] = {} | ||
| slaveQueues: Dict[str, Dict[str, dai.MessageQueue]] = {} | ||
|
|
||
| # keep track of sync node inputs for slaves | ||
| inputQueues = {} | ||
|
|
||
| # keep track of all sync node output names | ||
| outputNames = [] | ||
|
|
||
| for deviceInfo in deviceInfos: | ||
| # Create pipeline for each device | ||
| devicePipeline = stack.enter_context(dai.Pipeline(dai.Device(deviceInfo))) | ||
| device = devicePipeline.getDefaultDevice() | ||
| deviceName = getDeviceName(device) | ||
| fsyncRole = device.getExternalFrameSyncRole() | ||
|
|
||
| for socket in device.getConnectedCameras(): | ||
| # create a queue for each camera on the device | ||
| if fsyncRole == dai.ExternalFrameSyncRole.MASTER: | ||
| cam = devicePipeline.create(dai.node.Camera).build(socket, sensorFps=targetFps) | ||
| else: | ||
| # slaves will lock to the master's FPS | ||
| cam = devicePipeline.create(dai.node.Camera).build(socket) | ||
| outputNode = cam.requestOutput(resolution, dai.ImgFrame.Type.NV12, dai.ImgResizeMode.CROP) | ||
|
|
||
| # Master cameras will be linked to the sync node directly | ||
| if fsyncRole == dai.ExternalFrameSyncRole.MASTER: | ||
| if masterNode is None: | ||
| masterNode = {} | ||
|
|
||
| masterNode[socket.name] = outputNode | ||
|
|
||
| # Gather all slave camera outputs | ||
| elif fsyncRole == dai.ExternalFrameSyncRole.SLAVE: | ||
| if slaveQueues.get(deviceName) is None: | ||
| slaveQueues[deviceName] = {} | ||
| slaveQueues[deviceName][socket.name] = outputNode.createOutputQueue() | ||
|
|
||
| if fsyncRole == dai.ExternalFrameSyncRole.MASTER: | ||
| device.setExternalStrobeEnable(True) | ||
| print(f"{device.getDeviceId()} is master") | ||
|
|
||
| if masterPipeline is not None: | ||
| raise RuntimeError("Only one master pipeline is supported") | ||
|
|
||
| masterPipeline = devicePipeline | ||
| masterName = deviceName | ||
| elif fsyncRole == dai.ExternalFrameSyncRole.SLAVE: | ||
| slavePipelines[deviceName] = devicePipeline | ||
| print(f"{device.getDeviceId()} is slave") | ||
|
|
||
| if masterPipeline is None or masterNode is None: | ||
| raise RuntimeError("No master detected!") | ||
|
|
||
| if len(slavePipelines) < 1: | ||
| raise RuntimeError("No slaves detected!") | ||
|
|
||
| # Create sync node | ||
| syncNode = masterPipeline.create(dai.node.Sync) | ||
|
|
||
| # Sync node will run on the host, since it needs to sync multiple devices | ||
| syncNode.setRunOnHost(True) | ||
| # group frames into pairs that are within 1/2 frame period | ||
| syncNode.setSyncThreshold(timedelta(milliseconds=1000 / (2 * targetFps))) | ||
|
|
||
| # Link master camera outputs to the sync node | ||
| for socketName, camOutput in masterNode.items(): | ||
| name = f"master_{masterName}_{socketName}" | ||
| camOutput.link(syncNode.inputs[name]) | ||
| outputNames.append(name) | ||
|
|
||
| # For slaves, we must create an input queue for each output | ||
| # We will then manually forward the frames from each input queue to the output queue | ||
| # This is because slave devices have separate pipelines from the master | ||
| for deviceName, sockets in slaveQueues.items(): | ||
| for socketName, _ in sockets.items(): | ||
| name = f"slave_{deviceName}_{socketName}" | ||
| outputNames.append(name) | ||
| input_queue = syncNode.inputs[name].createInputQueue() | ||
| inputQueues[name] = input_queue | ||
|
|
||
| syncedGroups = syncNode.out.createOutputQueue() | ||
|
|
||
| # thread worker for forwarding slave queues to sync node | ||
| def data_collector(deviceName, socketName): | ||
| # Send frames from slave output queues to sync node input queues | ||
| camOutputQueue = slaveQueues[deviceName][socketName] | ||
| while running: | ||
| if camOutputQueue.has(): | ||
| inputQueues[f"slave_{deviceName}_{socketName}"].send(camOutputQueue.get()) | ||
| else: | ||
| time.sleep(0.001) | ||
|
|
||
| # Start pipelines | ||
| masterPipeline.start() | ||
| for _, slavePipeline in slavePipelines.items(): | ||
| slavePipeline.start() | ||
|
|
||
| # Start threads | ||
| threads = {} | ||
| for deviceName, sockets in slaveQueues.items(): | ||
| for socketName, camOutputQueue in sockets.items(): | ||
| threads[f"slave_{deviceName}_{socketName}"] = threading.Thread(target=data_collector, args=(deviceName, socketName)) | ||
| threads[f"slave_{deviceName}_{socketName}"].start() | ||
|
|
||
| # main display loop | ||
| latestFrameGroup = None | ||
| while running: | ||
| # Get frames from sync node output queue | ||
| while syncedGroups.has(): | ||
| latestFrameGroup = syncedGroups.get() | ||
|
|
||
| if latestFrameGroup is not None and latestFrameGroup.getNumMessages() == len(outputNames): | ||
| tsValues = {} | ||
| for name in outputNames: | ||
| tsValues[name] = latestFrameGroup[name].getTimestamp(dai.CameraExposureOffset.END).total_seconds() | ||
|
|
||
| delta = max(tsValues.values()) - min(tsValues.values()) | ||
| syncStatus = abs(delta) < syncThresholdSec | ||
|
|
||
| if not syncStatus: | ||
| print(f"Sync error: Sync lost, threshold exceeded {delta * 1e6} us") | ||
| continue | ||
|
|
||
| for outputName in outputNames: | ||
| msg = latestFrameGroup[outputName] | ||
| frame = msg.getCvFrame() | ||
| cv2.imshow(f"synced_view_{outputName}", frame) | ||
|
|
||
| latestFrameGroup = None # Wait for next batch | ||
|
|
||
| if cv2.waitKey(1) & 0xFF == ord("q"): | ||
| running = False | ||
| break | ||
|
|
||
| for t in threads.keys(): | ||
| threads[t].join() | ||
| cv2.destroyAllWindows() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Remove unused loop variable
camOutputQueue.The variable
camOutputQueueis bound in the loop but never used. The worker function retrieves the queue fromslaveQueuesdirectly (line 128).♻️ Simplify the loop
threads = {} for deviceName, sockets in slaveQueues.items(): - for socketName, camOutputQueue in sockets.items(): + for socketName in sockets.keys(): threads[f"slave_{deviceName}_{socketName}"] = threading.Thread(target=data_collector, args=(deviceName, socketName)) threads[f"slave_{deviceName}_{socketName}"].start()📝 Committable suggestion
🧰 Tools
🪛 Ruff (0.15.12)
[warning] 143-143: Loop control variable
camOutputQueuenot used within loop bodyRename unused
camOutputQueueto_camOutputQueue(B007)
[warning] 143-143: When using only the keys of a dict use the
keys()methodReplace
.items()with.keys()(PERF102)
🤖 Prompt for AI Agents