// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #pragma once #include #include #include // std::setw #include // This is the maximum difference between when we expected an image's timestamp to be and when it actually occurred. constexpr std::chrono::microseconds MAX_ALLOWABLE_TIME_OFFSET_ERROR_FOR_IMAGE_TIMESTAMP(100); constexpr int64_t WAIT_FOR_SYNCHRONIZED_CAPTURE_TIMEOUT = 60000; static void log_lagging_time(const char *lagger, k4a::capture &master, k4a::capture &sub) { std::cout << std::setw(6) << lagger << " lagging: mc:" << std::setw(6) << master.get_color_image().get_device_timestamp().count() << "us sc:" << std::setw(6) << sub.get_color_image().get_device_timestamp().count() << "us\n"; } static void log_synced_image_time(k4a::capture &master, k4a::capture &sub) { std::cout << "Sync'd capture: mc:" << std::setw(6) << master.get_color_image().get_device_timestamp().count() << "us sc:" << std::setw(6) << sub.get_color_image().get_device_timestamp().count() << "us\n"; } class MultiDeviceCapturer { public: // Set up all the devices. Note that the index order isn't necessarily preserved, because we might swap with master MultiDeviceCapturer(const vector &device_indices, int32_t color_exposure_usec, int32_t powerline_freq) { bool master_found = false; if (device_indices.size() == 0) { cerr << "Capturer must be passed at least one camera!\n "; exit(1); } for (uint32_t i : device_indices) { k4a::device next_device = k4a::device::open(i); // construct a device using this index // If you want to synchronize cameras, you need to manually set both their exposures next_device.set_color_control(K4A_COLOR_CONTROL_EXPOSURE_TIME_ABSOLUTE, K4A_COLOR_CONTROL_MODE_MANUAL, color_exposure_usec); // This setting compensates for the flicker of lights due to the frequency of AC power in your region. If // you are in an area with 50 Hz power, this may need to be updated (check the docs for // k4a_color_control_command_t) next_device.set_color_control(K4A_COLOR_CONTROL_POWERLINE_FREQUENCY, K4A_COLOR_CONTROL_MODE_MANUAL, powerline_freq); // We treat the first device found with a sync out cable attached as the master. If it's not supposed to be, // unplug the cable from it. Also, if there's only one device, just use it if ((next_device.is_sync_out_connected() && !master_found) || device_indices.size() == 1) { master_device = std::move(next_device); master_found = true; } else if (!next_device.is_sync_in_connected() && !next_device.is_sync_out_connected()) { cerr << "Each device must have sync in or sync out connected!\n "; exit(1); } else if (!next_device.is_sync_in_connected()) { cerr << "Non-master camera found that doesn't have the sync in port connected!\n "; exit(1); } else { subordinate_devices.emplace_back(std::move(next_device)); } } if (!master_found) { cerr << "No device with sync out connected found!\n "; exit(1); } } // configs[0] should be the master, the rest subordinate void start_devices(const k4a_device_configuration_t &master_config, const k4a_device_configuration_t &sub_config) { // Start by starting all of the subordinate devices. They must be started before the master! for (k4a::device &d : subordinate_devices) { d.start_cameras(&sub_config); } // Lastly, start the master device master_device.start_cameras(&master_config); } // Blocks until we have synchronized captures stored in the output. First is master, rest are subordinates std::vector get_synchronized_captures(const k4a_device_configuration_t &sub_config, bool compare_sub_depth_instead_of_color = false) { // Dealing with the synchronized cameras is complex. The Azure Kinect DK: // (a) does not guarantee exactly equal timestamps between depth and color or between cameras (delays can // be configured but timestamps will only be approximately the same) // (b) does not guarantee that, if the two most recent images were synchronized, that calling get_capture // just once on each camera will still be synchronized. // There are several reasons for all of this. Internally, devices keep a queue of a few of the captured images // and serve those images as requested by get_capture(). However, images can also be dropped at any moment, and // one device may have more images ready than another device at a given moment, et cetera. // // Also, the process of synchronizing is complex. The cameras are not guaranteed to exactly match in all of // their timestamps when synchronized (though they should be very close). All delays are relative to the master // camera's color camera. To deal with these complexities, we employ a fairly straightforward algorithm. Start // by reading in two captures, then if the camera images were not taken at roughly the same time read a new one // from the device that had the older capture until the timestamps roughly match. // The captures used in the loop are outside of it so that they can persist across loop iterations. This is // necessary because each time this loop runs we'll only update the older capture. // The captures are stored in a vector where the first element of the vector is the master capture and // subsequent elements are subordinate captures std::vector captures(subordinate_devices.size() + 1); // add 1 for the master size_t current_index = 0; master_device.get_capture(&captures[current_index], std::chrono::milliseconds{ K4A_WAIT_INFINITE }); ++current_index; for (k4a::device &d : subordinate_devices) { d.get_capture(&captures[current_index], std::chrono::milliseconds{ K4A_WAIT_INFINITE }); ++current_index; } // If there are no subordinate devices, just return captures which only has the master image if (subordinate_devices.empty()) { return captures; } bool have_synced_images = false; std::chrono::system_clock::time_point start = std::chrono::system_clock::now(); while (!have_synced_images) { // Timeout if this is taking too long int64_t duration_ms = std::chrono::duration_cast(std::chrono::system_clock::now() - start).count(); if (duration_ms > WAIT_FOR_SYNCHRONIZED_CAPTURE_TIMEOUT) { cerr << "ERROR: Timedout waiting for synchronized captures\n"; exit(1); } k4a::image master_color_image = captures[0].get_color_image(); std::chrono::microseconds master_color_image_time = master_color_image.get_device_timestamp(); for (size_t i = 0; i < subordinate_devices.size(); ++i) { k4a::image sub_image; if (compare_sub_depth_instead_of_color) { sub_image = captures[i + 1].get_depth_image(); // offset of 1 because master capture is at front } else { sub_image = captures[i + 1].get_color_image(); // offset of 1 because master capture is at front } if (master_color_image && sub_image) { std::chrono::microseconds sub_image_time = sub_image.get_device_timestamp(); // The subordinate's color image timestamp, ideally, is the master's color image timestamp plus the // delay we configured between the master device color camera and subordinate device color camera std::chrono::microseconds expected_sub_image_time = master_color_image_time + std::chrono::microseconds{ sub_config.subordinate_delay_off_master_usec } + std::chrono::microseconds{ sub_config.depth_delay_off_color_usec }; std::chrono::microseconds sub_image_time_error = sub_image_time - expected_sub_image_time; // The time error's absolute value must be within the permissible range. So, for example, if // MAX_ALLOWABLE_TIME_OFFSET_ERROR_FOR_IMAGE_TIMESTAMP is 2, offsets of -2, -1, 0, 1, and -2 are // permitted if (sub_image_time_error < -MAX_ALLOWABLE_TIME_OFFSET_ERROR_FOR_IMAGE_TIMESTAMP) { // Example, where MAX_ALLOWABLE_TIME_OFFSET_ERROR_FOR_IMAGE_TIMESTAMP is 1 // time t=1 t=2 t=3 // actual timestamp x . . // expected timestamp . . x // error: 1 - 3 = -2, which is less than the worst-case-allowable offset of -1 // the subordinate camera image timestamp was earlier than it is allowed to be. This means the // subordinate is lagging and we need to update the subordinate to get the subordinate caught up log_lagging_time("sub", captures[0], captures[i + 1]); subordinate_devices[i].get_capture(&captures[i + 1], std::chrono::milliseconds{ K4A_WAIT_INFINITE }); break; } else if (sub_image_time_error > MAX_ALLOWABLE_TIME_OFFSET_ERROR_FOR_IMAGE_TIMESTAMP) { // Example, where MAX_ALLOWABLE_TIME_OFFSET_ERROR_FOR_IMAGE_TIMESTAMP is 1 // time t=1 t=2 t=3 // actual timestamp . . x // expected timestamp x . . // error: 3 - 1 = 2, which is more than the worst-case-allowable offset of 1 // the subordinate camera image timestamp was later than it is allowed to be. This means the // subordinate is ahead and we need to update the master to get the master caught up log_lagging_time("master", captures[0], captures[i + 1]); master_device.get_capture(&captures[0], std::chrono::milliseconds{ K4A_WAIT_INFINITE }); break; } else { // These captures are sufficiently synchronized. If we've gotten to the end, then all are // synchronized. if (i == subordinate_devices.size() - 1) { log_synced_image_time(captures[0], captures[i + 1]); have_synced_images = true; // now we'll finish the for loop and then exit the while loop } } } else if (!master_color_image) { std::cout << "Master image was bad!\n"; master_device.get_capture(&captures[0], std::chrono::milliseconds{ K4A_WAIT_INFINITE }); break; } else if (!sub_image) { std::cout << "Subordinate image was bad!" << endl; subordinate_devices[i].get_capture(&captures[i + 1], std::chrono::milliseconds{ K4A_WAIT_INFINITE }); break; } } } // if we've made it to here, it means that we have synchronized captures. return captures; } const k4a::device &get_master_device() const { return master_device; } const k4a::device &get_subordinate_device_by_index(size_t i) const { // devices[0] is the master. There are only devices.size() - 1 others. So, indices greater or equal are invalid if (i >= subordinate_devices.size()) { cerr << "Subordinate index too large!\n "; exit(1); } return subordinate_devices[i]; } private: // Once the constuctor finishes, devices[0] will always be the master k4a::device master_device; std::vector subordinate_devices; };