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:
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.
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:
|
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 ...
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)–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.
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.