Message ID | 20211209092906.37303-5-tomi.valkeinen@ideasonboard.com |
---|---|
State | Superseded |
Headers | show |
Series |
|
Related | show |
Quoting Tomi Valkeinen (2021-12-09 09:29:05) > Add libcamera Python bindings. pybind11 is used to generate the C++ <-> > Python layer. > > Only a subset of libcamera classes are exposed. > > Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> > --- > .gitignore | 1 + > meson.build | 1 + > meson_options.txt | 5 + > src/meson.build | 1 + > src/py/meson.build | 1 + > src/py/pycamera/__init__.py | 10 + > src/py/pycamera/meson.build | 43 ++++ > src/py/pycamera/pymain.cpp | 424 ++++++++++++++++++++++++++++++++++++ > subprojects/pybind11.wrap | 12 + > 9 files changed, 498 insertions(+) > create mode 100644 src/py/meson.build > create mode 100644 src/py/pycamera/__init__.py > create mode 100644 src/py/pycamera/meson.build > create mode 100644 src/py/pycamera/pymain.cpp > create mode 100644 subprojects/pybind11.wrap > > diff --git a/.gitignore b/.gitignore > index cca829fa..aae56b2d 100644 > --- a/.gitignore > +++ b/.gitignore > @@ -3,6 +3,7 @@ > __pycache__/ > build/ > patches/ > +subprojects/pybind11-2.*/ > *.patch > *.pyc > .cache > diff --git a/meson.build b/meson.build > index a20cc29e..0f885c14 100644 > --- a/meson.build > +++ b/meson.build > @@ -181,6 +181,7 @@ summary({ > 'qcam application': qcam_enabled, > 'lc-compliance application': lc_compliance_enabled, > 'Unit tests': test_enabled, > + 'Python bindings': pycamera_enabled, > }, > section : 'Configuration', > bool_yn : true) > 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..4a1e3cb0 100644 > --- a/src/meson.build > +++ b/src/meson.build > @@ -38,3 +38,4 @@ subdir('qcam') > > subdir('gstreamer') > subdir('v4l2') > +subdir('py') > diff --git a/src/py/meson.build b/src/py/meson.build > new file mode 100644 > index 00000000..42ffa221 > --- /dev/null > +++ b/src/py/meson.build > @@ -0,0 +1 @@ > +subdir('pycamera') > diff --git a/src/py/pycamera/__init__.py b/src/py/pycamera/__init__.py > new file mode 100644 > index 00000000..a5c198dc > --- /dev/null > +++ b/src/py/pycamera/__init__.py > @@ -0,0 +1,10 @@ > +# SPDX-License-Identifier: GPL-2.0-or-later > +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> > + > +from .pycamera 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/pycamera/meson.build b/src/py/pycamera/meson.build > new file mode 100644 > index 00000000..c490a18d > --- /dev/null > +++ b/src/py/pycamera/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', > +]) > + > +pycamera_deps = [ > + libcamera_public, > + py3_dep, > + pybind11_dep, > +] > + > +pycamera_args = [ '-fvisibility=hidden' ] > +pycamera_args += [ '-Wno-shadow' ] Does python shadow variables explicitly? Does this lead to any potential bugs? I recall putting -Wshadow in explicitly because I hit a nasty bug due to shadowing, so in my eyes, shadowed variables are a bad-thing (tm) :-) You have to be sure you know which variable you are writing to, and it might not always be clear to a reader, or cause potential confusion..? (of course subclassing, and having functions with the same name is essentially 'shadowing' the function names I guess...) > + > +destdir = get_option('libdir') + '/python' + py3_dep.version() + '/site-packages/pycamera' > + > +pycamera = shared_module('pycamera', > + pycamera_sources, > + install : true, > + install_dir : destdir, > + name_prefix : '', > + dependencies : pycamera_deps, > + cpp_args : pycamera_args) > + > +# XXX > You could also create a symlink. There's an example in the top-level > +# > meson.build. > +# Copy __init__.py to build dir so that we can run without installing > +configure_file(input: '__init__.py', output: '__init__.py', copy: true) I think a symlink would be better too. > + > +install_data(['__init__.py'], install_dir : destdir) > diff --git a/src/py/pycamera/pymain.cpp b/src/py/pycamera/pymain.cpp > new file mode 100644 > index 00000000..0088e133 > --- /dev/null > +++ b/src/py/pycamera/pymain.cpp > @@ -0,0 +1,424 @@ > +/* 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; > + > +static py::object ControlValueToPy(const ControlValue &cv) > +{ > + //assert(!cv.isArray()); > + //assert(cv.numElements() == 1); Are these asserts not necessary? Is it better to keep them in, in a way that they are used on debug buidls and compiled out on release builds? I think we have ASSERT() for that, but that's an 'internal' helper I think and I expect the src/py is building on top of the public api.. > + > + switch (cv.type()) { > + case ControlTypeBool: > + return py::cast(cv.get<bool>()); > + case ControlTypeByte: > + return py::cast(cv.get<uint8_t>()); > + case ControlTypeInteger32: > + return py::cast(cv.get<int32_t>()); > + case ControlTypeInteger64: > + return py::cast(cv.get<int64_t>()); > + case ControlTypeFloat: > + return py::cast(cv.get<float>()); > + 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 handle_request_completed(Request *req) > +{ > + { > + lock_guard guard(g_reqlist_mutex); > + g_reqlist.push_back(req); > + } > + > + uint64_t v = 1; > + write(g_eventfd, &v, 8); > +} > + > +PYBIND11_MODULE(pycamera, 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(handle_request_completed); > + > + int ret = self->start(); > + if (ret) > + self->requestCompleted.disconnect(handle_request_completed); > + > + return ret; > + }) > + > + .def("stop", [](shared_ptr<Camera> &self) { > + int ret = self->stop(); > + if (!ret) > + self->requestCompleted.disconnect(handle_request_completed); > + > + return ret; > + }) > + > + .def("__repr__", [](shared_ptr<Camera> &self) { > + return "<pycamera.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( > + "fmt", > + [](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") > + .def_property_readonly("camera", &Request::camera) > + .def("addBuffer", &Request::addBuffer, 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) > + .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/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 > + > -- > 2.25.1 >
On 09/12/2021 11:58, Kieran Bingham wrote: >> +pycamera_args = [ '-fvisibility=hidden' ] >> +pycamera_args += [ '-Wno-shadow' ] > > Does python shadow variables explicitly? Does this lead to any potential > bugs? Yes, it has many cases of: class Foo { int bar; Foo(int bar) : bar(bar) { } } Tomi
Hi Tomi Thanks for this patch! On Thu, 9 Dec 2021 at 09:58, Kieran Bingham <kieran.bingham@ideasonboard.com> wrote: > > Quoting Tomi Valkeinen (2021-12-09 09:29:05) > > Add libcamera Python bindings. pybind11 is used to generate the C++ <-> > > Python layer. > > > > Only a subset of libcamera classes are exposed. > > > > Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> > > --- > > .gitignore | 1 + > > meson.build | 1 + > > meson_options.txt | 5 + > > src/meson.build | 1 + > > src/py/meson.build | 1 + > > src/py/pycamera/__init__.py | 10 + > > src/py/pycamera/meson.build | 43 ++++ > > src/py/pycamera/pymain.cpp | 424 ++++++++++++++++++++++++++++++++++++ > > subprojects/pybind11.wrap | 12 + > > 9 files changed, 498 insertions(+) > > create mode 100644 src/py/meson.build > > create mode 100644 src/py/pycamera/__init__.py > > create mode 100644 src/py/pycamera/meson.build > > create mode 100644 src/py/pycamera/pymain.cpp > > create mode 100644 subprojects/pybind11.wrap > > > > diff --git a/.gitignore b/.gitignore > > index cca829fa..aae56b2d 100644 > > --- a/.gitignore > > +++ b/.gitignore > > @@ -3,6 +3,7 @@ > > __pycache__/ > > build/ > > patches/ > > +subprojects/pybind11-2.*/ > > *.patch > > *.pyc > > .cache > > diff --git a/meson.build b/meson.build > > index a20cc29e..0f885c14 100644 > > --- a/meson.build > > +++ b/meson.build > > @@ -181,6 +181,7 @@ summary({ > > 'qcam application': qcam_enabled, > > 'lc-compliance application': lc_compliance_enabled, > > 'Unit tests': test_enabled, > > + 'Python bindings': pycamera_enabled, > > }, > > section : 'Configuration', > > bool_yn : true) > > 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..4a1e3cb0 100644 > > --- a/src/meson.build > > +++ b/src/meson.build > > @@ -38,3 +38,4 @@ subdir('qcam') > > > > subdir('gstreamer') > > subdir('v4l2') > > +subdir('py') > > diff --git a/src/py/meson.build b/src/py/meson.build > > new file mode 100644 > > index 00000000..42ffa221 > > --- /dev/null > > +++ b/src/py/meson.build > > @@ -0,0 +1 @@ > > +subdir('pycamera') > > diff --git a/src/py/pycamera/__init__.py b/src/py/pycamera/__init__.py > > new file mode 100644 > > index 00000000..a5c198dc > > --- /dev/null > > +++ b/src/py/pycamera/__init__.py > > @@ -0,0 +1,10 @@ > > +# SPDX-License-Identifier: GPL-2.0-or-later > > +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> > > + > > +from .pycamera 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/pycamera/meson.build b/src/py/pycamera/meson.build > > new file mode 100644 > > index 00000000..c490a18d > > --- /dev/null > > +++ b/src/py/pycamera/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', > > +]) > > + > > +pycamera_deps = [ > > + libcamera_public, > > + py3_dep, > > + pybind11_dep, > > +] > > + > > +pycamera_args = [ '-fvisibility=hidden' ] > > +pycamera_args += [ '-Wno-shadow' ] > > Does python shadow variables explicitly? Does this lead to any potential > bugs? > > I recall putting -Wshadow in explicitly because I hit a nasty bug due to > shadowing, so in my eyes, shadowed variables are a bad-thing (tm) :-) > > You have to be sure you know which variable you are writing to, > and it might not always be clear to a reader, or cause potential > confusion..? > > (of course subclassing, and having functions with the same name is > essentially 'shadowing' the function names I guess...) > > > > + > > +destdir = get_option('libdir') + '/python' + py3_dep.version() + '/site-packages/pycamera' > > + > > +pycamera = shared_module('pycamera', > > + pycamera_sources, > > + install : true, > > + install_dir : destdir, > > + name_prefix : '', > > + dependencies : pycamera_deps, > > + cpp_args : pycamera_args) > > + > > +# XXX > You could also create a symlink. There's an example in the top-level > > +# > meson.build. > > +# Copy __init__.py to build dir so that we can run without installing > > +configure_file(input: '__init__.py', output: '__init__.py', copy: true) > > I think a symlink would be better too. > > > + > > +install_data(['__init__.py'], install_dir : destdir) > > diff --git a/src/py/pycamera/pymain.cpp b/src/py/pycamera/pymain.cpp > > new file mode 100644 > > index 00000000..0088e133 > > --- /dev/null > > +++ b/src/py/pycamera/pymain.cpp pylibcamera?? :) > > @@ -0,0 +1,424 @@ > > +/* 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; > > + > > +static py::object ControlValueToPy(const ControlValue &cv) > > +{ > > + //assert(!cv.isArray()); > > + //assert(cv.numElements() == 1); > > Are these asserts not necessary? Is it better to keep them in, in a way > that they are used on debug buidls and compiled out on release builds? > > I think we have ASSERT() for that, but that's an 'internal' helper I > think and I expect the src/py is building on top of the public api.. > > > > + > > + switch (cv.type()) { > > + case ControlTypeBool: > > + return py::cast(cv.get<bool>()); > > + case ControlTypeByte: > > + return py::cast(cv.get<uint8_t>()); > > + case ControlTypeInteger32: > > + return py::cast(cv.get<int32_t>()); > > + case ControlTypeInteger64: > > + return py::cast(cv.get<int64_t>()); > > + case ControlTypeFloat: > > + return py::cast(cv.get<float>()); > > + 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"); > > + } > > +} I wonder whether we should be supporting array controls even in the initial version? Some of the other things (like transforms) simply manifest themselves as "features that you don't get", but I found that any attempts to use metadata basically resulted in failures until I bodged the array controls to work. What do you thinK? > > + > > +static weak_ptr<CameraManager> g_camera_manager; > > +static int g_eventfd; > > +static mutex g_reqlist_mutex; > > +static vector<Request *> g_reqlist; > > + > > +static void handle_request_completed(Request *req) > > +{ > > + { > > + lock_guard guard(g_reqlist_mutex); > > + g_reqlist.push_back(req); > > + } > > + > > + uint64_t v = 1; > > + write(g_eventfd, &v, 8); > > +} > > + > > +PYBIND11_MODULE(pycamera, 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) { This is the one that needs to take a dictionary of controls, but we can add that later! > > + self->requestCompleted.connect(handle_request_completed); > > + > > + int ret = self->start(); > > + if (ret) > > + self->requestCompleted.disconnect(handle_request_completed); > > + > > + return ret; > > + }) > > + > > + .def("stop", [](shared_ptr<Camera> &self) { > > + int ret = self->stop(); > > + if (!ret) > > + self->requestCompleted.disconnect(handle_request_completed); > > + > > + return ret; > > + }) > > + > > + .def("__repr__", [](shared_ptr<Camera> &self) { > > + return "<pycamera.Camera '" + self->id() + "'>"; > > + }) > > + > > + // Keep the camera alive, as StreamConfiguration contains a Stream* > > + .def("generateConfiguration", &Camera::generateConfiguration, py::keep_alive<0, 1>()) I was sometimes finding generateConfiguration a bit awkward because it's the only way to create a CameraConfiguration (I think, maybe I'm wrong?), and it always needs a list of stream roles. So I seem to spend a lot of my time converting Python dicts (what the user sees) to CameraConfigurations and vice versa. I guess I'm not sure what I'm asking for, perhaps I just need to try and tidy up my translation code which has become a bit too twisty. > > + .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( > > + "fmt", > > + [](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") > > + .def_property_readonly("camera", &Request::camera) > > + .def("addBuffer", &Request::addBuffer, 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) { I found this one just a bit clunky to use, as I always have a dictionary of control values and then have to loop through them myself. If it took the dictionary (like my modified start method) that would be more convenient. And I suppose we'd call it set_controls, so we could actually have both. But these are all minor points, these bindings, perhaps with the exception of array controls, are already working very well for me. Thanks! David > > + 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) > > + .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/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 > > + > > -- > > 2.25.1 > >
Quoting David Plowman (2021-12-09 11:16:17) > Hi Tomi > > Thanks for this patch! > > On Thu, 9 Dec 2021 at 09:58, Kieran Bingham > <kieran.bingham@ideasonboard.com> wrote: > > > > Quoting Tomi Valkeinen (2021-12-09 09:29:05) > > > Add libcamera Python bindings. pybind11 is used to generate the C++ <-> > > > Python layer. > > > > > > Only a subset of libcamera classes are exposed. > > > > > > Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> > > > --- > > > .gitignore | 1 + > > > meson.build | 1 + > > > meson_options.txt | 5 + > > > src/meson.build | 1 + > > > src/py/meson.build | 1 + > > > src/py/pycamera/__init__.py | 10 + > > > src/py/pycamera/meson.build | 43 ++++ > > > src/py/pycamera/pymain.cpp | 424 ++++++++++++++++++++++++++++++++++++ > > > subprojects/pybind11.wrap | 12 + > > > 9 files changed, 498 insertions(+) > > > create mode 100644 src/py/meson.build > > > create mode 100644 src/py/pycamera/__init__.py > > > create mode 100644 src/py/pycamera/meson.build > > > create mode 100644 src/py/pycamera/pymain.cpp > > > create mode 100644 subprojects/pybind11.wrap > > > > > > diff --git a/.gitignore b/.gitignore > > > index cca829fa..aae56b2d 100644 > > > --- a/.gitignore > > > +++ b/.gitignore > > > @@ -3,6 +3,7 @@ > > > __pycache__/ > > > build/ > > > patches/ > > > +subprojects/pybind11-2.*/ > > > *.patch > > > *.pyc > > > .cache > > > diff --git a/meson.build b/meson.build > > > index a20cc29e..0f885c14 100644 > > > --- a/meson.build > > > +++ b/meson.build > > > @@ -181,6 +181,7 @@ summary({ > > > 'qcam application': qcam_enabled, > > > 'lc-compliance application': lc_compliance_enabled, > > > 'Unit tests': test_enabled, > > > + 'Python bindings': pycamera_enabled, > > > }, > > > section : 'Configuration', > > > bool_yn : true) > > > 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..4a1e3cb0 100644 > > > --- a/src/meson.build > > > +++ b/src/meson.build > > > @@ -38,3 +38,4 @@ subdir('qcam') > > > > > > subdir('gstreamer') > > > subdir('v4l2') > > > +subdir('py') > > > diff --git a/src/py/meson.build b/src/py/meson.build > > > new file mode 100644 > > > index 00000000..42ffa221 > > > --- /dev/null > > > +++ b/src/py/meson.build > > > @@ -0,0 +1 @@ > > > +subdir('pycamera') > > > diff --git a/src/py/pycamera/__init__.py b/src/py/pycamera/__init__.py > > > new file mode 100644 > > > index 00000000..a5c198dc > > > --- /dev/null > > > +++ b/src/py/pycamera/__init__.py > > > @@ -0,0 +1,10 @@ > > > +# SPDX-License-Identifier: GPL-2.0-or-later > > > +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> > > > + > > > +from .pycamera 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/pycamera/meson.build b/src/py/pycamera/meson.build > > > new file mode 100644 > > > index 00000000..c490a18d > > > --- /dev/null > > > +++ b/src/py/pycamera/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', > > > +]) > > > + > > > +pycamera_deps = [ > > > + libcamera_public, > > > + py3_dep, > > > + pybind11_dep, > > > +] > > > + > > > +pycamera_args = [ '-fvisibility=hidden' ] > > > +pycamera_args += [ '-Wno-shadow' ] > > > > Does python shadow variables explicitly? Does this lead to any potential > > bugs? > > > > I recall putting -Wshadow in explicitly because I hit a nasty bug due to > > shadowing, so in my eyes, shadowed variables are a bad-thing (tm) :-) > > > > You have to be sure you know which variable you are writing to, > > and it might not always be clear to a reader, or cause potential > > confusion..? > > > > (of course subclassing, and having functions with the same name is > > essentially 'shadowing' the function names I guess...) > > > > > > > + > > > +destdir = get_option('libdir') + '/python' + py3_dep.version() + '/site-packages/pycamera' > > > + > > > +pycamera = shared_module('pycamera', > > > + pycamera_sources, > > > + install : true, > > > + install_dir : destdir, > > > + name_prefix : '', > > > + dependencies : pycamera_deps, > > > + cpp_args : pycamera_args) > > > + > > > +# XXX > You could also create a symlink. There's an example in the top-level > > > +# > meson.build. > > > +# Copy __init__.py to build dir so that we can run without installing > > > +configure_file(input: '__init__.py', output: '__init__.py', copy: true) > > > > I think a symlink would be better too. > > > > > + > > > +install_data(['__init__.py'], install_dir : destdir) > > > diff --git a/src/py/pycamera/pymain.cpp b/src/py/pycamera/pymain.cpp > > > new file mode 100644 > > > index 00000000..0088e133 > > > --- /dev/null > > > +++ b/src/py/pycamera/pymain.cpp > > pylibcamera?? :) > > > > @@ -0,0 +1,424 @@ > > > +/* 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; > > > + > > > +static py::object ControlValueToPy(const ControlValue &cv) > > > +{ > > > + //assert(!cv.isArray()); > > > + //assert(cv.numElements() == 1); > > > > Are these asserts not necessary? Is it better to keep them in, in a way > > that they are used on debug buidls and compiled out on release builds? > > > > I think we have ASSERT() for that, but that's an 'internal' helper I > > think and I expect the src/py is building on top of the public api.. > > > > > > > + > > > + switch (cv.type()) { > > > + case ControlTypeBool: > > > + return py::cast(cv.get<bool>()); > > > + case ControlTypeByte: > > > + return py::cast(cv.get<uint8_t>()); > > > + case ControlTypeInteger32: > > > + return py::cast(cv.get<int32_t>()); > > > + case ControlTypeInteger64: > > > + return py::cast(cv.get<int64_t>()); > > > + case ControlTypeFloat: > > > + return py::cast(cv.get<float>()); > > > + 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"); > > > + } > > > +} > > I wonder whether we should be supporting array controls even in the > initial version? Some of the other things (like transforms) simply > manifest themselves as "features that you don't get", but I found that > any attempts to use metadata basically resulted in failures until I > bodged the array controls to work. > > What do you thinK? > > > > + > > > +static weak_ptr<CameraManager> g_camera_manager; > > > +static int g_eventfd; > > > +static mutex g_reqlist_mutex; > > > +static vector<Request *> g_reqlist; > > > + > > > +static void handle_request_completed(Request *req) > > > +{ > > > + { > > > + lock_guard guard(g_reqlist_mutex); > > > + g_reqlist.push_back(req); > > > + } > > > + > > > + uint64_t v = 1; > > > + write(g_eventfd, &v, 8); > > > +} > > > + > > > +PYBIND11_MODULE(pycamera, 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) { > > This is the one that needs to take a dictionary of controls, but we > can add that later! > > > > + self->requestCompleted.connect(handle_request_completed); > > > + > > > + int ret = self->start(); > > > + if (ret) > > > + self->requestCompleted.disconnect(handle_request_completed); > > > + > > > + return ret; > > > + }) > > > + > > > + .def("stop", [](shared_ptr<Camera> &self) { > > > + int ret = self->stop(); > > > + if (!ret) > > > + self->requestCompleted.disconnect(handle_request_completed); > > > + > > > + return ret; > > > + }) > > > + > > > + .def("__repr__", [](shared_ptr<Camera> &self) { > > > + return "<pycamera.Camera '" + self->id() + "'>"; > > > + }) > > > + > > > + // Keep the camera alive, as StreamConfiguration contains a Stream* > > > + .def("generateConfiguration", &Camera::generateConfiguration, py::keep_alive<0, 1>()) > > I was sometimes finding generateConfiguration a bit awkward because > it's the only way to create a CameraConfiguration (I think, maybe I'm > wrong?), and it always needs a list of stream roles. So I seem to It is the only way to 'create' a CameraConfiguration, as it has to be owned by the Camera I believe, but it shouldn't require a list of StreamRoles. The stream roles can be empty, and the streams added (or removed?) manually after. And you should be able to reuse it while negotiating with the Camera - you wouldn't hve to recreate new ones each time do you? So it's not something I'd expect to happen a lot... > spend a lot of my time converting Python dicts (what the user sees) to > CameraConfigurations and vice versa. I guess I'm not sure what I'm > asking for, perhaps I just need to try and tidy up my translation code > which has become a bit too twisty. Or perhaps this is more refering to how the CameraConfiguration is being filled in by your layer ...? > > > + .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( > > > + "fmt", > > > + [](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") > > > + .def_property_readonly("camera", &Request::camera) > > > + .def("addBuffer", &Request::addBuffer, 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) { > > I found this one just a bit clunky to use, as I always have a > dictionary of control values and then have to loop through them > myself. If it took the dictionary (like my modified start method) that > would be more convenient. And I suppose we'd call it set_controls, so > we could actually have both. > > But these are all minor points, these bindings, perhaps with the > exception of array controls, are already working very well for me. > > Thanks! > > David > > > > + 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) > > > + .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/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 > > > + > > > -- > > > 2.25.1 > > >
Hi Tomi, Thank you for the patch. On Thu, Dec 09, 2021 at 11:54:45AM +0000, Kieran Bingham wrote: > Quoting David Plowman (2021-12-09 11:16:17) > > On Thu, 9 Dec 2021 at 09:58, Kieran Bingham wrote: > > > Quoting Tomi Valkeinen (2021-12-09 09:29:05) > > > > Add libcamera Python bindings. pybind11 is used to generate the C++ <-> > > > > Python layer. > > > > > > > > Only a subset of libcamera classes are exposed. Could you elaborate a little bit on your plans in this area ? Was this done to keep the development effort limited for a first version ? Are there any particular design issues that would make support for the rest of the API more difficult, or is it "just" a matter of plumbind the rest ? > > > > Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> > > > > --- > > > > .gitignore | 1 + > > > > meson.build | 1 + > > > > meson_options.txt | 5 + > > > > src/meson.build | 1 + > > > > src/py/meson.build | 1 + > > > > src/py/pycamera/__init__.py | 10 + > > > > src/py/pycamera/meson.build | 43 ++++ > > > > src/py/pycamera/pymain.cpp | 424 ++++++++++++++++++++++++++++++++++++ > > > > subprojects/pybind11.wrap | 12 + > > > > 9 files changed, 498 insertions(+) > > > > create mode 100644 src/py/meson.build > > > > create mode 100644 src/py/pycamera/__init__.py > > > > create mode 100644 src/py/pycamera/meson.build > > > > create mode 100644 src/py/pycamera/pymain.cpp > > > > create mode 100644 subprojects/pybind11.wrap > > > > > > > > diff --git a/.gitignore b/.gitignore > > > > index cca829fa..aae56b2d 100644 > > > > --- a/.gitignore > > > > +++ b/.gitignore > > > > @@ -3,6 +3,7 @@ > > > > __pycache__/ > > > > build/ > > > > patches/ > > > > +subprojects/pybind11-2.*/ We have a .gitignore in subprojects/ that can be used for this. > > > > *.patch > > > > *.pyc > > > > .cache > > > > diff --git a/meson.build b/meson.build > > > > index a20cc29e..0f885c14 100644 > > > > --- a/meson.build > > > > +++ b/meson.build > > > > @@ -181,6 +181,7 @@ summary({ > > > > 'qcam application': qcam_enabled, > > > > 'lc-compliance application': lc_compliance_enabled, > > > > 'Unit tests': test_enabled, > > > > + 'Python bindings': pycamera_enabled, Could you please move this with the adaptation layers, between GStreamer and V4L2 ? > > > > }, > > > > section : 'Configuration', > > > > bool_yn : true) > > > > 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..4a1e3cb0 100644 > > > > --- a/src/meson.build > > > > +++ b/src/meson.build > > > > @@ -38,3 +38,4 @@ subdir('qcam') > > > > > > > > subdir('gstreamer') > > > > subdir('v4l2') > > > > +subdir('py') Alphabetical order please. > > > > diff --git a/src/py/meson.build b/src/py/meson.build > > > > new file mode 100644 > > > > index 00000000..42ffa221 > > > > --- /dev/null > > > > +++ b/src/py/meson.build > > > > @@ -0,0 +1 @@ > > > > +subdir('pycamera') > > > > diff --git a/src/py/pycamera/__init__.py b/src/py/pycamera/__init__.py > > > > new file mode 100644 > > > > index 00000000..a5c198dc > > > > --- /dev/null > > > > +++ b/src/py/pycamera/__init__.py > > > > @@ -0,0 +1,10 @@ > > > > +# SPDX-License-Identifier: GPL-2.0-or-later Shouldn't this be LGPL, as src/py/pycamera/pymain.cpp ? > > > > +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> > > > > + > > > > +from .pycamera import * > > > > +import mmap > > > > + > > > > +def __FrameBuffer__mmap(self, plane): > > > > + return mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ) No need to validate plane ? I'm also not sure extending the FrameBuffer class API this way is the best option (there are reasons why the C++ class doesn't implement mmap()). It's fine for now as the Python bindings are work in progress, but among the things that will need to be handled if we extend the Python API beyond what the C++ API provides is documentation. > > > > + > > > > +FrameBuffer.mmap = __FrameBuffer__mmap > > > > diff --git a/src/py/pycamera/meson.build b/src/py/pycamera/meson.build > > > > new file mode 100644 > > > > index 00000000..c490a18d > > > > --- /dev/null > > > > +++ b/src/py/pycamera/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', > > > > +]) > > > > + > > > > +pycamera_deps = [ > > > > + libcamera_public, > > > > + py3_dep, > > > > + pybind11_dep, > > > > +] > > > > + > > > > +pycamera_args = [ '-fvisibility=hidden' ] No space after [ and before ] (that's the meson coding style AFAIK). > > > > +pycamera_args += [ '-Wno-shadow' ] > > > > > > Does python shadow variables explicitly? Does this lead to any potential > > > bugs? > > > > > > I recall putting -Wshadow in explicitly because I hit a nasty bug due to > > > shadowing, so in my eyes, shadowed variables are a bad-thing (tm) :-) > > > > > > You have to be sure you know which variable you are writing to, > > > and it might not always be clear to a reader, or cause potential > > > confusion..? > > > > > > (of course subclassing, and having functions with the same name is > > > essentially 'shadowing' the function names I guess...) > > > > > > > + > > > > +destdir = get_option('libdir') + '/python' + py3_dep.version() + '/site-packages/pycamera' > > > > + > > > > +pycamera = shared_module('pycamera', > > > > + pycamera_sources, > > > > + install : true, > > > > + install_dir : destdir, > > > > + name_prefix : '', > > > > + dependencies : pycamera_deps, > > > > + cpp_args : pycamera_args) > > > > + > > > > +# XXX > You could also create a symlink. There's an example in the top-level > > > > +# > meson.build. > > > > +# Copy __init__.py to build dir so that we can run without installing > > > > +configure_file(input: '__init__.py', output: '__init__.py', copy: true) > > > > > > I think a symlink would be better too. What do meson-based Python projects usually do ? I could imagine having more .py files in the future, do we need to copy or symlink them all manually, could there be another option ? > > > > + > > > > +install_data(['__init__.py'], install_dir : destdir) > > > > diff --git a/src/py/pycamera/pymain.cpp b/src/py/pycamera/pymain.cpp > > > > new file mode 100644 > > > > index 00000000..0088e133 > > > > --- /dev/null > > > > +++ b/src/py/pycamera/pymain.cpp > > > > pylibcamera?? :) I've replied to the naming question in a different patch of this series, let's continue there. > > > > @@ -0,0 +1,424 @@ > > > > +/* 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; If it's not too much churn, I'd prefer limiting usage of "using namespace" and use the std:: prefix explicitly. > > > > +using namespace libcamera; > > > > + > > > > +static py::object ControlValueToPy(const ControlValue &cv) > > > > +{ > > > > + //assert(!cv.isArray()); > > > > + //assert(cv.numElements() == 1); > > > > > > Are these asserts not necessary? Is it better to keep them in, in a way > > > that they are used on debug buidls and compiled out on release builds? They should be enabled or removed in any case :-) > > > I think we have ASSERT() for that, but that's an 'internal' helper I > > > think and I expect the src/py is building on top of the public api.. ASSERT() integrates with the logging infrastructure and the backtrace generation, so it's nice to use, but I can also imagine and accept that language bindings may need specific rules. > > > > + > > > > + switch (cv.type()) { > > > > + case ControlTypeBool: > > > > + return py::cast(cv.get<bool>()); > > > > + case ControlTypeByte: > > > > + return py::cast(cv.get<uint8_t>()); > > > > + case ControlTypeInteger32: > > > > + return py::cast(cv.get<int32_t>()); > > > > + case ControlTypeInteger64: > > > > + return py::cast(cv.get<int64_t>()); > > > > + case ControlTypeFloat: > > > > + return py::cast(cv.get<float>()); > > > > + 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"); > > > > + } > > > > +} > > > > I wonder whether we should be supporting array controls even in the > > initial version? Some of the other things (like transforms) simply > > manifest themselves as "features that you don't get", but I found that > > any attempts to use metadata basically resulted in failures until I > > bodged the array controls to work. > > > > What do you thinK? Patches are welcome :-) > > > > + > > > > +static weak_ptr<CameraManager> g_camera_manager; > > > > +static int g_eventfd; > > > > +static mutex g_reqlist_mutex; > > > > +static vector<Request *> g_reqlist; > > > > + > > > > +static void handle_request_completed(Request *req) handleRequestCompleted to match the libcamera coding style ? > > > > +{ > > > > + { > > > > + lock_guard guard(g_reqlist_mutex); > > > > + g_reqlist.push_back(req); > > > > + } > > > > + > > > > + uint64_t v = 1; > > > > + write(g_eventfd, &v, 8); > > > > +} > > > > + > > > > +PYBIND11_MODULE(pycamera, 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; > > > > + }) I understand why this is needed (documentation is required though), but why is the eventfd exposed by the CameraManager class and not the Camera class ? Moving it to Camera would also allow moving the requests lists there, and making them member variables instead of global variables. I'm also tempted to look at asyncio and other frameworks, as it seems that callbacks from a different thread are a common enough issue that it should have generic solutions. There are more signals than request completion that need to be exposed (buffer completion is another important one, so is camera hotplug), and I'm sure we'll have even more the future. A completely generic solution isn't a blocker to get this merged (it's work in progress, the API isn't stable) but will likely be required at some point. > > > > + > > > > + .def("getReadyRequests", [](CameraManager &) { I need to bring this up: https://www.python.org/dev/peps/pep-0008/#method-names-and-instance-variables. Should we follow the recommended Python convention of snake_case names for methods, or stick to the libcamera naming convention ? I have a feeling we'll end up with the latter. > > > > + 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>()) The C++ camera enumeration API isn't the greatest, if there are improvements that could be made there that would also benefit the Python API, feedback and proposals are welcome. > > > > + > > > > + // Create a list of Cameras, where each camera has a keep-alive to CameraManager Let's keep the comment style consistent with libcamera please :-) > > > > + .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") Also discussed in a different mail in this thread is whether or not we should create a custom wrapper class for std::shared_ptr<Camera>. Let's continue the discussion there. > > > > + .def_property_readonly("id", &Camera::id) > > > > + .def("acquire", &Camera::acquire) > > > > + .def("release", &Camera::release) > > > > + .def("start", [](shared_ptr<Camera> &self) { > > > > This is the one that needs to take a dictionary of controls, but we > > can add that later! If it's not too difficult (and especially as David has an implementation already), it could be done in v4 (as there will be a v4 to address miscellaneous small review comments). > > > > + self->requestCompleted.connect(handle_request_completed); > > > > + > > > > + int ret = self->start(); > > > > + if (ret) > > > > + self->requestCompleted.disconnect(handle_request_completed); > > > > + > > > > + return ret; > > > > + }) > > > > + > > > > + .def("stop", [](shared_ptr<Camera> &self) { > > > > + int ret = self->stop(); > > > > + if (!ret) > > > > + self->requestCompleted.disconnect(handle_request_completed); > > > > + > > > > + return ret; > > > > + }) > > > > + > > > > + .def("__repr__", [](shared_ptr<Camera> &self) { > > > > + return "<pycamera.Camera '" + self->id() + "'>"; > > > > + }) > > > > + > > > > + // Keep the camera alive, as StreamConfiguration contains a Stream* > > > > + .def("generateConfiguration", &Camera::generateConfiguration, py::keep_alive<0, 1>()) > > > > I was sometimes finding generateConfiguration a bit awkward because > > it's the only way to create a CameraConfiguration (I think, maybe I'm > > wrong?), and it always needs a list of stream roles. So I seem to > > It is the only way to 'create' a CameraConfiguration, as it has to be > owned by the Camera I believe, but it shouldn't require a list of > StreamRoles. > > The stream roles can be empty, and the streams added (or removed?) > manually after. > > And you should be able to reuse it while negotiating with the Camera - > you wouldn't hve to recreate new ones each time do you? So it's not > something I'd expect to happen a lot... > > > spend a lot of my time converting Python dicts (what the user sees) to > > CameraConfigurations and vice versa. I guess I'm not sure what I'm > > asking for, perhaps I just need to try and tidy up my translation code > > which has become a bit too twisty. > > Or perhaps this is more refering to how the CameraConfiguration is being > filled in by your layer ...? The configuration API is in need of a big rework. Seeing the trouble that Python code has with the current API will be useful. > > > > + .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( > > > > + "fmt", "pixelFormat" for consistency ? > > > > + [](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") > > > > + .def_property_readonly("camera", &Request::camera) > > > > + .def("addBuffer", &Request::addBuffer, 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) { > > > > I found this one just a bit clunky to use, as I always have a > > dictionary of control values and then have to loop through them > > myself. If it took the dictionary (like my modified start method) that > > would be more convenient. And I suppose we'd call it set_controls, so > > we could actually have both. Should we expose the ControlList class in the Python API ? > > But these are all minor points, these bindings, perhaps with the > > exception of array controls, are already working very well for me. > > > > > > + 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") Could this be a member of the Python Request class, or would it be frowned upon from a Python coding style point of view ? Same for FrameMetadataStatus bekiwn abd ReuseFlag above. > > > > + .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) > > > > + .def_property_readonly("bytesused", [](FrameMetadata &self) { Will this function go away when implementing support for planes ? There's a todo comment above about that, let's add one here too. > > > > + 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/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 > > > > + Extra blank line.
On 09/12/2021 21:28, Laurent Pinchart wrote: >>>>> + .def_property_readonly("efd", [](CameraManager &) { >>>>> + return g_eventfd; >>>>> + }) > > I understand why this is needed (documentation is required though), but > why is the eventfd exposed by the CameraManager class and not the Camera > class ? Moving it to Camera would also allow moving the requests lists > there, and making them member variables instead of global variables. Because g_eventfd is a global variable and CameraManager is a singleton. We don't have CameraManger or Camera classes in the python bindings, in the sense that we could add fields to them. There's no python-bindings-specific-state for class instances. In other words, eventfd is not part of CameraManager, it's just accessed via it. I've struggled with this multiple times, and I haven't figured out a simple solution. We can build new C++ classes that wrap the libcamera C++ classes, say, PyCamera for Camera, but then that affects all the places in the bindings where Camera instance is handled, producing possibly quite a bit of extra code. I haven't tried this out, but it's been in my mind as it would solve some problems. Tomi
Hi Tomi, On Fri, Dec 10, 2021 at 02:27:59PM +0200, Tomi Valkeinen wrote: > On 09/12/2021 21:28, Laurent Pinchart wrote: > > >>>>> + .def_property_readonly("efd", [](CameraManager &) { > >>>>> + return g_eventfd; > >>>>> + }) > > > > I understand why this is needed (documentation is required though), but > > why is the eventfd exposed by the CameraManager class and not the Camera > > class ? Moving it to Camera would also allow moving the requests lists > > there, and making them member variables instead of global variables. > > Because g_eventfd is a global variable and CameraManager is a singleton. > > We don't have CameraManger or Camera classes in the python bindings, in > the sense that we could add fields to them. There's no > python-bindings-specific-state for class instances. In other words, > eventfd is not part of CameraManager, it's just accessed via it. > > I've struggled with this multiple times, and I haven't figured out a > simple solution. > > We can build new C++ classes that wrap the libcamera C++ classes, say, > PyCamera for Camera, but then that affects all the places in the > bindings where Camera instance is handled, producing possibly quite a > bit of extra code. I haven't tried this out, but it's been in my mind as > it would solve some problems. I have to say I lack experience in this area, but PyCamera C++ wrapper class sounds like it could really help. I expect it would expose a function to retrieve the Camera pointer, and I wonder if exposing that using a custom `operator Camera *()` could help interfacing with code that expects a Camera pointer, or if there are better ways. I'm sure we're not the only ones dealing with this.
diff --git a/.gitignore b/.gitignore index cca829fa..aae56b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ build/ patches/ +subprojects/pybind11-2.*/ *.patch *.pyc .cache diff --git a/meson.build b/meson.build index a20cc29e..0f885c14 100644 --- a/meson.build +++ b/meson.build @@ -181,6 +181,7 @@ summary({ 'qcam application': qcam_enabled, 'lc-compliance application': lc_compliance_enabled, 'Unit tests': test_enabled, + 'Python bindings': pycamera_enabled, }, section : 'Configuration', bool_yn : true) 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..4a1e3cb0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -38,3 +38,4 @@ subdir('qcam') subdir('gstreamer') subdir('v4l2') +subdir('py') diff --git a/src/py/meson.build b/src/py/meson.build new file mode 100644 index 00000000..42ffa221 --- /dev/null +++ b/src/py/meson.build @@ -0,0 +1 @@ +subdir('pycamera') diff --git a/src/py/pycamera/__init__.py b/src/py/pycamera/__init__.py new file mode 100644 index 00000000..a5c198dc --- /dev/null +++ b/src/py/pycamera/__init__.py @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + +from .pycamera 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/pycamera/meson.build b/src/py/pycamera/meson.build new file mode 100644 index 00000000..c490a18d --- /dev/null +++ b/src/py/pycamera/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', +]) + +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/pycamera' + +pycamera = shared_module('pycamera', + pycamera_sources, + install : true, + install_dir : destdir, + name_prefix : '', + dependencies : pycamera_deps, + cpp_args : pycamera_args) + +# XXX > You could also create a symlink. There's an example in the top-level +# > meson.build. +# Copy __init__.py to build dir so that we can run without installing +configure_file(input: '__init__.py', output: '__init__.py', copy: true) + +install_data(['__init__.py'], install_dir : destdir) diff --git a/src/py/pycamera/pymain.cpp b/src/py/pycamera/pymain.cpp new file mode 100644 index 00000000..0088e133 --- /dev/null +++ b/src/py/pycamera/pymain.cpp @@ -0,0 +1,424 @@ +/* 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; + +static py::object ControlValueToPy(const ControlValue &cv) +{ + //assert(!cv.isArray()); + //assert(cv.numElements() == 1); + + switch (cv.type()) { + case ControlTypeBool: + return py::cast(cv.get<bool>()); + case ControlTypeByte: + return py::cast(cv.get<uint8_t>()); + case ControlTypeInteger32: + return py::cast(cv.get<int32_t>()); + case ControlTypeInteger64: + return py::cast(cv.get<int64_t>()); + case ControlTypeFloat: + return py::cast(cv.get<float>()); + 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 handle_request_completed(Request *req) +{ + { + lock_guard guard(g_reqlist_mutex); + g_reqlist.push_back(req); + } + + uint64_t v = 1; + write(g_eventfd, &v, 8); +} + +PYBIND11_MODULE(pycamera, 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(handle_request_completed); + + int ret = self->start(); + if (ret) + self->requestCompleted.disconnect(handle_request_completed); + + return ret; + }) + + .def("stop", [](shared_ptr<Camera> &self) { + int ret = self->stop(); + if (!ret) + self->requestCompleted.disconnect(handle_request_completed); + + return ret; + }) + + .def("__repr__", [](shared_ptr<Camera> &self) { + return "<pycamera.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( + "fmt", + [](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") + .def_property_readonly("camera", &Request::camera) + .def("addBuffer", &Request::addBuffer, 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) + .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/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 +
Add libcamera Python bindings. pybind11 is used to generate the C++ <-> Python layer. Only a subset of libcamera classes are exposed. Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> --- .gitignore | 1 + meson.build | 1 + meson_options.txt | 5 + src/meson.build | 1 + src/py/meson.build | 1 + src/py/pycamera/__init__.py | 10 + src/py/pycamera/meson.build | 43 ++++ src/py/pycamera/pymain.cpp | 424 ++++++++++++++++++++++++++++++++++++ subprojects/pybind11.wrap | 12 + 9 files changed, 498 insertions(+) create mode 100644 src/py/meson.build create mode 100644 src/py/pycamera/__init__.py create mode 100644 src/py/pycamera/meson.build create mode 100644 src/py/pycamera/pymain.cpp create mode 100644 subprojects/pybind11.wrap