[libcamera-devel,v2,10/19] py: examples: Add simple-capture.py
diff mbox series

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

Commit Message

Tomi Valkeinen May 24, 2022, 11:46 a.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 | 153 ++++++++++++++++++++++++++++++
 1 file changed, 153 insertions(+)
 create mode 100755 src/py/examples/simple-capture.py

Comments

Laurent Pinchart May 26, 2022, 3:55 p.m. UTC | #1
Hi Tomi,

Thank you for the patch.

On Tue, May 24, 2022 at 02:46:01PM +0300, Tomi Valkeinen wrote:
> Add an example to showcase the more-or-less minimal capture case.

Niiiice :-) This is a really good start. I'd like more comments, but
that can be done on top.

> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
> ---
>  src/py/examples/simple-capture.py | 153 ++++++++++++++++++++++++++++++
>  1 file changed, 153 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..45df9e7a
> --- /dev/null
> +++ b/src/py/examples/simple-capture.py
> @@ -0,0 +1,153 @@
> +#!/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
> +
> +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
> +
> +    camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture])

I'm going to bikeshed a bit as this will be used as a starting point by
many people, sorry.

As the Python coding style uses snake_case, should this be cam_config ?
The next variable would be stream_config.

I'd also use the ViewFinder role to match the C++ simple-cam.

> +
> +    streamconfig = camconfig.at(0)
> +
> +    if args.format:
> +        fmt = libcam.PixelFormat(args.format)
> +        streamconfig.pixel_format = fmt
> +
> +    if args.size:
> +        w, h = [int(v) for v in args.size.split('x')]
> +        streamconfig.size = libcam.Size(w, h)
> +
> +    ret = cam.configure(camconfig)
> +    assert ret == 0
> +
> +    stream = streamconfig.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 {streamconfig}')

This can be done on top, but I'd like to capture more frames (100 for
instance), to show how to requeue requests.

> +
> +    # 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()

Integrating a simple event loop (maybe based on the Python asyncio
module ?) would be good here.

> +
> +        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]

Mapping buffers is expensive, this should be done before starting the
camera.

> +
> +        meta = fb.metadata
> +
> +        print('seq {}, bytes {}, CRCs {}'
> +              .format(meta.sequence, meta.bytesused, 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 27, 2022, 6:08 a.m. UTC | #2
On 26/05/2022 18:55, Laurent Pinchart wrote:
> Hi Tomi,
> 
> Thank you for the patch.
> 
> On Tue, May 24, 2022 at 02:46:01PM +0300, Tomi Valkeinen wrote:
>> Add an example to showcase the more-or-less minimal capture case.
> 
> Niiiice :-) This is a really good start. I'd like more comments, but
> that can be done on top.
> 
>> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
>> ---
>>   src/py/examples/simple-capture.py | 153 ++++++++++++++++++++++++++++++
>>   1 file changed, 153 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..45df9e7a
>> --- /dev/null
>> +++ b/src/py/examples/simple-capture.py
>> @@ -0,0 +1,153 @@
>> +#!/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
>> +
>> +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
>> +
>> +    camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture])
> 
> I'm going to bikeshed a bit as this will be used as a starting point by
> many people, sorry.
> 
> As the Python coding style uses snake_case, should this be cam_config ?
> The next variable would be stream_config.
> 
> I'd also use the ViewFinder role to match the C++ simple-cam.

Sounds good to me.

>> +
>> +    streamconfig = camconfig.at(0)
>> +
>> +    if args.format:
>> +        fmt = libcam.PixelFormat(args.format)
>> +        streamconfig.pixel_format = fmt
>> +
>> +    if args.size:
>> +        w, h = [int(v) for v in args.size.split('x')]
>> +        streamconfig.size = libcam.Size(w, h)
>> +
>> +    ret = cam.configure(camconfig)
>> +    assert ret == 0
>> +
>> +    stream = streamconfig.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 {streamconfig}')
> 
> This can be done on top, but I'd like to capture more frames (100 for
> instance), to show how to requeue requests.
> 
>> +
>> +    # 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()
> 
> Integrating a simple event loop (maybe based on the Python asyncio
> module ?) would be good here.

Let's discuss these in the thread for the following patch.

>> +
>> +        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]
> 
> Mapping buffers is expensive, this should be done before starting the
> camera.

Hmm... How do we invalidate the caches if we keep the buffers mapped?

  Tomi
Laurent Pinchart May 27, 2022, 6:14 a.m. UTC | #3
Hi Tomi,

