[libcamera-devel,v3,27/30] py: examples: Add simple-capture.py
diff mbox series

Message ID 20220527144447.94891-28-tomi.valkeinen@ideasonboard.com
State Superseded
Headers show
Series
  • More misc Python patches
Related show

Commit Message

Tomi Valkeinen May 27, 2022, 2:44 p.m. UTC
Add an example to showcase the more-or-less minimal capture case.

Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
---
 src/py/examples/simple-capture.py | 159 ++++++++++++++++++++++++++++++
 1 file changed, 159 insertions(+)
 create mode 100755 src/py/examples/simple-capture.py

Comments

Laurent Pinchart May 30, 2022, 9:57 a.m. UTC | #1
Hi Tomi,

Thank you for the patch.

On Fri, May 27, 2022 at 05:44:44PM +0300, Tomi Valkeinen wrote:
> Add an example to showcase the more-or-less minimal capture case.
> 
> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
> ---
>  src/py/examples/simple-capture.py | 159 ++++++++++++++++++++++++++++++
>  1 file changed, 159 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..4b0c3f0c
> --- /dev/null
> +++ b/src/py/examples/simple-capture.py
> @@ -0,0 +1,159 @@
> +#!/usr/bin/env python3
> +
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
> +
> +# A simple minimal capture example showing:
> +# - How to setup the camera
> +# - Capture frames in a blocking manner
> +# - Memory map the frames
> +# - 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 binascii
> +import libcamera as libcam
> +import libcamera.utils
> +import sys
> +
> +
> +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
> +
> +    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'Capturing {num_bufs} frames 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(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
> +
> +    # Queue the requests to the camera
> +
> +    for req in reqs:
> +        ret = cam.queue_request(req)
> +        assert ret == 0
> +
> +    # Wait until the requests are finished
> +
> +    reqs = []
> +
> +    while True:
> +        # cm.read_event() blocks until there is an event
> +        cm.read_event()

This is the only example I'm not sure about, as I'd like to avoid
encouraging people to not use an event loop. I'm tempted to drop this
one and rename simple-continuous-capture.py to simple-capture.py.

> +
> +        # Get all the ready requests
> +        ready_reqs = cm.get_ready_requests()
> +
> +        reqs += ready_reqs
> +
> +        if len(reqs) >= num_bufs:
> +            break
> +
> +    # Process the captured frames
> +
> +    for i, req in enumerate(reqs):
> +        assert i == req.cookie
> +
> +        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()))
> +
> +        # Use MappedFrameBuffer to access the pixel data with CPU. We calculate
> +        # the crc for each plane.
> +
> +        with libcamera.utils.MappedFrameBuffer(fb) as mfb:
> +            crcs = [binascii.crc32(p) for p in mfb.planes]
> +
> +        meta = fb.metadata
> +
> +        print('seq {}, bytes {}, CRCs {}'
> +              .format(meta.sequence,
> +                      '/'.join([str(p.bytes_used) for p in meta.planes]),
> +                      crcs))
> +
> +    # 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())
Tomi Valkeinen May 30, 2022, 10:36 a.m. UTC | #2
On 30/05/2022 12:57, Laurent Pinchart wrote:
> Hi Tomi,
> 
> Thank you for the patch.
> 
> On Fri, May 27, 2022 at 05:44:44PM +0300, Tomi Valkeinen wrote:
>> Add an example to showcase the more-or-less minimal capture case.
>>
>> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
>> ---
>>   src/py/examples/simple-capture.py | 159 ++++++++++++++++++++++++++++++
>>   1 file changed, 159 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..4b0c3f0c
>> --- /dev/null
>> +++ b/src/py/examples/simple-capture.py
>> @@ -0,0 +1,159 @@
>> +#!/usr/bin/env python3
>> +
>> +# SPDX-License-Identifier: BSD-3-Clause
>> +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
>> +
>> +# A simple minimal capture example showing:
>> +# - How to setup the camera
>> +# - Capture frames in a blocking manner
>> +# - Memory map the frames
>> +# - 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 binascii
>> +import libcamera as libcam
>> +import libcamera.utils
>> +import sys
>> +
>> +
>> +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
>> +
>> +    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'Capturing {num_bufs} frames 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(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
>> +
>> +    # Queue the requests to the camera
>> +
>> +    for req in reqs:
>> +        ret = cam.queue_request(req)
>> +        assert ret == 0
>> +
>> +    # Wait until the requests are finished
>> +
>> +    reqs = []
>> +
>> +    while True:
>> +        # cm.read_event() blocks until there is an event
>> +        cm.read_event()
> 
> This is the only example I'm not sure about, as I'd like to avoid
> encouraging people to not use an event loop. I'm tempted to drop this
> one and rename simple-continuous-capture.py to simple-capture.py.

I can change this from a loop blocking in cm.read_event() to a loop 
blocking in select, as it's just a few lines more. But I don't think it 
materially changes the example.

