[libcamera-devel] [RFC v3 4/5] Add Python bindings

David Plowman david.plowman at raspberrypi.com
Thu Dec 9 12:16:17 CET 2021


Hi Tomi

Thanks for this patch!

On Thu, 9 Dec 2021 at 09:58, Kieran Bingham
<kieran.bingham at 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 at 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 at 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 at 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
> >


More information about the libcamera-devel mailing list