[libcamera-devel] [PATCH v10 4/7] Add Python bindings

Laurent Pinchart laurent.pinchart at ideasonboard.com
Mon May 9 23:35:25 CEST 2022


On Mon, May 09, 2022 at 09:56:09PM +0100, Kieran Bingham wrote:
> Quoting Tomi Valkeinen (2022-05-09 11:10:20)
> > Add libcamera Python bindings. pybind11 is used to generate the C++ <->
> > Python layer.
> > 
> > We use pybind11 'smart_holder' version to avoid issues with private
> > destructors and shared_ptr. There is also an alternative solution here:
> > 
> > https://github.com/pybind/pybind11/pull/2067
> > 
> > Only a subset of libcamera classes are exposed. Implementing and testing
> > the wrapper classes is challenging, and as such only classes that I have
> > needed have been added so far.
> > 
> > Signed-off-by: Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
> > Reviewed-by: Laurent Pinchart <laurent.pinchart at ideasonboard.com>
> > ---
> >  meson.build                                   |   1 +
> >  meson_options.txt                             |   5 +
> >  src/meson.build                               |   1 +
> >  src/py/libcamera/__init__.py                  |  84 +++
> >  src/py/libcamera/meson.build                  |  52 ++
> >  src/py/libcamera/pyenums.cpp                  |  34 +
> >  src/py/libcamera/pymain.cpp                   | 648 ++++++++++++++++++
> >  src/py/meson.build                            |   1 +
> >  subprojects/.gitignore                        |   3 +-
> >  subprojects/packagefiles/pybind11/meson.build |   7 +
> >  subprojects/pybind11.wrap                     |   9 +
> >  11 files changed, 844 insertions(+), 1 deletion(-)
> >  create mode 100644 src/py/libcamera/__init__.py
> >  create mode 100644 src/py/libcamera/meson.build
> >  create mode 100644 src/py/libcamera/pyenums.cpp
> >  create mode 100644 src/py/libcamera/pymain.cpp
> >  create mode 100644 src/py/meson.build
> >  create mode 100644 subprojects/packagefiles/pybind11/meson.build
> >  create mode 100644 subprojects/pybind11.wrap
> > 
> > diff --git a/meson.build b/meson.build
> > index 0124e7d3..60a911e0 100644
> > --- a/meson.build
> > +++ b/meson.build
> > @@ -177,6 +177,7 @@ summary({
> >              'Tracing support': tracing_enabled,
> >              'Android support': android_enabled,
> >              'GStreamer support': gst_enabled,
> > +            'Python bindings': pycamera_enabled,
> >              'V4L2 emulation support': v4l2_enabled,
> >              'cam application': cam_enabled,
> >              'qcam application': qcam_enabled,
> > diff --git a/meson_options.txt b/meson_options.txt
> > index 2c80ad8b..ca00c78e 100644
> > --- a/meson_options.txt
> > +++ b/meson_options.txt
> > @@ -58,3 +58,8 @@ option('v4l2',
> >          type : 'boolean',
> >          value : false,
> >          description : 'Compile the V4L2 compatibility layer')
> > +
> > +option('pycamera',
> > +        type : 'feature',
> > +        value : 'auto',
> > +        description : 'Enable libcamera Python bindings (experimental)')
> > diff --git a/src/meson.build b/src/meson.build
> > index e0ea9c35..34663a6f 100644
> > --- a/src/meson.build
> > +++ b/src/meson.build
> > @@ -37,4 +37,5 @@ subdir('cam')
> >  subdir('qcam')
> >  
> >  subdir('gstreamer')
> > +subdir('py')
> >  subdir('v4l2')
> > diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py
> > new file mode 100644
> > index 00000000..0d7da9e2
> > --- /dev/null
> > +++ b/src/py/libcamera/__init__.py
> > @@ -0,0 +1,84 @@
> > +# SPDX-License-Identifier: LGPL-2.1-or-later
> > +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
> > +
> > +from ._libcamera import *
> > +
> > +
> > +class MappedFrameBuffer:
> > +    def __init__(self, fb):
> > +        self.__fb = fb
> > +
> > +    def __enter__(self):
> > +        import os
> > +        import mmap
> > +
> > +        fb = self.__fb
> > +
> > +        # Collect information about the buffers
> > +
> > +        bufinfos = {}
> > +
> > +        for i in range(fb.num_planes):
> > +            fd = fb.fd(i)
> > +
> > +            if fd not in bufinfos:
> > +                buflen = os.lseek(fd, 0, os.SEEK_END)
> > +                bufinfos[fd] = {'maplen': 0, 'buflen': buflen}
> > +            else:
> > +                buflen = bufinfos[fd]['buflen']
> > +
> > +            if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen:
> > +                raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' +
> > +                                   f'plane offset={fb.offset(i)}, plane length={fb.length(i)}')
> > +
> > +            bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i))
> > +
> > +        # mmap the buffers
> > +
> > +        maps = []
> > +
> > +        for fd, info in bufinfos.items():
> > +            map = mmap.mmap(fd, info['maplen'], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)
> > +            info['map'] = map
> > +            maps.append(map)
> > +
> > +        self.__maps = tuple(maps)
> > +
> > +        # Create memoryviews for the planes
> > +
> > +        planes = []
> > +
> > +        for i in range(fb.num_planes):
> > +            fd = fb.fd(i)
> > +            info = bufinfos[fd]
> > +
> > +            mv = memoryview(info['map'])
> > +
> > +            start = fb.offset(i)
> > +            end = fb.offset(i) + fb.length(i)
> > +
> > +            mv = mv[start:end]
> > +
> > +            planes.append(mv)
> > +
> > +        self.__planes = tuple(planes)
> > +
> > +        return self
> > +
> > +    def __exit__(self, exc_type, exc_value, exc_traceback):
> > +        for p in self.__planes:
> > +            p.release()
> > +
> > +        for mm in self.__maps:
> > +            mm.close()
> > +
> > +    @property
> > +    def planes(self):
> > +        return self.__planes
> > +
> > +
> > +def __FrameBuffer__mmap(self):
> > +    return MappedFrameBuffer(self)
> > +
> > +
> > +FrameBuffer.mmap = __FrameBuffer__mmap
> > diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build
> > new file mode 100644
> > index 00000000..edf4a629
> > --- /dev/null
> > +++ b/src/py/libcamera/meson.build
> > @@ -0,0 +1,52 @@
> > +# SPDX-License-Identifier: CC0-1.0
> > +
> > +py3_dep = dependency('python3', required : get_option('pycamera'))
> > +
> > +if not py3_dep.found()
> > +    pycamera_enabled = false
> > +    subdir_done()
> > +endif
> > +
> > +pycamera_enabled = true
> > +
> > +pybind11_proj = subproject('pybind11')
> > +pybind11_dep = pybind11_proj.get_variable('pybind11_dep')
> > +
> > +pycamera_sources = files([
> > +    'pyenums.cpp',
> > +    'pymain.cpp',
> > +])
> > +
> > +pycamera_deps = [
> > +    libcamera_public,
> > +    py3_dep,
> > +    pybind11_dep,
> > +]
> > +
> > +pycamera_args = [
> > +    '-fvisibility=hidden',
> > +    '-Wno-shadow',
> > +    '-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT',
> > +    '-DLIBCAMERA_BASE_PRIVATE',
> > +]
> > +
> > +destdir = get_option('libdir') / ('python' + py3_dep.version()) / 'site-packages' / 'libcamera'
> > +
> > +pycamera = shared_module('_libcamera',
> > +                         pycamera_sources,
> > +                         install : true,
> > +                         install_dir : destdir,
> > +                         name_prefix : '',
> > +                         dependencies : pycamera_deps,
> > +                         cpp_args : pycamera_args)
> > +
> > +run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py',
> > +            meson.current_build_dir() / '__init__.py',
> > +            check: true)
> > +
> > +install_data(['__init__.py'], install_dir : destdir)
> > +
> > +# \todo Generate stubs when building. See https://peps.python.org/pep-0484/#stub-files
> > +# Note: Depends on pybind11-stubgen. To generate pylibcamera stubs:
> > +# $ PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera
> > +# $ mv build/src/py/libcamera-stubs/* build/src/py/libcamera/
> > diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp
> > new file mode 100644
> > index 00000000..b655e622
> > --- /dev/null
> > +++ b/src/py/libcamera/pyenums.cpp
> > @@ -0,0 +1,34 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
> > + *
> > + * Python bindings - Enumerations
> > + */
> > +
> > +#include <libcamera/libcamera.h>
> > +
> > +#include <pybind11/smart_holder.h>
> > +
> > +namespace py = pybind11;
> > +
> > +using namespace libcamera;
> > +
> > +void init_pyenums(py::module &m)
> > +{
> > +       py::enum_<StreamRole>(m, "StreamRole")
> > +               .value("StillCapture", StreamRole::StillCapture)
> > +               .value("Raw", StreamRole::Raw)
> > +               .value("VideoRecording", StreamRole::VideoRecording)
> > +               .value("Viewfinder", StreamRole::Viewfinder);
> > +
> > +       py::enum_<ControlType>(m, "ControlType")
> > +               .value("None", ControlType::ControlTypeNone)
> > +               .value("Bool", ControlType::ControlTypeBool)
> > +               .value("Byte", ControlType::ControlTypeByte)
> > +               .value("Integer32", ControlType::ControlTypeInteger32)
> > +               .value("Integer64", ControlType::ControlTypeInteger64)
> > +               .value("Float", ControlType::ControlTypeFloat)
> > +               .value("String", ControlType::ControlTypeString)
> > +               .value("Rectangle", ControlType::ControlTypeRectangle)
> > +               .value("Size", ControlType::ControlTypeSize);
> > +}
> > diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp
> > new file mode 100644
> > index 00000000..a4087541
> > --- /dev/null
> > +++ b/src/py/libcamera/pymain.cpp
> > @@ -0,0 +1,648 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
> > + *
> > + * Python bindings
> > + */
> > +
> > +/*
> > + * \todo Add geometry classes (Point, Rectangle...)
> > + * \todo Add bindings for the ControlInfo class
> > + * \todo Add bindings for the PixelFormat class
> > + */
> > +
> > +#include <mutex>
> > +#include <stdexcept>
> > +#include <sys/eventfd.h>
> > +#include <unistd.h>
> > +
> > +#include <libcamera/base/log.h>
> > +#include <libcamera/libcamera.h>
> > +
> > +#include <pybind11/functional.h>
> > +#include <pybind11/smart_holder.h>
> > +#include <pybind11/stl.h>
> > +#include <pybind11/stl_bind.h>
> > +
> > +namespace py = pybind11;
> > +
> > +using namespace libcamera;
> > +
> > +template<typename T>
> > +static py::object valueOrTuple(const ControlValue &cv)
> > +{
> > +       if (cv.isArray()) {
> > +               const T *v = reinterpret_cast<const T *>(cv.data().data());
> > +               auto t = py::tuple(cv.numElements());
> > +
> > +               for (size_t i = 0; i < cv.numElements(); ++i)
> > +                       t[i] = v[i];
> > +
> > +               return std::move(t);
> > +       }
> > +
> > +       return py::cast(cv.get<T>());
> > +}
> > +
> > +static py::object controlValueToPy(const ControlValue &cv)
> > +{
> > +       switch (cv.type()) {
> > +       case ControlTypeBool:
> > +               return valueOrTuple<bool>(cv);
> > +       case ControlTypeByte:
> > +               return valueOrTuple<uint8_t>(cv);
> > +       case ControlTypeInteger32:
> > +               return valueOrTuple<int32_t>(cv);
> > +       case ControlTypeInteger64:
> > +               return valueOrTuple<int64_t>(cv);
> > +       case ControlTypeFloat:
> > +               return valueOrTuple<float>(cv);
> > +       case ControlTypeString:
> > +               return py::cast(cv.get<std::string>());
> > +       case ControlTypeRectangle: {
> > +               const Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());
> > +               return py::make_tuple(v->x, v->y, v->width, v->height);
> > +       }
> > +       case ControlTypeSize: {
> > +               const Size *v = reinterpret_cast<const Size *>(cv.data().data());
> > +               return py::make_tuple(v->width, v->height);
> > +       }
> > +       case ControlTypeNone:
> > +       default:
> > +               throw std::runtime_error("Unsupported ControlValue type");
> > +       }
> > +}
> > +
> > +template<typename T>
> > +static ControlValue controlValueMaybeArray(const py::object &ob)
> > +{
> > +       if (py::isinstance<py::list>(ob) || py::isinstance<py::tuple>(ob)) {
> > +               std::vector<T> vec = ob.cast<std::vector<T>>();
> > +               return ControlValue(Span<const T>(vec));
> > +       }
> > +
> > +       return ControlValue(ob.cast<T>());
> > +}
> > +
> > +static ControlValue pyToControlValue(const py::object &ob, ControlType type)
> > +{
> > +       switch (type) {
> > +       case ControlTypeBool:
> > +               return ControlValue(ob.cast<bool>());
> > +       case ControlTypeByte:
> > +               return controlValueMaybeArray<uint8_t>(ob);
> > +       case ControlTypeInteger32:
> > +               return controlValueMaybeArray<int32_t>(ob);
> > +       case ControlTypeInteger64:
> > +               return controlValueMaybeArray<int64_t>(ob);
> > +       case ControlTypeFloat:
> > +               return controlValueMaybeArray<float>(ob);
> > +       case ControlTypeString:
> > +               return ControlValue(ob.cast<std::string>());
> > +       case ControlTypeRectangle: {
> > +               auto array = ob.cast<std::array<int32_t, 4>>();
> > +               return ControlValue(Rectangle(array[0], array[1], array[2], array[3]));
> > +       }
> > +       case ControlTypeSize: {
> > +               auto array = ob.cast<std::array<int32_t, 2>>();
> > +               return ControlValue(Size(array[0], array[1]));
> > +       }
> > +       case ControlTypeNone:
> > +       default:
> > +               throw std::runtime_error("Control type not implemented");
> > +       }
> > +}
> > +
> > +static std::weak_ptr<CameraManager> gCameraManager;
> > +static int gEventfd;
> > +static std::mutex gReqlistMutex;
> > +static std::vector<Request *> gReqList;
> > +
> > +static void handleRequestCompleted(Request *req)
> > +{
> > +       {
> > +               std::lock_guard guard(gReqlistMutex);
> > +               gReqList.push_back(req);
> > +       }
> > +
> > +       uint64_t v = 1;
> > +       size_t s = write(gEventfd, &v, 8);
> > +       /*
> > +        * We should never fail, and have no simple means to manage the error,
> > +        * so let's use LOG(Fatal).
> > +        */
> > +       if (s != 8)
> > +               LOG(Fatal) << "Unable to write to eventfd";
> > +}
> > +
> > +void init_pyenums(py::module &m);
> > +
> 
> I'd like to add the following here while / when merging:
> 
> ================================================================================
> 
> diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp
> index c983da3a94f1..09646faacd5a 100644
> --- a/src/py/libcamera/pymain.cpp
> +++ b/src/py/libcamera/pymain.cpp
> @@ -17,6 +17,7 @@
>  #include <unistd.h>
> 
>  #include <libcamera/base/log.h>
> +
>  #include <libcamera/libcamera.h>
> 
>  #include <pybind11/functional.h>
> @@ -138,6 +139,14 @@ static void handleRequestCompleted(Request *req)
>  void init_pyenums(py::module &m);
>  void init_pyenums_generated(py::module &m);
> 
> +/*
> + * The python module is not expected to be directly compatible with our usual
> + * code style, and clang-format can't easily handle the specific styles used
> + * with chaining here. Disable clang-format for the remainder of this module.
> + */
> +
> +// clang-format off
> +
>  PYBIND11_MODULE(_libcamera, m)
>  {
>         init_pyenums(m);
> ================================================================================
> 
> Any objections, or modifications to the comment?

No objection.

> But beyond that I see no reason to further delay merging python support.
> 
> Reviewed-by: Kieran Bingham <kieran.bingham at ideasonboard.com>
> 
> > +PYBIND11_MODULE(_libcamera, m)
> > +{
> > +       init_pyenums(m);
> > +
> > +       /* Forward declarations */
> > +
> > +       /*
> > +        * We need to declare all the classes here so that Python docstrings
> > +        * can be generated correctly.
> > +        * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings
> > +        */
> > +
> > +       auto pyCameraManager = py::class_<CameraManager>(m, "CameraManager");
> > +       auto pyCamera = py::class_<Camera>(m, "Camera");
> > +       auto pyCameraConfiguration = py::class_<CameraConfiguration>(m, "CameraConfiguration");
> > +       auto pyCameraConfigurationStatus = py::enum_<CameraConfiguration::Status>(pyCameraConfiguration, "Status");
> > +       auto pyStreamConfiguration = py::class_<StreamConfiguration>(m, "StreamConfiguration");
> > +       auto pyStreamFormats = py::class_<StreamFormats>(m, "StreamFormats");
> > +       auto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, "FrameBufferAllocator");
> > +       auto pyFrameBuffer = py::class_<FrameBuffer>(m, "FrameBuffer");
> > +       auto pyStream = py::class_<Stream>(m, "Stream");
> > +       auto pyControlId = py::class_<ControlId>(m, "ControlId");
> > +       auto pyRequest = py::class_<Request>(m, "Request");
> > +       auto pyRequestStatus = py::enum_<Request::Status>(pyRequest, "Status");
> > +       auto pyRequestReuse = py::enum_<Request::ReuseFlag>(pyRequest, "Reuse");
> > +       auto pyFrameMetadata = py::class_<FrameMetadata>(m, "FrameMetadata");
> > +       auto pyFrameMetadataStatus = py::enum_<FrameMetadata::Status>(pyFrameMetadata, "Status");
> > +       auto pyTransform = py::class_<Transform>(m, "Transform");
> > +       auto pyColorSpace = py::class_<ColorSpace>(m, "ColorSpace");
> > +       auto pyColorSpacePrimaries = py::enum_<ColorSpace::Primaries>(pyColorSpace, "Primaries");
> > +       auto pyColorSpaceTransferFunction = py::enum_<ColorSpace::TransferFunction>(pyColorSpace, "TransferFunction");
> > +       auto pyColorSpaceYcbcrEncoding = py::enum_<ColorSpace::YcbcrEncoding>(pyColorSpace, "YcbcrEncoding");
> > +       auto pyColorSpaceRange = py::enum_<ColorSpace::Range>(pyColorSpace, "Range");
> > +
> > +       /* Global functions */
> > +       m.def("log_set_level", &logSetLevel);
> > +
> > +       /* Classes */
> > +       pyCameraManager
> > +               .def_static("singleton", []() {
> > +                       std::shared_ptr<CameraManager> cm = gCameraManager.lock();
> > +                       if (cm)
> > +                               return cm;
> > +
> > +                       int fd = eventfd(0, 0);
> > +                       if (fd == -1)
> > +                               throw std::system_error(errno, std::generic_category(),
> > +                                                       "Failed to create eventfd");
> > +
> > +                       cm = std::shared_ptr<CameraManager>(new CameraManager, [](auto p) {
> > +                               close(gEventfd);
> > +                               gEventfd = -1;
> > +                               delete p;
> > +                       });
> > +
> > +                       gEventfd = fd;
> > +                       gCameraManager = cm;
> > +
> > +                       int ret = cm->start();
> > +                       if (ret)
> > +                               throw std::system_error(-ret, std::generic_category(),
> > +                                                       "Failed to start CameraManager");
> > +
> > +                       return cm;
> > +               })
> > +
> > +               .def_property_readonly("version", &CameraManager::version)
> > +
> > +               .def_property_readonly("efd", [](CameraManager &) {
> > +                       return gEventfd;
> > +               })
> > +
> > +               .def("get_ready_requests", [](CameraManager &) {
> > +                       std::vector<Request *> v;
> > +
> > +                       {
> > +                               std::lock_guard guard(gReqlistMutex);
> > +                               swap(v, gReqList);
> > +                       }
> > +
> > +                       std::vector<py::object> ret;
> > +
> > +                       for (Request *req : v) {
> > +                               py::object o = py::cast(req);
> > +                               /* Decrease the ref increased in Camera.queue_request() */
> > +                               o.dec_ref();
> > +                               ret.push_back(o);
> > +                       }
> > +
> > +                       return ret;
> > +               })
> > +
> > +               .def("get", py::overload_cast<const std::string &>(&CameraManager::get), py::keep_alive<0, 1>())
> > +
> > +               /* Create a list of Cameras, where each camera has a keep-alive to CameraManager */
> > +               .def_property_readonly("cameras", [](CameraManager &self) {
> > +                       py::list l;
> > +
> > +                       for (auto &c : self.cameras()) {
> > +                               py::object py_cm = py::cast(self);
> > +                               py::object py_cam = py::cast(c);
> > +                               py::detail::keep_alive_impl(py_cam, py_cm);
> > +                               l.append(py_cam);
> > +                       }
> > +
> > +                       return l;
> > +               });
> > +
> > +       pyCamera
> > +               .def_property_readonly("id", &Camera::id)
> > +               .def("acquire", &Camera::acquire)
> > +               .def("release", &Camera::release)
> > +               .def("start", [](Camera &self, py::dict controls) {
> > +                       /* \todo What happens if someone calls start() multiple times? */
> > +
> > +                       self.requestCompleted.connect(handleRequestCompleted);
> > +
> > +                       const ControlInfoMap &controlMap = self.controls();
> > +                       ControlList controlList(controlMap);
> > +                       for (const auto& [hkey, hval]: controls) {
> > +                               auto key = hkey.cast<std::string>();
> > +
> > +                               auto it = std::find_if(controlMap.begin(), controlMap.end(),
> > +                                                      [&key](const auto &kvp) {
> > +                                                               return kvp.first->name() == key;
> > +                                                      });
> > +
> > +                               if (it == controlMap.end())
> > +                                       throw std::runtime_error("Control " + key + " not found");
> > +
> > +                               const auto &id = it->first;
> > +                               auto obj = py::cast<py::object>(hval);
> > +
> > +                               controlList.set(id->id(), pyToControlValue(obj, id->type()));
> > +                       }
> > +
> > +                       int ret = self.start(&controlList);
> > +                       if (ret) {
> > +                               self.requestCompleted.disconnect(handleRequestCompleted);
> > +                               return ret;
> > +                       }
> > +
> > +                       return 0;
> > +               }, py::arg("controls") = py::dict())
> > +
> > +               .def("stop", [](Camera &self) {
> > +                       int ret = self.stop();
> > +                       if (ret)
> > +                               return ret;
> > +
> > +                       self.requestCompleted.disconnect(handleRequestCompleted);
> > +
> > +                       return 0;
> > +               })
> > +
> > +               .def("__str__", [](Camera &self) {
> > +                       return "<libcamera.Camera '" + self.id() + "'>";
> > +               })
> > +
> > +               /* Keep the camera alive, as StreamConfiguration contains a Stream* */
> > +               .def("generate_configuration", &Camera::generateConfiguration, py::keep_alive<0, 1>())
> > +               .def("configure", &Camera::configure)
> > +
> > +               .def("create_request", &Camera::createRequest, py::arg("cookie") = 0)
> > +
> > +               .def("queue_request", [](Camera &self, Request *req) {
> > +                       py::object py_req = py::cast(req);
> > +
> > +                       /*
> > +                        * Increase the reference count, will be dropped in
> > +                        * CameraManager.get_ready_requests().
> > +                        */
> > +
> > +                       py_req.inc_ref();
> > +
> > +                       int ret = self.queueRequest(req);
> > +                       if (ret)
> > +                               py_req.dec_ref();
> > +
> > +                       return ret;
> > +               })
> > +
> > +               .def_property_readonly("streams", [](Camera &self) {
> > +                       py::set set;
> > +                       for (auto &s : self.streams()) {
> > +                               py::object py_self = py::cast(self);
> > +                               py::object py_s = py::cast(s);
> > +                               py::detail::keep_alive_impl(py_s, py_self);
> > +                               set.add(py_s);
> > +                       }
> > +                       return set;
> > +               })
> > +
> > +               .def("find_control", [](Camera &self, const std::string &name) {
> > +                       const auto &controls = self.controls();
> > +
> > +                       auto it = std::find_if(controls.begin(), controls.end(),
> > +                                              [&name](const auto &kvp) {
> > +                                                       return kvp.first->name() == name;
> > +                                              });
> > +
> > +                       if (it == controls.end())
> > +                               throw std::runtime_error("Control '" + name + "' not found");
> > +
> > +                       return it->first;
> > +               }, py::return_value_policy::reference_internal)
> > +
> > +               .def_property_readonly("controls", [](Camera &self) {
> > +                       py::dict ret;
> > +
> > +                       for (const auto &[id, ci] : self.controls()) {
> > +                               ret[id->name().c_str()] = std::make_tuple<py::object>(controlValueToPy(ci.min()),
> > +                                                                                     controlValueToPy(ci.max()),
> > +                                                                                     controlValueToPy(ci.def()));
> > +                       }
> > +
> > +                       return ret;
> > +               })
> > +
> > +               .def_property_readonly("properties", [](Camera &self) {
> > +                       py::dict ret;
> > +
> > +                       for (const auto &[key, cv] : self.properties()) {
> > +                               const ControlId *id = properties::properties.at(key);
> > +                               py::object ob = controlValueToPy(cv);
> > +
> > +                               ret[id->name().c_str()] = ob;
> > +                       }
> > +
> > +                       return ret;
> > +               });
> > +
> > +       pyCameraConfiguration
> > +               .def("__iter__", [](CameraConfiguration &self) {
> > +                       return py::make_iterator<py::return_value_policy::reference_internal>(self);
> > +               }, py::keep_alive<0, 1>())
> > +               .def("__len__", [](CameraConfiguration &self) {
> > +                       return self.size();
> > +               })
> > +               .def("validate", &CameraConfiguration::validate)
> > +               .def("at", py::overload_cast<unsigned int>(&CameraConfiguration::at),
> > +                    py::return_value_policy::reference_internal)
> > +               .def_property_readonly("size", &CameraConfiguration::size)
> > +               .def_property_readonly("empty", &CameraConfiguration::empty)
> > +               .def_readwrite("transform", &CameraConfiguration::transform);
> > +
> > +       pyCameraConfigurationStatus
> > +               .value("Valid", CameraConfiguration::Valid)
> > +               .value("Adjusted", CameraConfiguration::Adjusted)
> > +               .value("Invalid", CameraConfiguration::Invalid);
> > +
> > +       pyStreamConfiguration
> > +               .def("__str__", &StreamConfiguration::toString)
> > +               .def_property_readonly("stream", &StreamConfiguration::stream,
> > +                                      py::return_value_policy::reference_internal)
> > +               .def_property(
> > +                       "size",
> > +                       [](StreamConfiguration &self) {
> > +                               return std::make_tuple(self.size.width, self.size.height);
> > +                       },
> > +                       [](StreamConfiguration &self, std::tuple<uint32_t, uint32_t> size) {
> > +                               self.size.width = std::get<0>(size);
> > +                               self.size.height = std::get<1>(size);
> > +                       })
> > +               .def_property(
> > +                       "pixel_format",
> > +                       [](StreamConfiguration &self) {
> > +                               return self.pixelFormat.toString();
> > +                       },
> > +                       [](StreamConfiguration &self, std::string fmt) {
> > +                               self.pixelFormat = PixelFormat::fromString(fmt);
> > +                       })
> > +               .def_readwrite("stride", &StreamConfiguration::stride)
> > +               .def_readwrite("frame_size", &StreamConfiguration::frameSize)
> > +               .def_readwrite("buffer_count", &StreamConfiguration::bufferCount)
> > +               .def_property_readonly("formats", &StreamConfiguration::formats,
> > +                                      py::return_value_policy::reference_internal)
> > +               .def_readwrite("color_space", &StreamConfiguration::colorSpace);
> > +
> > +       pyStreamFormats
> > +               .def_property_readonly("pixel_formats", [](StreamFormats &self) {
> > +                       std::vector<std::string> fmts;
> > +                       for (auto &fmt : self.pixelformats())
> > +                               fmts.push_back(fmt.toString());
> > +                       return fmts;
> > +               })
> > +               .def("sizes", [](StreamFormats &self, const std::string &pixelFormat) {
> > +                       auto fmt = PixelFormat::fromString(pixelFormat);
> > +                       std::vector<std::tuple<uint32_t, uint32_t>> fmts;
> > +                       for (const auto &s : self.sizes(fmt))
> > +                               fmts.push_back(std::make_tuple(s.width, s.height));
> > +                       return fmts;
> > +               })
> > +               .def("range", [](StreamFormats &self, const std::string &pixelFormat) {
> > +                       auto fmt = PixelFormat::fromString(pixelFormat);
> > +                       const auto &range = self.range(fmt);
> > +                       return make_tuple(std::make_tuple(range.hStep, range.vStep),
> > +                                         std::make_tuple(range.min.width, range.min.height),
> > +                                         std::make_tuple(range.max.width, range.max.height));
> > +               });
> > +
> > +       pyFrameBufferAllocator
> > +               .def(py::init<std::shared_ptr<Camera>>(), py::keep_alive<1, 2>())
> > +               .def("allocate", &FrameBufferAllocator::allocate)
> > +               .def_property_readonly("allocated", &FrameBufferAllocator::allocated)
> > +               /* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */
> > +               .def("buffers", [](FrameBufferAllocator &self, Stream *stream) {
> > +                       py::object py_self = py::cast(self);
> > +                       py::list l;
> > +                       for (auto &ub : self.buffers(stream)) {
> > +                               py::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self);
> > +                               l.append(py_buf);
> > +                       }
> > +                       return l;
> > +               });
> > +
> > +       pyFrameBuffer
> > +               /* \todo implement FrameBuffer::Plane properly */
> > +               .def(py::init([](std::vector<std::tuple<int, unsigned int>> planes, unsigned int cookie) {
> > +                       std::vector<FrameBuffer::Plane> v;
> > +                       for (const auto &t : planes)
> > +                               v.push_back({ SharedFD(std::get<0>(t)), FrameBuffer::Plane::kInvalidOffset, std::get<1>(t) });
> > +                       return new FrameBuffer(v, cookie);
> > +               }))
> > +               .def_property_readonly("metadata", &FrameBuffer::metadata, py::return_value_policy::reference_internal)
> > +               .def_property_readonly("num_planes", [](const FrameBuffer &self) {
> > +                       return self.planes().size();
> > +               })
> > +               .def("length", [](FrameBuffer &self, uint32_t idx) {
> > +                       const FrameBuffer::Plane &plane = self.planes()[idx];
> > +                       return plane.length;
> > +               })
> > +               .def("fd", [](FrameBuffer &self, uint32_t idx) {
> > +                       const FrameBuffer::Plane &plane = self.planes()[idx];
> > +                       return plane.fd.get();
> > +               })
> > +               .def("offset", [](FrameBuffer &self, uint32_t idx) {
> > +                       const FrameBuffer::Plane &plane = self.planes()[idx];
> > +                       return plane.offset;
> > +               })
> > +               .def_property("cookie", &FrameBuffer::cookie, &FrameBuffer::setCookie);
> > +
> > +       pyStream
> > +               .def_property_readonly("configuration", &Stream::configuration);
> > +
> > +       pyControlId
> > +               .def_property_readonly("id", &ControlId::id)
> > +               .def_property_readonly("name", &ControlId::name)
> > +               .def_property_readonly("type", &ControlId::type);
> > +
> > +       pyRequest
> > +               /* \todo Fence is not supported, so we cannot expose addBuffer() directly */
> > +               .def("add_buffer", [](Request &self, const Stream *stream, FrameBuffer *buffer) {
> > +                       return self.addBuffer(stream, buffer);
> > +               }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */
> > +               .def_property_readonly("status", &Request::status)
> > +               .def_property_readonly("buffers", &Request::buffers)
> > +               .def_property_readonly("cookie", &Request::cookie)
> > +               .def_property_readonly("has_pending_buffers", &Request::hasPendingBuffers)
> > +               .def("set_control", [](Request &self, ControlId &id, py::object value) {
> > +                       self.controls().set(id.id(), pyToControlValue(value, id.type()));
> > +               })
> > +               .def_property_readonly("metadata", [](Request &self) {
> > +                       py::dict ret;
> > +
> > +                       for (const auto &[key, cv] : self.metadata()) {
> > +                               const ControlId *id = controls::controls.at(key);
> > +                               py::object ob = controlValueToPy(cv);
> > +
> > +                               ret[id->name().c_str()] = ob;
> > +                       }
> > +
> > +                       return ret;
> > +               })
> > +               /*
> > +                * \todo As we add a keep_alive to the fb in addBuffers(), we
> > +                * can only allow reuse with ReuseBuffers.
> > +                */
> > +               .def("reuse", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); });
> > +
> > +       pyRequestStatus
> > +               .value("Pending", Request::RequestPending)
> > +               .value("Complete", Request::RequestComplete)
> > +               .value("Cancelled", Request::RequestCancelled);
> > +
> > +       pyRequestReuse
> > +               .value("Default", Request::ReuseFlag::Default)
> > +               .value("ReuseBuffers", Request::ReuseFlag::ReuseBuffers);
> > +
> > +       pyFrameMetadata
> > +               .def_readonly("status", &FrameMetadata::status)
> > +               .def_readonly("sequence", &FrameMetadata::sequence)
> > +               .def_readonly("timestamp", &FrameMetadata::timestamp)
> > +               /* \todo Implement FrameMetadata::Plane properly */
> > +               .def_property_readonly("bytesused", [](FrameMetadata &self) {
> > +                       std::vector<unsigned int> v;
> > +                       v.resize(self.planes().size());
> > +                       transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; });
> > +                       return v;
> > +               });
> > +
> > +       pyFrameMetadataStatus
> > +               .value("Success", FrameMetadata::FrameSuccess)
> > +               .value("Error", FrameMetadata::FrameError)
> > +               .value("Cancelled", FrameMetadata::FrameCancelled);
> > +
> > +       pyTransform
> > +               .def(py::init([](int rotation, bool hflip, bool vflip, bool transpose) {
> > +                       bool ok;
> > +
> > +                       Transform t = transformFromRotation(rotation, &ok);
> > +                       if (!ok)
> > +                               throw std::invalid_argument("Invalid rotation");
> > +
> > +                       if (hflip)
> > +                               t ^= Transform::HFlip;
> > +                       if (vflip)
> > +                               t ^= Transform::VFlip;
> > +                       if (transpose)
> > +                               t ^= Transform::Transpose;
> > +                       return t;
> > +               }), py::arg("rotation") = 0, py::arg("hflip") = false,
> > +                   py::arg("vflip") = false, py::arg("transpose") = false)
> > +               .def(py::init([](Transform &other) { return other; }))
> > +               .def("__str__", [](Transform &self) {
> > +                       return "<libcamera.Transform '" + std::string(transformToString(self)) + "'>";
> > +               })
> > +               .def_property("hflip",
> > +                             [](Transform &self) {
> > +                                     return !!(self & Transform::HFlip);
> > +                             },
> > +                             [](Transform &self, bool hflip) {
> > +                                     if (hflip)
> > +                                             self |= Transform::HFlip;
> > +                                     else
> > +                                             self &= ~Transform::HFlip;
> > +                             })
> > +               .def_property("vflip",
> > +                             [](Transform &self) {
> > +                                     return !!(self & Transform::VFlip);
> > +                             },
> > +                             [](Transform &self, bool vflip) {
> > +                                     if (vflip)
> > +                                             self |= Transform::VFlip;
> > +                                     else
> > +                                             self &= ~Transform::VFlip;
> > +                             })
> > +               .def_property("transpose",
> > +                             [](Transform &self) {
> > +                                     return !!(self & Transform::Transpose);
> > +                             },
> > +                             [](Transform &self, bool transpose) {
> > +                                     if (transpose)
> > +                                             self |= Transform::Transpose;
> > +                                     else
> > +                                             self &= ~Transform::Transpose;
> > +                             })
> > +               .def("inverse", [](Transform &self) { return -self; })
> > +               .def("invert", [](Transform &self) {
> > +                       self = -self;
> > +               })
> > +               .def("compose", [](Transform &self, Transform &other) {
> > +                       self = self * other;
> > +               });
> > +
> > +       pyColorSpace
> > +               .def(py::init([](ColorSpace::Primaries primaries,
> > +                                ColorSpace::TransferFunction transferFunction,
> > +                                ColorSpace::YcbcrEncoding ycbcrEncoding,
> > +                                ColorSpace::Range range) {
> > +                       return ColorSpace(primaries, transferFunction, ycbcrEncoding, range);
> > +               }), py::arg("primaries"), py::arg("transferFunction"),
> > +                   py::arg("ycbcrEncoding"), py::arg("range"))
> > +               .def(py::init([](ColorSpace &other) { return other; }))
> > +               .def("__str__", [](ColorSpace &self) {
> > +                       return "<libcamera.ColorSpace '" + self.toString() + "'>";
> > +               })
> > +               .def_readwrite("primaries", &ColorSpace::primaries)
> > +               .def_readwrite("transferFunction", &ColorSpace::transferFunction)
> > +               .def_readwrite("ycbcrEncoding", &ColorSpace::ycbcrEncoding)
> > +               .def_readwrite("range", &ColorSpace::range)
> > +               .def_static("Raw", []() { return ColorSpace::Raw; })
> > +               .def_static("Jpeg", []() { return ColorSpace::Jpeg; })
> > +               .def_static("Srgb", []() { return ColorSpace::Srgb; })
> > +               .def_static("Smpte170m", []() { return ColorSpace::Smpte170m; })
> > +               .def_static("Rec709", []() { return ColorSpace::Rec709; })
> > +               .def_static("Rec2020", []() { return ColorSpace::Rec2020; });
> > +
> > +       pyColorSpacePrimaries
> > +               .value("Raw", ColorSpace::Primaries::Raw)
> > +               .value("Smpte170m", ColorSpace::Primaries::Smpte170m)
> > +               .value("Rec709", ColorSpace::Primaries::Rec709)
> > +               .value("Rec2020", ColorSpace::Primaries::Rec2020);
> > +
> > +       pyColorSpaceTransferFunction
> > +               .value("Linear", ColorSpace::TransferFunction::Linear)
> > +               .value("Srgb", ColorSpace::TransferFunction::Srgb)
> > +               .value("Rec709", ColorSpace::TransferFunction::Rec709);
> > +
> > +       pyColorSpaceYcbcrEncoding
> > +               .value("Null", ColorSpace::YcbcrEncoding::None)
> > +               .value("Rec601", ColorSpace::YcbcrEncoding::Rec601)
> > +               .value("Rec709", ColorSpace::YcbcrEncoding::Rec709)
> > +               .value("Rec2020", ColorSpace::YcbcrEncoding::Rec2020);
> > +
> > +       pyColorSpaceRange
> > +               .value("Full", ColorSpace::Range::Full)
> > +               .value("Limited", ColorSpace::Range::Limited);
> > +}
> > diff --git a/src/py/meson.build b/src/py/meson.build
> > new file mode 100644
> > index 00000000..4ce9668c
> > --- /dev/null
> > +++ b/src/py/meson.build
> > @@ -0,0 +1 @@
> > +subdir('libcamera')
> > diff --git a/subprojects/.gitignore b/subprojects/.gitignore
> > index 391fde2c..0e194289 100644
> > --- a/subprojects/.gitignore
> > +++ b/subprojects/.gitignore
> > @@ -1,3 +1,4 @@
> >  /googletest-release*
> >  /libyuv
> > -/packagecache
> > \ No newline at end of file
> > +/packagecache
> > +/pybind11
> > diff --git a/subprojects/packagefiles/pybind11/meson.build b/subprojects/packagefiles/pybind11/meson.build
> > new file mode 100644
> > index 00000000..1be47ca4
> > --- /dev/null
> > +++ b/subprojects/packagefiles/pybind11/meson.build
> > @@ -0,0 +1,7 @@
> > +project('pybind11', 'cpp',
> > +    version : '2.9.1',
> > +    license : 'BSD-3-Clause')
> > +
> > +pybind11_incdir = include_directories('include')
> > +
> > +pybind11_dep = declare_dependency(include_directories : pybind11_incdir)
> > diff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap
> > new file mode 100644
> > index 00000000..e8037a5d
> > --- /dev/null
> > +++ b/subprojects/pybind11.wrap
> > @@ -0,0 +1,9 @@
> > +[wrap-git]
> > +url = https://github.com/pybind/pybind11.git
> > +# This is the head of 'smart_holder' branch
> > +revision = aebdf00cd060b871c5a1e0c2cf4a333503dd0431
> > +depth = 1
> > +patch_directory = pybind11
> > +
> > +[provide]
> > +pybind11 = pybind11_dep

-- 
Regards,

Laurent Pinchart


More information about the libcamera-devel mailing list