[libcamera-devel,v4,3/5] Add Python bindings
diff mbox series

Message ID 20220204133814.303217-4-tomi.valkeinen@ideasonboard.com
State Superseded
Headers show
Series
  • Python bindings
Related show

Commit Message

Tomi Valkeinen Feb. 4, 2022, 1:38 p.m. UTC
Add libcamera Python bindings. pybind11 is used to generate the C++ <->
Python layer.

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>
---
 meson.build                  |   1 +
 meson_options.txt            |   5 +
 src/meson.build              |   1 +
 src/py/libcamera/__init__.py |  10 +
 src/py/libcamera/meson.build |  41 ++++
 src/py/libcamera/pymain.cpp  | 440 +++++++++++++++++++++++++++++++++++
 src/py/meson.build           |   1 +
 subprojects/.gitignore       |   3 +-
 subprojects/pybind11.wrap    |  12 +
 9 files changed, 513 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/pymain.cpp
 create mode 100644 src/py/meson.build
 create mode 100644 subprojects/pybind11.wrap

Patch
diff mbox series

diff --git a/meson.build b/meson.build
index 9684d562..fe1a09e1 100644
--- a/meson.build
+++ b/meson.build
@@ -180,6 +180,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@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..0926b995
--- /dev/null
+++ b/src/py/libcamera/meson.build
@@ -0,0 +1,41 @@ 
+# 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',
+])
+
+pycamera_deps = [
+    libcamera_public,
+    py3_dep,
+    pybind11_dep,
+]
+
+pycamera_args = ['-fvisibility=hidden']
+pycamera_args += ['-Wno-shadow']
+
+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/pymain.cpp b/src/py/libcamera/pymain.cpp
new file mode 100644
index 00000000..d35ec2f2
--- /dev/null
+++ b/src/py/libcamera/pymain.cpp
@@ -0,0 +1,440 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
+ *
+ * Python bindings
+ */
+
+#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/functional.h>
+#include <pybind11/pybind11.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);
+}
+
+PYBIND11_MODULE(_libcamera, m)
+{
+	m.def("logSetLevel", &logSetLevel);
+
+	py::class_<CameraManager, std::shared_ptr<CameraManager>>(m, "CameraManager")
+		.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;
+		});
+
+	py::class_<Camera, shared_ptr<Camera>>(m, "Camera")
+		.def_property_readonly("id", &Camera::id)
+		.def("acquire", &Camera::acquire)
+		.def("release", &Camera::release)
+		.def("start", [](shared_ptr<Camera> &self) {
+			self->requestCompleted.connect(handleRequestCompleted);
+
+			int ret = self->start();
+			if (ret)
+				self->requestCompleted.disconnect(handleRequestCompleted);
+
+			return ret;
+		})
+
+		.def("stop", [](shared_ptr<Camera> &self) {
+			int ret = self->stop();
+			if (!ret)
+				self->requestCompleted.disconnect(handleRequestCompleted);
+
+			return ret;
+		})
+
+		.def("__repr__", [](shared_ptr<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_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;
+		});
+
+	py::enum_<CameraConfiguration::Status>(m, "ConfigurationStatus")
+		.value("Valid", CameraConfiguration::Valid)
+		.value("Adjusted", CameraConfiguration::Adjusted)
+		.value("Invalid", CameraConfiguration::Invalid);
+
+	py::class_<CameraConfiguration>(m, "CameraConfiguration")
+		.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);
+
+	py::class_<StreamConfiguration>(m, "StreamConfiguration")
+		.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);
+	;
+
+	py::class_<StreamFormats>(m, "StreamFormats")
+		.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));
+		});
+
+	py::enum_<StreamRole>(m, "StreamRole")
+		.value("StillCapture", StreamRole::StillCapture)
+		.value("Raw", StreamRole::Raw)
+		.value("VideoRecording", StreamRole::VideoRecording)
+		.value("Viewfinder", StreamRole::Viewfinder);
+
+	py::class_<FrameBufferAllocator>(m, "FrameBufferAllocator")
+		.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;
+		});
+
+	py::class_<FrameBuffer>(m, "FrameBuffer")
+		/* 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);
+
+	py::class_<Stream>(m, "Stream")
+		.def_property_readonly("configuration", &Stream::configuration);
+
+	py::enum_<Request::ReuseFlag>(m, "ReuseFlag")
+		.value("Default", Request::ReuseFlag::Default)
+		.value("ReuseBuffers", Request::ReuseFlag::ReuseBuffers);
+
+	py::class_<Request>(m, "Request")
+		/* 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, string &control, py::object value) {
+			const auto &controls = self.camera()->controls();
+
+			auto it = find_if(controls.begin(), controls.end(),
+					  [&control](const auto &kvp) { return kvp.first->name() == control; });
+
+			if (it == controls.end())
+				throw runtime_error("Control not found");
+
+			const auto &id = it->first;
+
+			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;
+		})
+		/* 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); });
+
+	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::class_<FrameMetadata>(m, "FrameMetadata")
+		.def_readonly("status", &FrameMetadata::status)
+		.def_readonly("sequence", &FrameMetadata::sequence)
+		.def_readonly("timestamp", &FrameMetadata::timestamp)
+		/* temporary helper, to be removed */
+		.def_property_readonly("bytesused", [](FrameMetadata &self) {
+			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;
+		});
+}
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..757bb072 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/pybind11.wrap b/subprojects/pybind11.wrap
new file mode 100644
index 00000000..9d6e7acb
--- /dev/null
+++ b/subprojects/pybind11.wrap
@@ -0,0 +1,12 @@ 
+[wrap-file]
+directory = pybind11-2.6.1
+source_url = https://github.com/pybind/pybind11/archive/v2.6.1.tar.gz
+source_filename = pybind11-2.6.1.tar.gz
+source_hash = cdbe326d357f18b83d10322ba202d69f11b2f49e2d87ade0dc2be0c5c34f8e2a
+patch_url = https://wrapdb.mesonbuild.com/v2/pybind11_2.6.1-1/get_patch
+patch_filename = pybind11-2.6.1-1-wrap.zip
+patch_hash = 6de5477598b56c8a2e609196420c783ac35b79a31d6622121602e6ade6b3cee8
+
+[provide]
+pybind11 = pybind11_dep
+