diff options
Diffstat (limited to 'src/py/libcamera')
-rwxr-xr-x | src/py/libcamera/gen-py-controls.py | 90 | ||||
-rw-r--r-- | src/py/libcamera/meson.build | 52 | ||||
-rw-r--r-- | src/py/libcamera/py_camera_manager.cpp | 131 | ||||
-rw-r--r-- | src/py/libcamera/py_camera_manager.h | 45 | ||||
-rw-r--r-- | src/py/libcamera/py_color_space.cpp | 70 | ||||
-rw-r--r-- | src/py/libcamera/py_controls_generated.cpp.in | 8 | ||||
-rw-r--r-- | src/py/libcamera/py_enums.cpp | 12 | ||||
-rw-r--r-- | src/py/libcamera/py_formats_generated.cpp.in | 2 | ||||
-rw-r--r-- | src/py/libcamera/py_geometry.cpp | 2 | ||||
-rw-r--r-- | src/py/libcamera/py_helpers.cpp | 97 | ||||
-rw-r--r-- | src/py/libcamera/py_helpers.h | 13 | ||||
-rw-r--r-- | src/py/libcamera/py_main.cpp | 455 | ||||
-rw-r--r-- | src/py/libcamera/py_main.h | 14 | ||||
-rw-r--r-- | src/py/libcamera/py_properties_generated.cpp.in | 8 | ||||
-rw-r--r-- | src/py/libcamera/py_transform.cpp | 81 |
15 files changed, 718 insertions, 362 deletions
diff --git a/src/py/libcamera/gen-py-controls.py b/src/py/libcamera/gen-py-controls.py index 99f3bbcf..8efbf95b 100755 --- a/src/py/libcamera/gen-py-controls.py +++ b/src/py/libcamera/gen-py-controls.py @@ -24,48 +24,57 @@ def find_common_prefix(strings): def generate_py(controls, mode): out = '' - for ctrl in controls: - name, ctrl = ctrl.popitem() - - if ctrl.get('draft'): - ns = 'libcamera::{}::draft::'.format(mode) - container = 'draft' - else: - ns = 'libcamera::{}::'.format(mode) - container = 'controls' + vendors_class_def = [] + vendor_defs = [] + vendors = [] + for vendor, ctrl_list in controls.items(): + for ctrls in ctrl_list: + name, ctrl = ctrls.popitem() + + if vendor not in vendors and vendor != 'libcamera': + vendor_mode_str = f'{vendor.capitalize()}{mode.capitalize()}' + vendors_class_def.append('class Py{}\n{{\n}};\n'.format(vendor_mode_str)) + vendor_defs.append('\tauto {} = py::class_<Py{}>(controls, \"{}\");'.format(vendor, vendor_mode_str, vendor)) + vendors.append(vendor) + + if vendor != 'libcamera': + ns = 'libcamera::{}::{}::'.format(mode, vendor) + container = vendor + else: + ns = 'libcamera::{}::'.format(mode) + container = 'controls' - out += f'\t{container}.def_readonly_static("{name}", static_cast<const libcamera::ControlId *>(&{ns}{name}));\n\n' + out += f'\t{container}.def_readonly_static("{name}", static_cast<const libcamera::ControlId *>(&{ns}{name}));\n\n' - enum = ctrl.get('enum') - if not enum: - continue + enum = ctrl.get('enum') + if not enum: + continue - cpp_enum = name + 'Enum' + cpp_enum = name + 'Enum' - out += '\tpy::enum_<{}{}>({}, \"{}\")\n'.format(ns, cpp_enum, container, cpp_enum) + out += '\tpy::enum_<{}{}>({}, \"{}\")\n'.format(ns, cpp_enum, container, cpp_enum) - if mode == 'controls': - # Adjustments for controls - if name == 'LensShadingMapMode': - prefix = 'LensShadingMapMode' - elif name == 'SceneFlicker': - # If we strip the prefix, we would get '50Hz', which is illegal name - prefix = '' + if mode == 'controls': + # Adjustments for controls + if name == 'LensShadingMapMode': + prefix = 'LensShadingMapMode' + else: + prefix = find_common_prefix([e['name'] for e in enum]) else: + # Adjustments for properties prefix = find_common_prefix([e['name'] for e in enum]) - else: - # Adjustments for properties - prefix = find_common_prefix([e['name'] for e in enum]) - for entry in enum: - cpp_enum = entry['name'] - py_enum = entry['name'][len(prefix):] + for entry in enum: + cpp_enum = entry['name'] + py_enum = entry['name'][len(prefix):] - out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum) + out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum) - out += '\t;\n\n' + out += '\t;\n\n' - return {'controls': out} + return {'controls': out, + 'vendors_class_def': '\n'.join(vendors_class_def), + 'vendors_defs': '\n'.join(vendor_defs)} def fill_template(template, data): @@ -78,22 +87,25 @@ def fill_template(template, data): def main(argv): # Parse command line arguments parser = argparse.ArgumentParser() - parser.add_argument('-o', dest='output', metavar='file', type=str, + parser.add_argument('--mode', '-m', type=str, required=True, + help='Mode is either "controls" or "properties"') + parser.add_argument('--output', '-o', metavar='file', type=str, help='Output file name. Defaults to standard output if not specified.') - parser.add_argument('input', type=str, - help='Input file name.') - parser.add_argument('template', type=str, + parser.add_argument('--template', '-t', type=str, required=True, help='Template file name.') - parser.add_argument('--mode', type=str, required=True, - help='Mode is either "controls" or "properties"') + parser.add_argument('input', type=str, nargs='+', + help='Input file name.') args = parser.parse_args(argv[1:]) if args.mode not in ['controls', 'properties']: print(f'Invalid mode option "{args.mode}"', file=sys.stderr) return -1 - data = open(args.input, 'rb').read() - controls = yaml.safe_load(data)['controls'] + controls = {} + for input in args.input: + data = open(input, 'rb').read() + vendor = yaml.safe_load(data)['vendor'] + controls[vendor] = yaml.safe_load(data)['controls'] data = generate_py(controls, args.mode) diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build index eb884538..4807ca7d 100644 --- a/src/py/libcamera/meson.build +++ b/src/py/libcamera/meson.build @@ -7,42 +7,56 @@ if not py3_dep.found() subdir_done() endif -pycamera_enabled = true +pybind11_dep = dependency('pybind11', required : get_option('pycamera')) + +if not pybind11_dep.found() + pycamera_enabled = false + subdir_done() +endif -pybind11_proj = subproject('pybind11') -pybind11_dep = pybind11_proj.get_variable('pybind11_dep') +pycamera_enabled = true pycamera_sources = files([ + 'py_camera_manager.cpp', + 'py_color_space.cpp', 'py_enums.cpp', 'py_geometry.cpp', + 'py_helpers.cpp', 'py_main.cpp', + 'py_transform.cpp', ]) # Generate controls -gen_py_controls_input_files = files([ - '../../libcamera/control_ids.yaml', - 'py_controls_generated.cpp.in', -]) +gen_py_controls_input_files = [] +gen_py_controls_template = files('py_controls_generated.cpp.in') gen_py_controls = files('gen-py-controls.py') +foreach file : controls_files + gen_py_controls_input_files += files('../../libcamera/' + file) +endforeach + pycamera_sources += custom_target('py_gen_controls', input : gen_py_controls_input_files, output : ['py_controls_generated.cpp'], - command : [gen_py_controls, '--mode', 'controls', '-o', '@OUTPUT@', '@INPUT@']) + command : [gen_py_controls, '--mode', 'controls', '-o', '@OUTPUT@', + '-t', gen_py_controls_template, '@INPUT@']) # Generate properties -gen_py_property_enums_input_files = files([ - '../../libcamera/property_ids.yaml', - 'py_properties_generated.cpp.in', -]) +gen_py_property_enums_input_files = [] +gen_py_properties_template = files('py_properties_generated.cpp.in') + +foreach file : properties_files + gen_py_property_enums_input_files += files('../../libcamera/' + file) +endforeach pycamera_sources += custom_target('py_gen_properties', input : gen_py_property_enums_input_files, output : ['py_properties_generated.cpp'], - command : [gen_py_controls, '--mode', 'properties', '-o', '@OUTPUT@', '@INPUT@']) + command : [gen_py_controls, '--mode', 'properties', '-o', '@OUTPUT@', + '-t', gen_py_properties_template, '@INPUT@']) # Generate formats @@ -59,7 +73,7 @@ pycamera_sources += custom_target('py_gen_formats', command : [gen_py_formats, '-o', '@OUTPUT@', '@INPUT@']) pycamera_deps = [ - libcamera_public, + libcamera_private, py3_dep, pybind11_dep, ] @@ -68,7 +82,6 @@ pycamera_args = [ '-fvisibility=hidden', '-Wno-shadow', '-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT', - '-DLIBCAMERA_BASE_PRIVATE', ] destdir = get_option('libdir') / ('python' + py3_dep.version()) / 'site-packages' / 'libcamera' @@ -77,6 +90,7 @@ pycamera = shared_module('_libcamera', pycamera_sources, install : true, install_dir : destdir, + install_tag : 'python-runtime', name_prefix : '', dependencies : pycamera_deps, cpp_args : pycamera_args) @@ -86,13 +100,15 @@ pycamera = shared_module('_libcamera', run_command('ln', '-fsrT', files('__init__.py'), meson.current_build_dir() / '__init__.py', - check: true) + check : true) run_command('ln', '-fsrT', meson.current_source_dir() / 'utils', meson.current_build_dir() / 'utils', - check: true) + check : true) -install_data(['__init__.py'], install_dir : destdir) +install_data(['__init__.py'], + install_dir : destdir, + install_tag : 'python-runtime') # \todo Generate stubs when building. See https://peps.python.org/pep-0484/#stub-files # Note: Depends on pybind11-stubgen. To generate pylibcamera stubs: diff --git a/src/py/libcamera/py_camera_manager.cpp b/src/py/libcamera/py_camera_manager.cpp new file mode 100644 index 00000000..9ccb7aad --- /dev/null +++ b/src/py/libcamera/py_camera_manager.cpp @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + */ + +#include "py_camera_manager.h" + +#include <errno.h> +#include <memory> +#include <sys/eventfd.h> +#include <system_error> +#include <unistd.h> +#include <vector> + +#include "py_main.h" + +namespace py = pybind11; + +using namespace libcamera; + +PyCameraManager::PyCameraManager() +{ + LOG(Python, Debug) << "PyCameraManager()"; + + cameraManager_ = std::make_unique<CameraManager>(); + + int fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (fd == -1) + throw std::system_error(errno, std::generic_category(), + "Failed to create eventfd"); + + eventFd_ = UniqueFD(fd); + + int ret = cameraManager_->start(); + if (ret) + throw std::system_error(-ret, std::generic_category(), + "Failed to start CameraManager"); +} + +PyCameraManager::~PyCameraManager() +{ + LOG(Python, Debug) << "~PyCameraManager()"; +} + +py::list PyCameraManager::cameras() +{ + /* + * Create a list of Cameras, where each camera has a keep-alive to + * CameraManager. + */ + py::list l; + + for (auto &camera : cameraManager_->cameras()) { + py::object py_cm = py::cast(this); + py::object py_cam = py::cast(camera); + py::detail::keep_alive_impl(py_cam, py_cm); + l.append(py_cam); + } + + return l; +} + +std::vector<py::object> PyCameraManager::getReadyRequests() +{ + int ret = readFd(); + + if (ret == -EAGAIN) + return std::vector<py::object>(); + + if (ret != 0) + throw std::system_error(-ret, std::generic_category()); + + std::vector<py::object> py_reqs; + + for (Request *request : getCompletedRequests()) { + py::object o = py::cast(request); + /* Decrease the ref increased in Camera.queue_request() */ + o.dec_ref(); + py_reqs.push_back(o); + } + + return py_reqs; +} + +/* Note: Called from another thread */ +void PyCameraManager::handleRequestCompleted(Request *req) +{ + pushRequest(req); + writeFd(); +} + +void PyCameraManager::writeFd() +{ + uint64_t v = 1; + + size_t s = write(eventFd_.get(), &v, 8); + /* + * We should never fail, and have no simple means to manage the error, + * so let's log a fatal error. + */ + if (s != 8) + LOG(Python, Fatal) << "Unable to write to eventfd"; +} + +int PyCameraManager::readFd() +{ + uint8_t buf[8]; + + ssize_t ret = read(eventFd_.get(), buf, 8); + + if (ret == 8) + return 0; + else if (ret < 0) + return -errno; + else + return -EIO; +} + +void PyCameraManager::pushRequest(Request *req) +{ + MutexLocker guard(completedRequestsMutex_); + completedRequests_.push_back(req); +} + +std::vector<Request *> PyCameraManager::getCompletedRequests() +{ + std::vector<Request *> v; + MutexLocker guard(completedRequestsMutex_); + swap(v, completedRequests_); + return v; +} diff --git a/src/py/libcamera/py_camera_manager.h b/src/py/libcamera/py_camera_manager.h new file mode 100644 index 00000000..3574db23 --- /dev/null +++ b/src/py/libcamera/py_camera_manager.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + */ + +#pragma once + +#include <libcamera/base/mutex.h> + +#include <libcamera/libcamera.h> + +#include <pybind11/pybind11.h> + +using namespace libcamera; + +class PyCameraManager +{ +public: + PyCameraManager(); + ~PyCameraManager(); + + pybind11::list cameras(); + std::shared_ptr<Camera> get(const std::string &name) { return cameraManager_->get(name); } + + static const std::string &version() { return CameraManager::version(); } + + int eventFd() const { return eventFd_.get(); } + + std::vector<pybind11::object> getReadyRequests(); + + void handleRequestCompleted(Request *req); + +private: + std::unique_ptr<CameraManager> cameraManager_; + + UniqueFD eventFd_; + libcamera::Mutex completedRequestsMutex_; + std::vector<Request *> completedRequests_ + LIBCAMERA_TSA_GUARDED_BY(completedRequestsMutex_); + + void writeFd(); + int readFd(); + void pushRequest(Request *req); + std::vector<Request *> getCompletedRequests(); +}; diff --git a/src/py/libcamera/py_color_space.cpp b/src/py/libcamera/py_color_space.cpp new file mode 100644 index 00000000..5201121a --- /dev/null +++ b/src/py/libcamera/py_color_space.cpp @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + * + * Python bindings - Color Space classes + */ + +#include <libcamera/color_space.h> +#include <libcamera/libcamera.h> + +#include <pybind11/operators.h> +#include <pybind11/pybind11.h> +#include <pybind11/stl.h> + +namespace py = pybind11; + +using namespace libcamera; + +void init_py_color_space(py::module &m) +{ + auto pyColorSpace = py::class_<ColorSpace>(m, "ColorSpace"); + auto pyColorSpacePrimaries = py::enum_<ColorSpace::Primaries>(pyColorSpace, "Primaries"); + auto pyColorSpaceTransferFunction = py::enum_<ColorSpace::TransferFunction>(pyColorSpace, "TransferFunction"); + auto pyColorSpaceYcbcrEncoding = py::enum_<ColorSpace::YcbcrEncoding>(pyColorSpace, "YcbcrEncoding"); + auto pyColorSpaceRange = py::enum_<ColorSpace::Range>(pyColorSpace, "Range"); + + pyColorSpace + .def(py::init([](ColorSpace::Primaries primaries, + ColorSpace::TransferFunction transferFunction, + ColorSpace::YcbcrEncoding ycbcrEncoding, + ColorSpace::Range range) { + return ColorSpace(primaries, transferFunction, ycbcrEncoding, range); + }), py::arg("primaries"), py::arg("transferFunction"), + py::arg("ycbcrEncoding"), py::arg("range")) + .def(py::init([](ColorSpace &other) { return other; })) + .def("__str__", [](ColorSpace &self) { + return "<libcamera.ColorSpace '" + self.toString() + "'>"; + }) + .def_readwrite("primaries", &ColorSpace::primaries) + .def_readwrite("transferFunction", &ColorSpace::transferFunction) + .def_readwrite("ycbcrEncoding", &ColorSpace::ycbcrEncoding) + .def_readwrite("range", &ColorSpace::range) + .def_static("Raw", []() { return ColorSpace::Raw; }) + .def_static("Srgb", []() { return ColorSpace::Srgb; }) + .def_static("Sycc", []() { return ColorSpace::Sycc; }) + .def_static("Smpte170m", []() { return ColorSpace::Smpte170m; }) + .def_static("Rec709", []() { return ColorSpace::Rec709; }) + .def_static("Rec2020", []() { return ColorSpace::Rec2020; }); + + pyColorSpacePrimaries + .value("Raw", ColorSpace::Primaries::Raw) + .value("Smpte170m", ColorSpace::Primaries::Smpte170m) + .value("Rec709", ColorSpace::Primaries::Rec709) + .value("Rec2020", ColorSpace::Primaries::Rec2020); + + pyColorSpaceTransferFunction + .value("Linear", ColorSpace::TransferFunction::Linear) + .value("Srgb", ColorSpace::TransferFunction::Srgb) + .value("Rec709", ColorSpace::TransferFunction::Rec709); + + pyColorSpaceYcbcrEncoding + .value("Null", ColorSpace::YcbcrEncoding::None) + .value("Rec601", ColorSpace::YcbcrEncoding::Rec601) + .value("Rec709", ColorSpace::YcbcrEncoding::Rec709) + .value("Rec2020", ColorSpace::YcbcrEncoding::Rec2020); + + pyColorSpaceRange + .value("Full", ColorSpace::Range::Full) + .value("Limited", ColorSpace::Range::Limited); +} diff --git a/src/py/libcamera/py_controls_generated.cpp.in b/src/py/libcamera/py_controls_generated.cpp.in index cb8442ba..8d282ce5 100644 --- a/src/py/libcamera/py_controls_generated.cpp.in +++ b/src/py/libcamera/py_controls_generated.cpp.in @@ -9,7 +9,7 @@ #include <libcamera/control_ids.h> -#include <pybind11/smart_holder.h> +#include <pybind11/pybind11.h> namespace py = pybind11; @@ -17,14 +17,12 @@ class PyControls { }; -class PyDraftControls -{ -}; +${vendors_class_def} void init_py_controls_generated(py::module& m) { auto controls = py::class_<PyControls>(m, "controls"); - auto draft = py::class_<PyDraftControls>(controls, "draft"); +${vendors_defs} ${controls} } diff --git a/src/py/libcamera/py_enums.cpp b/src/py/libcamera/py_enums.cpp index 96d4beef..e25689c6 100644 --- a/src/py/libcamera/py_enums.cpp +++ b/src/py/libcamera/py_enums.cpp @@ -7,7 +7,7 @@ #include <libcamera/libcamera.h> -#include <pybind11/smart_holder.h> +#include <pybind11/pybind11.h> namespace py = pybind11; @@ -31,4 +31,14 @@ void init_py_enums(py::module &m) .value("String", ControlType::ControlTypeString) .value("Rectangle", ControlType::ControlTypeRectangle) .value("Size", ControlType::ControlTypeSize); + + py::enum_<Orientation>(m, "Orientation") + .value("Rotate0", Orientation::Rotate0) + .value("Rotate0Mirror", Orientation::Rotate0Mirror) + .value("Rotate180", Orientation::Rotate180) + .value("Rotate180Mirror", Orientation::Rotate180Mirror) + .value("Rotate90Mirror", Orientation::Rotate90Mirror) + .value("Rotate270", Orientation::Rotate270) + .value("Rotate270Mirror", Orientation::Rotate270Mirror) + .value("Rotate90", Orientation::Rotate90); } diff --git a/src/py/libcamera/py_formats_generated.cpp.in b/src/py/libcamera/py_formats_generated.cpp.in index b88807f3..a3f7f94d 100644 --- a/src/py/libcamera/py_formats_generated.cpp.in +++ b/src/py/libcamera/py_formats_generated.cpp.in @@ -9,7 +9,7 @@ #include <libcamera/formats.h> -#include <pybind11/smart_holder.h> +#include <pybind11/pybind11.h> namespace py = pybind11; diff --git a/src/py/libcamera/py_geometry.cpp b/src/py/libcamera/py_geometry.cpp index 84b0cb08..5c2aeac4 100644 --- a/src/py/libcamera/py_geometry.cpp +++ b/src/py/libcamera/py_geometry.cpp @@ -11,7 +11,7 @@ #include <libcamera/libcamera.h> #include <pybind11/operators.h> -#include <pybind11/smart_holder.h> +#include <pybind11/pybind11.h> #include <pybind11/stl.h> namespace py = pybind11; diff --git a/src/py/libcamera/py_helpers.cpp b/src/py/libcamera/py_helpers.cpp new file mode 100644 index 00000000..79891ab6 --- /dev/null +++ b/src/py/libcamera/py_helpers.cpp @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + */ + +#include "py_helpers.h" + +#include <libcamera/libcamera.h> + +#include <pybind11/functional.h> +#include <pybind11/stl.h> +#include <pybind11/stl_bind.h> + +namespace py = pybind11; + +using namespace libcamera; + +template<typename T> +static py::object valueOrTuple(const ControlValue &cv) +{ + if (cv.isArray()) { + const T *v = reinterpret_cast<const T *>(cv.data().data()); + auto t = py::tuple(cv.numElements()); + + for (size_t i = 0; i < cv.numElements(); ++i) + t[i] = v[i]; + + return std::move(t); + } + + return py::cast(cv.get<T>()); +} + +py::object controlValueToPy(const ControlValue &cv) +{ + switch (cv.type()) { + case ControlTypeBool: + return valueOrTuple<bool>(cv); + case ControlTypeByte: + return valueOrTuple<uint8_t>(cv); + case ControlTypeInteger32: + return valueOrTuple<int32_t>(cv); + case ControlTypeInteger64: + return valueOrTuple<int64_t>(cv); + case ControlTypeFloat: + return valueOrTuple<float>(cv); + case ControlTypeString: + return py::cast(cv.get<std::string>()); + case ControlTypeRectangle: + return valueOrTuple<Rectangle>(cv); + case ControlTypeSize: { + const Size *v = reinterpret_cast<const Size *>(cv.data().data()); + return py::cast(v); + } + case ControlTypeNone: + return py::none(); + default: + throw std::runtime_error("Unsupported ControlValue type"); + } +} + +template<typename T> +static ControlValue controlValueMaybeArray(const py::object &ob) +{ + if (py::isinstance<py::list>(ob) || py::isinstance<py::tuple>(ob)) { + std::vector<T> vec = ob.cast<std::vector<T>>(); + return ControlValue(Span<const T>(vec)); + } + + return ControlValue(ob.cast<T>()); +} + +ControlValue pyToControlValue(const py::object &ob, ControlType type) +{ + switch (type) { + case ControlTypeBool: + return ControlValue(ob.cast<bool>()); + case ControlTypeByte: + return controlValueMaybeArray<uint8_t>(ob); + case ControlTypeInteger32: + return controlValueMaybeArray<int32_t>(ob); + case ControlTypeInteger64: + return controlValueMaybeArray<int64_t>(ob); + case ControlTypeFloat: + return controlValueMaybeArray<float>(ob); + case ControlTypeString: + return ControlValue(ob.cast<std::string>()); + case ControlTypeRectangle: + return controlValueMaybeArray<Rectangle>(ob); + case ControlTypeSize: + return ControlValue(ob.cast<Size>()); + case ControlTypeNone: + return ControlValue(); + default: + throw std::runtime_error("Control type not implemented"); + } +} diff --git a/src/py/libcamera/py_helpers.h b/src/py/libcamera/py_helpers.h new file mode 100644 index 00000000..983969df --- /dev/null +++ b/src/py/libcamera/py_helpers.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + */ + +#pragma once + +#include <libcamera/libcamera.h> + +#include <pybind11/pybind11.h> + +pybind11::object controlValueToPy(const libcamera::ControlValue &cv); +libcamera::ControlValue pyToControlValue(const pybind11::object &ob, libcamera::ControlType type); diff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp index 505cc3dc..bce08218 100644 --- a/src/py/libcamera/py_main.cpp +++ b/src/py/libcamera/py_main.cpp @@ -5,132 +5,93 @@ * Python bindings */ -#include <mutex> +#include "py_main.h" + +#include <memory> #include <stdexcept> -#include <sys/eventfd.h> -#include <unistd.h> +#include <string> +#include <vector> #include <libcamera/base/log.h> #include <libcamera/libcamera.h> #include <pybind11/functional.h> -#include <pybind11/smart_holder.h> +#include <pybind11/pybind11.h> #include <pybind11/stl.h> #include <pybind11/stl_bind.h> +#include "py_camera_manager.h" +#include "py_helpers.h" + namespace py = pybind11; using namespace libcamera; -template<typename T> -static py::object valueOrTuple(const ControlValue &cv) -{ - if (cv.isArray()) { - const T *v = reinterpret_cast<const T *>(cv.data().data()); - auto t = py::tuple(cv.numElements()); +namespace libcamera { - for (size_t i = 0; i < cv.numElements(); ++i) - t[i] = v[i]; +LOG_DEFINE_CATEGORY(Python) - return std::move(t); - } - - return py::cast(cv.get<T>()); } -static py::object controlValueToPy(const ControlValue &cv) +/* + * This is a holder class used only for the Camera class, for the sole purpose + * of avoiding the compilation issue with Camera's private destructor. + * + * pybind11 requires a public destructor for classes held with shared_ptrs, even + * in cases where the public destructor is not strictly needed. The current + * understanding is that there are the following options to solve the problem: + * + * - Use pybind11 'smart_holder' branch. The downside is that 'smart_holder' + * is not the mainline branch, and not available in distributions. + * - https://github.com/pybind/pybind11/pull/2067 + * - Make the Camera destructor public + * - Something like the PyCameraSmartPtr here, which adds a layer, hiding the + * issue. + */ +template<typename T> +class PyCameraSmartPtr { - switch (cv.type()) { - case ControlTypeBool: - return valueOrTuple<bool>(cv); - case ControlTypeByte: - return valueOrTuple<uint8_t>(cv); - case ControlTypeInteger32: - return valueOrTuple<int32_t>(cv); - case ControlTypeInteger64: - return valueOrTuple<int64_t>(cv); - case ControlTypeFloat: - return valueOrTuple<float>(cv); - case ControlTypeString: - return py::cast(cv.get<std::string>()); - case ControlTypeRectangle: { - const Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data()); - return py::cast(v); - } - case ControlTypeSize: { - const Size *v = reinterpret_cast<const Size *>(cv.data().data()); - return py::cast(v); +public: + using element_type = T; + + PyCameraSmartPtr() + { } - case ControlTypeNone: - default: - throw std::runtime_error("Unsupported ControlValue type"); + + explicit PyCameraSmartPtr(T *) + { + throw std::runtime_error("invalid SmartPtr constructor call"); } -} -template<typename T> -static ControlValue controlValueMaybeArray(const py::object &ob) -{ - if (py::isinstance<py::list>(ob) || py::isinstance<py::tuple>(ob)) { - std::vector<T> vec = ob.cast<std::vector<T>>(); - return ControlValue(Span<const T>(vec)); + explicit PyCameraSmartPtr(std::shared_ptr<T> p) + : ptr_(p) + { } - return ControlValue(ob.cast<T>()); -} + T *get() const { return ptr_.get(); } -static ControlValue pyToControlValue(const py::object &ob, ControlType type) -{ - switch (type) { - case ControlTypeBool: - return ControlValue(ob.cast<bool>()); - case ControlTypeByte: - return controlValueMaybeArray<uint8_t>(ob); - case ControlTypeInteger32: - return controlValueMaybeArray<int32_t>(ob); - case ControlTypeInteger64: - return controlValueMaybeArray<int64_t>(ob); - case ControlTypeFloat: - return controlValueMaybeArray<float>(ob); - case ControlTypeString: - return ControlValue(ob.cast<std::string>()); - case ControlTypeRectangle: - return ControlValue(ob.cast<Rectangle>()); - case ControlTypeSize: - return ControlValue(ob.cast<Size>()); - case ControlTypeNone: - default: - throw std::runtime_error("Control type not implemented"); - } -} + operator std::shared_ptr<T>() const { return ptr_; } -static std::weak_ptr<CameraManager> gCameraManager; -static int gEventfd; -static std::mutex gReqlistMutex; -static std::vector<Request *> gReqList; +private: + std::shared_ptr<T> ptr_; +}; -static void handleRequestCompleted(Request *req) -{ - { - std::lock_guard guard(gReqlistMutex); - gReqList.push_back(req); - } +PYBIND11_DECLARE_HOLDER_TYPE(T, PyCameraSmartPtr<T>) - uint64_t v = 1; - size_t s = write(gEventfd, &v, 8); - /* - * We should never fail, and have no simple means to manage the error, - * so let's use LOG(Fatal). - */ - if (s != 8) - LOG(Fatal) << "Unable to write to eventfd"; -} +/* + * Note: global C++ destructors can be ran on this before the py module is + * destructed. + */ +static std::weak_ptr<PyCameraManager> gCameraManager; -void init_py_enums(py::module &m); +void init_py_color_space(py::module &m); void init_py_controls_generated(py::module &m); +void init_py_enums(py::module &m); void init_py_formats_generated(py::module &m); void init_py_geometry(py::module &m); void init_py_properties_generated(py::module &m); +void init_py_transform(py::module &m); PYBIND11_MODULE(_libcamera, m) { @@ -138,6 +99,8 @@ PYBIND11_MODULE(_libcamera, m) init_py_controls_generated(m); init_py_geometry(m); init_py_properties_generated(m); + init_py_color_space(m); + init_py_transform(m); /* Forward declarations */ @@ -147,8 +110,9 @@ PYBIND11_MODULE(_libcamera, m) * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings */ - auto pyCameraManager = py::class_<CameraManager>(m, "CameraManager"); - auto pyCamera = py::class_<Camera>(m, "Camera"); + auto pyCameraManager = py::class_<PyCameraManager, std::shared_ptr<PyCameraManager>>(m, "CameraManager"); + auto pyCamera = py::class_<Camera, PyCameraSmartPtr<Camera>>(m, "Camera"); + auto pySensorConfiguration = py::class_<SensorConfiguration>(m, "SensorConfiguration"); auto pyCameraConfiguration = py::class_<CameraConfiguration>(m, "CameraConfiguration"); auto pyCameraConfigurationStatus = py::enum_<CameraConfiguration::Status>(pyCameraConfiguration, "Status"); auto pyStreamConfiguration = py::class_<StreamConfiguration>(m, "StreamConfiguration"); @@ -165,12 +129,6 @@ PYBIND11_MODULE(_libcamera, m) auto pyFrameMetadata = py::class_<FrameMetadata>(m, "FrameMetadata"); auto pyFrameMetadataStatus = py::enum_<FrameMetadata::Status>(pyFrameMetadata, "Status"); auto pyFrameMetadataPlane = py::class_<FrameMetadata::Plane>(pyFrameMetadata, "Plane"); - auto pyTransform = py::class_<Transform>(m, "Transform"); - auto pyColorSpace = py::class_<ColorSpace>(m, "ColorSpace"); - auto pyColorSpacePrimaries = py::enum_<ColorSpace::Primaries>(pyColorSpace, "Primaries"); - auto pyColorSpaceTransferFunction = py::enum_<ColorSpace::TransferFunction>(pyColorSpace, "TransferFunction"); - auto pyColorSpaceYcbcrEncoding = py::enum_<ColorSpace::YcbcrEncoding>(pyColorSpace, "YcbcrEncoding"); - auto pyColorSpaceRange = py::enum_<ColorSpace::Range>(pyColorSpace, "Range"); auto pyPixelFormat = py::class_<PixelFormat>(m, "PixelFormat"); init_py_formats_generated(m); @@ -181,113 +139,69 @@ PYBIND11_MODULE(_libcamera, m) /* Classes */ pyCameraManager .def_static("singleton", []() { - std::shared_ptr<CameraManager> cm = gCameraManager.lock(); - if (cm) - return cm; - - int fd = eventfd(0, 0); - if (fd == -1) - throw std::system_error(errno, std::generic_category(), - "Failed to create eventfd"); - - cm = std::shared_ptr<CameraManager>(new CameraManager, [](auto p) { - close(gEventfd); - gEventfd = -1; - delete p; - }); - - gEventfd = fd; - gCameraManager = cm; - - int ret = cm->start(); - if (ret) - throw std::system_error(-ret, std::generic_category(), - "Failed to start CameraManager"); - - return cm; - }) - - .def_property_readonly("version", &CameraManager::version) - - .def_property_readonly("event_fd", [](CameraManager &) { - return gEventfd; - }) - - .def("get_ready_requests", [](CameraManager &) { - uint8_t buf[8]; - - if (read(gEventfd, buf, 8) != 8) - throw std::system_error(errno, std::generic_category()); - - std::vector<Request *> v; - - { - std::lock_guard guard(gReqlistMutex); - swap(v, gReqList); - } - - std::vector<py::object> ret; + std::shared_ptr<PyCameraManager> cm = gCameraManager.lock(); - for (Request *req : v) { - py::object o = py::cast(req); - /* Decrease the ref increased in Camera.queue_request() */ - o.dec_ref(); - ret.push_back(o); + if (!cm) { + cm = std::make_shared<PyCameraManager>(); + gCameraManager = cm; } - return ret; + return cm; }) - .def("get", py::overload_cast<const std::string &>(&CameraManager::get), py::keep_alive<0, 1>()) + .def_property_readonly_static("version", [](py::object /* self */) { return PyCameraManager::version(); }) + .def("get", &PyCameraManager::get, py::keep_alive<0, 1>()) + .def_property_readonly("cameras", &PyCameraManager::cameras) - /* Create a list of Cameras, where each camera has a keep-alive to CameraManager */ - .def_property_readonly("cameras", [](CameraManager &self) { - py::list l; - - for (auto &c : self.cameras()) { - py::object py_cm = py::cast(self); - py::object py_cam = py::cast(c); - py::detail::keep_alive_impl(py_cam, py_cm); - l.append(py_cam); - } - - return l; - }); + .def_property_readonly("event_fd", &PyCameraManager::eventFd) + .def("get_ready_requests", &PyCameraManager::getReadyRequests); pyCamera .def_property_readonly("id", &Camera::id) - .def("acquire", &Camera::acquire) - .def("release", &Camera::release) + .def("acquire", [](Camera &self) { + int ret = self.acquire(); + if (ret) + throw std::system_error(-ret, std::generic_category(), + "Failed to acquire camera"); + }) + .def("release", [](Camera &self) { + int ret = self.release(); + if (ret) + throw std::system_error(-ret, std::generic_category(), + "Failed to release camera"); + }) .def("start", [](Camera &self, const std::unordered_map<const ControlId *, py::object> &controls) { /* \todo What happens if someone calls start() multiple times? */ - self.requestCompleted.connect(handleRequestCompleted); + auto cm = gCameraManager.lock(); + ASSERT(cm); + + self.requestCompleted.connect(cm.get(), &PyCameraManager::handleRequestCompleted); ControlList controlList(self.controls()); - for (const auto& [id, obj]: controls) { + for (const auto &[id, obj] : controls) { auto val = pyToControlValue(obj, id->type()); controlList.set(id->id(), val); } int ret = self.start(&controlList); if (ret) { - self.requestCompleted.disconnect(handleRequestCompleted); - return ret; + self.requestCompleted.disconnect(); + throw std::system_error(-ret, std::generic_category(), + "Failed to start camera"); } - - return 0; }, py::arg("controls") = std::unordered_map<const ControlId *, py::object>()) .def("stop", [](Camera &self) { int ret = self.stop(); - if (ret) - return ret; - self.requestCompleted.disconnect(handleRequestCompleted); + self.requestCompleted.disconnect(); - return 0; + if (ret) + throw std::system_error(-ret, std::generic_category(), + "Failed to stop camera"); }) .def("__str__", [](Camera &self) { @@ -295,10 +209,24 @@ PYBIND11_MODULE(_libcamera, m) }) /* Keep the camera alive, as StreamConfiguration contains a Stream* */ - .def("generate_configuration", &Camera::generateConfiguration, py::keep_alive<0, 1>()) - .def("configure", &Camera::configure) + .def("generate_configuration", [](Camera &self, const std::vector<StreamRole> &roles) { + return self.generateConfiguration(roles); + }, py::keep_alive<0, 1>()) + + .def("configure", [](Camera &self, CameraConfiguration *config) { + int ret = self.configure(config); + if (ret) + throw std::system_error(-ret, std::generic_category(), + "Failed to configure camera"); + }) - .def("create_request", &Camera::createRequest, py::arg("cookie") = 0) + .def("create_request", [](Camera &self, uint64_t cookie) { + std::unique_ptr<Request> req = self.createRequest(cookie); + if (!req) + throw std::system_error(ENOMEM, std::generic_category(), + "Failed to create request"); + return req; + }, py::arg("cookie") = 0) .def("queue_request", [](Camera &self, Request *req) { py::object py_req = py::cast(req); @@ -311,10 +239,11 @@ PYBIND11_MODULE(_libcamera, m) py_req.inc_ref(); int ret = self.queueRequest(req); - if (ret) + if (ret) { py_req.dec_ref(); - - return ret; + throw std::system_error(-ret, std::generic_category(), + "Failed to queue request"); + } }) .def_property_readonly("streams", [](Camera &self) { @@ -353,6 +282,40 @@ PYBIND11_MODULE(_libcamera, m) return ret; }); + pySensorConfiguration + .def(py::init<>()) + .def_readwrite("bit_depth", &SensorConfiguration::bitDepth) + .def_readwrite("analog_crop", &SensorConfiguration::analogCrop) + .def_property( + "binning", + [](SensorConfiguration &self) { + return py::make_tuple(self.binning.binX, self.binning.binY); + }, + [](SensorConfiguration &self, py::object value) { + auto vec = value.cast<std::vector<unsigned int>>(); + if (vec.size() != 2) + throw std::runtime_error("binning requires iterable of 2 values"); + self.binning.binX = vec[0]; + self.binning.binY = vec[1]; + }) + .def_property( + "skipping", + [](SensorConfiguration &self) { + return py::make_tuple(self.skipping.xOddInc, self.skipping.xEvenInc, + self.skipping.yOddInc, self.skipping.yEvenInc); + }, + [](SensorConfiguration &self, py::object value) { + auto vec = value.cast<std::vector<unsigned int>>(); + if (vec.size() != 4) + throw std::runtime_error("skipping requires iterable of 4 values"); + self.skipping.xOddInc = vec[0]; + self.skipping.xEvenInc = vec[1]; + self.skipping.yOddInc = vec[2]; + self.skipping.yEvenInc = vec[3]; + }) + .def_readwrite("output_size", &SensorConfiguration::outputSize) + .def("is_valid", &SensorConfiguration::isValid); + pyCameraConfiguration .def("__iter__", [](CameraConfiguration &self) { return py::make_iterator<py::return_value_policy::reference_internal>(self); @@ -365,7 +328,8 @@ PYBIND11_MODULE(_libcamera, m) py::return_value_policy::reference_internal) .def_property_readonly("size", &CameraConfiguration::size) .def_property_readonly("empty", &CameraConfiguration::empty) - .def_readwrite("transform", &CameraConfiguration::transform); + .def_readwrite("sensor_config", &CameraConfiguration::sensorConfig) + .def_readwrite("orientation", &CameraConfiguration::orientation); pyCameraConfigurationStatus .value("Valid", CameraConfiguration::Valid) @@ -391,8 +355,14 @@ PYBIND11_MODULE(_libcamera, m) .def("range", &StreamFormats::range); pyFrameBufferAllocator - .def(py::init<std::shared_ptr<Camera>>(), py::keep_alive<1, 2>()) - .def("allocate", &FrameBufferAllocator::allocate) + .def(py::init<PyCameraSmartPtr<Camera>>(), py::keep_alive<1, 2>()) + .def("allocate", [](FrameBufferAllocator &self, Stream *stream) { + int ret = self.allocate(stream); + if (ret < 0) + throw std::system_error(-ret, std::generic_category(), + "Failed to allocate buffers"); + return ret; + }) .def_property_readonly("allocated", &FrameBufferAllocator::allocated) /* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */ .def("buffers", [](FrameBufferAllocator &self, Stream *stream) { @@ -469,11 +439,15 @@ PYBIND11_MODULE(_libcamera, m) pyRequest /* \todo Fence is not supported, so we cannot expose addBuffer() directly */ .def("add_buffer", [](Request &self, const Stream *stream, FrameBuffer *buffer) { - return self.addBuffer(stream, buffer); + int ret = self.addBuffer(stream, buffer); + if (ret) + throw std::system_error(-ret, std::generic_category(), + "Failed to add buffer"); }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */ .def_property_readonly("status", &Request::status) .def_property_readonly("buffers", &Request::buffers) .def_property_readonly("cookie", &Request::cookie) + .def_property_readonly("sequence", &Request::sequence) .def_property_readonly("has_pending_buffers", &Request::hasPendingBuffers) .def("set_control", [](Request &self, const ControlId &id, py::object value) { self.controls().set(id.id(), pyToControlValue(value, id.type())); @@ -526,109 +500,6 @@ PYBIND11_MODULE(_libcamera, m) pyFrameMetadataPlane .def_readwrite("bytes_used", &FrameMetadata::Plane::bytesused); - pyTransform - .def(py::init([](int rotation, bool hflip, bool vflip, bool transpose) { - bool ok; - - Transform t = transformFromRotation(rotation, &ok); - if (!ok) - throw std::invalid_argument("Invalid rotation"); - - if (hflip) - t ^= Transform::HFlip; - if (vflip) - t ^= Transform::VFlip; - if (transpose) - t ^= Transform::Transpose; - return t; - }), py::arg("rotation") = 0, py::arg("hflip") = false, - py::arg("vflip") = false, py::arg("transpose") = false) - .def(py::init([](Transform &other) { return other; })) - .def("__str__", [](Transform &self) { - return "<libcamera.Transform '" + std::string(transformToString(self)) + "'>"; - }) - .def_property("hflip", - [](Transform &self) { - return !!(self & Transform::HFlip); - }, - [](Transform &self, bool hflip) { - if (hflip) - self |= Transform::HFlip; - else - self &= ~Transform::HFlip; - }) - .def_property("vflip", - [](Transform &self) { - return !!(self & Transform::VFlip); - }, - [](Transform &self, bool vflip) { - if (vflip) - self |= Transform::VFlip; - else - self &= ~Transform::VFlip; - }) - .def_property("transpose", - [](Transform &self) { - return !!(self & Transform::Transpose); - }, - [](Transform &self, bool transpose) { - if (transpose) - self |= Transform::Transpose; - else - self &= ~Transform::Transpose; - }) - .def("inverse", [](Transform &self) { return -self; }) - .def("invert", [](Transform &self) { - self = -self; - }) - .def("compose", [](Transform &self, Transform &other) { - self = self * other; - }); - - pyColorSpace - .def(py::init([](ColorSpace::Primaries primaries, - ColorSpace::TransferFunction transferFunction, - ColorSpace::YcbcrEncoding ycbcrEncoding, - ColorSpace::Range range) { - return ColorSpace(primaries, transferFunction, ycbcrEncoding, range); - }), py::arg("primaries"), py::arg("transferFunction"), - py::arg("ycbcrEncoding"), py::arg("range")) - .def(py::init([](ColorSpace &other) { return other; })) - .def("__str__", [](ColorSpace &self) { - return "<libcamera.ColorSpace '" + self.toString() + "'>"; - }) - .def_readwrite("primaries", &ColorSpace::primaries) - .def_readwrite("transferFunction", &ColorSpace::transferFunction) - .def_readwrite("ycbcrEncoding", &ColorSpace::ycbcrEncoding) - .def_readwrite("range", &ColorSpace::range) - .def_static("Raw", []() { return ColorSpace::Raw; }) - .def_static("Jpeg", []() { return ColorSpace::Jpeg; }) - .def_static("Srgb", []() { return ColorSpace::Srgb; }) - .def_static("Smpte170m", []() { return ColorSpace::Smpte170m; }) - .def_static("Rec709", []() { return ColorSpace::Rec709; }) - .def_static("Rec2020", []() { return ColorSpace::Rec2020; }); - - pyColorSpacePrimaries - .value("Raw", ColorSpace::Primaries::Raw) - .value("Smpte170m", ColorSpace::Primaries::Smpte170m) - .value("Rec709", ColorSpace::Primaries::Rec709) - .value("Rec2020", ColorSpace::Primaries::Rec2020); - - pyColorSpaceTransferFunction - .value("Linear", ColorSpace::TransferFunction::Linear) - .value("Srgb", ColorSpace::TransferFunction::Srgb) - .value("Rec709", ColorSpace::TransferFunction::Rec709); - - pyColorSpaceYcbcrEncoding - .value("Null", ColorSpace::YcbcrEncoding::None) - .value("Rec601", ColorSpace::YcbcrEncoding::Rec601) - .value("Rec709", ColorSpace::YcbcrEncoding::Rec709) - .value("Rec2020", ColorSpace::YcbcrEncoding::Rec2020); - - pyColorSpaceRange - .value("Full", ColorSpace::Range::Full) - .value("Limited", ColorSpace::Range::Limited); - pyPixelFormat .def(py::init<>()) .def(py::init<uint32_t, uint64_t>()) diff --git a/src/py/libcamera/py_main.h b/src/py/libcamera/py_main.h new file mode 100644 index 00000000..5bb5f2d1 --- /dev/null +++ b/src/py/libcamera/py_main.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + */ + +#pragma once + +#include <libcamera/base/log.h> + +namespace libcamera { + +LOG_DECLARE_CATEGORY(Python) + +} diff --git a/src/py/libcamera/py_properties_generated.cpp.in b/src/py/libcamera/py_properties_generated.cpp.in index 044b2b2a..e3802b81 100644 --- a/src/py/libcamera/py_properties_generated.cpp.in +++ b/src/py/libcamera/py_properties_generated.cpp.in @@ -9,7 +9,7 @@ #include <libcamera/property_ids.h> -#include <pybind11/smart_holder.h> +#include <pybind11/pybind11.h> namespace py = pybind11; @@ -17,14 +17,12 @@ class PyProperties { }; -class PyDraftProperties -{ -}; +${vendors_class_def} void init_py_properties_generated(py::module& m) { auto controls = py::class_<PyProperties>(m, "properties"); - auto draft = py::class_<PyDraftProperties>(controls, "draft"); +${vendors_defs} ${controls} } diff --git a/src/py/libcamera/py_transform.cpp b/src/py/libcamera/py_transform.cpp new file mode 100644 index 00000000..f3a0bfaf --- /dev/null +++ b/src/py/libcamera/py_transform.cpp @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + * + * Python bindings - Transform class + */ + +#include <libcamera/transform.h> +#include <libcamera/libcamera.h> + +#include <pybind11/operators.h> +#include <pybind11/pybind11.h> +#include <pybind11/stl.h> + +namespace py = pybind11; + +using namespace libcamera; + +void init_py_transform(py::module &m) +{ + auto pyTransform = py::class_<Transform>(m, "Transform"); + + pyTransform + .def(py::init([](int rotation, bool hflip, bool vflip, bool transpose) { + bool ok; + + Transform t = transformFromRotation(rotation, &ok); + if (!ok) + throw std::invalid_argument("Invalid rotation"); + + if (hflip) + t ^= Transform::HFlip; + if (vflip) + t ^= Transform::VFlip; + if (transpose) + t ^= Transform::Transpose; + return t; + }), py::arg("rotation") = 0, py::arg("hflip") = false, + py::arg("vflip") = false, py::arg("transpose") = false) + .def(py::init([](Transform &other) { return other; })) + .def("__str__", [](Transform &self) { + return "<libcamera.Transform '" + std::string(transformToString(self)) + "'>"; + }) + .def_property("hflip", + [](Transform &self) { + return !!(self & Transform::HFlip); + }, + [](Transform &self, bool hflip) { + if (hflip) + self |= Transform::HFlip; + else + self &= ~Transform::HFlip; + }) + .def_property("vflip", + [](Transform &self) { + return !!(self & Transform::VFlip); + }, + [](Transform &self, bool vflip) { + if (vflip) + self |= Transform::VFlip; + else + self &= ~Transform::VFlip; + }) + .def_property("transpose", + [](Transform &self) { + return !!(self & Transform::Transpose); + }, + [](Transform &self, bool transpose) { + if (transpose) + self |= Transform::Transpose; + else + self &= ~Transform::Transpose; + }) + .def("inverse", [](Transform &self) { return -self; }) + .def("invert", [](Transform &self) { + self = -self; + }) + .def("compose", [](Transform &self, Transform &other) { + self = self * other; + }); +} |