From patchwork Fri May 27 14:44:47 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tomi Valkeinen X-Patchwork-Id: 16101 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 BD4B2C3273 for ; Fri, 27 May 2022 14:45:43 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id B02FE656AF; Fri, 27 May 2022 16:45:42 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org; s=mail; t=1653662742; bh=6g/hoz2maPEpfUMnMG2cGK6We+Z1IdFOdMGdL+Pg1yc=; 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=tTalZfSDgeb1DYwwGuynfNF4/AzrQthAj/iNZBdDm/xb1blNQlH6mY0ps8K+rRynq qMyPpfh8+BC96MmW8ImsNC7GFoLoHO032dzLJMvhV4FTi1AWJHpJqk2v7xqxpg8R8p l6zdFkGsqLCD0Wp0FZOWzAcZEB3xQhFPaheRc37NaPLp+kEMHiymmEiRF+mrv2yzTS 8IAJO7EBErefDv3GARXnle9QJ0gQzu95NPWPZKn1mthsXqBAwa5+mW8sZdlRy3TMbz 8dk3GSHP75UCDZlvT3sZp2IEYiy+GgHhvoeSv7ujO9u5wQT7e/HGd76vjvayUb2Ez/ dBdhG6YctnlZQ== 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 1049965653 for ; Fri, 27 May 2022 16:45:20 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="pQXWvaJO"; 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 744C14E8E; Fri, 27 May 2022 16:45:19 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1653662719; bh=6g/hoz2maPEpfUMnMG2cGK6We+Z1IdFOdMGdL+Pg1yc=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=pQXWvaJOclBIVru0uf25umT/0mM5WBMbo3KMan4C9tusns6wxV/aBJD1Shy57TR2+ u8TYPbFs8fwjgetycLZwMHvfw/rQFwVWRQXwAWqARNkv9ug5MIwcmiW2gVOpZYlYuh TLZsi7pIit61ooCWJZyQff+qDxjfjfG5mW2WVi/U= To: libcamera-devel@lists.libcamera.org, David Plowman , Kieran Bingham , Laurent Pinchart , Jacopo Mondi Date: Fri, 27 May 2022 17:44:47 +0300 Message-Id: <20220527144447.94891-31-tomi.valkeinen@ideasonboard.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220527144447.94891-1-tomi.valkeinen@ideasonboard.com> References: <20220527144447.94891-1-tomi.valkeinen@ideasonboard.com> MIME-Version: 1.0 Subject: [libcamera-devel] [PATCH v3 30/30] 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 --- src/py/examples/simple-cam.py | 352 ++++++++++++++++++++++++++++++++++ 1 file changed, 352 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..ce17175b --- /dev/null +++ b/src/py/examples/simple-cam.py @@ -0,0 +1,352 @@ +#!/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.read_event() will not block here, as we know there is an event to read. + # We have to read the event to clear it. + + cm.read_event() + + 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())