Skip to content

3. Tables

Keep ROIs, features and measurements alongside the image.

Tables are not part of the core OME-Zarr specification, but ngio uses them to store regions of interest (ROIs), per-object measurements and other tabular data next to the pixel data. The on-disk layout follows ngio's table specifications. It was originally defined as part of Fractal; ngio is now where the spec lives and is maintained.

Getting a table

List all available tables and load a specific one:

# List all available tables
print(ome_zarr_container.list_tables())
['FOV_ROI_table', 'nuclei_ROI_table', 'well_ROI_table', 'regionprops_DAPI', 'nuclei_measurements_wf3', 'nuclei_measurements_wf4', 'nuclei_lamin_measurements_wf4']

ngio recognises four typed tables — roi_table, masking_roi_table, feature_table and condition_table — plus the untyped generic_table, which is what anything it cannot classify is loaded as. The three you will meet most often are below; see the table specifications for the rest.

ROI tables can be used to store arbitrary regions of interest (ROIs) in the image. For example, load the FOV_ROI_table, which contains the microscope field of view (FOV) ROIs:

roi_table = ome_zarr_container.get_table("FOV_ROI_table")  # Get a ROI table
print(roi_table.get("FOV_1"))
name='FOV_1' slices=[x: 0.0->416.0, y: 0.0->351.0, z: 0.0->1.0] label=None space='world' y_micrometer_original=-1517.699951171875 x_micrometer_original=-1448.300048828125
2026-08-11T12:16:23.364491 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ FOV_1 ROI
get returns the single ROI with that name; rois() returns them all as a list. A ROI can then be used to slice the image data:
roi = roi_table.get("FOV_1")
roi_data = image.get_roi_as_numpy(roi)
print(roi_data.shape)
(3, 1, 540, 640)
This will return the image data for the specified ROI.
2026-08-11T12:16:23.585278 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ FOV_1 ROI

Masking ROIs are a special type of ROIs that can be used to store ROIs for masked objects in the image. The nuclei_ROI_table contains the masks for the nuclei label in the image, and is indexed by the label id.

# Get a mask table
masking_table = ome_zarr_container.get_table("nuclei_ROI_table")
print(masking_table.get_label(100))
name='100' slices=[x: 33.63750076293945->45.01250076293945, y: 18.850000381469727->33.47500038146973, z: 0.0->1.0] label=100 space='world'
ROIs can be used to slice the image data:
roi = masking_table.get_label(100)
roi_data = image.get_roi_as_numpy(roi)
print(roi_data.shape)
(3, 1, 23, 19)
This will return the image data for the specified ROI.
2026-08-11T12:16:23.818529 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ Label 100 ROI
See 4. Masked images and labels for more details on how to use the masking ROIs to load masked data.

Feature tables are used to store measurements and are indexed by the label id

# Get a feature table
feature_table = ome_zarr_container.get_table("regionprops_DAPI")
# only show the first 5 rows
print(table_html(feature_table.dataframe.head(5)))
label area bbox_area equivalent_diameter max_intensity mean_intensity min_intensity standard_deviation_intensity
1 2120.00 2655.00 15.94 476.00 278.64 86.00 54.34
2 327.00 456.00 8.55 604.00 324.16 118.00 90.85
3 1381.00 1749.00 13.82 386.00 212.68 60.00 50.17
4 2566.00 3588.00 16.99 497.00 251.73 61.00 53.31
5 4201.00 5472.00 20.02 466.00 223.86 51.00 56.72

Creating a table

Tables (unlike images and labels) can be purely in-memory objects, and don't need to be saved on disk.

from ngio import Roi
from ngio.tables import RoiTable

roi = Roi.from_values(slices={"x": (0, 128), "y": (0, 128)}, name="FOV_1")
roi_table = RoiTable(rois=[roi])
print(roi_table)
RoiTableV1(num_rois=1)
If you would like to create on-the-fly a ROI table for the whole image:
roi_table = ome_zarr_container.build_image_roi_table("whole_image")
print(roi_table)
RoiTableV1(num_rois=1)
The build_image_roi_table method will create a ROI table with a single ROI that covers the whole image. This table is not associated with the image and is purely in memory. To save it to disk, use the add_table method:
ome_zarr_container.add_table("new_roi_table", roi_table, overwrite=True)
roi_table = ome_zarr_container.get_table("new_roi_table")
print(roi_table)
RoiTableV1(num_rois=1)

As with the ROI table, you can create a masking ROI table on the fly, here for the nuclei label:

masking_table = ome_zarr_container.build_masking_roi_table("nuclei")
print(masking_table)
MaskingRoiTableV1(num_rois=3006, reference_label=nuclei)

Feature tables can be created from a pandas Dataframe:

import pandas as pd

from ngio.tables import FeatureTable

example_data = pd.DataFrame({"label": [1, 2, 3], "area": [100, 200, 300]})
feature_table = FeatureTable(table_data=example_data)
print(feature_table)
FeatureTableV1(num_rows=3, num_columns=1)

Sometimes you might want to create a table that doesn't fit into the ROI, Masking ROI, or Feature categories. In this case, you can use the GenericTable class, which allows you to store any tabular data. It can be created from a pandas Dataframe:

import pandas as pd

from ngio.tables import GenericTable

example_data = pd.DataFrame({"area": [100, 200, 300], "perimeter": [50, 60, 70]})
generic_table = GenericTable(table_data=example_data)
print(generic_table)
GenericTable
Or from an AnnData object:
import anndata as ad
import numpy as np
import pandas as pd

from ngio.tables import GenericTable

adata = ad.AnnData(
    X=np.random.rand(10, 5),
    obs=pd.DataFrame({"cell_type": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]}),
)
generic_table = GenericTable(table_data=adata)
print(generic_table)
GenericTable

Next steps