On Fri, May 27, 2022 at 09:08:37AM +0300, Tomi Valkeinen wrote:
> On 26/05/2022 18:55, Laurent Pinchart wrote:
> > On Tue, May 24, 2022 at 02:46:01PM +0300, Tomi Valkeinen wrote:
> >> Add an example to showcase the more-or-less minimal capture case.
> > 
> > Niiiice :-) This is a really good start. I'd like more comments, but
> > that can be done on top.
> > 
> >> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
> >> ---
> >>   src/py/examples/simple-capture.py | 153 ++++++++++++++++++++++++++++++
> >>   1 file changed, 153 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..45df9e7a
> >> --- /dev/null
> >> +++ b/src/py/examples/simple-capture.py
> >> @@ -0,0 +1,153 @@
> >> +#!/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
> >> +
> >> +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
> >> +
> >> +    camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture])
> > 
> > I'm going to bikeshed a bit as this will be used as a starting point by
> > many people, sorry.
> > 
> > As the Python coding style uses snake_case, should this be cam_config ?
> > The next variable would be stream_config.
> > 
> > I'd also use the ViewFinder role to match the C++ simple-cam.
> 
> Sounds good to me.
> 
> >> +
> >> +    streamconfig = camconfig.at(0)
> >> +
> >> +    if args.format:
> >> +        fmt = libcam.PixelFormat(args.format)
> >> +        streamconfig.pixel_format = fmt
> >> +
> >> +    if args.size:
> >> +        w, h = [int(v) for v in args.size.split('x')]
> >> +        streamconfig.size = libcam.Size(w, h)
> >> +
> >> +    ret = cam.configure(camconfig)
> >> +    assert ret == 0
> >> +
> >> +    stream = streamconfig.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 {streamconfig}')
> > 
> > This can be done on top, but I'd like to capture more frames (100 for
> > instance), to show how to requeue requests.
> > 
> >> +
> >> +    # 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()
> > 
> > Integrating a simple event loop (maybe based on the Python asyncio
> > module ?) would be good here.
> 
> Let's discuss these in the thread for the following patch.
> 
> >> +
> >> +        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]
> > 
> > Mapping buffers is expensive, this should be done before starting the
> > camera.
> 
> Hmm... How do we invalidate the caches if we keep the buffers mapped?

QBUF/DQBUF are supposed to handle that.
Tomi Valkeinen May 27, 2022, 6:16 a.m. UTC | #4
On 27/05/2022 09:14, Laurent Pinchart wrote:

>>>> +        with libcamera.utils.MappedFrameBuffer(fb) as mfb:
>>>> +            crcs = [binascii.crc32(p) for p in mfb.planes]
>>>
>>> Mapping buffers is expensive, this should be done before starting the
>>> camera.
>>
>> Hmm... How do we invalidate the caches if we keep the buffers mapped?
> 
> QBUF/DQBUF are supposed to handle that.

Ah, ok. Isn't that horribly inefficient if you never actually access the 
buffer with CPU?

  Tomi
Laurent Pinchart May 27, 2022, 6:25 a.m. UTC | #5
On Fri, May 27, 2022 at 09:16:01AM +0300, Tomi Valkeinen wrote:
> On 27/05/2022 09:14, Laurent Pinchart wrote:
> 
> >>>> +        with libcamera.utils.MappedFrameBuffer(fb) as mfb:
> >>>> +            crcs = [binascii.crc32(p) for p in mfb.planes]
> >>>
> >>> Mapping buffers is expensive, this should be done before starting the
> >>> camera.
> >>
> >> Hmm... How do we invalidate the caches if we keep the buffers mapped?
> > 
> > QBUF/DQBUF are supposed to handle that.
> 
> Ah, ok. Isn't that horribly inefficient if you never actually access the 
> buffer with CPU?

There are hints that can be passed to the ioctls to have cache
operations when not touching the buffers with the CPU.

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..45df9e7a
--- /dev/null
+++ b/src/py/examples/simple-capture.py
@@ -0,0 +1,153 @@ 
+#!/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
+
+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
+
+    camconfig = cam.generate_configuration([libcam.StreamRole.StillCapture])
+
+    streamconfig = camconfig.at(0)
+
+    if args.format:
+        fmt = libcam.PixelFormat(args.format)
+        streamconfig.pixel_format = fmt
+
+    if args.size:
+        w, h = [int(v) for v in args.size.split('x')]
+        streamconfig.size = libcam.Size(w, h)
+
+    ret = cam.configure(camconfig)
+    assert ret == 0
+
+    stream = streamconfig.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 {streamconfig}')
+
+    # 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, meta.bytesused, 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())