[libcamera-devel,v10,4/7] Add Python bindings
diff mbox series

Message ID 20220509101023.35569-5-tomi.valkeinen@ideasonboard.com
State Accepted
Headers show
Series
  • Python bindings
Related show

Commit Message

Tomi Valkeinen May 9, 2022, 10:10 a.m. UTC
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@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@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

Comments

Kieran Bingham May 9, 2022, 8:56 p.m. UTC | #1
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@ideasonboard.com>
> Reviewed-by: Laurent Pinchart <laurent.pinchart@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@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@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@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?

But beyond that I see no reason to further delay merging python support.

Reviewed-by: Kieran Bingham <kieran.bingham@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
> -- 
> 2.34.1
>
Laurent Pinchart May 9, 2022, 9:35 p.m. UTC | #2
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@ideasonboard.com>
> > Reviewed-by: Laurent Pinchart <laurent.pinchart@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@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@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@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@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
Tomi Valkeinen May 10, 2022, 6:04 a.m. UTC | #3
On 09/05/2022 23:56, Kieran Bingham wrote:

> 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?

Looks good to me.

I'd really like to use clang-format, though. It almost looks to me that 
clang-format bugs here. E.g. first clang-format formats to this:

	pyCamera
		.def(
			"start", [](Camera &self, py::dict controls) {
				return 0;
			},
			py::arg("controls") = py::dict());

I have no idea why it wants to add the line feed after .def. But I 
remove the py::arg, and format:

	pyCamera
		.def(
			"start", [](Camera &self, py::dict controls) {
				return 0;
			});

