Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/cpp/Misc/MultiDevice/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ cmake_minimum_required(VERSION 3.10)
## function: dai_add_example(example_name example_src enable_test use_pcl)
## function: dai_set_example_test_labels(example_name ...)

dai_add_example(multi_device_frame_sync multi_device_frame_sync.cpp OFF OFF)
dai_add_example(multi_device_frame_sync multi_device_frame_sync.cpp OFF OFF)
dai_add_example(ptp_frame_sync_minimal ptp_frame_sync_minimal.cpp OFF OFF)
dai_add_example(external_sync_frame_sync_minimal external_sync_frame_sync_minimal.cpp OFF OFF)
233 changes: 233 additions & 0 deletions examples/cpp/Misc/MultiDevice/external_sync_frame_sync_minimal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <thread>
#include <utility>
#include <vector>

#include <opencv2/highgui.hpp>

#include "depthai/depthai.hpp"

namespace {

constexpr float TARGET_FPS = 30.0f;
constexpr std::pair<uint32_t, uint32_t> RESOLUTION{640, 480};
constexpr double SYNC_THRESHOLD_SEC = 1e-3;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical threshold mismatch between sync node and validation.

Same issue as ptp_frame_sync_minimal.cpp: the sync node threshold at line 129 (~16.67ms) does not match the validation threshold SYNC_THRESHOLD_SEC (1ms) used at line 208. Most synchronized frames will fail validation.

🐛 Proposed fix: align both thresholds
-constexpr double SYNC_THRESHOLD_SEC = 1e-3;
+constexpr double SYNC_THRESHOLD_SEC = 0.5 / TARGET_FPS;

Also applies to: 129-129, 208-211

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cpp/Misc/MultiDevice/external_sync_frame_sync_minimal.cpp` at line
25, The validation constant SYNC_THRESHOLD_SEC is 1e-3 but the sync node uses a
~16.67ms threshold, causing false failures; fix by making both use the same
value — either set SYNC_THRESHOLD_SEC to 1.0/60.0 (≈0.016667) or replace the
literal threshold in the sync node setup with SYNC_THRESHOLD_SEC so the sync
node and the validation (SYNC_THRESHOLD_SEC) are identical.


std::atomic_bool running{true};

void interruptHandler(int) {
if(running.exchange(false)) {
std::cout << "Interrupted! Exiting..." << std::endl;
} else {
std::cout << "Exiting now!" << std::endl;
std::exit(0);
}
}

std::string getDeviceName(const std::shared_ptr<dai::Device>& device) {
const auto info = device->getDeviceInfo();
auto name = info.deviceId;
if(!info.name.empty()) {
name += "[" + info.name + "]";
}
return name;
}

} // namespace

int main() {
signal(SIGINT, interruptHandler);

// This example only works on devices that are connected with M8 cables with FSYNC Y splitters.
const auto deviceInfos = dai::Device::getAllAvailableDevices();

// Variables to keep track of master and slave pipelines and outputs
std::shared_ptr<dai::Pipeline> masterPipeline;
std::optional<std::map<std::string, dai::Node::Output*>> masterOutputs;
std::optional<std::string> masterName;

std::map<std::string, std::shared_ptr<dai::Pipeline>> slavePipelines;
std::map<std::string, std::map<std::string, std::shared_ptr<dai::MessageQueue>>> slaveQueues;

// keep track of sync node inputs for slaves
std::map<std::string, std::shared_ptr<dai::InputQueue>> inputQueues;
// keep track of all sync node output names
std::vector<std::string> outputNames;

for(const auto& deviceInfo : deviceInfos) {
// Create pipeline for each device
auto pipeline = std::make_shared<dai::Pipeline>(std::make_shared<dai::Device>(deviceInfo));
auto device = pipeline->getDefaultDevice();
const auto deviceName = getDeviceName(device);
const auto fsyncRole = device->getExternalFrameSyncRole();

for(const auto socket : device->getConnectedCameras()) {
// create a queue for each camera on the device
std::shared_ptr<dai::node::Camera> cam;
if(fsyncRole == dai::ExternalFrameSyncRole::MASTER) {
cam = pipeline->create<dai::node::Camera>()->build(socket, std::nullopt, TARGET_FPS);
} else {
// slaves will lock to the master's FPS
cam = pipeline->create<dai::node::Camera>()->build(socket);
}

auto* output = cam->requestOutput(RESOLUTION, dai::ImgFrame::Type::NV12, dai::ImgResizeMode::CROP);
const auto socketName = dai::toString(socket);

// Master cameras will be linked to the sync node directly
if(fsyncRole == dai::ExternalFrameSyncRole::MASTER) {
if(!masterOutputs.has_value()) {
masterOutputs.emplace();
}
(*masterOutputs)[socketName] = output;
// Gather all slave camera outputs
} else if(fsyncRole == dai::ExternalFrameSyncRole::SLAVE) {
slaveQueues[deviceName][socketName] = output->createOutputQueue();
}
}

if(fsyncRole == dai::ExternalFrameSyncRole::MASTER) {
device->setExternalStrobeEnable(true);
std::cout << device->getDeviceId() << " is master" << std::endl;

if(masterPipeline != nullptr) {
throw std::runtime_error("Only one master pipeline is supported");
}

masterPipeline = pipeline;
masterName = deviceName;
} else if(fsyncRole == dai::ExternalFrameSyncRole::SLAVE) {
slavePipelines[deviceName] = pipeline;
std::cout << device->getDeviceId() << " is slave" << std::endl;
}
}

if(masterPipeline == nullptr || !masterOutputs.has_value() || !masterName.has_value()) {
throw std::runtime_error("No master detected!");
}

if(slavePipelines.empty()) {
throw std::runtime_error("No slaves detected!");
}

// Create sync node
auto sync = masterPipeline->create<dai::node::Sync>();
// Sync node will run on the host, since it needs to sync multiple devices
sync->setRunOnHost(true);
// group frames into pairs that are within 1/2 frame period
sync->setSyncThreshold(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::duration<double>(0.5 / TARGET_FPS)));

// Link master camera outputs to the sync node
for(const auto& [socketName, output] : *masterOutputs) {
const auto name = "master_" + *masterName + "_" + socketName;
output->link(sync->inputs[name]);
outputNames.push_back(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(const auto& [deviceName, sockets] : slaveQueues) {
for(const auto& [socketName, queue] : sockets) {
(void)queue;
const auto name = "slave_" + deviceName + "_" + socketName;
outputNames.push_back(name);
inputQueues[name] = sync->inputs[name].createInputQueue();
}
}

auto syncedGroups = sync->out.createOutputQueue();

// thread worker for forwarding slave queues to sync node
auto dataCollector = [&](std::string deviceName, std::string socketName) {
const auto queueName = "slave_" + deviceName + "_" + socketName;
auto camOutputQueue = slaveQueues.at(deviceName).at(socketName);
auto inputQueue = inputQueues.at(queueName);

// Send frames from slave output queues to sync node input queues
while(running.load()) {
if(camOutputQueue->has()) {
inputQueue->send(camOutputQueue->get());
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
};

// Start pipelines
masterPipeline->start();
for(const auto& [deviceName, pipeline] : slavePipelines) {
(void)deviceName;
pipeline->start();
}

// Start threads
std::vector<std::thread> threads;
for(const auto& [deviceName, sockets] : slaveQueues) {
for(const auto& [socketName, queue] : sockets) {
(void)queue;
threads.emplace_back(dataCollector, deviceName, socketName);
}
}

std::optional<std::shared_ptr<dai::MessageGroup>> latestFrameGroup;

// main display loop
while(running.load()) {
// Get frames from sync node output queue
while(syncedGroups->has()) {
latestFrameGroup = syncedGroups->get<dai::MessageGroup>();
}

if(latestFrameGroup.has_value() && static_cast<size_t>(latestFrameGroup.value()->getNumMessages()) == outputNames.size()) {
using ts_type = std::chrono::time_point<std::chrono::steady_clock>;
std::map<std::string, ts_type> tsValues;
for(auto name : outputNames) {
auto frame = latestFrameGroup.value()->get<dai::ImgFrame>(name);
tsValues.emplace(name, frame->getTimestamp(dai::CameraExposureOffset::END));
}
Comment on lines +196 to +199

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Use const reference to avoid string copies.

Same issue as ptp_frame_sync_minimal.cpp - name is copied by value.

♻️ Proposed fix
-            for(auto name : outputNames) {
+            for(const auto& name : outputNames) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for(auto name : outputNames) {
auto frame = latestFrameGroup.value()->get<dai::ImgFrame>(name);
tsValues.emplace(name, frame->getTimestamp(dai::CameraExposureOffset::END));
}
for(const auto& name : outputNames) {
auto frame = latestFrameGroup.value()->get<dai::ImgFrame>(name);
tsValues.emplace(name, frame->getTimestamp(dai::CameraExposureOffset::END));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/cpp/Misc/MultiDevice/external_sync_frame_sync_minimal.cpp` around
lines 196 - 199, The loop over outputNames copies each string into the loop
variable; change the range-for to use a const reference (e.g., for(const auto&
name : outputNames)) in the block where tsValues is filled (the loop that calls
latestFrameGroup.value()->get<dai::ImgFrame>(name) and tsValues.emplace(name,
...)) to avoid unnecessary string copies while preserving usage of name as the
map key.

auto compFunct = [](const std::pair<std::string, ts_type>& p1, const std::pair<std::string, ts_type>& p2) -> bool { return p1.second < p2.second; };

auto maxElement = std::max_element(tsValues.begin(), tsValues.end(), compFunct);
auto minElement = std::min_element(tsValues.begin(), tsValues.end(), compFunct);

auto delta = maxElement->second - minElement->second;
auto deltaUs = std::chrono::duration_cast<std::chrono::microseconds>(delta).count();

if(deltaUs >= SYNC_THRESHOLD_SEC * 1e6) {
std::cout << "Sync error: Sync lost, threshold exceeded " << deltaUs << " us" << std::endl;
continue;
}

for(const auto& outputName : outputNames) {
auto frame = latestFrameGroup.value()->get<dai::ImgFrame>(outputName);
cv::imshow("synced_view_" + outputName, frame->getCvFrame());
}

latestFrameGroup.reset(); // Wait for next batch
}

