Skip to content

2. Images and labels

Read and write the pixel data.

An Image gives you the data of one resolution level: as numpy or dask, sliced by axis or by a region of interest in world coordinates. A Label is a segmentation stored the same way, and behaves the same way.

A multiscale pyramid Every level covers the same physical region of the sample. Each step doubles the pixel size, so the same area is stored with a quarter of the pixels. level 0 level 1 level 2 full resolution half in each axis a quarter in each axis 4320 × 5120 px 2160 × 2560 px 1080 × 1280 px 0.325 µm per pixel 0.65 µm per pixel 1.3 µm per pixel the physical region never changes — only how finely it is sampled

Images

To start working with the image data, instantiate an Image object. ngio provides a high-level API to access the image data at different resolution levels and pixel sizes.

Getting an image

By default, the get_image method returns the highest resolution image:

print(ome_zarr_container.get_image())  # Get the highest resolution image
Image(path=0, Dimensions(c: 3, z: 1, y: 4320, x: 5120))

To get a specific pyramid level, you can use the path parameter:

print(ome_zarr_container.get_image(path="1"))  # Get a specific pyramid level
Image(path=1, Dimensions(c: 3, z: 1, y: 2160, x: 2560))
This will return the image at the specified pyramid level.

If you want to get an image with a specific pixel size, you can use the pixel_size parameter:

from ngio import PixelSize

pixel_size = PixelSize(x=0.65, y=0.65, z=1.0)
image = ome_zarr_container.get_image(pixel_size=pixel_size)
print(image)
Image(path=2, Dimensions(c: 3, z: 1, y: 1080, x: 1280))

ngio returns the level whose pixel size is nearest to the one you ask for. That is the default, strict=False, spelled out here:

from ngio import PixelSize

pixel_size = PixelSize(x=0.60, y=0.60, z=1.0)
image = ome_zarr_container.get_image(pixel_size=pixel_size, strict=False)
print(image)
Image(path=2, Dimensions(c: 3, z: 1, y: 1080, x: 1280))
Pass strict=True instead to require an exact match, and raise NgioValueError when no level has that pixel size. The module-level open_image and open_label functions default the other way, to strict=True.

Similarly to the OME-Zarr container, the Image object provides a high-level API to access the image metadata.

print(image.dimensions)
Dimensions(c: 3, z: 1, y: 1080, x: 1280)
The dimensions attribute returns an object with the image dimensions for each axis.

print(image.pixel_size)
x=0.65 y=0.65 z=1.0 t=1.0 space_unit='micrometer' time_unit=None
The pixel_size attribute returns the pixel size for each axis.

print(image.shape, image.dtype, image.chunks, image.axes)
(3, 1, 1080, 1280) uint16 (1, 1, 1080, 1280) ('c', 'z', 'y', 'x')
shape, dtype and chunks come straight from the underlying Zarr array; axes gives the order those dimensions are stored in.

Working with image data

Once you have the Image object, you can access the image data as a:

data = image.get_as_numpy()  # Get the image as a numpy array
print(data.shape, data.dtype)
(3, 1, 1080, 1280) uint16
dask_array = image.get_as_dask()  # Get the image as a dask array
print(dask_array)
dask.array

get_array is the generic form of the two above: one entry point that picks the backend from a mode argument. Reach for it when the backend is decided at runtime; otherwise prefer the explicit get_as_numpy / get_as_dask.

# One entry point for both, selected with mode="numpy" or mode="dask"
data = image.get_array(mode="numpy")
print(data.shape, data.dtype)
(3, 1, 1080, 1280) uint16

The get_as_* methods can also slice the image data, and return the axes in an order you choose:

# Get a specific channel and axes order
image_slice = image.get_as_numpy(
    channel_selection="DAPI",
    x=slice(0, 128),
    axes_order=["t", "z", "y", "x", "c"],
)
print(image_slice.shape)
(1, 1, 1080, 128, 1)

To write pixel data back, use the set_array method:

image.set_array(data)