So no change. Then I first manually remove the line feed, i.e. 
.def("start", ... and then format:

	pyCamera
		.def("start", [](Camera &self, py::dict controls) {
			return 0;
		});

Ta-da! Now it's fine. But now, adding back the py::arg, and format, it 
goes back to the first case.

I've been fighting with clang-format before, and I have never managed it 
to format the code quite properly. Looking at the formatted pymain.cpp, 
I see two issues: the first is the extra line split like above, the 
second is that it likes to create very long lines for lambdas. Neither 
of those look like intended usual libcamera coding style, but rather 
bugs or misconfigured rules.

  Tomi
Kieran Bingham May 10, 2022, 9:08 a.m. UTC | #4
Quoting Tomi Valkeinen (2022-05-10 07:04:58)
> On 09/05/2022 23:56, Kieran Bingham wrote:
> 
> > 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?
> 
> Looks good to me.
> 
> I'd really like to use clang-format, though. It almost looks to me that 
> clang-format bugs here. E.g. first clang-format formats to this:
> 
>         pyCamera
>                 .def(
>                         "start", [](Camera &self, py::dict controls) {
>                                 return 0;
>                         },
>                         py::arg("controls") = py::dict());
> 
> I have no idea why it wants to add the line feed after .def. But I 
> remove the py::arg, and format:
> 
>         pyCamera
>                 .def(
>                         "start", [](Camera &self, py::dict controls) {
>                                 return 0;
>                         });
> 
> So no change. Then I first manually remove the line feed, i.e. 
> .def("start", ... and then format:
> 
>         pyCamera
>                 .def("start", [](Camera &self, py::dict controls) {
>                         return 0;
>                 });
> 
> Ta-da! Now it's fine. But now, adding back the py::arg, and format, it 
> goes back to the first case.
> 
> I've been fighting with clang-format before, and I have never managed it 
> to format the code quite properly. Looking at the formatted pymain.cpp, 
> I see two issues: the first is the extra line split like above, the 
> second is that it likes to create very long lines for lambdas. Neither 
> of those look like intended usual libcamera coding style, but rather 
> bugs or misconfigured rules.

If you can find a way to satiate clang-format then that's probably
preferred too - but I don't want to break the way you've styled these
indents, and I think they make sense, given the modelling used by the
pybind.

Would you prefer me to leave the clang-format on and we can ignore the
checkstyle warnings here ? Or perhaps try to improve in the future?

Or we could also try to improve in the future and then re-enable
clang-format?

--
Kieran


> 
>   Tomi
Tomi Valkeinen May 10, 2022, 9:22 a.m. UTC | #5
On 10/05/2022 12:08, Kieran Bingham wrote:
> Quoting Tomi Valkeinen (2022-05-10 07:04:58)
>> On 09/05/2022 23:56, Kieran Bingham wrote:
>>
>>> 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?
>>
>> Looks good to me.
>>
>> I'd really like to use clang-format, though. It almost looks to me that
>> clang-format bugs here. E.g. first clang-format formats to this:
>>
>>          pyCamera
>>                  .def(
>>                          "start", [](Camera &self, py::dict controls) {
>>                                  return 0;
>>                          },
>>                          py::arg("controls") = py::dict());
>>
>> I have no idea why it wants to add the line feed after .def. But I
>> remove the py::arg, and format:
>>
>>          pyCamera
>>                  .def(
>>                          "start", [](Camera &self, py::dict controls) {
>>                                  return 0;
>>                          });
>>
>> So no change. Then I first manually remove the line feed, i.e.
>> .def("start", ... and then format:
>>
>>          pyCamera
>>                  .def("start", [](Camera &self, py::dict controls) {
>>                          return 0;
>>                  });
>>
>> Ta-da! Now it's fine. But now, adding back the py::arg, and format, it
>> goes back to the first case.
>>
>> I've been fighting with clang-format before, and I have never managed it
>> to format the code quite properly. Looking at the formatted pymain.cpp,
>> I see two issues: the first is the extra line split like above, the
>> second is that it likes to create very long lines for lambdas. Neither
>> of those look like intended usual libcamera coding style, but rather
>> bugs or misconfigured rules.
> 
> If you can find a way to satiate clang-format then that's probably
> preferred too - but I don't want to break the way you've styled these
> indents, and I think they make sense, given the modelling used by the
> pybind.

pybind11 project has its own .clang-format file, but even that formats 
the bindings badly. I just can't see why clang-format wants to do this:

      .def(
          "add_buffer",
          [](Request &self, const Stream *stream, FrameBuffer *buffer) {
              return self.addBuffer(stream, buffer);
          },
          py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */

None of the lines are close to the 80 char limit, but still clang-format 
insist on line break there.

> Would you prefer me to leave the clang-format on and we can ignore the
> checkstyle warnings here ? Or perhaps try to improve in the future?
> 
> Or we could also try to improve in the future and then re-enable
> clang-format?

Well, as I said, I'd definitely want to use clang-format, but until 
something happens which makes it possible, I think disabling 
clang-format is fine.

However, it does seem to have a practical issue. I still use 
clang-format to manually format pieces of the code, and then manually 
correct it if clang-format gets it wrong. With "clang-format off" that 
manual formatting is not possible anymore, as clang-format does not 
format anything after "clang-format off".

So I'd prefer to leave the "clang-format off" out for now and ignore the 
warnings.

  Tomi
Kieran Bingham May 10, 2022, 9:37 a.m. UTC | #6
Quoting Tomi Valkeinen (2022-05-10 10:22:57)
> On 10/05/2022 12:08, Kieran Bingham wrote:
> > Quoting Tomi Valkeinen (2022-05-10 07:04:58)
> >> On 09/05/2022 23:56, Kieran Bingham wrote:
> >>
> >>> 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?
> >>
> >> Looks good to me.
> >>
> >> I'd really like to use clang-format, though. It almost looks to me that
> >> clang-format bugs here. E.g. first clang-format formats to this:
> >>
> >>          pyCamera
> >>                  .def(
> >>                          "start", [](Camera &self, py::dict controls) {
> >>                                  return 0;
> >>                          },
> >>                          py::arg("controls") = py::dict());
> >>
> >> I have no idea why it wants to add the line feed after .def. But I
> >> remove the py::arg, and format:
> >>
> >>          pyCamera
> >>                  .def(
> >>                          "start", [](Camera &self, py::dict controls) {
> >>                                  return 0;
> >>                          });
> >>
> >> So no change. Then I first manually remove the line feed, i.e.
> >> .def("start", ... and then format:
> >>
> >>          pyCamera
> >>                  .def("start", [](Camera &self, py::dict controls) {
> >>                          return 0;
> >>                  });
> >>
> >> Ta-da! Now it's fine. But now, adding back the py::arg, and format, it
> >> goes back to the first case.
> >>
> >> I've been fighting with clang-format before, and I have never managed it
> >> to format the code quite properly. Looking at the formatted pymain.cpp,
> >> I see two issues: the first is the extra line split like above, the
> >> second is that it likes to create very long lines for lambdas. Neither
> >> of those look like intended usual libcamera coding style, but rather
> >> bugs or misconfigured rules.
> > 
> > If you can find a way to satiate clang-format then that's probably
> > preferred too - but I don't want to break the way you've styled these
> > indents, and I think they make sense, given the modelling used by the
> > pybind.
> 
> pybind11 project has its own .clang-format file, but even that formats 
> the bindings badly. I just can't see why clang-format wants to do this:
> 
>       .def(
>           "add_buffer",
>           [](Request &self, const Stream *stream, FrameBuffer *buffer) {
>               return self.addBuffer(stream, buffer);
>           },
>           py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */
> 
> None of the lines are close to the 80 char limit, but still clang-format 
> insist on line break there.
> 
> > Would you prefer me to leave the clang-format on and we can ignore the
> > checkstyle warnings here ? Or perhaps try to improve in the future?
> > 
> > Or we could also try to improve in the future and then re-enable
> > clang-format?
> 
> Well, as I said, I'd definitely want to use clang-format, but until 
> something happens which makes it possible, I think disabling 
> clang-format is fine.
> 
> However, it does seem to have a practical issue. I still use 
> clang-format to manually format pieces of the code, and then manually 
> correct it if clang-format gets it wrong. With "clang-format off" that 
> manual formatting is not possible anymore, as clang-format does not 
> format anything after "clang-format off".
> 
> So I'd prefer to leave the "clang-format off" out for now and ignore the 
> warnings.

Ok - I can go with that. But the line fix on the headers probably goes
in still ;-)

I think that's enough for me to keep going with the integrations.

--
Kieran


> 
>   Tomi

Patch
diff mbox series

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@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@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@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);
+
+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