6.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:
image.Image.to_ndarray()– copy the pixels of an image into anndarray.The
image.Imageconstructor – build a fresh image from anndarray.
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.
6.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:
| element | mapping for an 8-bit pixel value |
|---|---|---|
|
|
|
|
|
|
|
|
|
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 ...
6.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)–GRAYSCALEimage.shape
(h, w, 3)–RGB565image.
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.
6.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
6.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.