You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
401 lines
12 KiB
401 lines
12 KiB
// License: Apache 2.0. See LICENSE file in root directory.
|
|
// Copyright(c) 2017 Intel Corporation. All Rights Reserved.
|
|
|
|
#pragma once
|
|
|
|
#include <librealsense2/rs.hpp> // Include RealSense Cross Platform API
|
|
#include <librealsense2/h/rs_internal.h> // Access librealsense internal clock
|
|
#include <opencv2/opencv.hpp> // Include OpenCV API
|
|
#include "../cv-helpers.hpp" // Helper functions for conversions between RealSense and OpenCV
|
|
#include <rsutils/concurrency/concurrency.h> // We are borrowing from librealsense concurrency infrastructure for this sample
|
|
#include <algorithm>
|
|
|
|
// Helper class to keep track of measured data
|
|
// and generate basic statistics
|
|
template<class T>
|
|
class measurement
|
|
{
|
|
public:
|
|
measurement(int cap = 10) : _capacity(cap), _sum() {}
|
|
|
|
// Media over rolling window
|
|
T median() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(_m);
|
|
|
|
if (_total == 0) return _sum;
|
|
|
|
std::vector<T> copy(begin(_data), end(_data));
|
|
std::sort(begin(copy), end(copy));
|
|
return copy[copy.size() / 2];
|
|
}
|
|
|
|
// Average over all samples
|
|
T avg() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(_m);
|
|
if (_total > 0) return _sum / _total;
|
|
return _sum;
|
|
}
|
|
|
|
// Total count of measurements
|
|
int total() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(_m);
|
|
return _total;
|
|
}
|
|
|
|
// Add new measurement
|
|
void add(T val)
|
|
{
|
|
std::lock_guard<std::mutex> lock(_m);
|
|
_data.push_back(val);
|
|
if (_data.size() > _capacity)
|
|
{
|
|
_data.pop_front();
|
|
}
|
|
_total++;
|
|
_sum += val;
|
|
}
|
|
|
|
private:
|
|
mutable std::mutex _m;
|
|
T _sum;
|
|
std::deque<T> _data;
|
|
int _capacity;
|
|
int _total = 0;
|
|
};
|
|
|
|
// Helper class to encode / decode numbers into
|
|
// binary sequences, with 2-bit checksum
|
|
class bit_packer
|
|
{
|
|
public:
|
|
bit_packer(int digits)
|
|
: _digits(digits), _bits(digits, false)
|
|
{
|
|
}
|
|
|
|
void reset()
|
|
{
|
|
std::fill(_bits.begin(), _bits.end(), false);
|
|
}
|
|
|
|
// Try to reconstruct the number from bits inside the class
|
|
bool try_unpack(int* number)
|
|
{
|
|
// Calculate and verify Checksum
|
|
auto on_bits = std::count_if(_bits.begin() + 2, _bits.end(),
|
|
[](bool f) { return f; });
|
|
if ((on_bits % 2 == 1) == _bits[0] &&
|
|
((on_bits / 2) % 2 == 1) == _bits[1])
|
|
{
|
|
int res = 0;
|
|
for (int i = 2; i < _digits; i++)
|
|
{
|
|
res = res * 2 + _bits[i];
|
|
}
|
|
*number = res;
|
|
return true;
|
|
}
|
|
else return false;
|
|
}
|
|
|
|
// Try to store the number as bits into the class
|
|
bool try_pack(int number)
|
|
{
|
|
if (number < 1 << (_digits - 2))
|
|
{
|
|
_bits.clear();
|
|
while (number)
|
|
{
|
|
_bits.push_back(number & 1);
|
|
number >>= 1;
|
|
}
|
|
|
|
// Pad with zeros
|
|
while (_bits.size() < _digits) _bits.push_back(false);
|
|
reverse(_bits.begin(), _bits.end());
|
|
|
|
// Apply 2-bit Checksum
|
|
auto on_bits = std::count_if(_bits.begin() + 2, _bits.end(),
|
|
[](bool f) { return f; });
|
|
_bits[0] = (on_bits % 2 == 1);
|
|
_bits[1] = ((on_bits / 2) % 2 == 1);
|
|
|
|
return true;
|
|
}
|
|
else return false;
|
|
}
|
|
|
|
// Access bits array
|
|
std::vector<bool>& get() { return _bits; }
|
|
|
|
private:
|
|
std::vector<bool> _bits;
|
|
int _digits;
|
|
};
|
|
|
|
// Main class in charge of detecting latency measurements
|
|
class detector
|
|
{
|
|
public:
|
|
detector(int digits, int display_w)
|
|
: _digits(digits), _packer(digits),
|
|
_display_w(display_w),
|
|
_t([this]() { detect(); }),
|
|
_preview_size(600, 350),
|
|
_next_value(0), _next(false), _alive(true)
|
|
{
|
|
using namespace cv;
|
|
|
|
_start_time = std::chrono::high_resolution_clock::now();
|
|
_render_start = std::chrono::high_resolution_clock::now();
|
|
|
|
_last_preview = Mat::zeros(_preview_size, CV_8UC1);
|
|
_instructions = Mat::zeros(Size(display_w, 120), CV_8UC1);
|
|
|
|
putText(_instructions, "Point the camera at the screen. Ensure all white circles are being captured",
|
|
Point(display_w / 2 - 470, 30), FONT_HERSHEY_SIMPLEX,
|
|
0.8, Scalar(255, 255, 255), 2, LINE_AA);
|
|
|
|
putText(_instructions, "Press any key to exit...",
|
|
Point(display_w / 2 - 160, 70), FONT_HERSHEY_SIMPLEX,
|
|
0.8, Scalar(255, 255, 255), 2, LINE_AA);
|
|
}
|
|
|
|
void begin_render()
|
|
{
|
|
_render_start = std::chrono::high_resolution_clock::now();
|
|
}
|
|
|
|
void end_render()
|
|
{
|
|
auto duration = std::chrono::high_resolution_clock::now() - _render_start;
|
|
_render_time.add(static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()));
|
|
}
|
|
|
|
~detector()
|
|
{
|
|
_alive = false;
|
|
_t.join();
|
|
}
|
|
|
|
// Add new frame from the camera
|
|
void submit_frame(rs2::frame f)
|
|
{
|
|
record frame_for_processing;
|
|
|
|
frame_for_processing.f = f;
|
|
|
|
// Read how much time the frame spent from
|
|
// being released by the OS-dependent driver (Windows Media Foundation, V4L2 or libuvc)
|
|
// to this point in time (when it is first accessible to the application)
|
|
auto toa = f.get_frame_metadata(RS2_FRAME_METADATA_TIME_OF_ARRIVAL);
|
|
rs2_error* e;
|
|
_processing_time.add(static_cast<int>(rs2_get_time(&e) - toa));
|
|
|
|
auto duration = std::chrono::high_resolution_clock::now() - _start_time;
|
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
|
|
frame_for_processing.ms = ms; // Store current clock into the record
|
|
|
|
_queue.enqueue(std::move(frame_for_processing));
|
|
}
|
|
|
|
// Get next value to transmit
|
|
// This will either be the same as the last time
|
|
// Or a new value when needed
|
|
int get_next_value()
|
|
{
|
|
// Capture clock for next cycle
|
|
if (_next.exchange(false))
|
|
{
|
|
auto now = std::chrono::high_resolution_clock::now();
|
|
auto duration = now - _start_time;
|
|
auto ms = static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
|
|
auto next = ms % (1 << (_digits - 2));
|
|
if (next == _next_value) next++;
|
|
_next_value = next;
|
|
}
|
|
return _next_value;
|
|
}
|
|
|
|
// Copy preview image stored inside into a matrix
|
|
void copy_preview_to(cv::Mat& display)
|
|
{
|
|
std::lock_guard<std::mutex> lock(_preview_mutex);
|
|
cv::Rect roi(cv::Point(_display_w / 2 - _preview_size.width / 2, 200), _last_preview.size());
|
|
_last_preview.copyTo(display(roi));
|
|
|
|
cv::Rect text_roi(cv::Point(0, 580), _instructions.size());
|
|
_instructions.copyTo(display(text_roi));
|
|
}
|
|
|
|
private:
|
|
struct record
|
|
{
|
|
rs2::frame f;
|
|
long long ms;
|
|
};
|
|
|
|
void next()
|
|
{
|
|
record r;
|
|
while (_queue.try_dequeue(&r));
|
|
_next = true;
|
|
}
|
|
|
|
struct detector_lock
|
|
{
|
|
detector_lock(detector* owner)
|
|
: _owner(owner) {}
|
|
|
|
void abort() { _owner = nullptr; }
|
|
|
|
~detector_lock() { if(_owner) _owner->next(); }
|
|
|
|
detector* _owner;
|
|
};
|
|
|
|
// Render textual instructions
|
|
void update_instructions()
|
|
{
|
|
using namespace cv;
|
|
|
|
std::lock_guard<std::mutex> lock(_preview_mutex);
|
|
_instructions = Mat::zeros(Size(_display_w, 120), CV_8UC1);
|
|
|
|
std::stringstream ss;
|
|
|
|
ss << "Total Collected Samples: " << _latency.total();
|
|
|
|
putText(_instructions, ss.str().c_str(),
|
|
Point(80, 20), FONT_HERSHEY_SIMPLEX,
|
|
0.8, Scalar(255, 255, 255), 2, LINE_AA);
|
|
|
|
ss.str("");
|
|
ss << "Estimated Latency: (Rolling-Median)" << _latency.median() << "ms, ";
|
|
ss << "(Average)" << _latency.avg() << "ms";
|
|
|
|
putText(_instructions, ss.str().c_str(),
|
|
Point(80, 60), FONT_HERSHEY_SIMPLEX,
|
|
0.8, Scalar(255, 255, 255), 2, LINE_AA);
|
|
|
|
ss.str("");
|
|
ss << "Software Processing: " << _processing_time.median() << "ms";
|
|
|
|
putText(_instructions, ss.str().c_str(),
|
|
Point(80, 100), FONT_HERSHEY_SIMPLEX,
|
|
0.8, Scalar(255, 255, 255), 2, LINE_AA);
|
|
}
|
|
|
|
// Detector main loop
|
|
void detect()
|
|
{
|
|
using namespace cv;
|
|
|
|
while (_alive)
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
record r;
|
|
if (_queue.try_dequeue(&r))
|
|
{
|
|
// Make sure we request new number,
|
|
// UNLESS we decide to keep waiting
|
|
detector_lock flush_queue_after(this);
|
|
|
|
auto color_mat = frame_to_mat(r.f);
|
|
|
|
if (color_mat.channels() > 1)
|
|
cvtColor(color_mat, color_mat, COLOR_BGR2GRAY);
|
|
medianBlur(color_mat, color_mat, 5);
|
|
|
|
std::vector<Vec3f> circles;
|
|
cv::Rect roi(Point(0, 0), Size(color_mat.size().width, color_mat.size().height / 4));
|
|
HoughCircles(color_mat(roi), circles, HOUGH_GRADIENT, 1, 10, 100, 30, 1, 100);
|
|
for (size_t i = 0; i < circles.size(); i++)
|
|
{
|
|
Vec3i c = circles[i];
|
|
Rect r(c[0] - c[2] - 5, c[1] - c[2] - 5, 2 * c[2] + 10, 2 * c[2] + 10);
|
|
rectangle(color_mat, r, Scalar(0, 100, 100), -1, LINE_AA);
|
|
}
|
|
|
|
cv::resize(color_mat, color_mat, _preview_size);
|
|
{
|
|
std::lock_guard<std::mutex> lock(_preview_mutex);
|
|
_last_preview = color_mat;
|
|
}
|
|
|
|
sort(circles.begin(), circles.end(),
|
|
[](const Vec3f& a, const Vec3f& b) -> bool
|
|
{
|
|
return a[0] < b[0];
|
|
});
|
|
if (circles.size() > 1)
|
|
{
|
|
int min_x = static_cast<int>(circles[0][0]);
|
|
int max_x = static_cast<int>(circles[circles.size() - 1][0]);
|
|
|
|
int circle_est_size = (max_x - min_x) / (_digits + 1);
|
|
min_x += circle_est_size / 2;
|
|
max_x -= circle_est_size / 2;
|
|
|
|
_packer.reset();
|
|
for (int i = 1; i < circles.size() - 1; i++)
|
|
{
|
|
const int x = static_cast<int>(circles[i][0]);
|
|
const int idx = static_cast<int>(_digits * ((float)(x - min_x) / (max_x - min_x)));
|
|
if (idx >= 0 && idx < _packer.get().size())
|
|
_packer.get()[idx] = true;
|
|
}
|
|
|
|
int res;
|
|
if (_packer.try_unpack(&res))
|
|
{
|
|
if (res == _next_value)
|
|
{
|
|
auto cropped = static_cast<int>(r.ms) % (1 << (_digits - 2));
|
|
if (cropped > res)
|
|
{
|
|
auto avg_render_time = _render_time.avg();
|
|
_latency.add(static_cast<int>((cropped - res) - avg_render_time));
|
|
update_instructions();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Only in case we detected valid number other then expected
|
|
// We continue processing (since this was most likely older valid frame)
|
|
flush_queue_after.abort();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const int _digits;
|
|
const int _display_w;
|
|
|
|
std::atomic_bool _alive;
|
|
bit_packer _packer;
|
|
std::thread _t;
|
|
single_consumer_queue<record> _queue;
|
|
std::chrono::high_resolution_clock::time_point _start_time;
|
|
std::chrono::high_resolution_clock::time_point _render_start;
|
|
|
|
std::atomic_bool _next;
|
|
std::atomic<int> _next_value;
|
|
|
|
const cv::Size _preview_size;
|
|
std::mutex _preview_mutex;
|
|
cv::Mat _last_preview;
|
|
cv::Mat _instructions;
|
|
|
|
measurement<int> _render_time;
|
|
measurement<int> _processing_time;
|
|
measurement<int> _latency;
|
|
};
|
|
|
|
|