if((cv::waitKey(1) & 0xFF) == 'q') {
running.store(false);
break;
}
}

for(auto& thread : threads) {
thread.join();
}

cv::destroyAllWindows();
return 0;
}
9 changes: 6 additions & 3 deletions examples/cpp/Misc/MultiDevice/multi_device_frame_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ void setUpCameraSocket(std::shared_ptr<dai::Pipeline>& pipeline,
std::optional<dai::ExternalFrameSyncRole> role,
std::optional<std::map<std::string, dai::Node::Output*>>& masterNode,
std::map<std::string, std::map<std::string, std::shared_ptr<dai::MessageQueue>>>& slaveQueues,
std::vector<std::string>& camSockets) {
std::vector<std::string>& camSockets,
std::optional<std::string>& masterName) {
auto outNode = createCameraOutputs(pipeline, socket, targetFps, syncType, role);

if(syncType == SyncType::EXTERNAL) {
Expand All @@ -162,6 +163,9 @@ void setUpCameraSocket(std::shared_ptr<dai::Pipeline>& pipeline,
// Actual PTP master might be different, but it doesn't matter for this example.
if(!masterNode.has_value()) {
masterNode.emplace();
masterName = name;
}
if (masterName == name) {
masterNode.value().emplace(dai::toString(socket), outNode);
} else {
if(slaveQueues.find(name) == slaveQueues.end()) {
Expand Down Expand Up @@ -214,7 +218,7 @@ void setupDevice(dai::DeviceInfo& deviceInfo,
std::cout << " Num of cameras: " << device->getConnectedCameras().size() << std::endl;

for(auto socket : device->getConnectedCameras()) {
setUpCameraSocket(pipeline, socket, name, targetFps, syncType, role, masterNode, slaveQueues, camSockets);
setUpCameraSocket(pipeline, socket, name, targetFps, syncType, role, masterNode, slaveQueues, camSockets, masterName);
}

if(syncType == SyncType::EXTERNAL) {
Expand All @@ -238,7 +242,6 @@ void setupDevice(dai::DeviceInfo& deviceInfo,
// Actual PTP master might be different, but it doesn't matter for this example.
if(masterPipeline == nullptr) {
masterPipeline = pipeline;
masterName = name;
} else {
slavePipelines[name] = pipeline;
}
Expand Down
Loading