Skip to content

Commit

Permalink
Merge pull request #84 from nqminds/feat/add-get-frame-monotonic
Browse files Browse the repository at this point in the history
feat: add `get_frame_monotonic()` Python method
  • Loading branch information
aloisklink authored Oct 31, 2023
2 parents 3bb346b + c13bb59 commit 47c1b94
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ 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][]).
- 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
[PEP 343]: https://peps.python.org/pep-0343/

## [1.0.0] - 2023-10-30
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ add_custom_command(
--output "${CMAKE_CURRENT_BINARY_DIR}/docstrings.h"
"-I;$<JOIN:$<TARGET_PROPERTY:irimager,INCLUDE_DIRECTORIES>,;-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"
Expand Down Expand Up @@ -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
Expand Down
38 changes: 37 additions & 1 deletion src/nqm/irimager/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ __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.
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:
"""IRImager object - interfaces with a camera."""

Expand Down Expand Up @@ -60,7 +84,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
Expand Down
10 changes: 10 additions & 0 deletions src/nqm/irimager/chrono.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
#define CHRONO_HPP

#include <chrono>
#include <ctime>
#include <sstream>
#include <system_error>
#include <thread>

#include <spdlog/spdlog.h>
#include <spdlog/fmt/chrono.h> // needed for logging/formatting std::chrono
Expand All @@ -22,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<std::chrono::system_clock> clock_cast(
const std::chrono::time_point<std::chrono::steady_clock>
Expand Down
8 changes: 8 additions & 0 deletions src/nqm/irimager/irimager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <pybind11/stl/filesystem.h>
#include <pybind11/stl_bind.h>

#include "./chrono.hpp"
#include "./irimager_class.hpp"
#include "./logger.hpp"
#include "./logger_context_manager.hpp"
Expand Down Expand Up @@ -44,10 +45,15 @@ to control these cameras.)";
// helps prevent deadlock when calling code that doesn't touch Python objs
const auto no_gil = pybind11::call_guard<pybind11::gil_scoped_release>();

m.def("monotonic_to_system_clock", &nqm::irimager::clock_cast,
DOC(nqm, irimager, clock_cast), no_gil);

pybind11::class_<IRImager>(m, "IRImager", DOC(IRImager))
.def(pybind11::init<const std::filesystem::path &>(),
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,
Expand All @@ -65,6 +71,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,
Expand Down
34 changes: 24 additions & 10 deletions src/nqm/irimager/irimager_class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,17 @@ struct IRImager::impl {
virtual void stop_streaming() { streaming_ = false; }

/** @copydoc IRImager::get_frame() */
virtual std::tuple<IRImager::ThermalFrame,
std::chrono::system_clock::time_point>
std::tuple<IRImager::ThermalFrame, std::chrono::system_clock::time_point>
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<IRImager::ThermalFrame,
std::chrono::steady_clock::time_point>
get_frame_monotonic() {
if (!streaming_) {
throw std::runtime_error("IRIMAGER_STREAMOFF: Not streaming");
}
Expand All @@ -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() */
Expand Down Expand Up @@ -220,8 +228,8 @@ struct IRImagerRealImpl final : public IRImager::impl {
}
}

std::tuple<IRImager::ThermalFrame, std::chrono::system_clock::time_point>
get_frame() override {
std::tuple<IRImager::ThermalFrame, std::chrono::steady_clock::time_point>
get_frame_monotonic() override {
auto raw_frame_bytes =
std::vector<unsigned char>(ir_device_->getRawBufferSize());
/** time of frame, in monotonic seconds since std::chrono::steady_clock */
Expand Down Expand Up @@ -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
}

Expand All @@ -274,12 +282,13 @@ struct IRImagerRealImpl final : public IRImager::impl {
std::chrono::floor<std::chrono::nanoseconds>(seconds_since_epoch);

// need to convert our double duration to an integer duration
auto steady_time_point = std::chrono::time_point<std::chrono::steady_clock>(
nanoseconds_since_epoch);
auto monotonic_time_point =
std::chrono::time_point<std::chrono::steady_clock>(
nanoseconds_since_epoch);

return std::make_tuple(
std::get<IRImager::ThermalFrame>(std::move(thermal_data_result)),
nqm::irimager::clock_cast(steady_time_point));
std::move(monotonic_time_point));
}

short get_temp_range_decimal() override {
Expand Down Expand Up @@ -396,6 +405,11 @@ IRImager::get_frame() {
return pImpl_->get_frame();
}

std::tuple<IRImager::ThermalFrame, std::chrono::steady_clock::time_point>
IRImager::get_frame_monotonic() {
return pImpl_->get_frame_monotonic();
}

short IRImager::get_temp_range_decimal() {
return pImpl_->get_temp_range_decimal();
}
Expand Down
14 changes: 13 additions & 1 deletion src/nqm/irimager/irimager_class.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ThermalFrame, std::chrono::system_clock::time_point> 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<ThermalFrame, std::chrono::steady_clock::time_point>
get_frame_monotonic();

/**
* The number of decimal places in the thermal data
*
Expand Down
27 changes: 26 additions & 1 deletion tests/test_irimager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -79,6 +79,31 @@ 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

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"""
irimager = IRImager(XML_FILE)
Expand Down

0 comments on commit 47c1b94

Please sign in to comment.