15.3.1.3. Streaming frames

A script that captures frames on the cam can stream each frame back to the host over USB. The pattern is two calls on the openmv.Camera instance: streaming() to turn the stream on or off, and read_frame() to pull the next frame out of the channel.

15.3.1.3.1. A minimal stream-and-display loop

The cam-side script is the usual snapshot loop; what is new is that the host opens streaming and reads the result back:

from openmv import Camera

script = """
import csi
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)
while True:
    csi0.snapshot()
"""

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(script)
    cam.streaming(True)

    while True:
        if frame := cam.read_frame():
            print(f"{frame['width']}x{frame['height']}, "
                  f"{frame['raw_size']} bytes")

The cam captures frames continuously; the host pulls each one out of the stream buffer as it lands. The cam overwrites the stream buffer on every new snapshot, so a host that polls slower than the cam captures will silently drop frames – that is the right behaviour for viewer-style use cases.

15.3.1.3.2. The frame dict

read_frame() returns either None (no frame is waiting) or a dict with five entries:

Key

Meaning

width

Frame width in pixels.

height

Frame height in pixels.

format

Pixel-format identifier the cam declared (an integer from the cam’s csi constants).

depth

For compressed formats (JPEG, PNG), the size of the compressed image in bytes. Unused for uncompressed formats.

data

The frame as a bytes buffer in RGB888. Each pixel is three bytes (R, G, B); total length is width * height * 3.

raw_size

Bytes the cam sent over USB before decode. Useful for the actual throughput calculation.

The package converts the cam’s native format (GRAYSCALE, RGB565, JPEG) to RGB888 before returning, so the host never has to deal with the bit-packed RGB565 or JPEG-decompression path itself. Grayscale frames come back with the luma value replicated into all three channels.

The data buffer is laid out row-by-row, top to bottom; feeding it straight to a display library or saving it as a raw RGB file works without any further shuffling.

15.3.1.3.3. Raw streaming mode

By default the cam JPEG-compresses each captured frame before placing it in the stream channel and read_frame() decompresses on the host. On cams without hardware JPEG support, the software compression is the slowest step in the loop. Passing raw=True skips it:

cam.streaming(True, raw=True, resolution=(320, 240))

The cam then sends the pixel buffer uncompressed. Uncompressed frames are much larger than their JPEG equivalents, so the cam scales each captured frame down to fit the stream channel before sending it; the resolution=(width, height) argument sets that target. The host still receives RGB888 in the data field – the package converts from whatever pixel format the cam reported in format.

15.3.1.3.4. Letting events drive the loop

A polling loop that calls read_frame() faster than the cam produces frames spends most of its time getting None back. When the host also has other work to do (a UI to update, other channels to poll), read_status() is the cheaper check: it returns a dict mapping every registered channel name to a boolean of “data is ready”:

while True:
    status = cam.read_status()

    if status.get('stream'):
        frame = cam.read_frame()
        # ... process the frame ...

    if status.get('stdout'):
        text = cam.read_stdout()
        print(text, end='')

    if status.get('my_channel'):
        data = cam.channel_read('my_channel')
        # ... process custom-channel data ...

This is the loop shape the CLI viewer itself uses.

15.3.1.3.5. Stopping the stream

Call streaming() with enable=False to stop. The cam keeps running its script but no longer fills the stream buffer; read_frame() just returns None from that point on. Calling stop() does the same thing implicitly by halting the script.