I really like the simple-capture.py, as it's an example in a single 
function, so you just read it from top to bottom and it makes sense. In 
simple-continuous-capture.py you need to jump back and forth the 
functions a bit.

Although I could also add a few more lines of code to add reuse & 
requeuing of the requests and interrupt via keypress, and still keep it 
in a single function.

  Tomi
Tomi Valkeinen May 30, 2022, 10:45 a.m. UTC | #3
On 30/05/2022 13:36, Tomi Valkeinen wrote:
> On 30/05/2022 12:57, Laurent Pinchart wrote:
>> Hi Tomi,
>>
>> Thank you for the patch.
>>
>> On Fri, May 27, 2022 at 05:44:44PM +0300, Tomi Valkeinen wrote:
>>> Add an example to showcase the more-or-less minimal capture case.
>>>
>>> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
>>> ---
>>>   src/py/examples/simple-capture.py | 159 ++++++++++++++++++++++++++++++
>>>   1 file changed, 159 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..4b0c3f0c
>>> --- /dev/null
>>> +++ b/src/py/examples/simple-capture.py
>>> @@ -0,0 +1,159 @@
>>> +#!/usr/bin/env python3
>>> +
>>> +# SPDX-License-Identifier: BSD-3-Clause
>>> +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
>>> +
>>> +# A simple minimal capture example showing:
>>> +# - How to setup the camera
>>> +# - Capture frames in a blocking manner
>>> +# - Memory map the frames
>>> +# - 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 binascii
>>> +import libcamera as libcam
>>> +import libcamera.utils
>>> +import sys
>>> +
>>> +
>>> +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
>>> +
>>> +    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'Capturing {num_bufs} frames 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(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
>>> +
>>> +    # Queue the requests to the camera
>>> +
>>> +    for req in reqs:
>>> +        ret = cam.queue_request(req)
>>> +        assert ret == 0
>>> +
>>> +    # Wait until the requests are finished
>>> +
>>> +    reqs = []
>>> +
>>> +    while True:
>>> +        # cm.read_event() blocks until there is an event
>>> +        cm.read_event()
>>
>> This is the only example I'm not sure about, as I'd like to avoid
>> encouraging people to not use an event loop. I'm tempted to drop this
>> one and rename simple-continuous-capture.py to simple-capture.py.
> 
> I can change this from a loop blocking in cm.read_event() to a loop 
> blocking in select, as it's just a few lines more. But I don't think it 
> materially changes the example.
> 
> I really like the simple-capture.py, as it's an example in a single 
> function, so you just read it from top to bottom and it makes sense. In 
> simple-continuous-capture.py you need to jump back and forth the 
> functions a bit.
> 
> Although I could also add a few more lines of code to add reuse & 
> requeuing of the requests and interrupt via keypress, and still keep it 
> in a single function.

Actually, I don't know if that makes sense. If you want to write a short 
script that captures, say, 50 frames from a camera, why would you 
implement an event loop instead of just a blocking loop until you got 
your frames? I don't see anything wrong with that.

  Tomi

Patch
diff mbox series

diff --git a/src/py/examples/simple-capture.py b/src/py/examples/simple-capture.py
new file mode 100755
index 00000000..4b0c3f0c
--- /dev/null
+++ b/src/py/examples/simple-capture.py
@@ -0,0 +1,159 @@ 
+#!/usr/bin/env python3
+
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
+
+# A simple minimal capture example showing:
+# - How to setup the camera
+# - Capture frames in a blocking manner
+# - Memory map the frames
+# - 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 binascii
+import libcamera as libcam
+import libcamera.utils
+import sys
+
+
+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
+
+    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'Capturing {num_bufs} frames 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(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
+
+    # Queue the requests to the camera
+
+    for req in reqs:
+        ret = cam.queue_request(req)
+        assert ret == 0
+
+    # Wait until the requests are finished
+
+    reqs = []
+
+    while True:
+        # cm.read_event() blocks until there is an event
+        cm.read_event()
+
+        # Get all the ready requests
+        ready_reqs = cm.get_ready_requests()
+
+        reqs += ready_reqs
+
+        if len(reqs) >= num_bufs:
+            break
+
+    # Process the captured frames
+
+    for i, req in enumerate(reqs):
+        assert i == req.cookie
+
+        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()))
+
+        # Use MappedFrameBuffer to access the pixel data with CPU. We calculate
+        # the crc for each plane.
+
+        with libcamera.utils.MappedFrameBuffer(fb) as mfb:
+            crcs = [binascii.crc32(p) for p in mfb.planes]
+
+        meta = fb.metadata
+
+        print('seq {}, bytes {}, CRCs {}'
+              .format(meta.sequence,
+                      '/'.join([str(p.bytes_used) for p in meta.planes]),
+                      crcs))
+
+    # 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())