[libcamera-devel] [PATCH v5 4/4] py: examples: Add simple-cam.py
Tomi Valkeinen
tomi.valkeinen at ideasonboard.com
Wed Jun 8 09:24:18 CEST 2022
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 <tomi.valkeinen at ideasonboard.com>
Reviewed-by: Jacopo Mondi <jacopo at jmondi.org>
Reviewed-by: Laurent Pinchart <laurent.pinchart 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