[libcamera-devel] [PATCH v4 14/16] py: examples: Add simple-continuous-capture.py
Tomi Valkeinen
tomi.valkeinen at ideasonboard.com
Mon Jun 6 10:56:11 CEST 2022
On 05/06/2022 15:31, Jacopo Mondi wrote:
> Hi Tomi,
>
> On Mon, May 30, 2022 at 05:27:20PM +0300, Tomi Valkeinen wrote:
>> Add a slightly more complex, and I think a more realistic, example,
>> where the script reacts to events and re-queues the buffers.
>>
>
> My first question is if such similar examples are useful or they will
> become a maintainership burden.
>
> From my side, the more examples the better, but I think the question
> if it's woth it stays..
We can always change and drop these later.
But my reasoning was summarized in the intro letter. I think this and
simple-capture.py are quite different. Maybe this could be renamed.
simple-capture-app.py?
>> Signed-off-by: Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
>> ---
>> src/py/examples/simple-continuous-capture.py | 189 +++++++++++++++++++
>> 1 file changed, 189 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..d0f8a7e9
>> --- /dev/null
>> +++ b/src/py/examples/simple-continuous-capture.py
>> @@ -0,0 +1,189 @@
>> +#!/usr/bin/env python3
>> +
>> +# SPDX-License-Identifier: BSD-3-Clause
>> +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen at ideasonboard.com>
>> +
>> +# 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):
>> + # 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
>> +
>> + reqs = []
>> + 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
>> +
>> + reqs.append(req)
>> +
>> + self.idx = idx
>> + self.cam = cam
>> + self.reqs = reqs
>> + self.mfbs = dict([(fb, libcamera.utils.MappedFrameBuffer(fb).mmap()) for fb in allocator.buffers(stream)])
>
> Not that relevant, but you could populate self.reqs directly and
> append a {buffer, libcamera.utils.MappedFrameBuffer(fb).mmap()} pair
> to the self.mfbs dictionary in the request creation loop ?
Yes, you're right. It makes the code easier to read.
>> +
>> + 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 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, 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 handle_key_event():
>> + sys.stdin.readline()
>> + print('Exiting...')
>> + return False
>
> Nit: why is this a global function handle_camera_event() a class
> member ?
Hmm, yes, I think I can move this inside CaptureContext too.
> To be honest I would get rid of the CaptureContext class and open-code
> CaptureContext::capture() in your main function, with all the handlers
> being global functions. Not a big deal though
I wanted to structure this example to look a bit more like a bigger
application, even if it's not really needed in this example.
>> +
>> +
>> +def main():
>> + cm = libcam.CameraManager.singleton()
>> +
>> + ctx = CaptureContext()
>> + ctx.cm = cm
>> +
>> + for idx, cam in enumerate(cm.cameras):
>> + cam_ctx = CameraCaptureContext(cam, idx)
>
> Can't you start the camera here ?
I could. My thinking was that you'd first collect the cameras and
configure them, and if everything is still good, then you go and start
them. Is there a reason you think it's better to start here?
>> + 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())
>
> Nits apart
> Reviewed-by: Jacopo Mondi <jacopo at jmondi.org>
Thanks!
Tomi
More information about the libcamera-devel
mailing list