From patchwork Wed Jun 8 07:24:15 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16180 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 21E57C326B for ; Wed, 8 Jun 2022 07:24:44 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id A89B465643; Wed, 8 Jun 2022 09:24:43 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1654673083; bh=ZerpC6qWLVXQO4+Oe2e1AGXdEBr8pAzdJmXxp8KMZW0=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=YtRAcv79PV6g+mwd5UM4XrazT8+KAVMwqwXENflkw8aNaSjvnhtv4dgTXhPqqJMVu 2Kraw6rlWCTI7G4xbamSk3HgTfEo3f1cqtIAsUlUwXvvINYIpCF5++mlGWlT6ulrge 5R9/xXxs6zWMTBZ+bv9WnPuQ3QFQZyNs2MFuqSzT5PRGoy4focvCjWBn4GC158TGp5 5HRn/TlG6umNZOgsQNsAx/rJPVIhyZgl1O2HRpyhqoau/2y2RiJV5ebMRdv1WW5Ypy iRm8iHrslmKw7jFj4xPA1yxw2OuUqflzhPPzF6AQy7U9urtS22zLkiAm/B9ZuLk9Sl LOYkiWx6hvBpQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id D7EC965631 for ; Wed, 8 Jun 2022 09:24:40 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="iYX7o/hw"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id DBCAAA58; Wed, 8 Jun 2022 09:24:39 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1654673080; bh=ZerpC6qWLVXQO4+Oe2e1AGXdEBr8pAzdJmXxp8KMZW0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=iYX7o/hwubMfB+s5nfR9zRPXHP7jSwzEcDrWnUGnGWq4BKnTPCGBsXu3dHVAAHnNG w7ZwxLfXubH1FHlwUAR4JGdXHDbuiQ2eJRdFoj69hTE9tg5TZwEc03oWHOEIICKbFb EO0Oi2a8k+YDFHW70q5GMy6tz+ZXb55Hmj0dID7E= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Wed, 8 Jun 2022 10:24:15 +0300 Message-Id: <20220608072418.13154-2-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220608072418.13154-1-tomi.valkeinen@ideasonboard.com> References: <20220608072418.13154-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v5 1/4] Documentation: Add python-bindings.rst X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add a basic doc for the Python bindings. While not really proper documentation yet, the file and the examples should give enough guidance for users who are somewhat familiar with libcamera. Signed-off-by: Tomi Valkeinen Reviewed-by: Laurent Pinchart Reviewed-by: Jacopo Mondi --- Documentation/index.rst | 1 + Documentation/meson.build | 1 + Documentation/python-bindings.rst | 69 +++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 Documentation/python-bindings.rst diff --git a/Documentation/index.rst b/Documentation/index.rst index 0ee10044..43d8b017 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -22,3 +22,4 @@ Environment variables Sensor driver requirements Lens driver requirements + Python Bindings diff --git a/Documentation/meson.build b/Documentation/meson.build index 8e2eacc6..7695bcb1 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -67,6 +67,7 @@ if sphinx.found() 'guides/tracing.rst', 'index.rst', 'lens_driver_requirements.rst', + 'python-bindings.rst', 'sensor_driver_requirements.rst', '../README.rst', ] diff --git a/Documentation/python-bindings.rst b/Documentation/python-bindings.rst new file mode 100644 index 00000000..d59935ae --- /dev/null +++ b/Documentation/python-bindings.rst @@ -0,0 +1,69 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +.. _python-bindings: + +Python Bindings for libcamera +============================= + +*WARNING* The bindings are under work, and the API will change. + +Differences to the C++ API +-------------------------- + +As a rule of thumb the bindings try to follow the C++ API when possible. This +chapter lists the differences. + +Mostly these differences fall under two categories: + +1. Differences caused by the inherent differences between C++ and Python. +These differences are usually caused by the use of threads or differences in +C++ vs Python memory management. + +2. Differences caused by the code being work-in-progress. It's not always +trivial to create a binding in a satisfying way, and the current bindings +contain simplified versions of the C++ API just to get forward. These +differences are expected to eventually go away. + +Coding Style +------------ + +The C++ code for the bindings follows the libcamera coding style as much as +possible. Note that the indentation does not quite follow the clang-format +style, as clang-format makes a mess of the style used. + +The API visible to the Python side follows the Python style as much as possible. + +This means that e.g. ``Camera::generateConfiguration`` maps to +``Camera.generate_configuration``. + +CameraManager +------------- + +The Python API provides a singleton CameraManager via ``CameraManager.singleton()``. +There is no need to start or stop the CameraManager. + +Handling Completed Requests +--------------------------- + +The Python bindings do not expose the ``Camera::requestCompleted`` signal +directly as the signal is invoked from another thread and it has real-time +constraints. Instead the bindings queue the completed requests internally and +use an eventfd to inform the user that there are completed requests. + +The user can wait on the eventfd, and upon getting an event, use +``CameraManager.get_ready_requests()`` to clear the eventfd event and to get +the completed requests. + +Controls & Properties +--------------------- + +The classes related to controls and properties are rather complex to implement +directly in the Python bindings. There are some simplifications in the Python +bindings: + +- There is no ControlValue class. Python objects are automatically converted + to ControlValues and vice versa. +- There is no ControlList class. A Python dict with ControlId keys and Python + object values is used instead. +- There is no ControlInfoMap class. A Python dict with ControlId keys and + ControlInfo values is used instead. From patchwork Wed Jun 8 07:24:16 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16181 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id E3FAAC3273 for ; Wed, 8 Jun 2022 07:24:45 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id A5C5A6563B; Wed, 8 Jun 2022 09:24:44 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1654673084; bh=4RuUVtJkrvfqg0VFAifC8duITz905YHYLMuO/ncpeQM=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=WSSuQ5M6aAKa4SdocVbLvDJAiGJSC+X/0ZkXuMkmszsaong0W/i9zhqKPPT9d5wpn 4Eez0NBSfGz47rdz/lyunVtu9fOg9IK63sqodifpz/fE+fvoEhUB76GKVGkgoeklSF NDZ+3ykkKD3u/OMXDkc3WymSGxYZSqDONTgR3bWM5TMeJHcD0d27nfGp/X6FQfUyAW rAonh/Lr8i1fef1uBTbARFlEecxxPeLbMF4qMuQmlWkFwj8B3sIy7g4JcJhWv0lD7q UkdR+GdItxtzug6bmdiiB3HaEabIugVhNvpyy4KE3iffuvIcvmeCcl3Z4vETvG8iMu a0IBZ31dFLACQ== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 30B5865631 for ; Wed, 8 Jun 2022 09:24:41 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="Ih10KoYL"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 670F2B91; Wed, 8 Jun 2022 09:24:40 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1654673080; bh=4RuUVtJkrvfqg0VFAifC8duITz905YHYLMuO/ncpeQM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Ih10KoYL3FqSnIrT75RUP+zgeAns5Igezn6Grf5SWpgbzRKgHuFUcoNm7dhu2kAsF evkIRBMrhe5zajYn9up9sLTjmHMOuBkD3uJPtCl0kjwNESAPxZyY9qnKlbppXZobOK UDTiu7f/oMKnQGgAMVeBPPsKSoICIQ9EQgypOgXc= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Wed, 8 Jun 2022 10:24:16 +0300 Message-Id: <20220608072418.13154-3-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220608072418.13154-1-tomi.valkeinen@ideasonboard.com> References: <20220608072418.13154-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v5 2/4] py: examples: Add simple-capture.py X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add an example to showcase the more-or-less minimal capture case. Signed-off-by: Tomi Valkeinen Reviewed-by: Jacopo Mondi --- src/py/examples/simple-capture.py | 162 ++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100755 src/py/examples/simple-capture.py diff --git a/src/py/examples/simple-capture.py b/src/py/examples/simple-capture.py new file mode 100755 index 00000000..a6a9b33e --- /dev/null +++ b/src/py/examples/simple-capture.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (C) 2022, Tomi Valkeinen + +# A simple capture example showing: +# - How to setup the camera +# - Capture certain number of frames in a blocking manner +# - How to stop the camera +# +# This simple example is, in many ways, too simple. The purpose of the example +# is to introduce the concepts. A more realistic example is given in +# simple-continuous-capture.py. + +import argparse +import libcamera as libcam +import sys + +# Number of frames to capture +TOTAL_FRAMES = 30 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--camera', type=str, default='1', + help='Camera index number (starting from 1) or part of the name') + parser.add_argument('-f', '--format', type=str, help='Pixel format') + parser.add_argument('-s', '--size', type=str, help='Size ("WxH")') + args = parser.parse_args() + + cm = libcam.CameraManager.singleton() + + try: + if args.camera.isnumeric(): + cam_idx = int(args.camera) + cam = next((cam for i, cam in enumerate(cm.cameras) if i + 1 == cam_idx)) + else: + cam = next((cam for cam in cm.cameras if args.camera in cam.id)) + except Exception: + print(f'Failed to find camera "{args.camera}"') + return -1 + + # Acquire the camera for our use + + ret = cam.acquire() + assert ret == 0 + + # Configure the camera + + cam_config = cam.generate_configuration([libcam.StreamRole.Viewfinder]) + + stream_config = cam_config.at(0) + + if args.format: + fmt = libcam.PixelFormat(args.format) + stream_config.pixel_format = fmt + + if args.size: + w, h = [int(v) for v in args.size.split('x')] + stream_config.size = libcam.Size(w, h) + + ret = cam.configure(cam_config) + assert ret == 0 + + print(f'Capturing {TOTAL_FRAMES} frames with {stream_config}') + + stream = stream_config.stream + + # Allocate the buffers for capture + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + assert ret > 0 + + num_bufs = len(allocator.buffers(stream)) + + # Create the requests and assign a buffer for each request + + reqs = [] + for i in range(num_bufs): + # Use the buffer index as the cookie + req = cam.create_request(i) + + buffer = allocator.buffers(stream)[i] + ret = req.add_buffer(stream, buffer) + assert ret == 0 + + reqs.append(req) + + # Start the camera + + ret = cam.start() + assert ret == 0 + + # frames_queued and frames_done track the number of frames queued and done + + frames_queued = 0 + frames_done = 0 + + # Queue the requests to the camera + + for req in reqs: + ret = cam.queue_request(req) + assert ret == 0 + frames_queued += 1 + + # The main loop. Wait for the queued Requests to complete, process them, + # and re-queue them again. + + while frames_done < TOTAL_FRAMES: + # cm.get_ready_requests() blocks until there is an event and returns + # all the ready requests. Here we should almost always get a single + # Request, but in some cases there could be multiple or none. + + reqs = cm.get_ready_requests() + + for req in reqs: + frames_done += 1 + + buffers = req.buffers + + # A ready Request could contain multiple buffers if multiple streams + # were being used. Here we know we only have a single stream, + # and we use next(iter()) to get the first and only buffer. + + assert len(buffers) == 1 + + stream, fb = next(iter(buffers.items())) + + # Here we could process the received buffer. In this example we only + # print a few details below. + + meta = fb.metadata + + print("seq {:3}, bytes {}, frames queued/done {:3}/{:<3}" + .format(meta.sequence, + '/'.join([str(p.bytes_used) for p in meta.planes]), + frames_queued, frames_done)) + + # If we want to capture more frames we need to queue more Requests. + # We could create a totally new Request, but it is more efficient + # to reuse the existing one that we just received. + if frames_queued < TOTAL_FRAMES: + req.reuse() + cam.queue_request(req) + frames_queued += 1 + + # Stop the camera + + ret = cam.stop() + assert ret == 0 + + # Release the camera + + ret = cam.release() + assert ret == 0 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From patchwork Wed Jun 8 07:24:17 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16182 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id A3A5CC3274 for ; Wed, 8 Jun 2022 07:24:46 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id D5A3665636; Wed, 8 Jun 2022 09:24:45 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1654673085; bh=y4VOcrGGpifJBOVzcwjaTzFy0r0DJ1RO+SPe5KtVKCg=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=e8EijUSHklM4DUUKMOzJcI6dG+1FUTmQWp4WRlL/K0iJhCN0yX3wYEyPLEBPeSQ5g EesCUIXMonfw6Qk/jWTeegfYFnK2QnX964a8/EilzXPvv1SR9WqHMHhXsufB7sm5jJ NbW5GujnC4mTNhrMxe4jooslK/Vry7ZctXWEe2RdekBNx24hbUWvNeamLzSiLo7Iu/ v938OJWq3YUYoFVQEYDHWYQFdumRHORPgGLqPAxDhGBUA0LOZXisvTMxJGgJMied/e PMbpWfgdcM21ArLSeaUZf/5lpFMBo5oNkfb3npZaZ7z5+2G/yWWYPRgYPh6ijT4Cwr ++DAO3bSo0HFw== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 82CE565631 for ; Wed, 8 Jun 2022 09:24:41 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="qbe/PrpM"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id EB923D1C; Wed, 8 Jun 2022 09:24:40 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1654673081; bh=y4VOcrGGpifJBOVzcwjaTzFy0r0DJ1RO+SPe5KtVKCg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=qbe/PrpMm0DFLfJcIB6bJYU7rGCLK6BGGH3nQzK7Oh6FpB9TTkwFqHMAyN7hFXPhc Ol/HQFbrL2Lgl2+1BSccBcgOx6sZn04FxJW9TXD176o8zsayzb7Br1gE0LnRUGHehN z9SiHydjtYOjPXoHQs9Ik/Xe7eV8L/dOrEE+rr/Y= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Wed, 8 Jun 2022 10:24:17 +0300 Message-Id: <20220608072418.13154-4-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220608072418.13154-1-tomi.valkeinen@ideasonboard.com> References: <20220608072418.13154-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v5 3/4] py: examples: Add simple-continuous-capture.py X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add a slightly more complex, and I think a more realistic, example, where the script reacts to events and re-queues the buffers. Signed-off-by: Tomi Valkeinen Reviewed-by: Jacopo Mondi --- src/py/examples/simple-continuous-capture.py | 191 +++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100755 src/py/examples/simple-continuous-capture.py diff --git a/src/py/examples/simple-continuous-capture.py b/src/py/examples/simple-continuous-capture.py new file mode 100755 index 00000000..fe78a2dd --- /dev/null +++ b/src/py/examples/simple-continuous-capture.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (C) 2022, Tomi Valkeinen + +# A simple capture example extending the simple-capture.py example: +# - Capture frames using events from multiple cameras +# - Listening events from stdin to exit the application +# - Memory mapping the frames and calculating CRC + +import binascii +import libcamera as libcam +import libcamera.utils +import selectors +import sys + + +# A container class for our state per camera +class CameraCaptureContext: + idx: int + cam: libcam.Camera + reqs: list[libcam.Request] + mfbs: dict[libcam.FrameBuffer, libcamera.utils.MappedFrameBuffer] + + def __init__(self, cam, idx): + self.idx = idx + self.cam = cam + + # Acquire the camera for our use + + ret = cam.acquire() + assert ret == 0 + + # Configure the camera + + cam_config = cam.generate_configuration([libcam.StreamRole.Viewfinder]) + + stream_config = cam_config.at(0) + + ret = cam.configure(cam_config) + assert ret == 0 + + stream = stream_config.stream + + # Allocate the buffers for capture + + allocator = libcam.FrameBufferAllocator(cam) + ret = allocator.allocate(stream) + assert ret > 0 + + num_bufs = len(allocator.buffers(stream)) + + print(f'cam{idx} ({cam.id}): capturing {num_bufs} buffers with {stream_config}') + + # Create the requests and assign a buffer for each request + + self.reqs = [] + self.mfbs = {} + + for i in range(num_bufs): + # Use the buffer index as the "cookie" + req = cam.create_request(idx) + + buffer = allocator.buffers(stream)[i] + ret = req.add_buffer(stream, buffer) + assert ret == 0 + + self.reqs.append(req) + + # Save a mmapped buffer so we can calculate the CRC later + self.mfbs[buffer] = libcamera.utils.MappedFrameBuffer(buffer).mmap() + + def uninit_camera(self): + # Stop the camera + + ret = self.cam.stop() + assert ret == 0 + + # Release the camera + + ret = self.cam.release() + assert ret == 0 + + +# A container class for our state +class CaptureContext: + cm: libcam.CameraManager + camera_contexts: list[CameraCaptureContext] = [] + + def handle_camera_event(self): + # cm.get_ready_requests() will not block here, as we know there is an event + # to read. + + reqs = self.cm.get_ready_requests() + + # Process the captured frames + + for req in reqs: + self.handle_request(req) + + return True + + def handle_request(self, req: libcam.Request): + cam_ctx = self.camera_contexts[req.cookie] + + buffers = req.buffers + + assert len(buffers) == 1 + + # A ready Request could contain multiple buffers if multiple streams + # were being used. Here we know we only have a single stream, + # and we use next(iter()) to get the first and only buffer. + + stream, fb = next(iter(buffers.items())) + + # Use the MappedFrameBuffer to access the pixel data with CPU. We calculate + # the crc for each plane. + + mfb = cam_ctx.mfbs[fb] + crcs = [binascii.crc32(p) for p in mfb.planes] + + meta = fb.metadata + + print('cam{:<6} seq {:<6} bytes {:10} CRCs {}' + .format(cam_ctx.idx, + meta.sequence, + '/'.join([str(p.bytes_used) for p in meta.planes]), + crcs)) + + # We want to re-queue the buffer we just handled. Instead of creating + # a new Request, we re-use the old one. We need to call req.reuse() + # to re-initialize the Request before queuing. + + req.reuse() + cam_ctx.cam.queue_request(req) + + def handle_key_event(self): + sys.stdin.readline() + print('Exiting...') + return False + + def capture(self): + # Queue the requests to the camera + + for cam_ctx in self.camera_contexts: + for req in cam_ctx.reqs: + ret = cam_ctx.cam.queue_request(req) + assert ret == 0 + + # Use Selector to wait for events from the camera and from the keyboard + + sel = selectors.DefaultSelector() + sel.register(sys.stdin, selectors.EVENT_READ, self.handle_key_event) + sel.register(self.cm.event_fd, selectors.EVENT_READ, lambda: self.handle_camera_event()) + + running = True + + while running: + events = sel.select() + for key, mask in events: + # If the handler return False, we should exit + if not key.data(): + running = False + + +def main(): + cm = libcam.CameraManager.singleton() + + ctx = CaptureContext() + ctx.cm = cm + + for idx, cam in enumerate(cm.cameras): + cam_ctx = CameraCaptureContext(cam, idx) + ctx.camera_contexts.append(cam_ctx) + + # Start the cameras + + for cam_ctx in ctx.camera_contexts: + ret = cam_ctx.cam.start() + assert ret == 0 + + ctx.capture() + + for cam_ctx in ctx.camera_contexts: + cam_ctx.uninit_camera() + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From patchwork Wed Jun 8 07:24:18 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16183 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 3E625C3275 for ; Wed, 8 Jun 2022 07:24:47 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 6C41B6563F; Wed, 8 Jun 2022 09:24:46 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1654673086; bh=OTVLS1D/s3vExZSeqjWQ4mBcQHsXcOdWJNohXP3/oXA=; h=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe: List-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To: From; b=r/rxK2ZdQPI4wHTiopKELaJY2faNok2cYks0gTu2cH3YByrLUOS+SpSPPyl6Q2/EC /wB4FE9K9HhTP445NGQuT4zMHJXIBLCbaWzlq1xOh5qal57rihTkGJqSrxNLKKeuN+ jMPMrPky34dM8Y0pWzCvqjPnA8D5Wzu8Tvw+5vsXw8XLU2zQ0axFbsrhQWdhjwJG7v NJMS3uwrTNUu9dLYFYLDO5r0kW6YRc5HBUjpEbZeQFSbGzXx9jO7BAPRDcztALx4l8 5HY27CMh3TJTaPj0CwqBzh3GIGa6JAZzacWovzPM4I0UWoFm5+oyQzR4mbg8rtxg9q yYKSwaz2S1hVw== Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 15E7265638 for ; Wed, 8 Jun 2022 09:24:42 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="mPCcOWnz"; dkim-atps=neutral Received: from deskari.lan (91-156-85-209.elisa-laajakaista.fi [91.156.85.209]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 77A05D84; Wed, 8 Jun 2022 09:24:41 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1654673081; bh=OTVLS1D/s3vExZSeqjWQ4mBcQHsXcOdWJNohXP3/oXA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=mPCcOWnzrR+D434b6atFz5KJBfPoIu/IsvjEMsVLZEaN2T0+B42cEZsKzmQxHNCEF 2Am3TMqq7CXqxaf1A99qrQovaQgN9HlFM73FrTzol6LWunNabIykeufaWK+1cxwvuj wmSytxfJWCfWgh66v+LqzbLlHCCinXqY6SjqKplk= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Wed, 8 Jun 2022 10:24:18 +0300 Message-Id: <20220608072418.13154-5-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220608072418.13154-1-tomi.valkeinen@ideasonboard.com> References: <20220608072418.13154-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v5 4/4] py: examples: Add simple-cam.py X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Tomi Valkeinen via libcamera-devel From: Tomi Valkeinen Reply-To: Tomi Valkeinen Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add a Python version of simple-cam from: https://git.libcamera.org/libcamera/simple-cam.git Let's keep this in the libcamera repository until the Python API has stabilized a bit more, and then we could move this to the simple-cam repo. Signed-off-by: Tomi Valkeinen Reviewed-by: Jacopo Mondi Reviewed-by: Laurent Pinchart --- src/py/examples/simple-cam.py | 350 ++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100755 src/py/examples/simple-cam.py diff --git a/src/py/examples/simple-cam.py b/src/py/examples/simple-cam.py new file mode 100755 index 00000000..2b81bb65 --- /dev/null +++ b/src/py/examples/simple-cam.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (C) 2022, Tomi Valkeinen + +# A simple libcamera capture example +# +# This is a python version of simple-cam from: +# https://git.libcamera.org/libcamera/simple-cam.git +# +# \todo Move to simple-cam repository when the Python API has stabilized more + +import libcamera as libcam +import selectors +import sys +import time + +TIMEOUT_SEC = 3 + + +def handle_camera_event(cm): + # cm.get_ready_requests() will not block here, as we know there is an event + # to read. + + reqs = cm.get_ready_requests() + + # Process the captured frames + + for req in reqs: + process_request(req) + + +def process_request(request): + global camera + + print() + + print(f'Request completed: {request}') + + # When a request has completed, it is populated with a metadata control + # list that allows an application to determine various properties of + # the completed request. This can include the timestamp of the Sensor + # capture, or its gain and exposure values, or properties from the IPA + # such as the state of the 3A algorithms. + # + # To examine each request, print all the metadata for inspection. A custom + # application can parse each of these items and process them according to + # its needs. + + requestMetadata = request.metadata + for id, value in requestMetadata.items(): + print(f'\t{id.name} = {value}') + + # Each buffer has its own FrameMetadata to describe its state, or the + # usage of each buffer. While in our simple capture we only provide one + # buffer per request, a request can have a buffer for each stream that + # is established when configuring the camera. + # + # This allows a viewfinder and a still image to be processed at the + # same time, or to allow obtaining the RAW capture buffer from the + # sensor along with the image as processed by the ISP. + + buffers = request.buffers + for _, buffer in buffers.items(): + metadata = buffer.metadata + + # Print some information about the buffer which has completed. + print(f' seq: {metadata.sequence:06} timestamp: {metadata.timestamp} bytesused: ' + + '/'.join([str(p.bytes_used) for p in metadata.planes])) + + # Image data can be accessed here, but the FrameBuffer + # must be mapped by the application + + # Re-queue the Request to the camera. + request.reuse() + camera.queue_request(request) + + +# ---------------------------------------------------------------------------- +# Camera Naming. +# +# Applications are responsible for deciding how to name cameras, and present +# that information to the users. Every camera has a unique identifier, though +# this string is not designed to be friendly for a human reader. +# +# To support human consumable names, libcamera provides camera properties +# that allow an application to determine a naming scheme based on its needs. +# +# In this example, we focus on the location property, but also detail the +# model string for external cameras, as this is more likely to be visible +# information to the user of an externally connected device. +# +# The unique camera ID is appended for informative purposes. +# +def camera_name(camera): + props = camera.properties + location = props.get(libcam.properties.Location, None) + + if location == libcam.properties.LocationEnum.Front: + name = 'Internal front camera' + elif location == libcam.properties.LocationEnum.Back: + name = 'Internal back camera' + elif location == libcam.properties.LocationEnum.External: + name = 'External camera' + if libcam.properties.Model in props: + name += f' "{props[libcam.properties.Model]}"' + else: + name = 'Undefined location' + + name += f' ({camera.id})' + + return name + + +def main(): + global camera + + # -------------------------------------------------------------------- + # Get the Camera Manager. + # + # The Camera Manager is responsible for enumerating all the Camera + # in the system, by associating Pipeline Handlers with media entities + # registered in the system. + # + # The CameraManager provides a list of available Cameras that + # applications can operate on. + # + # There can only be a single CameraManager within any process space. + + cm = libcam.CameraManager.singleton() + + # Just as a test, generate names of the Cameras registered in the + # system, and list them. + + for camera in cm.cameras: + print(f' - {camera_name(camera)}') + + # -------------------------------------------------------------------- + # Camera + # + # Camera are entities created by pipeline handlers, inspecting the + # entities registered in the system and reported to applications + # by the CameraManager. + # + # In general terms, a Camera corresponds to a single image source + # available in the system, such as an image sensor. + # + # Application lock usage of Camera by 'acquiring' them. + # Once done with it, application shall similarly 'release' the Camera. + # + # As an example, use the first available camera in the system after + # making sure that at least one camera is available. + # + # Cameras can be obtained by their ID or their index, to demonstrate + # this, the following code gets the ID of the first camera; then gets + # the camera associated with that ID (which is of course the same as + # cm.cameras[0]). + + if not cm.cameras: + print('No cameras were identified on the system.') + return -1 + + camera_id = cm.cameras[0].id + camera = cm.get(camera_id) + camera.acquire() + + # -------------------------------------------------------------------- + # Stream + # + # Each Camera supports a variable number of Stream. A Stream is + # produced by processing data produced by an image source, usually + # by an ISP. + # + # +-------------------------------------------------------+ + # | Camera | + # | +-----------+ | + # | +--------+ | |------> [ Main output ] | + # | | Image | | | | + # | | |---->| ISP |------> [ Viewfinder ] | + # | | Source | | | | + # | +--------+ | |------> [ Still Capture ] | + # | +-----------+ | + # +-------------------------------------------------------+ + # + # The number and capabilities of the Stream in a Camera are + # a platform dependent property, and it's the pipeline handler + # implementation that has the responsibility of correctly + # report them. + + # -------------------------------------------------------------------- + # Camera Configuration. + # + # Camera configuration is tricky! It boils down to assign resources + # of the system (such as DMA engines, scalers, format converters) to + # the different image streams an application has requested. + # + # Depending on the system characteristics, some combinations of + # sizes, formats and stream usages might or might not be possible. + # + # A Camera produces a CameraConfigration based on a set of intended + # roles for each Stream the application requires. + + config = camera.generate_configuration([libcam.StreamRole.Viewfinder]) + + # The CameraConfiguration contains a StreamConfiguration instance + # for each StreamRole requested by the application, provided + # the Camera can support all of them. + # + # Each StreamConfiguration has default size and format, assigned + # by the Camera depending on the Role the application has requested. + + stream_config = config.at(0) + print(f'Default viewfinder configuration is: {stream_config}') + + # Each StreamConfiguration parameter which is part of a + # CameraConfiguration can be independently modified by the + # application. + # + # In order to validate the modified parameter, the CameraConfiguration + # should be validated -before- the CameraConfiguration gets applied + # to the Camera. + # + # The CameraConfiguration validation process adjusts each + # StreamConfiguration to a valid value. + + # Validating a CameraConfiguration -before- applying it will adjust it + # to a valid configuration which is as close as possible to the one + # requested. + + config.validate() + print(f'Validated viewfinder configuration is: {stream_config}') + + # Once we have a validated configuration, we can apply it to the + # Camera. + + camera.configure(config) + + # -------------------------------------------------------------------- + # Buffer Allocation + # + # Now that a camera has been configured, it knows all about its + # Streams sizes and formats. The captured images need to be stored in + # framebuffers which can either be provided by the application to the + # library, or allocated in the Camera and exposed to the application + # by libcamera. + # + # An application may decide to allocate framebuffers from elsewhere, + # for example in memory allocated by the display driver that will + # render the captured frames. The application will provide them to + # libcamera by constructing FrameBuffer instances to capture images + # directly into. + # + # Alternatively libcamera can help the application by exporting + # buffers allocated in the Camera using a FrameBufferAllocator + # instance and referencing a configured Camera to determine the + # appropriate buffer size and types to create. + + allocator = libcam.FrameBufferAllocator(camera) + + for cfg in config: + ret = allocator.allocate(cfg.stream) + if ret < 0: + print('Can\'t allocate buffers') + return -1 + + allocated = len(allocator.buffers(cfg.stream)) + print(f'Allocated {allocated} buffers for stream') + + # -------------------------------------------------------------------- + # Frame Capture + # + # libcamera frames capture model is based on the 'Request' concept. + # For each frame a Request has to be queued to the Camera. + # + # A Request refers to (at least one) Stream for which a Buffer that + # will be filled with image data shall be added to the Request. + # + # A Request is associated with a list of Controls, which are tunable + # parameters (similar to v4l2_controls) that have to be applied to + # the image. + # + # Once a request completes, all its buffers will contain image data + # that applications can access and for each of them a list of metadata + # properties that reports the capture parameters applied to the image. + + stream = stream_config.stream + buffers = allocator.buffers(stream) + requests = [] + for i in range(len(buffers)): + request = camera.create_request() + if not request: + print('Can\'t create request') + return -1 + + buffer = buffers[i] + ret = request.add_buffer(stream, buffer) + if ret < 0: + print('Can\'t set buffer for request') + return -1 + + # Controls can be added to a request on a per frame basis. + request.set_control(libcam.controls.Brightness, 0.5) + + requests.append(request) + + # -------------------------------------------------------------------- + # Start Capture + # + # In order to capture frames the Camera has to be started and + # Request queued to it. Enough Request to fill the Camera pipeline + # depth have to be queued before the Camera start delivering frames. + # + # When a Request has been completed, it will be added to a list in the + # CameraManager and an event will be raised using eventfd. + # + # The list of completed Requests can be retrieved with + # CameraManager.get_ready_requests(), which will also clear the list in the + # CameraManager. + # + # The eventfd can be retrieved from CameraManager.event_fd, and the fd can + # be waited upon using e.g. Python's selectors. + + camera.start() + for request in requests: + camera.queue_request(request) + + sel = selectors.DefaultSelector() + sel.register(cm.event_fd, selectors.EVENT_READ, lambda fd: handle_camera_event(cm)) + + start_time = time.time() + + while time.time() - start_time < TIMEOUT_SEC: + events = sel.select() + for key, mask in events: + key.data(key.fileobj) + + # -------------------------------------------------------------------- + # Clean Up + # + # Stop the Camera, release resources and stop the CameraManager. + # libcamera has now released all resources it owned. + + camera.stop() + camera.release() + + return 0 + + +if __name__ == '__main__': + sys.exit(main())