Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use pybind11_mkdoc to make Python docstrings from C++ comments #7

Merged
merged 2 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
};