Skip to content

Commit

Permalink
Merge pull request #7 from nqminds/refactor/use-pybind11-mkdoc
Browse files Browse the repository at this point in the history
Use `pybind11_mkdoc` to make Python docstrings from C++ comments
  • Loading branch information
aloisklink authored Jul 25, 2023
2 parents 88834c9 + 345a44e commit 82db5ed
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 95 deletions.
22 changes: 20 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,26 @@ project(
find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
find_package(pybind11 CONFIG REQUIRED)

python_add_library(irimager MODULE src/nqm/irimager/irimager.cpp WITH_SOABI)
add_custom_command(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/docstrings.h"
COMMAND Python::Interpreter
ARGS
-m pybind11_mkdoc
--output "${CMAKE_CURRENT_BINARY_DIR}/docstrings.h"
"-I;$<JOIN:$<TARGET_PROPERTY:irimager,INCLUDE_DIRECTORIES>,;-I;>"
"${CMAKE_CURRENT_SOURCE_DIR}/src/nqm/irimager/irimager_class.hpp"
COMMAND_EXPAND_LISTS
VERBATIM
)

python_add_library(irimager MODULE
src/nqm/irimager/irimager.cpp "${CMAKE_CURRENT_BINARY_DIR}/docstrings.h"
WITH_SOABI
)
target_link_libraries(irimager PRIVATE pybind11::headers)
target_compile_definitions(irimager PRIVATE VERSION_INFO=${PROJECT_VERSION})
target_compile_definitions(irimager PRIVATE
VERSION_INFO=${PROJECT_VERSION}
DOCSTRINGS_H="${CMAKE_CURRENT_BINARY_DIR}/docstrings.h"
)

install(TARGETS irimager DESTINATION "nqm")
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ dependencies = [
]

[build-system]
requires = ["scikit-build-core>=0.4.7", "pybind11>=2.10.4"]
requires = [
"scikit-build-core>=0.4.7",
"pybind11>=2.10.4",
# until https://github.com/pybind/pybind11_mkdoc/pull/32 is merged
"pybind11_mkdoc @ git+https://github.com/corna/pybind11_mkdoc.git#e71d15f12f4c6eee1d04c724cb5946a5d8ef149d"
]
build-backend = "scikit_build_core.build"

[tool.scikit-build]
Expand Down
103 changes: 11 additions & 92 deletions src/nqm/irimager/irimager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,13 @@
#include <pybind11/stl/filesystem.h>
#include <pybind11/stl_bind.h>

#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include "./irimager_class.hpp"

class IRImager {
public:
IRImager() = default;
IRImager(const std::filesystem::path &xml_path) {
std::ifstream xml_stream(xml_path, std::fstream::in);
#ifndef DOCSTRINGS_H
#error DOCSTRINGS_H must be defined to the output of pybind11_mkdocs
#endif

std::string xml_header(5, '\0');
xml_stream.read(xml_header.data(), xml_header.size());
if (xml_header != std::string("<?xml")) {
throw std::runtime_error("Invalid XML file: The given XML file does not start with '<?xml'");
}
}

void start_streaming() {
streaming = true;
}

void stop_streaming() {
streaming = false;
}

IRImager* _enter_() {
start_streaming();
return this;
}

void _exit_(
[[maybe_unused]] const std::optional<pybind11::type> &exc_type,
[[maybe_unused]] const std::optional<pybind11::error_already_set> &exc_value,
[[maybe_unused]] const pybind11::object &traceback) {
stop_streaming();
}

std::tuple<pybind11::array_t<uint16_t>, std::chrono::system_clock::time_point> get_frame() {
if (!streaming) {
throw std::runtime_error("IRIMAGER_STREAMOFF: Not streaming");
}

auto frame_size = std::array<ssize_t, 2>{128, 128};
auto my_array = pybind11::array_t<uint16_t>(frame_size);

auto r = my_array.mutable_unchecked<frame_size.size()>();

for (ssize_t i = 0; i < frame_size[0]; i++) {
for (ssize_t j = 0; j < frame_size[1]; j++) {
r(i, j) = 1800 * std::pow(10, get_temp_range_decimal());
}
}

return std::make_tuple(my_array, std::chrono::system_clock::now());
}

short get_temp_range_decimal() {
return 1;
}

private:
bool streaming = false;
};
#include DOCSTRINGS_H

PYBIND11_MODULE(irimager, m) {
m.doc() = R"(Optris PI and XI imager IR camera controller
Expand All @@ -78,36 +20,13 @@ We use the IRImagerDirect SDK
(see http://documentation.evocortex.com/libirimager2/html/index.html)
to control these cameras.)";

pybind11::class_<IRImager>(m, "IRImager", R"(IRImager object - interfaces with a camera.)")
pybind11::class_<IRImager>(m, "IRImager", DOC(IRImager))
.def(pybind11::init<>())
.def(pybind11::init<const std::filesystem::path &>(), R"(Loads the configuration for an IR Camera from the given XML file)")
.def("get_frame", &IRImager::get_frame, R"(Return a frame
Raises:
RuntimeError: If a frame cannot be loaded, e.g. if the camera isn't streaming.
Returns:
A tuple containing:
- A 2-D numpy array containing the image. This must be adjusted
by :py:meth:`~IRImager.get_temp_range_decimal` to get the
actual temperature in degrees Celcius.
- The time the image was taken.
)")
.def("get_temp_range_decimal", &IRImager::get_temp_range_decimal, R"(The number of decimal places in the thermal data
For example, if :py:meth:`~IRImager.get_frame` returns 18000, you can
divide this number by 10 to the power of the result of
:py:meth:`~IRImager.get_temp_range_decimal` to get the actual
temperature in degrees Celcius.
)")
.def("start_streaming", &IRImager::start_streaming, R"(Start video grabbing
Prefer using `with irimager: ...` to automatically start/stop streaming on errors.
Raises:
RuntimeError: If streaming cannot be started, e.g. if the camera is not connected.
)")
.def("stop_streaming", &IRImager::stop_streaming, R"(Stop video grabbing)")
.def(pybind11::init<const std::filesystem::path &>(), DOC(IRImager, IRImager, 2))
.def("get_frame", &IRImager::get_frame, DOC(IRImager, get_frame))
.def("get_temp_range_decimal", &IRImager::get_temp_range_decimal, DOC(IRImager, get_temp_range_decimal))
.def("start_streaming", &IRImager::start_streaming, DOC(IRImager, start_streaming))
.def("stop_streaming", &IRImager::stop_streaming, DOC(IRImager, stop_streaming))
.def("__enter__", &IRImager::_enter_, pybind11::return_value_policy::reference_internal)
.def("__exit__", &IRImager::_exit_);
}
105 changes: 105 additions & 0 deletions src/nqm/irimager/irimager_class.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>

#include <pybind11/numpy.h>

/**
* IRImager object - interfaces with a camera.
*/
class IRImager {
public:
IRImager() = default;
/**
* Loads the configuration for an IR Camera from the given XML file
*/
IRImager(const std::filesystem::path &xml_path) {
std::ifstream xml_stream(xml_path, std::fstream::in);

std::string xml_header(5, '\0');
xml_stream.read(xml_header.data(), xml_header.size());
if (xml_header != std::string("<?xml")) {
throw std::runtime_error("Invalid XML file: The given XML file does not start with '<?xml'");
}
}

/**
* Start video grabbing
*
* Prefer using `with irimager: ...` to automatically start/stop streaming
* on errors.
*
* @throws RuntimeError if streaming cannot be started, e.g. if the camera
* is not connected.
*/
void start_streaming() {
streaming = true;
}

/**
* Stop video grabbing
*/
void stop_streaming() {
streaming = false;
}

IRImager* _enter_() {
start_streaming();
return this;
}

void _exit_(
[[maybe_unused]] const std::optional<pybind11::type> &exc_type,
[[maybe_unused]] const std::optional<pybind11::error_already_set> &exc_value,
[[maybe_unused]] const pybind11::object &traceback) {
stop_streaming();
}

/**
* Return a frame
*
* @throws RuntimeError if a frame cannot be loaded,
* e.g. if the camera isn't streaming.
*
* @returns A tuple containing:
* 1. A 2-D numpy array containing the image. This must be adjusted
* by :py:meth:`~IRImager.get_temp_range_decimal` to get the
* actual temperature in degrees Celcius.
* 2. The time the image was taken.
*/
std::tuple<pybind11::array_t<uint16_t>, std::chrono::system_clock::time_point> get_frame() {
if (!streaming) {
throw std::runtime_error("IRIMAGER_STREAMOFF: Not streaming");
}

auto frame_size = std::array<ssize_t, 2>{128, 128};
auto my_array = pybind11::array_t<uint16_t>(frame_size);

auto r = my_array.mutable_unchecked<frame_size.size()>();

for (ssize_t i = 0; i < frame_size[0]; i++) {
for (ssize_t j = 0; j < frame_size[1]; j++) {
r(i, j) = 1800 * std::pow(10, get_temp_range_decimal());
}
}

return std::make_tuple(my_array, std::chrono::system_clock::now());
}

/**
* The number of decimal places in the thermal data
*
* For example, if :py:meth:`~IRImager.get_frame` returns 18000, you can
* divide this number by 10 to the power of the result of
* :py:meth:`~IRImager.get_temp_range_decimal` to get the actual
* temperature in degrees Celcius.
*/
short get_temp_range_decimal() {
return 1;
}

private:
bool streaming = false;
};

0 comments on commit 82db5ed

Please sign in to comment.