{"id":15824,"url":"https://patchwork.libcamera.org/api/patches/15824/?format=json","web_url":"https://patchwork.libcamera.org/patch/15824/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20220507101152.31412-5-tomi.valkeinen@ideasonboard.com>","date":"2022-05-07T10:11:49","name":"[libcamera-devel,v9,4/7] Add Python bindings","commit_ref":null,"pull_url":null,"state":"accepted","archived":false,"hash":"d5dfe640af1d7e398bdd189ab755b635302c3bb1","submitter":{"id":109,"url":"https://patchwork.libcamera.org/api/people/109/?format=json","name":"Tomi Valkeinen","email":"tomi.valkeinen@ideasonboard.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/15824/mbox/","series":[{"id":3100,"url":"https://patchwork.libcamera.org/api/series/3100/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=3100","date":"2022-05-07T10:11:45","name":"Python bindings","version":9,"mbox":"https://patchwork.libcamera.org/series/3100/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/15824/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/15824/checks/","tags":{},"headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 1BE90C326D\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSat,  7 May 2022 10:12:23 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 527C665657;\n\tSat,  7 May 2022 12:12:20 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 473DD65644\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSat,  7 May 2022 12:12:17 +0200 (CEST)","from deskari.lan (91-156-85-209.elisa-laajakaista.fi\n\t[91.156.85.209])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id A37186D5;\n\tSat,  7 May 2022 12:12:16 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651918340;\n\tbh=mMuC+PWBro28mW9ViIlB5ICTKpcoGQm620o2V772W/c=;\n\th=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:\n\tFrom;\n\tb=shs0XZLlQXgH+/VuDFVTIkLIHAwIxzGy16//B/SKensbGyRYm4XaKzZzc5IVcC12m\n\tnr7m7cq5CWsfI+CEqeXczJdnsaaHacNAPe5s7bNgV3oY9j4jD0y7bTxoLtJgIIVugF\n\tDILv8bdnzeDnNiys03KGrBsnBtaYcct6H/8rZGSJm5i3INsyIc0RKGARp85idWZDnH\n\tk7kjDqiWoMkeHFk88fOjUA675vigQJg+qH9Gpp6KRJAF8X4WHUoTochRaKYeNzINqr\n\tncC3QsgjhZAXk138b/ZE0Y55m2L1PStTuQYC/ZyN/SCz7DMJ+K6w6ag+LOTkuPZw0Q\n\tJ3T/uiSOy147w==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651918337;\n\tbh=mMuC+PWBro28mW9ViIlB5ICTKpcoGQm620o2V772W/c=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=BsWQczCOqukOFuShoJ5G8R+Xh6sqW3P1FTXNwBkBQJ2uPh8+YwqvL/NxCS+8kDTD0\n\tpSlrW5D29sF7DCtODGetKJVfMHz8Lzqrwdbx3NsE3+FkEnewSrFgmOEfJHZy6XB55P\n\tuCvY+GxYlg5Ul0fgvCiEKgsrPwRlHnYADdt2fVUE="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"BsWQczCO\"; dkim-atps=neutral","To":"libcamera-devel@lists.libcamera.org,\n\tDavid Plowman <david.plowman@raspberrypi.com>,\n\tKieran Bingham <kieran.bingham@ideasonboard.com>,\n\tLaurent Pinchart <laurent.pinchart@ideasonboard.com>,\n\tJacopo Mondi <jacopo@jmondi.org>","Date":"Sat,  7 May 2022 13:11:49 +0300","Message-Id":"<20220507101152.31412-5-tomi.valkeinen@ideasonboard.com>","X-Mailer":"git-send-email 2.34.1","In-Reply-To":"<20220507101152.31412-1-tomi.valkeinen@ideasonboard.com>","References":"<20220507101152.31412-1-tomi.valkeinen@ideasonboard.com>","MIME-Version":"1.0","Content-Transfer-Encoding":"8bit","Subject":"[libcamera-devel] [PATCH v9 4/7] Add Python bindings","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","From":"Tomi Valkeinen via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"},"content":"Add libcamera Python bindings. pybind11 is used to generate the C++ <->\nPython layer.\n\nWe use pybind11 'smart_holder' version to avoid issues with private\ndestructors and shared_ptr. There is also an alternative solution here:\n\nhttps://github.com/pybind/pybind11/pull/2067\n\nOnly a subset of libcamera classes are exposed. Implementing and testing\nthe wrapper classes is challenging, and as such only classes that I have\nneeded have been added so far.\n\nSigned-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\nReviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n---\n meson.build                                   |   1 +\n meson_options.txt                             |   5 +\n src/meson.build                               |   1 +\n src/py/libcamera/__init__.py                  |  84 +++\n src/py/libcamera/meson.build                  |  51 ++\n src/py/libcamera/pyenums.cpp                  |  34 +\n src/py/libcamera/pymain.cpp                   | 642 ++++++++++++++++++\n src/py/meson.build                            |   1 +\n subprojects/.gitignore                        |   3 +-\n subprojects/packagefiles/pybind11/meson.build |   7 +\n subprojects/pybind11.wrap                     |   9 +\n 11 files changed, 837 insertions(+), 1 deletion(-)\n create mode 100644 src/py/libcamera/__init__.py\n create mode 100644 src/py/libcamera/meson.build\n create mode 100644 src/py/libcamera/pyenums.cpp\n create mode 100644 src/py/libcamera/pymain.cpp\n create mode 100644 src/py/meson.build\n create mode 100644 subprojects/packagefiles/pybind11/meson.build\n create mode 100644 subprojects/pybind11.wrap","diff":"diff --git a/meson.build b/meson.build\nindex 0124e7d3..60a911e0 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -177,6 +177,7 @@ summary({\n             'Tracing support': tracing_enabled,\n             'Android support': android_enabled,\n             'GStreamer support': gst_enabled,\n+            'Python bindings': pycamera_enabled,\n             'V4L2 emulation support': v4l2_enabled,\n             'cam application': cam_enabled,\n             'qcam application': qcam_enabled,\ndiff --git a/meson_options.txt b/meson_options.txt\nindex 2c80ad8b..ca00c78e 100644\n--- a/meson_options.txt\n+++ b/meson_options.txt\n@@ -58,3 +58,8 @@ option('v4l2',\n         type : 'boolean',\n         value : false,\n         description : 'Compile the V4L2 compatibility layer')\n+\n+option('pycamera',\n+        type : 'feature',\n+        value : 'auto',\n+        description : 'Enable libcamera Python bindings (experimental)')\ndiff --git a/src/meson.build b/src/meson.build\nindex e0ea9c35..34663a6f 100644\n--- a/src/meson.build\n+++ b/src/meson.build\n@@ -37,4 +37,5 @@ subdir('cam')\n subdir('qcam')\n \n subdir('gstreamer')\n+subdir('py')\n subdir('v4l2')\ndiff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py\nnew file mode 100644\nindex 00000000..0d7da9e2\n--- /dev/null\n+++ b/src/py/libcamera/__init__.py\n@@ -0,0 +1,84 @@\n+# SPDX-License-Identifier: LGPL-2.1-or-later\n+# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+\n+from ._libcamera import *\n+\n+\n+class MappedFrameBuffer:\n+    def __init__(self, fb):\n+        self.__fb = fb\n+\n+    def __enter__(self):\n+        import os\n+        import mmap\n+\n+        fb = self.__fb\n+\n+        # Collect information about the buffers\n+\n+        bufinfos = {}\n+\n+        for i in range(fb.num_planes):\n+            fd = fb.fd(i)\n+\n+            if fd not in bufinfos:\n+                buflen = os.lseek(fd, 0, os.SEEK_END)\n+                bufinfos[fd] = {'maplen': 0, 'buflen': buflen}\n+            else:\n+                buflen = bufinfos[fd]['buflen']\n+\n+            if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen:\n+                raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' +\n+                                   f'plane offset={fb.offset(i)}, plane length={fb.length(i)}')\n+\n+            bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i))\n+\n+        # mmap the buffers\n+\n+        maps = []\n+\n+        for fd, info in bufinfos.items():\n+            map = mmap.mmap(fd, info['maplen'], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)\n+            info['map'] = map\n+            maps.append(map)\n+\n+        self.__maps = tuple(maps)\n+\n+        # Create memoryviews for the planes\n+\n+        planes = []\n+\n+        for i in range(fb.num_planes):\n+            fd = fb.fd(i)\n+            info = bufinfos[fd]\n+\n+            mv = memoryview(info['map'])\n+\n+            start = fb.offset(i)\n+            end = fb.offset(i) + fb.length(i)\n+\n+            mv = mv[start:end]\n+\n+            planes.append(mv)\n+\n+        self.__planes = tuple(planes)\n+\n+        return self\n+\n+    def __exit__(self, exc_type, exc_value, exc_traceback):\n+        for p in self.__planes:\n+            p.release()\n+\n+        for mm in self.__maps:\n+            mm.close()\n+\n+    @property\n+    def planes(self):\n+        return self.__planes\n+\n+\n+def __FrameBuffer__mmap(self):\n+    return MappedFrameBuffer(self)\n+\n+\n+FrameBuffer.mmap = __FrameBuffer__mmap\ndiff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build\nnew file mode 100644\nindex 00000000..fce9fd5e\n--- /dev/null\n+++ b/src/py/libcamera/meson.build\n@@ -0,0 +1,51 @@\n+# SPDX-License-Identifier: CC0-1.0\n+\n+py3_dep = dependency('python3', required : get_option('pycamera'))\n+\n+if not py3_dep.found()\n+    pycamera_enabled = false\n+    subdir_done()\n+endif\n+\n+pycamera_enabled = true\n+\n+pybind11_proj = subproject('pybind11')\n+pybind11_dep = pybind11_proj.get_variable('pybind11_dep')\n+\n+pycamera_sources = files([\n+    'pyenums.cpp',\n+    'pymain.cpp',\n+])\n+\n+pycamera_deps = [\n+    libcamera_public,\n+    py3_dep,\n+    pybind11_dep,\n+]\n+\n+pycamera_args = [\n+    '-fvisibility=hidden',\n+    '-Wno-shadow',\n+    '-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT',\n+]\n+\n+destdir = get_option('libdir') / ('python' + py3_dep.version()) / 'site-packages' / 'libcamera'\n+\n+pycamera = shared_module('_libcamera',\n+                         pycamera_sources,\n+                         install : true,\n+                         install_dir : destdir,\n+                         name_prefix : '',\n+                         dependencies : pycamera_deps,\n+                         cpp_args : pycamera_args)\n+\n+run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py',\n+            meson.current_build_dir() / '__init__.py',\n+            check: true)\n+\n+install_data(['__init__.py'], install_dir : destdir)\n+\n+# \\todo Generate stubs when building. Depends on pybind11-stubgen. Sometimes\n+# this works, sometimes doesn't... To generate pylibcamera stubs.\n+# $ PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\n+# $ mv build/src/py/libcamera-stubs/* build/src/py/libcamera/\ndiff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\nnew file mode 100644\nindex 00000000..b655e622\n--- /dev/null\n+++ b/src/py/libcamera/pyenums.cpp\n@@ -0,0 +1,34 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+ *\n+ * Python bindings - Enumerations\n+ */\n+\n+#include <libcamera/libcamera.h>\n+\n+#include <pybind11/smart_holder.h>\n+\n+namespace py = pybind11;\n+\n+using namespace libcamera;\n+\n+void init_pyenums(py::module &m)\n+{\n+\tpy::enum_<StreamRole>(m, \"StreamRole\")\n+\t\t.value(\"StillCapture\", StreamRole::StillCapture)\n+\t\t.value(\"Raw\", StreamRole::Raw)\n+\t\t.value(\"VideoRecording\", StreamRole::VideoRecording)\n+\t\t.value(\"Viewfinder\", StreamRole::Viewfinder);\n+\n+\tpy::enum_<ControlType>(m, \"ControlType\")\n+\t\t.value(\"None\", ControlType::ControlTypeNone)\n+\t\t.value(\"Bool\", ControlType::ControlTypeBool)\n+\t\t.value(\"Byte\", ControlType::ControlTypeByte)\n+\t\t.value(\"Integer32\", ControlType::ControlTypeInteger32)\n+\t\t.value(\"Integer64\", ControlType::ControlTypeInteger64)\n+\t\t.value(\"Float\", ControlType::ControlTypeFloat)\n+\t\t.value(\"String\", ControlType::ControlTypeString)\n+\t\t.value(\"Rectangle\", ControlType::ControlTypeRectangle)\n+\t\t.value(\"Size\", ControlType::ControlTypeSize);\n+}\ndiff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp\nnew file mode 100644\nindex 00000000..ee306186\n--- /dev/null\n+++ b/src/py/libcamera/pymain.cpp\n@@ -0,0 +1,642 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n+ *\n+ * Python bindings\n+ */\n+\n+/*\n+ * \\todo Add geometry classes (Point, Rectangle...)\n+ * \\todo Add bindings for the ControlInfo class\n+ */\n+\n+#include <mutex>\n+#include <stdexcept>\n+#include <sys/eventfd.h>\n+#include <unistd.h>\n+\n+#include <libcamera/libcamera.h>\n+\n+#include <pybind11/functional.h>\n+#include <pybind11/smart_holder.h>\n+#include <pybind11/stl.h>\n+#include <pybind11/stl_bind.h>\n+\n+namespace py = pybind11;\n+\n+using namespace libcamera;\n+\n+template<typename T>\n+static py::object valueOrTuple(const ControlValue &cv)\n+{\n+\tif (cv.isArray()) {\n+\t\tconst T *v = reinterpret_cast<const T *>(cv.data().data());\n+\t\tauto t = py::tuple(cv.numElements());\n+\n+\t\tfor (size_t i = 0; i < cv.numElements(); ++i)\n+\t\t\tt[i] = v[i];\n+\n+\t\treturn std::move(t);\n+\t}\n+\n+\treturn py::cast(cv.get<T>());\n+}\n+\n+static py::object controlValueToPy(const ControlValue &cv)\n+{\n+\tswitch (cv.type()) {\n+\tcase ControlTypeBool:\n+\t\treturn valueOrTuple<bool>(cv);\n+\tcase ControlTypeByte:\n+\t\treturn valueOrTuple<uint8_t>(cv);\n+\tcase ControlTypeInteger32:\n+\t\treturn valueOrTuple<int32_t>(cv);\n+\tcase ControlTypeInteger64:\n+\t\treturn valueOrTuple<int64_t>(cv);\n+\tcase ControlTypeFloat:\n+\t\treturn valueOrTuple<float>(cv);\n+\tcase ControlTypeString:\n+\t\treturn py::cast(cv.get<std::string>());\n+\tcase ControlTypeRectangle: {\n+\t\tconst Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());\n+\t\treturn py::make_tuple(v->x, v->y, v->width, v->height);\n+\t}\n+\tcase ControlTypeSize: {\n+\t\tconst Size *v = reinterpret_cast<const Size *>(cv.data().data());\n+\t\treturn py::make_tuple(v->width, v->height);\n+\t}\n+\tcase ControlTypeNone:\n+\tdefault:\n+\t\tthrow std::runtime_error(\"Unsupported ControlValue type\");\n+\t}\n+}\n+\n+template<typename T>\n+static ControlValue controlValueMaybeArray(const py::object &ob)\n+{\n+\tif (py::isinstance<py::list>(ob) || py::isinstance<py::tuple>(ob)) {\n+\t\tstd::vector<T> vec = ob.cast<std::vector<T>>();\n+\t\treturn ControlValue(Span<const T>(vec));\n+\t}\n+\n+\treturn ControlValue(ob.cast<T>());\n+}\n+\n+static ControlValue pyToControlValue(const py::object &ob, ControlType type)\n+{\n+\tswitch (type) {\n+\tcase ControlTypeBool:\n+\t\treturn ControlValue(ob.cast<bool>());\n+\tcase ControlTypeByte:\n+\t\treturn controlValueMaybeArray<uint8_t>(ob);\n+\tcase ControlTypeInteger32:\n+\t\treturn controlValueMaybeArray<int32_t>(ob);\n+\tcase ControlTypeInteger64:\n+\t\treturn controlValueMaybeArray<int64_t>(ob);\n+\tcase ControlTypeFloat:\n+\t\treturn controlValueMaybeArray<float>(ob);\n+\tcase ControlTypeString:\n+\t\treturn ControlValue(ob.cast<std::string>());\n+\tcase ControlTypeRectangle: {\n+\t\tauto array = ob.cast<std::array<int32_t, 4>>();\n+\t\treturn ControlValue(Rectangle(array[0], array[1], array[2], array[3]));\n+\t}\n+\tcase ControlTypeSize: {\n+\t\tauto array = ob.cast<std::array<int32_t, 2>>();\n+\t\treturn ControlValue(Size(array[0], array[1]));\n+\t}\n+\tcase ControlTypeNone:\n+\tdefault:\n+\t\tthrow std::runtime_error(\"Control type not implemented\");\n+\t}\n+}\n+\n+static std::weak_ptr<CameraManager> gCameraManager;\n+static int gEventfd;\n+static std::mutex gReqlistMutex;\n+static std::vector<Request *> gReqList;\n+\n+static void handleRequestCompleted(Request *req)\n+{\n+\t{\n+\t\tstd::lock_guard guard(gReqlistMutex);\n+\t\tgReqList.push_back(req);\n+\t}\n+\n+\tuint64_t v = 1;\n+\tsize_t s = write(gEventfd, &v, 8);\n+\tif (s != 8)\n+\t\tthrow std::runtime_error(\"Unable to write to eventfd\");\n+}\n+\n+void init_pyenums(py::module &m);\n+\n+PYBIND11_MODULE(_libcamera, m)\n+{\n+\tinit_pyenums(m);\n+\n+\t/* Forward declarations */\n+\n+\t/*\n+\t * We need to declare all the classes here so that Python docstrings\n+\t * can be generated correctly.\n+\t * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\n+\t */\n+\n+\tauto pyCameraManager = py::class_<CameraManager>(m, \"CameraManager\");\n+\tauto pyCamera = py::class_<Camera>(m, \"Camera\");\n+\tauto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\n+\tauto pyCameraConfigurationStatus = py::enum_<CameraConfiguration::Status>(pyCameraConfiguration, \"Status\");\n+\tauto pyStreamConfiguration = py::class_<StreamConfiguration>(m, \"StreamConfiguration\");\n+\tauto pyStreamFormats = py::class_<StreamFormats>(m, \"StreamFormats\");\n+\tauto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\");\n+\tauto pyFrameBuffer = py::class_<FrameBuffer>(m, \"FrameBuffer\");\n+\tauto pyStream = py::class_<Stream>(m, \"Stream\");\n+\tauto pyControlId = py::class_<ControlId>(m, \"ControlId\");\n+\tauto pyRequest = py::class_<Request>(m, \"Request\");\n+\tauto pyRequestStatus = py::enum_<Request::Status>(pyRequest, \"Status\");\n+\tauto pyRequestReuse = py::enum_<Request::ReuseFlag>(pyRequest, \"Reuse\");\n+\tauto pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n+\tauto pyFrameMetadataStatus = py::enum_<FrameMetadata::Status>(pyFrameMetadata, \"Status\");\n+\tauto pyTransform = py::class_<Transform>(m, \"Transform\");\n+\tauto pyColorSpace = py::class_<ColorSpace>(m, \"ColorSpace\");\n+\tauto pyColorSpacePrimaries = py::enum_<ColorSpace::Primaries>(pyColorSpace, \"Primaries\");\n+\tauto pyColorSpaceTransferFunction = py::enum_<ColorSpace::TransferFunction>(pyColorSpace, \"TransferFunction\");\n+\tauto pyColorSpaceYcbcrEncoding = py::enum_<ColorSpace::YcbcrEncoding>(pyColorSpace, \"YcbcrEncoding\");\n+\tauto pyColorSpaceRange = py::enum_<ColorSpace::Range>(pyColorSpace, \"Range\");\n+\n+\t/* Global functions */\n+\tm.def(\"log_set_level\", &logSetLevel);\n+\n+\t/* Classes */\n+\tpyCameraManager\n+\t\t.def_static(\"singleton\", []() {\n+\t\t\tstd::shared_ptr<CameraManager> cm = gCameraManager.lock();\n+\t\t\tif (cm)\n+\t\t\t\treturn cm;\n+\n+\t\t\tint fd = eventfd(0, 0);\n+\t\t\tif (fd == -1)\n+\t\t\t\tthrow std::system_error(errno, std::generic_category(),\n+\t\t\t\t\t\t\t\"Failed to create eventfd\");\n+\n+\t\t\tcm = std::shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n+\t\t\t\tclose(gEventfd);\n+\t\t\t\tgEventfd = -1;\n+\t\t\t\tdelete p;\n+\t\t\t});\n+\n+\t\t\tgEventfd = fd;\n+\t\t\tgCameraManager = cm;\n+\n+\t\t\tint ret = cm->start();\n+\t\t\tif (ret)\n+\t\t\t\tthrow std::system_error(-ret, std::generic_category(),\n+\t\t\t\t\t\t\t\"Failed to start CameraManager\");\n+\n+\t\t\treturn cm;\n+\t\t})\n+\n+\t\t.def_property_readonly(\"version\", &CameraManager::version)\n+\n+\t\t.def_property_readonly(\"efd\", [](CameraManager &) {\n+\t\t\treturn gEventfd;\n+\t\t})\n+\n+\t\t.def(\"get_ready_requests\", [](CameraManager &) {\n+\t\t\tstd::vector<Request *> v;\n+\n+\t\t\t{\n+\t\t\t\tstd::lock_guard guard(gReqlistMutex);\n+\t\t\t\tswap(v, gReqList);\n+\t\t\t}\n+\n+\t\t\tstd::vector<py::object> ret;\n+\n+\t\t\tfor (Request *req : v) {\n+\t\t\t\tpy::object o = py::cast(req);\n+\t\t\t\t/* Decrease the ref increased in Camera.queue_request() */\n+\t\t\t\to.dec_ref();\n+\t\t\t\tret.push_back(o);\n+\t\t\t}\n+\n+\t\t\treturn ret;\n+\t\t})\n+\n+\t\t.def(\"get\", py::overload_cast<const std::string &>(&CameraManager::get), py::keep_alive<0, 1>())\n+\n+\t\t/* Create a list of Cameras, where each camera has a keep-alive to CameraManager */\n+\t\t.def_property_readonly(\"cameras\", [](CameraManager &self) {\n+\t\t\tpy::list l;\n+\n+\t\t\tfor (auto &c : self.cameras()) {\n+\t\t\t\tpy::object py_cm = py::cast(self);\n+\t\t\t\tpy::object py_cam = py::cast(c);\n+\t\t\t\tpy::detail::keep_alive_impl(py_cam, py_cm);\n+\t\t\t\tl.append(py_cam);\n+\t\t\t}\n+\n+\t\t\treturn l;\n+\t\t});\n+\n+\tpyCamera\n+\t\t.def_property_readonly(\"id\", &Camera::id)\n+\t\t.def(\"acquire\", &Camera::acquire)\n+\t\t.def(\"release\", &Camera::release)\n+\t\t.def(\"start\", [](Camera &self, py::dict controls) {\n+\t\t\t/* \\todo What happens if someone calls start() multiple times? */\n+\n+\t\t\tself.requestCompleted.connect(handleRequestCompleted);\n+\n+\t\t\tconst ControlInfoMap &controlMap = self.controls();\n+\t\t\tControlList controlList(controlMap);\n+\t\t\tfor (const auto& [hkey, hval]: controls) {\n+\t\t\t\tauto key = hkey.cast<std::string>();\n+\n+\t\t\t\tauto it = std::find_if(controlMap.begin(), controlMap.end(),\n+\t\t\t\t\t\t       [&key](const auto &kvp) {\n+\t\t\t\t\t\t\t\treturn kvp.first->name() == key;\n+\t\t\t\t\t\t       });\n+\n+\t\t\t\tif (it == controlMap.end())\n+\t\t\t\t\tthrow std::runtime_error(\"Control \" + key + \" not found\");\n+\n+\t\t\t\tconst auto &id = it->first;\n+\t\t\t\tauto obj = py::cast<py::object>(hval);\n+\n+\t\t\t\tcontrolList.set(id->id(), pyToControlValue(obj, id->type()));\n+\t\t\t}\n+\n+\t\t\tint ret = self.start(&controlList);\n+\t\t\tif (ret) {\n+\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n+\t\t\t\treturn ret;\n+\t\t\t}\n+\n+\t\t\treturn 0;\n+\t\t}, py::arg(\"controls\") = py::dict())\n+\n+\t\t.def(\"stop\", [](Camera &self) {\n+\t\t\tint ret = self.stop();\n+\t\t\tif (ret)\n+\t\t\t\treturn ret;\n+\n+\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n+\n+\t\t\treturn 0;\n+\t\t})\n+\n+\t\t.def(\"__str__\", [](Camera &self) {\n+\t\t\treturn \"<libcamera.Camera '\" + self.id() + \"'>\";\n+\t\t})\n+\n+\t\t/* Keep the camera alive, as StreamConfiguration contains a Stream* */\n+\t\t.def(\"generate_configuration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n+\t\t.def(\"configure\", &Camera::configure)\n+\n+\t\t.def(\"create_request\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n+\n+\t\t.def(\"queue_request\", [](Camera &self, Request *req) {\n+\t\t\tpy::object py_req = py::cast(req);\n+\n+\t\t\t/*\n+\t\t\t * Increase the reference count, will be dropped in\n+\t\t\t * CameraManager.get_ready_requests().\n+\t\t\t */\n+\n+\t\t\tpy_req.inc_ref();\n+\n+\t\t\tint ret = self.queueRequest(req);\n+\t\t\tif (ret)\n+\t\t\t\tpy_req.dec_ref();\n+\n+\t\t\treturn ret;\n+\t\t})\n+\n+\t\t.def_property_readonly(\"streams\", [](Camera &self) {\n+\t\t\tpy::set set;\n+\t\t\tfor (auto &s : self.streams()) {\n+\t\t\t\tpy::object py_self = py::cast(self);\n+\t\t\t\tpy::object py_s = py::cast(s);\n+\t\t\t\tpy::detail::keep_alive_impl(py_s, py_self);\n+\t\t\t\tset.add(py_s);\n+\t\t\t}\n+\t\t\treturn set;\n+\t\t})\n+\n+\t\t.def(\"find_control\", [](Camera &self, const std::string &name) {\n+\t\t\tconst auto &controls = self.controls();\n+\n+\t\t\tauto it = std::find_if(controls.begin(), controls.end(),\n+\t\t\t\t\t       [&name](const auto &kvp) {\n+\t\t\t\t\t\t\treturn kvp.first->name() == name;\n+\t\t\t\t\t       });\n+\n+\t\t\tif (it == controls.end())\n+\t\t\t\tthrow std::runtime_error(\"Control '\" + name + \"' not found\");\n+\n+\t\t\treturn it->first;\n+\t\t}, py::return_value_policy::reference_internal)\n+\n+\t\t.def_property_readonly(\"controls\", [](Camera &self) {\n+\t\t\tpy::dict ret;\n+\n+\t\t\tfor (const auto &[id, ci] : self.controls()) {\n+\t\t\t\tret[id->name().c_str()] = std::make_tuple<py::object>(controlValueToPy(ci.min()),\n+\t\t\t\t\t\t\t\t\t\t      controlValueToPy(ci.max()),\n+\t\t\t\t\t\t\t\t\t\t      controlValueToPy(ci.def()));\n+\t\t\t}\n+\n+\t\t\treturn ret;\n+\t\t})\n+\n+\t\t.def_property_readonly(\"properties\", [](Camera &self) {\n+\t\t\tpy::dict ret;\n+\n+\t\t\tfor (const auto &[key, cv] : self.properties()) {\n+\t\t\t\tconst ControlId *id = properties::properties.at(key);\n+\t\t\t\tpy::object ob = controlValueToPy(cv);\n+\n+\t\t\t\tret[id->name().c_str()] = ob;\n+\t\t\t}\n+\n+\t\t\treturn ret;\n+\t\t});\n+\n+\tpyCameraConfiguration\n+\t\t.def(\"__iter__\", [](CameraConfiguration &self) {\n+\t\t\treturn py::make_iterator<py::return_value_policy::reference_internal>(self);\n+\t\t}, py::keep_alive<0, 1>())\n+\t\t.def(\"__len__\", [](CameraConfiguration &self) {\n+\t\t\treturn self.size();\n+\t\t})\n+\t\t.def(\"validate\", &CameraConfiguration::validate)\n+\t\t.def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at),\n+\t\t     py::return_value_policy::reference_internal)\n+\t\t.def_property_readonly(\"size\", &CameraConfiguration::size)\n+\t\t.def_property_readonly(\"empty\", &CameraConfiguration::empty)\n+\t\t.def_readwrite(\"transform\", &CameraConfiguration::transform);\n+\n+\tpyCameraConfigurationStatus\n+\t\t.value(\"Valid\", CameraConfiguration::Valid)\n+\t\t.value(\"Adjusted\", CameraConfiguration::Adjusted)\n+\t\t.value(\"Invalid\", CameraConfiguration::Invalid);\n+\n+\tpyStreamConfiguration\n+\t\t.def(\"__str__\", &StreamConfiguration::toString)\n+\t\t.def_property_readonly(\"stream\", &StreamConfiguration::stream,\n+\t\t                       py::return_value_policy::reference_internal)\n+\t\t.def_property(\n+\t\t\t\"size\",\n+\t\t\t[](StreamConfiguration &self) {\n+\t\t\t\treturn std::make_tuple(self.size.width, self.size.height);\n+\t\t\t},\n+\t\t\t[](StreamConfiguration &self, std::tuple<uint32_t, uint32_t> size) {\n+\t\t\t\tself.size.width = std::get<0>(size);\n+\t\t\t\tself.size.height = std::get<1>(size);\n+\t\t\t})\n+\t\t.def_property(\n+\t\t\t\"pixel_format\",\n+\t\t\t[](StreamConfiguration &self) {\n+\t\t\t\treturn self.pixelFormat.toString();\n+\t\t\t},\n+\t\t\t[](StreamConfiguration &self, std::string fmt) {\n+\t\t\t\tself.pixelFormat = PixelFormat::fromString(fmt);\n+\t\t\t})\n+\t\t.def_readwrite(\"stride\", &StreamConfiguration::stride)\n+\t\t.def_readwrite(\"frame_size\", &StreamConfiguration::frameSize)\n+\t\t.def_readwrite(\"buffer_count\", &StreamConfiguration::bufferCount)\n+\t\t.def_property_readonly(\"formats\", &StreamConfiguration::formats,\n+\t\t                       py::return_value_policy::reference_internal)\n+\t\t.def_readwrite(\"color_space\", &StreamConfiguration::colorSpace);\n+\n+\tpyStreamFormats\n+\t\t.def_property_readonly(\"pixel_formats\", [](StreamFormats &self) {\n+\t\t\tstd::vector<std::string> fmts;\n+\t\t\tfor (auto &fmt : self.pixelformats())\n+\t\t\t\tfmts.push_back(fmt.toString());\n+\t\t\treturn fmts;\n+\t\t})\n+\t\t.def(\"sizes\", [](StreamFormats &self, const std::string &pixelFormat) {\n+\t\t\tauto fmt = PixelFormat::fromString(pixelFormat);\n+\t\t\tstd::vector<std::tuple<uint32_t, uint32_t>> fmts;\n+\t\t\tfor (const auto &s : self.sizes(fmt))\n+\t\t\t\tfmts.push_back(std::make_tuple(s.width, s.height));\n+\t\t\treturn fmts;\n+\t\t})\n+\t\t.def(\"range\", [](StreamFormats &self, const std::string &pixelFormat) {\n+\t\t\tauto fmt = PixelFormat::fromString(pixelFormat);\n+\t\t\tconst auto &range = self.range(fmt);\n+\t\t\treturn make_tuple(std::make_tuple(range.hStep, range.vStep),\n+\t\t\t\t\t  std::make_tuple(range.min.width, range.min.height),\n+\t\t\t\t\t  std::make_tuple(range.max.width, range.max.height));\n+\t\t});\n+\n+\tpyFrameBufferAllocator\n+\t\t.def(py::init<std::shared_ptr<Camera>>(), py::keep_alive<1, 2>())\n+\t\t.def(\"allocate\", &FrameBufferAllocator::allocate)\n+\t\t.def_property_readonly(\"allocated\", &FrameBufferAllocator::allocated)\n+\t\t/* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */\n+\t\t.def(\"buffers\", [](FrameBufferAllocator &self, Stream *stream) {\n+\t\t\tpy::object py_self = py::cast(self);\n+\t\t\tpy::list l;\n+\t\t\tfor (auto &ub : self.buffers(stream)) {\n+\t\t\t\tpy::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self);\n+\t\t\t\tl.append(py_buf);\n+\t\t\t}\n+\t\t\treturn l;\n+\t\t});\n+\n+\tpyFrameBuffer\n+\t\t/* \\todo implement FrameBuffer::Plane properly */\n+\t\t.def(py::init([](std::vector<std::tuple<int, unsigned int>> planes, unsigned int cookie) {\n+\t\t\tstd::vector<FrameBuffer::Plane> v;\n+\t\t\tfor (const auto &t : planes)\n+\t\t\t\tv.push_back({ SharedFD(std::get<0>(t)), FrameBuffer::Plane::kInvalidOffset, std::get<1>(t) });\n+\t\t\treturn new FrameBuffer(v, cookie);\n+\t\t}))\n+\t\t.def_property_readonly(\"metadata\", &FrameBuffer::metadata, py::return_value_policy::reference_internal)\n+\t\t.def_property_readonly(\"num_planes\", [](const FrameBuffer &self) {\n+\t\t\treturn self.planes().size();\n+\t\t})\n+\t\t.def(\"length\", [](FrameBuffer &self, uint32_t idx) {\n+\t\t\tconst FrameBuffer::Plane &plane = self.planes()[idx];\n+\t\t\treturn plane.length;\n+\t\t})\n+\t\t.def(\"fd\", [](FrameBuffer &self, uint32_t idx) {\n+\t\t\tconst FrameBuffer::Plane &plane = self.planes()[idx];\n+\t\t\treturn plane.fd.get();\n+\t\t})\n+\t\t.def(\"offset\", [](FrameBuffer &self, uint32_t idx) {\n+\t\t\tconst FrameBuffer::Plane &plane = self.planes()[idx];\n+\t\t\treturn plane.offset;\n+\t\t})\n+\t\t.def_property(\"cookie\", &FrameBuffer::cookie, &FrameBuffer::setCookie);\n+\n+\tpyStream\n+\t\t.def_property_readonly(\"configuration\", &Stream::configuration);\n+\n+\tpyControlId\n+\t\t.def_property_readonly(\"id\", &ControlId::id)\n+\t\t.def_property_readonly(\"name\", &ControlId::name)\n+\t\t.def_property_readonly(\"type\", &ControlId::type);\n+\n+\tpyRequest\n+\t\t/* \\todo Fence is not supported, so we cannot expose addBuffer() directly */\n+\t\t.def(\"add_buffer\", [](Request &self, const Stream *stream, FrameBuffer *buffer) {\n+\t\t\treturn self.addBuffer(stream, buffer);\n+\t\t}, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */\n+\t\t.def_property_readonly(\"status\", &Request::status)\n+\t\t.def_property_readonly(\"buffers\", &Request::buffers)\n+\t\t.def_property_readonly(\"cookie\", &Request::cookie)\n+\t\t.def_property_readonly(\"has_pending_buffers\", &Request::hasPendingBuffers)\n+\t\t.def(\"set_control\", [](Request &self, ControlId &id, py::object value) {\n+\t\t\tself.controls().set(id.id(), pyToControlValue(value, id.type()));\n+\t\t})\n+\t\t.def_property_readonly(\"metadata\", [](Request &self) {\n+\t\t\tpy::dict ret;\n+\n+\t\t\tfor (const auto &[key, cv] : self.metadata()) {\n+\t\t\t\tconst ControlId *id = controls::controls.at(key);\n+\t\t\t\tpy::object ob = controlValueToPy(cv);\n+\n+\t\t\t\tret[id->name().c_str()] = ob;\n+\t\t\t}\n+\n+\t\t\treturn ret;\n+\t\t})\n+\t\t/*\n+\t\t * \\todo As we add a keep_alive to the fb in addBuffers(), we\n+\t\t * can only allow reuse with ReuseBuffers.\n+\t\t */\n+\t\t.def(\"reuse\", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); });\n+\n+\tpyRequestStatus\n+\t\t.value(\"Pending\", Request::RequestPending)\n+\t\t.value(\"Complete\", Request::RequestComplete)\n+\t\t.value(\"Cancelled\", Request::RequestCancelled);\n+\n+\tpyRequestReuse\n+\t\t.value(\"Default\", Request::ReuseFlag::Default)\n+\t\t.value(\"ReuseBuffers\", Request::ReuseFlag::ReuseBuffers);\n+\n+\tpyFrameMetadata\n+\t\t.def_readonly(\"status\", &FrameMetadata::status)\n+\t\t.def_readonly(\"sequence\", &FrameMetadata::sequence)\n+\t\t.def_readonly(\"timestamp\", &FrameMetadata::timestamp)\n+\t\t/* \\todo Implement FrameMetadata::Plane properly */\n+\t\t.def_property_readonly(\"bytesused\", [](FrameMetadata &self) {\n+\t\t\tstd::vector<unsigned int> v;\n+\t\t\tv.resize(self.planes().size());\n+\t\t\ttransform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; });\n+\t\t\treturn v;\n+\t\t});\n+\n+\tpyFrameMetadataStatus\n+\t\t.value(\"Success\", FrameMetadata::FrameSuccess)\n+\t\t.value(\"Error\", FrameMetadata::FrameError)\n+\t\t.value(\"Cancelled\", FrameMetadata::FrameCancelled);\n+\n+\tpyTransform\n+\t\t.def(py::init([](int rotation, bool hflip, bool vflip, bool transpose) {\n+\t\t\tbool ok;\n+\n+\t\t\tTransform t = transformFromRotation(rotation, &ok);\n+\t\t\tif (!ok)\n+\t\t\t\tthrow std::invalid_argument(\"Invalid rotation\");\n+\n+\t\t\tif (hflip)\n+\t\t\t\tt ^= Transform::HFlip;\n+\t\t\tif (vflip)\n+\t\t\t\tt ^= Transform::VFlip;\n+\t\t\tif (transpose)\n+\t\t\t\tt ^= Transform::Transpose;\n+\t\t\treturn t;\n+\t\t}), py::arg(\"rotation\") = 0, py::arg(\"hflip\") = false,\n+\t\t    py::arg(\"vflip\") = false, py::arg(\"transpose\") = false)\n+\t\t.def(py::init([](Transform &other) { return other; }))\n+\t\t.def(\"__str__\", [](Transform &self) {\n+\t\t\treturn \"<libcamera.Transform '\" + std::string(transformToString(self)) + \"'>\";\n+\t\t})\n+\t\t.def_property(\"hflip\",\n+\t\t\t      [](Transform &self) {\n+\t\t\t\t      return !!(self & Transform::HFlip);\n+\t\t\t      },\n+\t\t\t      [](Transform &self, bool hflip) {\n+\t\t\t\t      if (hflip)\n+\t\t\t\t\t      self |= Transform::HFlip;\n+\t\t\t\t      else\n+\t\t\t\t\t      self &= ~Transform::HFlip;\n+\t\t\t      })\n+\t\t.def_property(\"vflip\",\n+\t\t\t      [](Transform &self) {\n+\t\t\t\t      return !!(self & Transform::VFlip);\n+\t\t\t      },\n+\t\t\t      [](Transform &self, bool vflip) {\n+\t\t\t\t      if (vflip)\n+\t\t\t\t\t      self |= Transform::VFlip;\n+\t\t\t\t      else\n+\t\t\t\t\t      self &= ~Transform::VFlip;\n+\t\t\t      })\n+\t\t.def_property(\"transpose\",\n+\t\t\t      [](Transform &self) {\n+\t\t\t\t      return !!(self & Transform::Transpose);\n+\t\t\t      },\n+\t\t\t      [](Transform &self, bool transpose) {\n+\t\t\t\t      if (transpose)\n+\t\t\t\t\t      self |= Transform::Transpose;\n+\t\t\t\t      else\n+\t\t\t\t\t      self &= ~Transform::Transpose;\n+\t\t\t      })\n+\t\t.def(\"inverse\", [](Transform &self) { return -self; })\n+\t\t.def(\"invert\", [](Transform &self) {\n+\t\t\tself = -self;\n+\t\t})\n+\t\t.def(\"compose\", [](Transform &self, Transform &other) {\n+\t\t\tself = self * other;\n+\t\t});\n+\n+\tpyColorSpace\n+\t\t.def(py::init([](ColorSpace::Primaries primaries,\n+\t\t\t\t ColorSpace::TransferFunction transferFunction,\n+\t\t\t\t ColorSpace::YcbcrEncoding ycbcrEncoding,\n+\t\t\t\t ColorSpace::Range range) {\n+\t\t\treturn ColorSpace(primaries, transferFunction, ycbcrEncoding, range);\n+\t\t}), py::arg(\"primaries\"), py::arg(\"transferFunction\"),\n+\t\t    py::arg(\"ycbcrEncoding\"), py::arg(\"range\"))\n+\t\t.def(py::init([](ColorSpace &other) { return other; }))\n+\t\t.def(\"__str__\", [](ColorSpace &self) {\n+\t\t\treturn \"<libcamera.ColorSpace '\" + self.toString() + \"'>\";\n+\t\t})\n+\t\t.def_readwrite(\"primaries\", &ColorSpace::primaries)\n+\t\t.def_readwrite(\"transferFunction\", &ColorSpace::transferFunction)\n+\t\t.def_readwrite(\"ycbcrEncoding\", &ColorSpace::ycbcrEncoding)\n+\t\t.def_readwrite(\"range\", &ColorSpace::range)\n+\t\t.def_static(\"Raw\", []() { return ColorSpace::Raw; })\n+\t\t.def_static(\"Jpeg\", []() { return ColorSpace::Jpeg; })\n+\t\t.def_static(\"Srgb\", []() { return ColorSpace::Srgb; })\n+\t\t.def_static(\"Smpte170m\", []() { return ColorSpace::Smpte170m; })\n+\t\t.def_static(\"Rec709\", []() { return ColorSpace::Rec709; })\n+\t\t.def_static(\"Rec2020\", []() { return ColorSpace::Rec2020; });\n+\n+\tpyColorSpacePrimaries\n+\t\t.value(\"Raw\", ColorSpace::Primaries::Raw)\n+\t\t.value(\"Smpte170m\", ColorSpace::Primaries::Smpte170m)\n+\t\t.value(\"Rec709\", ColorSpace::Primaries::Rec709)\n+\t\t.value(\"Rec2020\", ColorSpace::Primaries::Rec2020);\n+\n+\tpyColorSpaceTransferFunction\n+\t\t.value(\"Linear\", ColorSpace::TransferFunction::Linear)\n+\t\t.value(\"Srgb\", ColorSpace::TransferFunction::Srgb)\n+\t\t.value(\"Rec709\", ColorSpace::TransferFunction::Rec709);\n+\n+\tpyColorSpaceYcbcrEncoding\n+\t\t.value(\"Null\", ColorSpace::YcbcrEncoding::None)\n+\t\t.value(\"Rec601\", ColorSpace::YcbcrEncoding::Rec601)\n+\t\t.value(\"Rec709\", ColorSpace::YcbcrEncoding::Rec709)\n+\t\t.value(\"Rec2020\", ColorSpace::YcbcrEncoding::Rec2020);\n+\n+\tpyColorSpaceRange\n+\t\t.value(\"Full\", ColorSpace::Range::Full)\n+\t\t.value(\"Limited\", ColorSpace::Range::Limited);\n+}\ndiff --git a/src/py/meson.build b/src/py/meson.build\nnew file mode 100644\nindex 00000000..4ce9668c\n--- /dev/null\n+++ b/src/py/meson.build\n@@ -0,0 +1 @@\n+subdir('libcamera')\ndiff --git a/subprojects/.gitignore b/subprojects/.gitignore\nindex 391fde2c..0e194289 100644\n--- a/subprojects/.gitignore\n+++ b/subprojects/.gitignore\n@@ -1,3 +1,4 @@\n /googletest-release*\n /libyuv\n-/packagecache\n\\ No newline at end of file\n+/packagecache\n+/pybind11\ndiff --git a/subprojects/packagefiles/pybind11/meson.build b/subprojects/packagefiles/pybind11/meson.build\nnew file mode 100644\nindex 00000000..1be47ca4\n--- /dev/null\n+++ b/subprojects/packagefiles/pybind11/meson.build\n@@ -0,0 +1,7 @@\n+project('pybind11', 'cpp',\n+    version : '2.9.1',\n+    license : 'BSD-3-Clause')\n+\n+pybind11_incdir = include_directories('include')\n+\n+pybind11_dep = declare_dependency(include_directories : pybind11_incdir)\ndiff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap\nnew file mode 100644\nindex 00000000..e8037a5d\n--- /dev/null\n+++ b/subprojects/pybind11.wrap\n@@ -0,0 +1,9 @@\n+[wrap-git]\n+url = https://github.com/pybind/pybind11.git\n+# This is the head of 'smart_holder' branch\n+revision = aebdf00cd060b871c5a1e0c2cf4a333503dd0431\n+depth = 1\n+patch_directory = pybind11\n+\n+[provide]\n+pybind11 = pybind11_dep\n","prefixes":["libcamera-devel","v9","4/7"]}