Skip to content

Image segmentation

Segment an image one field of view at a time.

Segment an OME-Zarr image with ngio and skimage, one field of view at a time, and write the result back as a label. The second half repeats the segmentation inside a mask, so it only runs where you want it to.

Step 1: set up

Start with a function that segments an image, using skimage to do the work.

# Setup a simple segmentation function
import numpy as np
import skimage


def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray:
    """Simple segmentation using Otsu thresholding."""
    threshold = skimage.filters.threshold_otsu(image)
    binary = image > threshold
    label_image = skimage.measure.label(binary)
    label_image += max_label
    label_image = np.where(binary, label_image, 0)
    return label_image.astype(np.uint32)

Step 2: open the OME-Zarr container

from pathlib import Path

from ngio import open_ome_zarr_container
from ngio.utils import download_ome_zarr_dataset

# Download the dataset
download_dir = Path("./data").absolute()
hcs_path = download_ome_zarr_dataset("CardiomyocyteTiny", download_dir=download_dir)
image_path = hcs_path / "B" / "03" / "0"

# Open the OME-Zarr container
ome_zarr = open_ome_zarr_container(image_path)

Step 3: segment the image

Rather than segmenting the image all at once, iterate over its FOVs and segment them one by one.

from ngio.iterators import SegmentationIterator

# Take the image to read from, and the FOV table naming the regions to walk
image = ome_zarr.get_image()
roi_table = ome_zarr.get_roi_table("FOV_ROI_table")

# Derive an empty label image to write the segmentation into
label = ome_zarr.derive_label("new_label", overwrite=True)

# Setup the segmentation iterator
seg_iterator = SegmentationIterator(
    input_image=image,
    output_label=label,
    channel_selection="DAPI",
    axes_order=["z", "y", "x"],
)
seg_iterator = seg_iterator.product(roi_table)

# Split any remaining time axis, so each step yields one whole ZYX volume
seg_iterator = seg_iterator.by_zyx()

max_label = 0  # Carried across regions so the label ids never collide
for image_data, label_writer in seg_iterator.iter_as_numpy():
    roi_segmentation = otsu_threshold_segmentation(
        image_data, max_label
    )  # Segment the image

    max_label = roi_segmentation.max()  # Get the max label for the next iteration

    label_writer(patch=roi_segmentation)  # Write the segmentation back to the label

# No need to consolidate, the iterator does it automatically after the last write

Plot the segmentation

rand_cmap = random_label_cmap()
original = image.get_as_numpy(c=0, z=1, axes_order=["y", "x"])

# The data does not fill its uint16 range, so window it on percentiles.
vmin, vmax = np.percentile(original, (1, 99.8))

fig, axs = plt.subplots(2, 1, figsize=(8, 6))
axs[0].set_title("Original image")
axs[0].imshow(original, cmap="gray", vmin=vmin, vmax=vmax)
axs[1].set_title("Final segmentation")
axs[1].imshow(
    label.get_as_numpy(z=1, axes_order=["y", "x"]),
    cmap=rand_cmap,
    interpolation="nearest",
)
for ax in axs:
    ax.axis("off")
fig.tight_layout()
print(figure_html(fig))
2026-08-11T12:16:05.938470 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ Original image Final segmentation

Step 4: masked image segmentation

Now use a mask to restrict the segmentation to certain areas of the image. Here you create the mask by hand for illustration, but in a real pipeline it would usually come from another segmentation.

# Create a basic mask for illustration purposes
mask = ome_zarr.derive_label("mask", overwrite=True)
mask_data = mask.get_as_numpy(axes_order=["z", "y", "x"])
mask_data[:, 200:-200, 500:2000] = 1
mask_data[:, 200:-200, 3000:-500] = 2
mask_data[:, 600:-600, 1200:-1000] = 0
mask_data[:, 700:-700, 1600:-1500] = 3
mask.set_array(mask_data, axes_order=["z", "y", "x"])
mask.consolidate()
fig, axs = plt.subplots(2, 1, figsize=(8, 6))
axs[0].set_title("Original image")
axs[0].imshow(original, cmap="gray", vmin=vmin, vmax=vmax)
axs[1].set_title("Mask")
axs[1].imshow(
    mask.get_as_numpy(z=1, axes_order=["y", "x"]),
    cmap=rand_cmap,
    interpolation="nearest",
)
for ax in axs:
    ax.axis("off")
fig.tight_layout()
print(figure_html(fig))
2026-08-11T12:16:08.502413 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ Original image Mask

Note that the next step rebinds image to the masked image, so the plot below shows the masked image rather than the original one.

from ngio.iterators import MaskedSegmentationIterator

# Take a masked image, which carries its masking ROI table with it
image = ome_zarr.get_masked_image(masking_label_name="mask")

# Derive an empty label image to write the segmentation into
label = ome_zarr.derive_label("masked_new_label", overwrite=True)

# Setup the masked segmentation iterator
seg_iterator = MaskedSegmentationIterator(
    input_image=image,
    output_label=label,
    channel_selection="DAPI",
    axes_order=["z", "y", "x"],
)

# Split any remaining time axis, so each step yields one whole ZYX volume
seg_iterator = seg_iterator.by_zyx()

max_label = 0  # Carried across regions so the label ids never collide
for image_data, label_writer in seg_iterator.iter_as_numpy():
    roi_segmentation = otsu_threshold_segmentation(
        image_data, max_label
    )  # Segment the image

    max_label = roi_segmentation.max()  # Get the max label for the next iteration

    label_writer(patch=roi_segmentation)  # Write the segmentation back to the label

# No need to consolidate, the iterator does it automatically after the last write
fig, axs = plt.subplots(2, 1, figsize=(8, 6))
axs[0].set_title("Original image")
axs[0].imshow(
    image.get_as_numpy(c=0, z=1, axes_order=["y", "x"]),
    cmap="gray",
    vmin=vmin,
    vmax=vmax,
)
axs[1].set_title("Final segmentation")
axs[1].imshow(
    label.get_as_numpy(z=1, axes_order=["y", "x"]),
    cmap=rand_cmap,
    interpolation="nearest",
)
for ax in axs:
    ax.axis("off")
fig.tight_layout()
print(figure_html(fig))
2026-08-11T12:16:11.647805 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ Original image Final segmentation

Next steps

Beyond the tutorials

The ngio workshop has hands-on marimo notebooks covering containers, images, labels and tables, and the processing iterators. Run them locally with uv, in the browser via molab, or read them as static pages.