8.18. Images and ndarrays

The Image class is the fast surface for camera-native pixel work: every method on it operates directly on the frame buffer in the camera’s native pixel format. numpy is the generic numerical surface for everything else. Two methods bridge them:

Together they let an application snap a frame, hand it to numpy for a custom transform, then put the result back into an image to display, save, or feed back into the rest of the image library.

8.18.1. Image to ndarray

to_ndarray() allocates a new ndarray and copies the image’s pixel data into it (with the dtype mapping below). It is never a view onto the image’s frame buffer – the numpy array always owns its own bytes. The signature is to_ndarray(dtype, *, buffer=None), and the output shape depends on the image format:

  • GRAYSCALE – 2-D array, shape (height, width).

  • RGB565 – 3-D array, shape (height, width, 3), planes in R/G/B order.

The dtype argument controls how each 8-bit pixel value v is mapped:

dtype

element

mapping for an 8-bit pixel value v

'B'

uint8

v (raw)

'b'

int8

v - 128 (re-centred around zero)

'f'

float32

float(v) (0.0 … 255.0)

Example – view a grayscale frame as a uint8 matrix:

import csi
from ulab import numpy as np

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.GRAYSCALE)
csi0.framesize(csi.QVGA)

img = csi0.snapshot()
a   = img.to_ndarray('B')        # shape (240, 320), dtype=uint8

print(a.shape, a.dtype)
print("mean brightness:", np.mean(a))

The buffer= keyword lets the application reuse a bytearray it already allocated, so the cam does not have to allocate a new one every frame:

buf = bytearray(320 * 240)
while True:
    img = csi0.snapshot()
    a   = img.to_ndarray('B', buffer=buf)
    # ... process a ...

8.18.2. ndarray to image

Going the other way, pass the ndarray as the first argument to image.Image. The constructor allocates a new image buffer and copies the array’s values into it, clamped and rounded to 0..255:

image.Image(arr, *, buffer=None, copy_to_fb=False)

The constructor infers the geometry and pixel format from the array’s shape:

  • shape (h, w)GRAYSCALE image.

  • shape (h, w, 3)RGB565 image.

The ndarray must have dtype float; the constructor only supports that case today. Values are rounded and clamped to the 0..255 range.

buffer= lets the application supply a bytearray it already allocated for the resulting image. copy_to_fb=True writes the result into the camera’s frame buffer, which is the right choice when the result should appear in the IDE preview.

8.18.3. Round trip

import csi
import image
from ulab import numpy as np

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.GRAYSCALE)
csi0.framesize(csi.QVGA)

img = csi0.snapshot()
a   = img.to_ndarray('f')                  # work in float space

a = 255.0 * (a / 255.0) ** 0.5             # gamma correction

out = image.Image(a, copy_to_fb=True)      # back to an image

8.18.4. When to bridge

This bridge is the right answer when the application needs a generic numerical operation the built-in image methods do not provide – custom filters, custom blends, unusual non-linearities – or when pixel data has to be combined with non-image data (IMU axes, audio samples) in a single computation.

It is not the right answer for high-throughput pixel processing the Image class already covers. The built-in methods operate directly on the frame buffer in the camera’s native pixel format and are much faster than the equivalent numpy expression. Reach for the bridge for the operations the image library does not already provide.