Skip to content

Feature extraction

Measure per-label features and store them as a table.

Measure regionprops features from a segmented image with ngio and skimage, and write them back as a feature table in the OME-Zarr container. By the end the container holds a table with one row per label, ready to be read back or aggregated across a plate.

Step 1: write the measurement function

Start with the function that does the measuring — here a thin wrapper around skimage.measure.regionprops_table, taking one image patch and one label patch.

import numpy as np
import pandas as pd
from skimage import measure


def extract_features(image: np.ndarray, label: np.ndarray) -> pd.DataFrame:
    """Basic feature extraction using skimage.measure.regionprops_table."""
    label = label.squeeze(-1)  # Remove the channel axis if present
    roi_feat_table = measure.regionprops_table(
        label_image=label,
        intensity_image=image,
        properties=[
            "label",
            "area",
            "mean_intensity",
            "max_intensity",
            "min_intensity",
        ],
    )
    return pd.DataFrame(roi_feat_table)

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("CardiomyocyteTinyMip", 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: set up the inputs

from ngio.transforms import ZoomTransform

# Take the image to measure
image = ome_zarr.get_image()

# Get the nuclei label
nuclei = ome_zarr.get_label("nuclei")

# Here the image is stored at a higher resolution than the nuclei label
print(f"Image dimensions: {image.dimensions}, pixel size: {image.pixel_size}")
print(f"Nuclei dimensions: {nuclei.dimensions}, pixel size: {nuclei.pixel_size}")

# So resample the label up to the image resolution with a transform
zoom_transform = ZoomTransform(
    input_image=nuclei,
    target_image=image,
    order="nearest",  # Nearest-neighbour interpolation, so label ids stay intact
)
Image dimensions: Dimensions(c: 1, z: 1, y: 2160, x: 5120), pixel size: x=0.1625 y=0.1625 z=1.0 t=1.0 space_unit='micrometer' time_unit=None Nuclei dimensions: Dimensions(z: 1, y: 540, x: 1280), pixel size: x=0.65 y=0.65 z=1.0 t=1.0 space_unit='micrometer' time_unit=None

Step 4: use the FeatureExtractorIterator to create a feature table

from ngio.iterators import FeatureExtractorIterator
from ngio.tables import FeatureTable

iterator = FeatureExtractorIterator(
    input_image=image,
    input_label=nuclei,
    label_transforms=[zoom_transform],
    axes_order=["y", "x", "c"],
)

feat_table = []
for image_data, label_data, roi in iterator.iter_as_numpy():
    print(f"Processing ROI: {roi}")
    roi_feat_table = extract_features(image=image_data, label=label_data)
    feat_table.append(roi_feat_table)

# Concatenate the per-region frames into one table
feat_table = pd.concat(feat_table)
feat_table = FeatureTable(table_data=feat_table, reference_label="nuclei")
ome_zarr.add_table("nuclei_regionprops", feat_table, overwrite=True)
Processing ROI: name=None slices=[z: 0.0->1.0, y: 0.0->351.0, x: 0.0->832.0] label=None space='world'

Sanity check: read the table back

print(table_html(ome_zarr.get_table("nuclei_regionprops").dataframe.head()))
label area mean_intensity-0 max_intensity-0 min_intensity-0
1 1360.00 184.58 268.00 125.00
2 2464.00 273.25 461.00 132.00
3 1968.00 277.29 429.00 143.00
4 5120.00 279.04 413.00 118.00
5 288.00 243.32 341.00 147.00

Next steps