[libcamera-devel] [PATCH v5 1/3] Add Python bindings

Tomi Valkeinen tomi.valkeinen at ideasonboard.com
Wed Apr 13 09:24:45 CEST 2022


On 12/04/2022 20:49, Jacopo Mondi wrote:
> Hi Tomi,
>     I've been using the bindings in the last days, they're nice to
> work! Great job!

Nice to hear =).

> Once the question about a Request belonging to a Camera is
> clarified I think we should merge these soon, even if incomplete, to
> build on top.
> 
> My understanding of python is very limited so I have just a few minor
> comments and one larger question about controls.
> 
> On Mon, Mar 14, 2022 at 05:46:31PM +0200, Tomi Valkeinen via libcamera-devel wrote:
>> 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 at ideasonboard.com>
>> ---
>>   meson.build                  |   1 +
>>   meson_options.txt            |   5 +
>>   src/meson.build              |   1 +
>>   src/py/libcamera/__init__.py |  10 +
>>   src/py/libcamera/meson.build |  43 ++++
>>   src/py/libcamera/pyenums.cpp |  53 ++++
>>   src/py/libcamera/pymain.cpp  | 453 +++++++++++++++++++++++++++++++++++
>>   src/py/meson.build           |   1 +
>>   subprojects/.gitignore       |   3 +-
>>   subprojects/pybind11.wrap    |   6 +
>>   10 files changed, 575 insertions(+), 1 deletion(-)
>>   create mode 100644 src/py/libcamera/__init__.py
>>   create mode 100644 src/py/libcamera/meson.build
>>   create mode 100644 src/py/libcamera/pyenums.cpp
>>   create mode 100644 src/py/libcamera/pymain.cpp
>>   create mode 100644 src/py/meson.build
>>   create mode 100644 subprojects/pybind11.wrap
>>
>> diff --git a/meson.build b/meson.build
>> index 29d8542d..ff6c2ad6 100644
>> --- a/meson.build
>> +++ b/meson.build
>> @@ -179,6 +179,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..b30bf33a
>> --- /dev/null
>> +++ b/src/py/libcamera/__init__.py
>> @@ -0,0 +1,10 @@
>> +# SPDX-License-Identifier: LGPL-2.1-or-later
>> +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
>> +
>> +from ._libcamera import *
>> +import mmap
>> +
>> +def __FrameBuffer__mmap(self, plane):
>> +	return mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ)
>> +
>> +FrameBuffer.mmap = __FrameBuffer__mmap
>> diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build
>> new file mode 100644
>> index 00000000..82388efb
>> --- /dev/null
>> +++ b/src/py/libcamera/meson.build
>> @@ -0,0 +1,43 @@
>> +# 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([
>> +    'pymain.cpp',
>> +    'pyenums.cpp',
>> +])
>> +
>> +pycamera_deps = [
>> +    libcamera_public,
>> +    py3_dep,
>> +    pybind11_dep,
>> +]
>> +
>> +pycamera_args = ['-fvisibility=hidden']
>> +pycamera_args += ['-Wno-shadow']
>> +pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT']
>> +
>> +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')
>> +
>> +install_data(['__init__.py'], install_dir : destdir)
>> diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp
>> new file mode 100644
>> index 00000000..af6151c8
>> --- /dev/null
>> +++ b/src/py/libcamera/pyenums.cpp
>> @@ -0,0 +1,53 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
>> + *
>> + * Python bindings
>> + */
>> +
>> +#include <libcamera/libcamera.h>
>> +
>> +#include <pybind11/smart_holder.h>
>> +
>> +namespace py = pybind11;
>> +
>> +using namespace libcamera;
>> +
>> +void init_pyenums(py::module& m)
>> +{
>> +	py::enum_<CameraConfiguration::Status>(m, "ConfigurationStatus")
>> +		.value("Valid", CameraConfiguration::Valid)
>> +		.value("Adjusted", CameraConfiguration::Adjusted)
>> +		.value("Invalid", CameraConfiguration::Invalid);
>> +
>> +	py::enum_<StreamRole>(m, "StreamRole")
>> +		.value("StillCapture", StreamRole::StillCapture)
>> +		.value("Raw", StreamRole::Raw)
>> +		.value("VideoRecording", StreamRole::VideoRecording)
>> +		.value("Viewfinder", StreamRole::Viewfinder);
>> +
>> +	py::enum_<Request::Status>(m, "RequestStatus")
>> +		.value("Pending", Request::RequestPending)
>> +		.value("Complete", Request::RequestComplete)
>> +		.value("Cancelled", Request::RequestCancelled);
>> +
>> +	py::enum_<FrameMetadata::Status>(m, "FrameMetadataStatus")
>> +		.value("Success", FrameMetadata::FrameSuccess)
>> +		.value("Error", FrameMetadata::FrameError)
>> +		.value("Cancelled", FrameMetadata::FrameCancelled);
>> +
>> +	py::enum_<Request::ReuseFlag>(m, "ReuseFlag")
>> +		.value("Default", Request::ReuseFlag::Default)
>> +		.value("ReuseBuffers", Request::ReuseFlag::ReuseBuffers);
>> +
>> +	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..7701da40
>> --- /dev/null
>> +++ b/src/py/libcamera/pymain.cpp
>> @@ -0,0 +1,453 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
>> + *
>> + * Python bindings
>> + */
>> +
>> +/*
>> + * To generate pylibcamera stubs:
>> + * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera
>> + */
>> +
>> +#include <chrono>
>> +#include <fcntl.h>
>> +#include <mutex>
>> +#include <sys/eventfd.h>
>> +#include <sys/mman.h>
>> +#include <thread>
>> +#include <unistd.h>
>> +
>> +#include <libcamera/libcamera.h>
>> +
>> +#include <pybind11/smart_holder.h>
>> +#include <pybind11/functional.h>
>> +#include <pybind11/stl.h>
>> +#include <pybind11/stl_bind.h>
>> +
>> +namespace py = pybind11;
>> +
>> +using namespace std;
>> +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 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<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 runtime_error("Unsupported ControlValue type");
>> +	}
>> +}
>> +
>> +static ControlValue PyToControlValue(const py::object &ob, ControlType type)
>> +{
>> +	switch (type) {
>> +	case ControlTypeBool:
>> +		return ControlValue(ob.cast<bool>());
>> +	case ControlTypeByte:
>> +		return ControlValue(ob.cast<uint8_t>());
>> +	case ControlTypeInteger32:
>> +		return ControlValue(ob.cast<int32_t>());
>> +	case ControlTypeInteger64:
>> +		return ControlValue(ob.cast<int64_t>());
>> +	case ControlTypeFloat:
>> +		return ControlValue(ob.cast<float>());
>> +	case ControlTypeString:
>> +		return ControlValue(ob.cast<string>());
>> +	case ControlTypeRectangle:
>> +	case ControlTypeSize:
>> +	case ControlTypeNone:
>> +	default:
>> +		throw runtime_error("Control type not implemented");
>> +	}
>> +}
>> +
>> +static weak_ptr<CameraManager> g_camera_manager;
>> +static int g_eventfd;
>> +static mutex g_reqlist_mutex;
>> +static vector<Request *> g_reqlist;
>> +
>> +static void handleRequestCompleted(Request *req)
>> +{
>> +	{
>> +		lock_guard guard(g_reqlist_mutex);
>> +		g_reqlist.push_back(req);
>> +	}
>> +
>> +	uint64_t v = 1;
>> +	write(g_eventfd, &v, 8);
>> +}
>> +
>> +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 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 pyFrameMetadata = py::class_<FrameMetadata>(m, "FrameMetadata");
>> +
>> +	/* Global functions */
>> +	m.def("logSetLevel", &logSetLevel);
>> +
>> +	/* Classes */
>> +	pyCameraManager
>> +		.def_static("singleton", []() {
>> +			shared_ptr<CameraManager> cm = g_camera_manager.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 = shared_ptr<CameraManager>(new CameraManager, [](auto p) {
>> +				close(g_eventfd);
>> +				g_eventfd = -1;
>> +				delete p;
>> +			});
>> +
>> +			g_eventfd = fd;
>> +			g_camera_manager = 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 g_eventfd;
>> +		})
>> +
>> +		.def("getReadyRequests", [](CameraManager &) {
>> +			vector<Request *> v;
>> +
>> +			{
>> +				lock_guard guard(g_reqlist_mutex);
>> +				swap(v, g_reqlist);
>> +			}
>> +
>> +			vector<py::object> ret;
>> +
>> +			for (Request *req : v) {
>> +				py::object o = py::cast(req);
>> +				/* decrease the ref increased in Camera::queueRequest() */
>> +				o.dec_ref();
>> +				ret.push_back(o);
>> +			}
>> +
>> +			return ret;
>> +		})
>> +
>> +		.def("get", py::overload_cast<const string &>(&CameraManager::get), py::keep_alive<0, 1>())
>> +
>> +		.def("find", [](CameraManager &self, string str) {
>> +			std::transform(str.begin(), str.end(), str.begin(), ::tolower);
>> +
>> +			for (auto c : self.cameras()) {
>> +				string id = c->id();
>> +
>> +				std::transform(id.begin(), id.end(), id.begin(), ::tolower);
>> +
>> +				if (id.find(str) != string::npos)
>> +					return c;
>> +			}
>> +
>> +			return shared_ptr<Camera>();
>> +		}, 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) {
>> +			self.requestCompleted.connect(handleRequestCompleted);
>> +
>> +			int ret = self.start();
>> +			if (ret)
>> +				self.requestCompleted.disconnect(handleRequestCompleted);
>> +
>> +			return ret;
>> +		})
>> +
>> +		.def("stop", [](Camera &self) {
>> +			int ret = self.stop();
>> +			if (!ret)
>> +				self.requestCompleted.disconnect(handleRequestCompleted);
>> +
>> +			return ret;
>> +		})
>> +
>> +		.def("__repr__", [](Camera &self) {
>> +			return "<libcamera.Camera '" + self.id() + "'>";
>> +		})
>> +
>> +		/* Keep the camera alive, as StreamConfiguration contains a Stream* */
>> +		.def("generateConfiguration", &Camera::generateConfiguration, py::keep_alive<0, 1>())
>> +		.def("configure", &Camera::configure)
>> +
>> +		.def("createRequest", &Camera::createRequest, py::arg("cookie") = 0)
>> +
>> +		.def("queueRequest", [](Camera &self, Request *req) {
>> +			py::object py_req = py::cast(req);
>> +
>> +			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 string &name) {
>> +			const auto &controls = self.controls();
>> +
>> +			auto it = find_if(controls.begin(), controls.end(),
>> +					  [&name](const auto &kvp) { return kvp.first->name() == name; });
>> +
>> +			if (it == controls.end())
>> +				throw runtime_error("Control 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()] = 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);
>> +
>> +	pyStreamConfiguration
>> +		.def("toString", &StreamConfiguration::toString)
>> +		.def_property_readonly("stream", &StreamConfiguration::stream, py::return_value_policy::reference_internal)
>> +		.def_property(
>> +			"size",
>> +			[](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); },
>> +			[](StreamConfiguration &self, tuple<uint32_t, uint32_t> size) { self.size.width = get<0>(size); self.size.height = get<1>(size); })
>> +		.def_property(
>> +			"pixelFormat",
>> +			[](StreamConfiguration &self) { return self.pixelFormat.toString(); },
>> +			[](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); })
>> +		.def_readwrite("stride", &StreamConfiguration::stride)
>> +		.def_readwrite("frameSize", &StreamConfiguration::frameSize)
>> +		.def_readwrite("bufferCount", &StreamConfiguration::bufferCount)
>> +		.def_property_readonly("formats", &StreamConfiguration::formats, py::return_value_policy::reference_internal);
>> +	;
>> +
>> +	pyStreamFormats
>> +		.def_property_readonly("pixelFormats", [](StreamFormats &self) {
>> +			vector<string> fmts;
>> +			for (auto &fmt : self.pixelformats())
>> +				fmts.push_back(fmt.toString());
>> +			return fmts;
>> +		})
>> +		.def("sizes", [](StreamFormats &self, const string &pixelFormat) {
>> +			auto fmt = PixelFormat::fromString(pixelFormat);
>> +			vector<tuple<uint32_t, uint32_t>> fmts;
>> +			for (const auto &s : self.sizes(fmt))
>> +				fmts.push_back(make_tuple(s.width, s.height));
>> +			return fmts;
>> +		})
>> +		.def("range", [](StreamFormats &self, const string &pixelFormat) {
>> +			auto fmt = PixelFormat::fromString(pixelFormat);
>> +			const auto &range = self.range(fmt);
>> +			return make_tuple(make_tuple(range.hStep, range.vStep),
>> +					  make_tuple(range.min.width, range.min.height),
>> +					  make_tuple(range.max.width, range.max.height));
>> +		});
>> +
>> +	pyFrameBufferAllocator
>> +		.def(py::init<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([](vector<tuple<int, unsigned int>> planes, unsigned int cookie) {
>> +			vector<FrameBuffer::Plane> v;
>> +			for (const auto &t : planes)
>> +				v.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, get<1>(t) });
>> +			return new FrameBuffer(v, cookie);
>> +		}))
>> +		.def_property_readonly("metadata", &FrameBuffer::metadata, py::return_value_policy::reference_internal)
>> +		.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_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
>> +		/* Fence is not supported, so we cannot expose addBuffer() directly */
>> +		.def("addBuffer", [](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("hasPendingBuffers", &Request::hasPendingBuffers)
>> +		.def("set_control", [](Request &self, ControlId& id, py::object value) {
>> +			self.controls().set(id.id(), PyToControlValue(value, id.type()));
>> +		})
> 
> I see a mixture of camel case ("addBuffer") and snake case
> ("set_controls"). If there's a preferred coding style for Python
> should we uniform on it ?

Yes, the names should be consistent. I'm not sure what the naming should 
be, though.

We try to expose the C++ API as transparently as possible (while keeping 
it usable) to the Python side, and thus it makes sense to keep the 
naming same/similar to the C++ naming so that it's clear how the Py 
methods map to the C++ methods.

Then again, we can't follow the C++ API very strictly as some things 
have to be implemented in a different way, and possibly it would be 
better to hide a bit more if there's no possible performance harm. And 
if it's not quite the same as the C++ API anymore, maybe we don't need 
to follow the C++ naming...

Any convenience library on top of these bindings, like picamera2, should 
of course be "pure" Python.

> Minor comments apart, there's a thing which is missing:
> support for setting controls that accept a Span<> of values.
> 
> I tried several solutions and I got the following to compile
> 
>          .def("set_control_array", [](Request &self, ControlId& id, py::tuple &values) {
>                  py::bytearray bytes = py::bytearray(values);
>                  py::buffer_info info(py::buffer(bytes).request());
> 
>                  const int *data = reinterpret_cast<const int *>(info.ptr);
>                  size_t length = static_cast<size_t>(info.size);
> 
>                  self.controls().set(id.id(), Span<const int>{data, length});
>           })
> 
> (All types in casts<> should depend on id.type(), but that's for later).
> 
> Unfortunately, while length is correct, the values I access with
> 'data' seems invalid, which makes me think
> 
>                  py::bytearray bytes = py::bytearray(values);
> 
> Dosn't do what I think it does, or maybe Tuple in Python are simply
> not backed by a contigous memory buffer , hence there's no way to wrap
> their memory in a Span<>).
> 
> Please note I also tried to instrument:
> 
>          .def("set_control_array", [](Request &self, ControlId& id, vector<int> values)
> 
> Relying on pybind11 type casting, and it seems to work, but I see two issues:
> 
> 1) Converting from python to C++ types goes through a copy.
> Performances reasons apart, the vector lifetime is limited to the
> function scope. This isn't be a problem for now, as control
> values are copied in the Request's control list, but that would
> prevent passing controls values as pointers, if a control transports a
> large chunk of data (ie gamma tables). Not sure we'll ever want this
> (I don't think so as it won't play well with serialization between the
> IPA and the pipeline handler) but you never know.
> 
> 2) my understanding is that python does not support methods signature
> overloading, hence we would have
> 
>          .def("set_control_array_int", [](Request &self, ControlId& id, vector<int> values)
>          .def("set_control_array_float", [](Request &self, ControlId& id, vector<float> values)
>          ....
> 
> there are surely smart ways to handle this, but in my quick experiment
> I haven't found one yet :)

I think using the same name should work:

https://pybind11.readthedocs.io/en/stable/advanced/functions.html?highlight=template#binding-functions-with-template-parameters

I would just go with this latter way. We can try to optimize it later.

Avoiding copies means managing the lifetimes of the python objects, 
which can get rather tricky.

Also, I'm not sure if we can access the underlying array from a python 
list or tuple. It could mean that we'd need to take in a bytearray, and 
that would then be a rather annoying API for the user. Then again, a 
byte array for a gamma table sounds fine, so possibly would could have 
the simple templated methods and additionally a method that takes a byte 
array (or rather, a buffer_protocol).

That said, maybe it still wouldn't work, as if we just pass the 
underlying buffer to the C++ side, and even if we manage to keep the 
buffer alive, nothing stops the py side from modifying or maybe even 
resizing the buffer (depending on the object, I guess).

To safely do this we should transfer the ownership from the Python side 
to the C++ side (or the binding layer in this case). Which, I think, the 
pybind11 'smart_holders' branch used in this version should support, but 
I haven't looked at it.

In any case, as I said, I think just do it with the templated methods, 
which sound easy to write and easy to validate.

  Tomi


More information about the libcamera-devel mailing list