From ea4bbe42b518962406b2623159879b7bf47fbbe7 Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Mon, 30 Oct 2023 16:42:56 +0000 Subject: [PATCH 1/4] feat: add `get_frame_monotonic()` method The `evo::IRDevice::getFrame()` function doesn't return a UNIX timestamp when getting a frame. Instead it returns a monotonic/steady timestamp. To convert this back to a `std::chrono::system_clock`, I've added a function called `nqm::irimager::clock_cast`, but it's imprecise, so it's not ideal for very high frame-rates. Instead, we may want to use a monotonic time directly. --- CHANGELOG.md | 4 ++++ src/nqm/irimager/__init__.pyi | 14 +++++++++++- src/nqm/irimager/irimager.cpp | 4 ++++ src/nqm/irimager/irimager_class.cpp | 34 ++++++++++++++++++++--------- src/nqm/irimager/irimager_class.hpp | 14 +++++++++++- tests/test_irimager.py | 18 +++++++++++++++ 6 files changed, 76 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aedf57d..e2b6c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 creates/deletes a [`nqm.logger.Logger`](https://nqminds.github.io/nqm-irimager/apidoc/nqm.irimager.html#nqm.irimager.Logger) object when used in a [`with:` statement][PEP 343] ([#81][]). +- Add `nqm.irimager.IRImager.get_frame_monotonic` Python method, which can be + used to get the monotonic time of a frame directly from the + EvoCortex IRImagerDirect SDK ([#84][]). [#81]: https://github.com/nqminds/nqm-irimager/pull/81 +[#84]: https://github.com/nqminds/nqm-irimager/pull/84 [PEP 343]: https://peps.python.org/pep-0343/ ## [1.0.0] - 2023-10-30 diff --git a/src/nqm/irimager/__init__.pyi b/src/nqm/irimager/__init__.pyi index ebae296..ed7a047 100644 --- a/src/nqm/irimager/__init__.pyi +++ b/src/nqm/irimager/__init__.pyi @@ -60,7 +60,19 @@ class IRImager: 1. A 2-D matrix containing the image. This must be adjusted by :py:meth:`~IRImager.get_temp_range_decimal` to get the actual temperature in degrees Celcius, offset from -100 ℃. - 2. The time the image was taken. + 2. The approximate time the image was taken. + """ + def get_frame_monotonic( + self, + ) -> typing.Tuple[npt.NDArray[np.uint16], datetime.timedelta]: + """Return a frame, with a monotonic/steady_clock timestamp. + + Similar to :py:meth:`get_frame`, except returns a monotonic timepoint that the + IRImagerDirectSDK returns, which is more accurate. + + Please be aware that the epoch of the monotonic timepoint is undefined, + and may be the time since last boot or the time since the program + started. """ def get_temp_range_decimal(self) -> int: """The number of decimal places in the thermal data diff --git a/src/nqm/irimager/irimager.cpp b/src/nqm/irimager/irimager.cpp index aca53d9..747bc1e 100644 --- a/src/nqm/irimager/irimager.cpp +++ b/src/nqm/irimager/irimager.cpp @@ -48,6 +48,8 @@ to control these cameras.)"; .def(pybind11::init(), DOC(IRImager, IRImager), no_gil) .def("get_frame", &IRImager::get_frame, DOC(IRImager, get_frame), no_gil) + .def("get_frame_monotonic", &IRImager::get_frame_monotonic, + DOC(IRImager, get_frame_monotonic), no_gil) .def("get_temp_range_decimal", &IRImager::get_temp_range_decimal, DOC(IRImager, get_temp_range_decimal), no_gil) .def("get_library_version", &IRImager::get_library_version, @@ -65,6 +67,8 @@ to control these cameras.)"; DOC(IRImager, IRImager), no_gil) .def("get_frame", &IRImagerMock::get_frame, DOC(IRImager, get_frame), no_gil) + .def("get_frame_monotonic", &IRImager::get_frame_monotonic, + DOC(IRImager, get_frame_monotonic), no_gil) .def("get_temp_range_decimal", &IRImagerMock::get_temp_range_decimal, DOC(IRImager, get_temp_range_decimal), no_gil) .def("start_streaming", &IRImagerMock::start_streaming, diff --git a/src/nqm/irimager/irimager_class.cpp b/src/nqm/irimager/irimager_class.cpp index 6178fd3..b796691 100644 --- a/src/nqm/irimager/irimager_class.cpp +++ b/src/nqm/irimager/irimager_class.cpp @@ -46,9 +46,17 @@ struct IRImager::impl { virtual void stop_streaming() { streaming_ = false; } /** @copydoc IRImager::get_frame() */ - virtual std::tuple + std::tuple get_frame() { + auto [thermal_frame, monotonic_time_point] = this->get_frame_monotonic(); + return std::make_tuple(std::move(thermal_frame), + nqm::irimager::clock_cast(monotonic_time_point)); + } + + /** @copydoc IRImager::get_frame_monotonic() */ + virtual std::tuple + get_frame_monotonic() { if (!streaming_) { throw std::runtime_error("IRIMAGER_STREAMOFF: Not streaming"); } @@ -59,7 +67,7 @@ struct IRImager::impl { auto my_array = IRImager::ThermalFrame::Constant(frame_size[0], frame_size[1], max_value); - return std::make_tuple(my_array, std::chrono::system_clock::now()); + return std::make_tuple(my_array, std::chrono::steady_clock::now()); } /** @copydoc IRImager::get_temp_range_decimal() */ @@ -220,8 +228,8 @@ struct IRImagerRealImpl final : public IRImager::impl { } } - std::tuple - get_frame() override { + std::tuple + get_frame_monotonic() override { auto raw_frame_bytes = std::vector(ir_device_->getRawBufferSize()); /** time of frame, in monotonic seconds since std::chrono::steady_clock */ @@ -259,9 +267,9 @@ struct IRImagerRealImpl final : public IRImager::impl { // GCC will tail-call optimize too on x86_64 and ARM64, but even if it // doesn't we're extremely unlikely to have a stack overflow, even if // imaging at 1000Hz - [[clang::musttail]] return get_frame(); + [[clang::musttail]] return get_frame_monotonic(); #else - return get_frame(); + return get_frame_monotonic(); #endif } @@ -274,12 +282,13 @@ struct IRImagerRealImpl final : public IRImager::impl { std::chrono::floor(seconds_since_epoch); // need to convert our double duration to an integer duration - auto steady_time_point = std::chrono::time_point( - nanoseconds_since_epoch); + auto monotonic_time_point = + std::chrono::time_point( + nanoseconds_since_epoch); return std::make_tuple( std::get(std::move(thermal_data_result)), - nqm::irimager::clock_cast(steady_time_point)); + std::move(monotonic_time_point)); } short get_temp_range_decimal() override { @@ -396,6 +405,11 @@ IRImager::get_frame() { return pImpl_->get_frame(); } +std::tuple +IRImager::get_frame_monotonic() { + return pImpl_->get_frame_monotonic(); +} + short IRImager::get_temp_range_decimal() { return pImpl_->get_temp_range_decimal(); } diff --git a/src/nqm/irimager/irimager_class.hpp b/src/nqm/irimager/irimager_class.hpp index 8bcbb51..ce323f2 100644 --- a/src/nqm/irimager/irimager_class.hpp +++ b/src/nqm/irimager/irimager_class.hpp @@ -92,10 +92,22 @@ class IRImager { * 1. A 2-D matrix containing the image. This must be adjusted * by :py:meth:`~IRImager.get_temp_range_decimal` to get the * actual temperature in degrees Celcius, offset from -100 ℃. - * 2. The time the image was taken. + * 2. The approximate time the image was taken. */ std::tuple get_frame(); + /** + * @brief Return a frame, with a monotonic/steady_clock timestamp. + * + * Similar to :py:meth:`get_frame`, except returns a monotonic timepoint that + * the IRImagerDirectSDK returns, which is more accurate. + * + * Please be aware that the epoch of the monotonic timepoint is undefined, + * and may be the time since last boot or the time since the program started. + */ + std::tuple + get_frame_monotonic(); + /** * The number of decimal places in the thermal data * diff --git a/tests/test_irimager.py b/tests/test_irimager.py index 8021825..1781bdf 100644 --- a/tests/test_irimager.py +++ b/tests/test_irimager.py @@ -79,6 +79,24 @@ def test_irimager_get_frame(): assert timestamp > datetime.datetime.now() - datetime.timedelta(seconds=30) +def test_irimager_get_frame_monotonic(): + """Tests nqm.irimager.IRImager#get_frame_monotonic""" + irimager = IRImager(XML_FILE) + + with irimager: + array, steady_time = irimager.get_frame_monotonic() + + assert array.dtype == np.uint16 + # should be 2-dimensional + assert array.ndim == 2 + assert array.shape == (382, 288) + assert array.flags["C_CONTIGUOUS"] # check if the array is row-major + + assert steady_time > datetime.timedelta(seconds=0) + array, steady_time_2 = irimager.get_frame_monotonic() + assert steady_time_2 > steady_time + + def test_irimager_get_temp_range_decimal(): """Tests that nqm.irimager.IRImager#get_temp_range_decimal returns an int""" irimager = IRImager(XML_FILE) From 369b885271c08d761d139ca2231158a285d42baa Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 31 Oct 2023 15:06:19 +0000 Subject: [PATCH 2/4] fix: add missing `#include`s to `chrono.hpp` --- src/nqm/irimager/chrono.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/nqm/irimager/chrono.hpp b/src/nqm/irimager/chrono.hpp index 6b3bc04..2a81723 100644 --- a/src/nqm/irimager/chrono.hpp +++ b/src/nqm/irimager/chrono.hpp @@ -2,6 +2,10 @@ #define CHRONO_HPP #include +#include +#include +#include +#include #include #include // needed for logging/formatting std::chrono From 4ce312f277d4672796421371aff1035b35562b33 Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 31 Oct 2023 15:08:08 +0000 Subject: [PATCH 3/4] feat: add `monotonic_to_system_clock()` Py func Add the Python method `nqm.irimager.monotonic_to_system_clock()` that can be used to convert from a monotonic time (e.g. as returned from `get_frame_monotonic()`) to a normal Python `datetime.datetime`. --- CHANGELOG.md | 2 ++ CMakeLists.txt | 2 ++ src/nqm/irimager/__init__.pyi | 18 ++++++++++++++++++ src/nqm/irimager/irimager.cpp | 4 ++++ tests/test_irimager.py | 9 ++++++++- 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b6c37..dc3936d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `nqm.irimager.IRImager.get_frame_monotonic` Python method, which can be used to get the monotonic time of a frame directly from the EvoCortex IRImagerDirect SDK ([#84][]). +- Add `nqm.irimager.monotonic_to_system_clock` function to convert a monotonic + time to a system clock time ([#84][]). [#81]: https://github.com/nqminds/nqm-irimager/pull/81 [#84]: https://github.com/nqminds/nqm-irimager/pull/84 diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d89af1..e2655e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ add_custom_command( --output "${CMAKE_CURRENT_BINARY_DIR}/docstrings.h" "-I;$,;-I;>" -std=c++17 + "${CMAKE_CURRENT_SOURCE_DIR}/src/nqm/irimager/chrono.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/nqm/irimager/irimager_class.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/nqm/irimager/logger_context_manager.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/nqm/irimager/logger.hpp" @@ -191,6 +192,7 @@ target_compile_definitions(irimager PRIVATE "${IRImager_DEFINITIONS}") target_link_libraries(irimager PRIVATE pybind11::headers + spdlog::spdlog_header_only irimager_class irlogger_parser irlogger_to_spd diff --git a/src/nqm/irimager/__init__.pyi b/src/nqm/irimager/__init__.pyi index ed7a047..3d3982f 100644 --- a/src/nqm/irimager/__init__.pyi +++ b/src/nqm/irimager/__init__.pyi @@ -24,6 +24,24 @@ __version__: str This is *not* the version of the underlying C++ libirimager library. """ +def monotonic_to_system_clock( + steady_time_point: datetime.timedelta, +) -> datetime.datetime: + """ + Converts from `steady_clock` to `system_clock`. + + Converts a time_point from std::chrono::steady_clock (time since last boot) + to std::chrono::system_clock (aka time since UNIX epoch). + + C++20 has a function called std::chrono::clock_cast that will do this + for us, but we're stuck on C++17, so instead we have to do this imprecise + monstrosity to do the conversion. + + Remarks: + This function is imprecise!!! Calling it multiple times with the same + data will result in different results. + """ + class IRImager: """IRImager object - interfaces with a camera.""" diff --git a/src/nqm/irimager/irimager.cpp b/src/nqm/irimager/irimager.cpp index 747bc1e..88c6021 100644 --- a/src/nqm/irimager/irimager.cpp +++ b/src/nqm/irimager/irimager.cpp @@ -6,6 +6,7 @@ #include #include +#include "./chrono.hpp" #include "./irimager_class.hpp" #include "./logger.hpp" #include "./logger_context_manager.hpp" @@ -44,6 +45,9 @@ to control these cameras.)"; // helps prevent deadlock when calling code that doesn't touch Python objs const auto no_gil = pybind11::call_guard(); + m.def("monotonic_to_system_clock", &nqm::irimager::clock_cast, + DOC(nqm, irimager, clock_cast), no_gil); + pybind11::class_(m, "IRImager", DOC(IRImager)) .def(pybind11::init(), DOC(IRImager, IRImager), no_gil) diff --git a/tests/test_irimager.py b/tests/test_irimager.py index 1781bdf..1feda39 100644 --- a/tests/test_irimager.py +++ b/tests/test_irimager.py @@ -6,7 +6,7 @@ import pytest from nqm.irimager import IRImagerMock as IRImager -from nqm.irimager import Logger +from nqm.irimager import Logger, monotonic_to_system_clock XML_FILE = pathlib.Path(__file__).parent / "__fixtures__" / "382x288@27Hz.xml" README_FILE = pathlib.Path(__file__).parent.parent / "README.md" @@ -96,6 +96,13 @@ def test_irimager_get_frame_monotonic(): array, steady_time_2 = irimager.get_frame_monotonic() assert steady_time_2 > steady_time + assert monotonic_to_system_clock( + steady_time + ) > datetime.datetime.now() - datetime.timedelta(seconds=30) + assert monotonic_to_system_clock( + steady_time_2 + ) > datetime.datetime.now() - datetime.timedelta(seconds=30) + def test_irimager_get_temp_range_decimal(): """Tests that nqm.irimager.IRImager#get_temp_range_decimal returns an int""" From c13bb59deac643e46931c800a341a740d7bac291 Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 31 Oct 2023 16:09:26 +0000 Subject: [PATCH 4/4] docs: warn that steady_clock might be a bit weird Warn that the [`std::chrono::steady_clock`][1] may not increase linearly with the system's [`std::chrono::system_clock`][2]. For example, a steady_clock value of 1s, might be 12:02, but if the computer sleeps for an hour between 12:02 and 13:02, then a steady_clock value of 2s might be equal to 13:02. [1]: https://en.cppreference.com/w/cpp/chrono/steady_clock [2]: https://en.cppreference.com/w/cpp/chrono/system_clock --- src/nqm/irimager/__init__.pyi | 6 ++++++ src/nqm/irimager/chrono.hpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/nqm/irimager/__init__.pyi b/src/nqm/irimager/__init__.pyi index 3d3982f..9577935 100644 --- a/src/nqm/irimager/__init__.pyi +++ b/src/nqm/irimager/__init__.pyi @@ -40,6 +40,12 @@ def monotonic_to_system_clock( Remarks: This function is imprecise!!! Calling it multiple times with the same data will result in different results. + + Warning: + The monotonic/steady_clock might only count when the computer is powered + on. E.g. if the system was in a sleep state, the monotonic time may not + have increased. Because of this, you should not rely on this function + to return accurate results for past time points. """ class IRImager: diff --git a/src/nqm/irimager/chrono.hpp b/src/nqm/irimager/chrono.hpp index 2a81723..da2768a 100644 --- a/src/nqm/irimager/chrono.hpp +++ b/src/nqm/irimager/chrono.hpp @@ -26,6 +26,12 @@ namespace irimager { * @remarks * This function is imprecise!!! Calling it multiple times with the same data * will result in different results. + * + * @warning + * The monotonic/steady_clock might only count when the computer is powered on. + * E.g. if the system was in a sleep state, the monotonic time may not have + * increased. Because of this, you should not rely on this function to return + * accurate results for past time points. */ inline std::chrono::time_point clock_cast( const std::chrono::time_point