diff options
author | Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> | 2022-05-09 13:10:20 +0300 |
---|---|---|
committer | Kieran Bingham <kieran.bingham@ideasonboard.com> | 2022-05-10 13:53:43 +0200 |
commit | 8aa02271fd716ed046970a0b1f89176963303f50 (patch) | |
tree | da81430f03dc967addbdec3c143001b1490c751d | |
parent | 5cb17e0c138aa3afef92e8d95d876d2bd94516ed (diff) |
Add Python bindings
Add libcamera Python bindings. pybind11 is used to generate the C++ <->
Python layer.
We use pybind11 'smart_holder' version to avoid issues with private
destructors and shared_ptr. There is also an alternative solution here:
https://github.com/pybind/pybind11/pull/2067
Only a subset of libcamera classes are exposed. Implementing and testing
the wrapper classes is challenging, and as such only classes that I have
needed have been added so far.
Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
-rw-r--r-- | meson.build | 1 | ||||
-rw-r--r-- | meson_options.txt | 5 | ||||
-rw-r--r-- | src/meson.build | 1 | ||||
-rw-r--r-- | src/py/libcamera/__init__.py | 84 | ||||
-rw-r--r-- | src/py/libcamera/meson.build | 52 | ||||
-rw-r--r-- | src/py/libcamera/pyenums.cpp | 34 | ||||
-rw-r--r-- | src/py/libcamera/pymain.cpp | 649 | ||||
-rw-r--r-- | src/py/meson.build | 1 | ||||
-rw-r--r-- | subprojects/.gitignore | 3 | ||||
-rw-r--r-- | subprojects/packagefiles/pybind11/meson.build | 7 | ||||
-rw-r--r-- | subprojects/pybind11.wrap | 9 |
11 files changed, 845 insertions, 1 deletions
diff --git a/meson.build b/meson.build index ea0a4200..6d7d3ca6 100644 --- a/meson.build +++ b/meson.build @@ -177,6 +177,7 @@ summary({ 'Tracing support': tracing_enabled, 'Android support': android_enabled, 'GStreamer support': gst_enabled, + 'Python bindings': pycamera_enabled, 'V4L2 emulation support': v4l2_enabled, 'cam application': cam_enabled, 'qcam application': qcam_enabled, diff --git a/meson_options.txt b/meson_options.txt index 2c80ad8b..ca00c78e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -58,3 +58,8 @@ option('v4l2', type : 'boolean', value : false, description : 'Compile the V4L2 compatibility layer') + +option('pycamera', + type : 'feature', + value : 'auto', + description : 'Enable libcamera Python bindings (experimental)') diff --git a/src/meson.build b/src/meson.build index e0ea9c35..34663a6f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,4 +37,5 @@ subdir('cam') subdir('qcam') subdir('gstreamer') +subdir('py') subdir('v4l2') diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py new file mode 100644 index 00000000..0d7da9e2 --- /dev/null +++ b/src/py/libcamera/__init__.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + +from ._libcamera import * + + +class MappedFrameBuffer: + def __init__(self, fb): + self.__fb = fb + + def __enter__(self): + import os + import mmap + + fb = self.__fb + + # Collect information about the buffers + + bufinfos = {} + + for i in range(fb.num_planes): + fd = fb.fd(i) + + if fd not in bufinfos: + buflen = os.lseek(fd, 0, os.SEEK_END) + bufinfos[fd] = {'maplen': 0, 'buflen': buflen} + else: + buflen = bufinfos[fd]['buflen'] + + if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen: + raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' + + f'plane offset={fb.offset(i)}, plane length={fb.length(i)}') + + bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i)) + + # mmap the buffers + + maps = [] + + for fd, info in bufinfos.items(): + map = mmap.mmap(fd, info['maplen'], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) + info['map'] = map + maps.append(map) + + self.__maps = tuple(maps) + + # Create memoryviews for the planes + + planes = [] + + for i in range(fb.num_planes): + fd = fb.fd(i) + info = bufinfos[fd] + + mv = memoryview(info['map']) + + start = fb.offset(i) + end = fb.offset(i) + fb.length(i) + + mv = mv[start:end] + + planes.append(mv) + + self.__planes = tuple(planes) + + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + for p in self.__planes: + p.release() + + for mm in self.__maps: + mm.close() + + @property + def planes(self): + return self.__planes + + +def __FrameBuffer__mmap(self): + return MappedFrameBuffer(self) + + +FrameBuffer.mmap = __FrameBuffer__mmap diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build new file mode 100644 index 00000000..edf4a629 --- /dev/null +++ b/src/py/libcamera/meson.build @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: CC0-1.0 + +py3_dep = dependency('python3', required : get_option('pycamera')) + +if not py3_dep.found() + pycamera_enabled = false + subdir_done() +endif + +pycamera_enabled = true + +pybind11_proj = subproject('pybind11') +pybind11_dep = pybind11_proj.get_variable('pybind11_dep') + +pycamera_sources = files([ + 'pyenums.cpp', + 'pymain.cpp', +]) + +pycamera_deps = [ + libcamera_public, + py3_dep, + pybind11_dep, +] + +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' + +pycamera = shared_module('_libcamera', + pycamera_sources, + install : true, + install_dir : destdir, + name_prefix : '', + dependencies : pycamera_deps, + cpp_args : pycamera_args) + +run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py', + meson.current_build_dir() / '__init__.py', + check: true) + +install_data(['__init__.py'], install_dir : destdir) + +# \todo Generate stubs when building. See https://peps.python.org/pep-0484/#stub-files +# Note: Depends on pybind11-stubgen. To generate pylibcamera stubs: +# $ PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera +# $ mv build/src/py/libcamera-stubs/* build/src/py/libcamera/ diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp new file mode 100644 index 00000000..b655e622 --- /dev/null +++ b/src/py/libcamera/pyenums.cpp @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + * + * Python bindings - Enumerations + */ + +#include <libcamera/libcamera.h> + +#include <pybind11/smart_holder.h> + +namespace py = pybind11; + +using namespace libcamera; + +void init_pyenums(py::module &m) +{ + py::enum_<StreamRole>(m, "StreamRole") + .value("StillCapture", StreamRole::StillCapture) + .value("Raw", StreamRole::Raw) + .value("VideoRecording", StreamRole::VideoRecording) + .value("Viewfinder", StreamRole::Viewfinder); + + py::enum_<ControlType>(m, "ControlType") + .value("None", ControlType::ControlTypeNone) + .value("Bool", ControlType::ControlTypeBool) + .value("Byte", ControlType::ControlTypeByte) + .value("Integer32", ControlType::ControlTypeInteger32) + .value("Integer64", ControlType::ControlTypeInteger64) + .value("Float", ControlType::ControlTypeFloat) + .value("String", ControlType::ControlTypeString) + .value("Rectangle", ControlType::ControlTypeRectangle) + .value("Size", ControlType::ControlTypeSize); +} diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp new file mode 100644 index 00000000..84829e63 --- /dev/null +++ b/src/py/libcamera/pymain.cpp @@ -0,0 +1,649 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + * + * Python bindings + */ + +/* + * \todo Add geometry classes (Point, Rectangle...) + * \todo Add bindings for the ControlInfo class + * \todo Add bindings for the PixelFormat class + */ + +#include <mutex> +#include <stdexcept> +#include <sys/eventfd.h> +#include <unistd.h> + +#include <libcamera/base/log.h> + +#include <libcamera/libcamera.h> + +#include <pybind11/functional.h> +#include <pybind11/smart_holder.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>()); +} + +static 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: { + const Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data()); + return py::make_tuple(v->x, v->y, v->width, v->height); + } + case ControlTypeSize: { + const Size *v = reinterpret_cast<const Size *>(cv.data().data()); + return py::make_tuple(v->width, v->height); + } + case ControlTypeNone: + 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>()); +} + +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: { + auto array = ob.cast<std::array<int32_t, 4>>(); + return ControlValue(Rectangle(array[0], array[1], array[2], array[3])); + } + case ControlTypeSize: { + auto array = ob.cast<std::array<int32_t, 2>>(); + return ControlValue(Size(array[0], array[1])); + } + case ControlTypeNone: + default: + throw std::runtime_error("Control type not implemented"); + } +} + +static std::weak_ptr<CameraManager> gCameraManager; +static int gEventfd; +static std::mutex gReqlistMutex; +static std::vector<Request *> gReqList; + +static void handleRequestCompleted(Request *req) +{ + { + std::lock_guard guard(gReqlistMutex); + gReqList.push_back(req); + } + + 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"; +} + +void init_pyenums(py::module &m); + +PYBIND11_MODULE(_libcamera, m) +{ + init_pyenums(m); + + /* Forward declarations */ + + /* + * We need to declare all the classes here so that Python docstrings + * can be generated correctly. + * 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 pyCameraConfiguration = py::class_<CameraConfiguration>(m, "CameraConfiguration"); + auto pyCameraConfigurationStatus = py::enum_<CameraConfiguration::Status>(pyCameraConfiguration, "Status"); + auto pyStreamConfiguration = py::class_<StreamConfiguration>(m, "StreamConfiguration"); + auto pyStreamFormats = py::class_<StreamFormats>(m, "StreamFormats"); + auto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, "FrameBufferAllocator"); + auto pyFrameBuffer = py::class_<FrameBuffer>(m, "FrameBuffer"); + auto pyStream = py::class_<Stream>(m, "Stream"); + auto pyControlId = py::class_<ControlId>(m, "ControlId"); + auto pyRequest = py::class_<Request>(m, "Request"); + auto pyRequestStatus = py::enum_<Request::Status>(pyRequest, "Status"); + auto pyRequestReuse = py::enum_<Request::ReuseFlag>(pyRequest, "Reuse"); + auto pyFrameMetadata = py::class_<FrameMetadata>(m, "FrameMetadata"); + auto pyFrameMetadataStatus = py::enum_<FrameMetadata::Status>(pyFrameMetadata, "Status"); + 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"); + + /* Global functions */ + m.def("log_set_level", &logSetLevel); + + /* 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("efd", [](CameraManager &) { + return gEventfd; + }) + + .def("get_ready_requests", [](CameraManager &) { + std::vector<Request *> v; + + { + std::lock_guard guard(gReqlistMutex); + swap(v, gReqList); + } + + std::vector<py::object> ret; + + 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); + } + + return ret; + }) + + .def("get", py::overload_cast<const std::string &>(&CameraManager::get), py::keep_alive<0, 1>()) + + /* 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; + }); + + pyCamera + .def_property_readonly("id", &Camera::id) + .def("acquire", &Camera::acquire) + .def("release", &Camera::release) + .def("start", [](Camera &self, py::dict controls) { + /* \todo What happens if someone calls start() multiple times? */ + + self.requestCompleted.connect(handleRequestCompleted); + + const ControlInfoMap &controlMap = self.controls(); + ControlList controlList(controlMap); + for (const auto& [hkey, hval]: controls) { + auto key = hkey.cast<std::string>(); + + auto it = std::find_if(controlMap.begin(), controlMap.end(), + [&key](const auto &kvp) { + return kvp.first->name() == key; + }); + + if (it == controlMap.end()) + throw std::runtime_error("Control " + key + " not found"); + + const auto &id = it->first; + auto obj = py::cast<py::object>(hval); + + controlList.set(id->id(), pyToControlValue(obj, id->type())); + } + + int ret = self.start(&controlList); + if (ret) { + self.requestCompleted.disconnect(handleRequestCompleted); + return ret; + } + + return 0; + }, py::arg("controls") = py::dict()) + + .def("stop", [](Camera &self) { + int ret = self.stop(); + if (ret) + return ret; + + self.requestCompleted.disconnect(handleRequestCompleted); + + return 0; + }) + + .def("__str__", [](Camera &self) { + return "<libcamera.Camera '" + self.id() + "'>"; + }) + + /* Keep the camera alive, as StreamConfiguration contains a Stream* */ + .def("generate_configuration", &Camera::generateConfiguration, py::keep_alive<0, 1>()) + .def("configure", &Camera::configure) + + .def("create_request", &Camera::createRequest, py::arg("cookie") = 0) + + .def("queue_request", [](Camera &self, Request *req) { + py::object py_req = py::cast(req); + + /* + * Increase the reference count, will be dropped in + * CameraManager.get_ready_requests(). + */ + + py_req.inc_ref(); + + int ret = self.queueRequest(req); + if (ret) + py_req.dec_ref(); + + return ret; + }) + + .def_property_readonly("streams", [](Camera &self) { + py::set set; + for (auto &s : self.streams()) { + py::object py_self = py::cast(self); + py::object py_s = py::cast(s); + py::detail::keep_alive_impl(py_s, py_self); + set.add(py_s); + } + return set; + }) + + .def("find_control", [](Camera &self, const std::string &name) { + const auto &controls = self.controls(); + + auto it = std::find_if(controls.begin(), controls.end(), + [&name](const auto &kvp) { + return kvp.first->name() == name; + }); + + if (it == controls.end()) + throw std::runtime_error("Control '" + name + "' not found"); + + return it->first; + }, py::return_value_policy::reference_internal) + + .def_property_readonly("controls", [](Camera &self) { + py::dict ret; + + for (const auto &[id, ci] : self.controls()) { + ret[id->name().c_str()] = std::make_tuple<py::object>(controlValueToPy(ci.min()), + controlValueToPy(ci.max()), + controlValueToPy(ci.def())); + } + + return ret; + }) + + .def_property_readonly("properties", [](Camera &self) { + py::dict ret; + + for (const auto &[key, cv] : self.properties()) { + const ControlId *id = properties::properties.at(key); + py::object ob = controlValueToPy(cv); + + ret[id->name().c_str()] = ob; + } + + return ret; + }); + + pyCameraConfiguration + .def("__iter__", [](CameraConfiguration &self) { + return py::make_iterator<py::return_value_policy::reference_internal>(self); + }, py::keep_alive<0, 1>()) + .def("__len__", [](CameraConfiguration &self) { + return self.size(); + }) + .def("validate", &CameraConfiguration::validate) + .def("at", py::overload_cast<unsigned int>(&CameraConfiguration::at), + py::return_value_policy::reference_internal) + .def_property_readonly("size", &CameraConfiguration::size) + .def_property_readonly("empty", &CameraConfiguration::empty) + .def_readwrite("transform", &CameraConfiguration::transform); + + pyCameraConfigurationStatus + .value("Valid", CameraConfiguration::Valid) + .value("Adjusted", CameraConfiguration::Adjusted) + .value("Invalid", CameraConfiguration::Invalid); + + pyStreamConfiguration + .def("__str__", &StreamConfiguration::toString) + .def_property_readonly("stream", &StreamConfiguration::stream, + py::return_value_policy::reference_internal) + .def_property( + "size", + [](StreamConfiguration &self) { + return std::make_tuple(self.size.width, self.size.height); + }, + [](StreamConfiguration &self, std::tuple<uint32_t, uint32_t> size) { + self.size.width = std::get<0>(size); + self.size.height = std::get<1>(size); + }) + .def_property( + "pixel_format", + [](StreamConfiguration &self) { + return self.pixelFormat.toString(); + }, + [](StreamConfiguration &self, std::string fmt) { + self.pixelFormat = PixelFormat::fromString(fmt); + }) + .def_readwrite("stride", &StreamConfiguration::stride) + .def_readwrite("frame_size", &StreamConfiguration::frameSize) + .def_readwrite("buffer_count", &StreamConfiguration::bufferCount) + .def_property_readonly("formats", &StreamConfiguration::formats, + py::return_value_policy::reference_internal) + .def_readwrite("color_space", &StreamConfiguration::colorSpace); + + pyStreamFormats + .def_property_readonly("pixel_formats", [](StreamFormats &self) { + std::vector<std::string> fmts; + for (auto &fmt : self.pixelformats()) + fmts.push_back(fmt.toString()); + return fmts; + }) + .def("sizes", [](StreamFormats &self, const std::string &pixelFormat) { + auto fmt = PixelFormat::fromString(pixelFormat); + std::vector<std::tuple<uint32_t, uint32_t>> fmts; + for (const auto &s : self.sizes(fmt)) + fmts.push_back(std::make_tuple(s.width, s.height)); + return fmts; + }) + .def("range", [](StreamFormats &self, const std::string &pixelFormat) { + auto fmt = PixelFormat::fromString(pixelFormat); + const auto &range = self.range(fmt); + return make_tuple(std::make_tuple(range.hStep, range.vStep), + std::make_tuple(range.min.width, range.min.height), + std::make_tuple(range.max.width, range.max.height)); + }); + + pyFrameBufferAllocator + .def(py::init<std::shared_ptr<Camera>>(), py::keep_alive<1, 2>()) + .def("allocate", &FrameBufferAllocator::allocate) + .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) { + py::object py_self = py::cast(self); + py::list l; + for (auto &ub : self.buffers(stream)) { + py::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self); + l.append(py_buf); + } + return l; + }); + + pyFrameBuffer + /* \todo implement FrameBuffer::Plane properly */ + .def(py::init([](std::vector<std::tuple<int, unsigned int>> planes, unsigned int cookie) { + std::vector<FrameBuffer::Plane> v; + for (const auto &t : planes) + v.push_back({ SharedFD(std::get<0>(t)), FrameBuffer::Plane::kInvalidOffset, std::get<1>(t) }); + return new FrameBuffer(v, cookie); + })) + .def_property_readonly("metadata", &FrameBuffer::metadata, py::return_value_policy::reference_internal) + .def_property_readonly("num_planes", [](const FrameBuffer &self) { + return self.planes().size(); + }) + .def("length", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.length; + }) + .def("fd", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.fd.get(); + }) + .def("offset", [](FrameBuffer &self, uint32_t idx) { + const FrameBuffer::Plane &plane = self.planes()[idx]; + return plane.offset; + }) + .def_property("cookie", &FrameBuffer::cookie, &FrameBuffer::setCookie); + + pyStream + .def_property_readonly("configuration", &Stream::configuration); + + pyControlId + .def_property_readonly("id", &ControlId::id) + .def_property_readonly("name", &ControlId::name) + .def_property_readonly("type", &ControlId::type); + + 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); + }, 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("has_pending_buffers", &Request::hasPendingBuffers) + .def("set_control", [](Request &self, ControlId &id, py::object value) { + self.controls().set(id.id(), pyToControlValue(value, id.type())); + }) + .def_property_readonly("metadata", [](Request &self) { + py::dict ret; + + for (const auto &[key, cv] : self.metadata()) { + const ControlId *id = controls::controls.at(key); + py::object ob = controlValueToPy(cv); + + ret[id->name().c_str()] = ob; + } + + return ret; + }) + /* + * \todo As we add a keep_alive to the fb in addBuffers(), we + * can only allow reuse with ReuseBuffers. + */ + .def("reuse", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); }); + + pyRequestStatus + .value("Pending", Request::RequestPending) + .value("Complete", Request::RequestComplete) + .value("Cancelled", Request::RequestCancelled); + + pyRequestReuse + .value("Default", Request::ReuseFlag::Default) + .value("ReuseBuffers", Request::ReuseFlag::ReuseBuffers); + + pyFrameMetadata + .def_readonly("status", &FrameMetadata::status) + .def_readonly("sequence", &FrameMetadata::sequence) + .def_readonly("timestamp", &FrameMetadata::timestamp) + /* \todo Implement FrameMetadata::Plane properly */ + .def_property_readonly("bytesused", [](FrameMetadata &self) { + std::vector<unsigned int> v; + v.resize(self.planes().size()); + transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; }); + return v; + }); + + pyFrameMetadataStatus + .value("Success", FrameMetadata::FrameSuccess) + .value("Error", FrameMetadata::FrameError) + .value("Cancelled", FrameMetadata::FrameCancelled); + + 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); +} diff --git a/src/py/meson.build b/src/py/meson.build new file mode 100644 index 00000000..4ce9668c --- /dev/null +++ b/src/py/meson.build @@ -0,0 +1 @@ +subdir('libcamera') diff --git a/subprojects/.gitignore b/subprojects/.gitignore index 391fde2c..0e194289 100644 --- a/subprojects/.gitignore +++ b/subprojects/.gitignore @@ -1,3 +1,4 @@ /googletest-release* /libyuv -/packagecache
\ No newline at end of file +/packagecache +/pybind11 diff --git a/subprojects/packagefiles/pybind11/meson.build b/subprojects/packagefiles/pybind11/meson.build new file mode 100644 index 00000000..1be47ca4 --- /dev/null +++ b/subprojects/packagefiles/pybind11/meson.build @@ -0,0 +1,7 @@ +project('pybind11', 'cpp', + version : '2.9.1', + license : 'BSD-3-Clause') + +pybind11_incdir = include_directories('include') + +pybind11_dep = declare_dependency(include_directories : pybind11_incdir) diff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap new file mode 100644 index 00000000..e8037a5d --- /dev/null +++ b/subprojects/pybind11.wrap @@ -0,0 +1,9 @@ +[wrap-git] +url = https://github.com/pybind/pybind11.git +# This is the head of 'smart_holder' branch +revision = aebdf00cd060b871c5a1e0c2cf4a333503dd0431 +depth = 1 +patch_directory = pybind11 + +[provide] +pybind11 = pybind11_dep |