[libcamera-devel] [PATCH v4 16/16] py: examples: Add simple-cam.py

Jacopo Mondi jacopo at jmondi.org
Sun Jun 5 14:46:20 CEST 2022


Hi Tomi

On Mon, May 30, 2022 at 05:27:22PM +0300, Tomi Valkeinen wrote:
> 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.
>

Looks very nice and it could indeed be added to the simple-cam
repository (or should we create an example directory in libcamera
sources ?)

Reviewed-by: Jacopo Mondi <jacopo at jmondi.org>

Thanks
  j

> Signed-off-by: Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
> ---
>  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 <tomi.valkeinen at ideasonboard.com>
> +
> +# 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())
> --
> 2.34.1
>


More information about the libcamera-devel mailing list