It accepts a numpy array or a dask array, and takes the same slicing and axes_order arguments as the getters, so you can write back exactly the region you read.

A minimal read-modify-write example:

import numpy as np


def process(patch: np.ndarray) -> np.ndarray:
    """Placeholder for your own processing step.

    Replace the body with the operation you want to apply to the patch.
    """
    return patch


# Get the image data as a numpy array
data = image.get_as_numpy(
    channel_selection="DAPI",
    x=slice(0, 128),
    y=slice(0, 128),
    axes_order=["z", "y", "x", "c"],
)

# Modify the image data
data = process(data)

# Set the modified image data
image.set_array(
    data,
    channel_selection="DAPI",
    x=slice(0, 128),
    y=slice(0, 128),
    axes_order=["z", "y", "x", "c"],
)

# Consolidate the changes to all resolution levels, see below for more details
image.consolidate()

Important

set_array writes to one resolution level only. Once you have finished editing, consolidate the changes so the rest of the pyramid is rebuilt from it:

image.consolidate()

World coordinates slicing

To read or write a specific region of the image defined in world coordinates, you can use the Roi object.

from ngio import Roi

# Define a ROI in world coordinates
roi = Roi.from_values(slices={"x": (34.1, 321.6), "y": (10, 330)}, name=None)
# Get the image data in the ROI as a numpy array
print(image.get_roi_as_numpy(roi).shape)
(3, 1, 509, 496)

The ROI is defined in micrometres, so it names the same region whatever pyramid level you read it from — on the left it is outlined on the whole image, on the right it is the region that came back:

2026-08-11T12:16:17.687026 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ Whole image The ROI

Labels

A label is a segmentation mask that identifies objects in the image. In ngio a Label behaves like an Image, and is accessed and manipulated the same way.

Getting a label

See which labels are available in the image:

print(ome_zarr_container.list_labels())  # Available labels
['nuclei', 'wf_2_labels', 'wf_3_labels', 'wf_4_labels']

Here is how to reach one of them:

By default, the get_label method returns the highest resolution label:

# Get the highest resolution label
print(ome_zarr_container.get_label("nuclei"))
Label(path=0, Dimensions(z: 1, y: 4320, x: 5120))

To get a specific pyramid level, you can use the path parameter:

# Get a specific pyramid level
print(ome_zarr_container.get_label("nuclei", path="1"))
Label(path=1, Dimensions(z: 1, y: 2160, x: 2560))
This will return the label at the specified pyramid level.

If you want to get a label with a specific pixel size, you can use the pixel_size parameter:

from ngio import PixelSize

pixel_size = PixelSize(x=0.65, y=0.65, z=1.0)
label_nuclei = ome_zarr_container.get_label("nuclei", pixel_size=pixel_size)
print(label_nuclei)
Label(path=2, Dimensions(z: 1, y: 1080, x: 1280))

As with images, the nearest level wins unless you ask for an exact match with strict=True:

from ngio import PixelSize

pixel_size = PixelSize(x=0.60, y=0.60, z=1.0)
label_nuclei = ome_zarr_container.get_label(
    "nuclei", pixel_size=pixel_size, strict=False
)
print(label_nuclei)
Label(path=2, Dimensions(z: 1, y: 1080, x: 1280))

Each object in a label carries its own id, drawn here in its own colour over the channel it was segmented from:

2026-08-11T12:16:18.004837 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ nuclei over DAPI

Working with label data

Reading and writing label data works exactly as it does for images: get_as_numpy, get_as_dask, get_roi_as_numpy and set_array are all available on a Label.

Deriving a label

Often, you might want to create a new label based on an existing image. You can do this using the derive_label method:

# Derive a new label
new_label = ome_zarr_container.derive_label("new_label", overwrite=True)
print(new_label)
Label(path=0, Dimensions(z: 1, y: 4320, x: 5120))

This will create a new label with the same dimensions as the original image (without channels) and compatible metadata. If you want to create a new label with slightly different metadata see the images API reference.

Next steps