HCS Plate Tutorial¶
This tutorial shows how to convert microscopy images into an OME-Zarr HCS (High-Content Screening) plate
using ome-zarr-converters-tools.
The input to the library is a pandas DataFrame describing your tiles (one row per image file on disk). You can build this DataFrame from any source -- CSV, database, custom parsing, etc. In this example, we load it from a CSV file for convenience.
Dataset: cardiomyocyte differentiation dataset with:
- 1 well (A/1) in a plate layout
- 3 fields of view (FOVs)
- 2 Z-slices per FOV
- 1 channel (DAPI)
Step 1: Prepare the tile DataFrame¶
The DataFrame must contain one row per tile (one image file on disk). Columns are split into three groups:
Tile position and size columns¶
| Column | Description |
|---|---|
file_path |
Path to the raw image file (relative to the resource directory, or absolute) |
fov_name |
Field-of-view identifier (tiles with the same fov_name are stitched together) |
start_x, start_y |
XY position of this tile (in the coordinate system specified by AcquisitionDetails) |
start_z |
Z position (slice index or physical position) |
start_c |
Channel index (0-based) |
start_t |
Time-point index |
length_x, length_y |
Tile dimensions in um or pixels |
length_z |
Number of Z slices in this tile (usually 1) or physical size in um |
length_c |
Number of channels in this tile (usually 1) |
length_t |
Number of time points in this tile (usually 1) or physical size in s |
HCS plate columns¶
| Column | Description |
|---|---|
row |
Well row (letter like A, B, ... or 1-based index) |
column |
Well column (1-based integer) |
Extra columns¶
Any additional columns (e.g. drug, concentration) are stored as tile attributes and used to build condition tables in the plate metadata.
1.1 Load the metadata¶
Let's load the example CSV and inspect the DataFrame.
import warnings
import pandas as pd
# Suppress warnings for cleaner documentation output.
# Do not use this in production code.
warnings.filterwarnings("ignore")
tiles_table = pd.read_csv("../examples/hcs_plate/tiles.csv")
tiles_table
| file_path | row | column | fov_name | start_x | start_y | start_z | start_c | start_t | length_x | length_y | length_z | length_c | length_t | drug | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20200812-CardiomyocyteDifferentiation14-Cycle1... | A | 1 | FOV_1 | 10.0 | 10.0 | 0.0 | 0 | 0 | 2560 | 2160 | 1 | 1 | 1 | DMSO |
| 1 | 20200812-CardiomyocyteDifferentiation14-Cycle1... | A | 1 | FOV_1 | 10.1 | 10.1 | 1.0 | 0 | 0 | 2560 | 2160 | 1 | 1 | 1 | DMSO |
| 2 | 20200812-CardiomyocyteDifferentiation14-Cycle1... | A | 1 | FOV_2 | 1000.0 | 1000.0 | 0.0 | 0 | 0 | 2560 | 2160 | 1 | 1 | 1 | DMSO |
| 3 | 20200812-CardiomyocyteDifferentiation14-Cycle1... | A | 1 | FOV_2 | 1000.0 | 1000.1 | 1.0 | 0 | 0 | 2560 | 2160 | 1 | 1 | 1 | DMSO |
| 4 | 20200812-CardiomyocyteDifferentiation14-Cycle1... | A | 1 | FOV_3 | 10.1 | 1000.1 | 0.0 | 0 | 0 | 2560 | 2160 | 1 | 1 | 1 | DMSO |
| 5 | 20200812-CardiomyocyteDifferentiation14-Cycle1... | A | 1 | FOV_3 | 10.1 | 1000.0 | 1.0 | 0 | 0 | 2560 | 2160 | 1 | 1 | 1 | DMSO |
Each row represents one tile with its position, size, well location, and metadata.
Next, define the acquisition details -- pixel sizes, channel info, and coordinate systems.
Coordinate systems: The start_*_coo parameters tell the library how to interpret position values.
Use "world" for physical units (micrometers) or "pixel" for pixel indices.
In this example, start_x and start_y are in micrometers (world coordinates),
while start_z and start_t are integer indices (pixel coordinates).
Lengths are always in pixels.
from ome_zarr_converters_tools import AcquisitionDetails, ChannelInfo
acq = AcquisitionDetails(
channels=[ChannelInfo(channel_label="DAPI", wavelength_id="405")],
pixelsize=0.65, # micrometers
z_spacing=5.0, # micrometers
t_spacing=1.0, # seconds
axes=["t", "c", "z", "y", "x"],
# Coordinate systems: start positions are in world coordinates,
# lengths are in pixel coordinates
start_x_coo="world",
start_y_coo="world",
start_z_coo="pixel",
start_t_coo="pixel",
)
acq
AcquisitionDetails(start_x_coo='world', start_y_coo='world', start_z_coo='pixel', start_t_coo='pixel', length_x_coo='pixel', length_y_coo='pixel', length_z_coo='pixel', length_t_coo='pixel', pixelsize=0.65, z_spacing=5.0, t_spacing=1.0, channels=[ChannelInfo(channel_label='DAPI', wavelength_id='405', colors=<DefaultColors.blue: 'Blue (0000FF)'>)], axes=['t', 'c', 'z', 'y', 'x'], data_type=None, condition_table_path=None, stage_corrections=StageOrientation(flip_x=False, flip_y=False, swap_xy=False))
Step 2: Parse tiles from the DataFrame¶
Use hcs_images_from_dataframe() to create Tile objects from the DataFrame.
The plate_name and acquisition_id parameters are set at the function level
(they are not DataFrame columns).
from ome_zarr_converters_tools.core import hcs_images_from_dataframe
tiles = hcs_images_from_dataframe(
tiles_table=tiles_table,
acquisition_details=acq,
plate_name="CardiomyocytePlate",
acquisition_id=0,
)
fov_names = {t.fov_name for t in tiles}
print(f"Number of tiles: {len(tiles)}")
print(f"FOV names: {fov_names}")
print(f"Collection type: {type(tiles[0].collection).__name__}")
tiles[0]
Number of tiles: 6
FOV names: {'FOV_2', 'FOV_1', 'FOV_3'}
Collection type: ImageInPlate
Tile(fov_name='FOV_1', start_x=10.0, start_y=10.0, start_z=0.0, start_c=0, start_t=0.0, length_x=2560.0, length_y=2160.0, length_z=1.0, length_c=1, length_t=1.0, attributes={'drug': ['DMSO']}, collection=ImageInPlate(plate_name='CardiomyocytePlate', row='A', column=1, acquisition=0), image_loader=DefaultImageLoader(file_path='20200812-CardiomyocyteDifferentiation14-Cycle1_B03_T0001F001L01A01Z01C01.png'), acquisition_details=AcquisitionDetails(start_x_coo='world', start_y_coo='world', start_z_coo='pixel', start_t_coo='pixel', length_x_coo='pixel', length_y_coo='pixel', length_z_coo='pixel', length_t_coo='pixel', pixelsize=0.65, z_spacing=5.0, t_spacing=1.0, channels=[ChannelInfo(channel_label='DAPI', wavelength_id='405', colors=<DefaultColors.blue: 'Blue (0000FF)'>)], axes=['t', 'c', 'z', 'y', 'x'], data_type=None, condition_table_path=None, stage_corrections=StageOrientation(flip_x=False, flip_y=False, swap_xy=False)))
Each Tile object bundles:
- Position and size (
start_x,start_y,start_z, ... andlength_x,length_y, ...) - Collection (
ImageInPlate) -- determines where this tile lands in the plate hierarchy - Image loader (
DefaultImageLoader) -- knows how to read the file from disk - Acquisition details -- shared pixel sizes, channels, and coordinate systems
- Attributes -- extra columns from the DataFrame (here:
drug: ["DMSO"]), used later for condition tables
Step 3: Aggregate tiles into TiledImages¶
The tiles_aggregation_pipeline() groups tiles that belong to the same image and creates TiledImage objects.
The resource parameter is the base directory for resolving relative file paths.
When the DataFrame contains relative paths like "image_001.png", the DefaultImageLoader
joins resource + file_path to find the actual file on disk.
If your paths are absolute, you can omit resource.
from ome_zarr_converters_tools import ConverterOptions, tiles_aggregation_pipeline
data_dir = "../examples/hcs_plate/data"
opts = ConverterOptions()
tiled_images = tiles_aggregation_pipeline(
tiles=tiles,
converter_options=opts,
resource=data_dir,
)
print(f"Number of TiledImages: {len(tiled_images)}")
for ti in tiled_images:
print(
f" Path: {ti.path}, regions: {len(ti.regions)}, FOVs: {len(ti.group_by_fov())}"
)
--------------------------------------------------------------------------- NotImplementedError Traceback (most recent call last) Cell In[4], line 6 2 3 data_dir = "../examples/hcs_plate/data" 4 opts = ConverterOptions() 5 ----> 6 tiled_images = tiles_aggregation_pipeline( 7 tiles=tiles, 8 converter_options=opts, 9 resource=data_dir, File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ome_zarr_converters_tools/pipelines/_tiles_aggregation_pipeline.py:45, in tiles_aggregation_pipeline(tiles, converter_options, filters, validators, resource) 43 if filters is not None: 44 tiles = apply_filter_pipeline(tiles, filters_config=filters) ---> 45 tiled_images = tiled_image_from_tiles( 46 tiles=tiles, 47 converter_options=converter_options, 48 resource=resource, 49 ) 50 if validators is not None: 51 tiled_images = apply_validator_pipeline( 52 tiled_images, validators_config=validators 53 ) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ome_zarr_converters_tools/core/_tile_to_tiled_images.py:32, in tiled_image_from_tiles(tiles, converter_options, resource) 30 if len(tiles) == 0: 31 raise ValueError("No tiles provided to build TiledImage.") ---> 32 data_type = tiles[0].find_data_type(resource=resource) 33 for tile in tiles: 34 if not split_tiles: File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ome_zarr_converters_tools/core/_tile.py:151, in Tile.find_data_type(self, resource) 149 if self.acquisition_details.data_type is not None: 150 return self.acquisition_details.data_type --> 151 return self.image_loader.find_data_type(resource) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ome_zarr_converters_tools/models/_loader.py:27, in ImageLoaderInterface.find_data_type(self, resource) 25 def find_data_type(self, resource: Any = None) -> str: 26 """Find the data type of the image data.""" ---> 27 return str(self.load_data(resource).dtype) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ome_zarr_converters_tools/models/_loader.py:57, in DefaultImageLoader.load_data(self, resource) 55 return self.load_tiff(path) 56 elif suffix in ["png", "jpg", "jpeg", "bmp"]: ---> 57 return self.load_png(path) 58 elif suffix == "npy": 59 return self.load_npy(path) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ome_zarr_converters_tools/models/_loader.py:73, in DefaultImageLoader.load_png(self, path) 72 def load_png(self, path: str) -> np.ndarray: ---> 73 fs = filesystem_for_url(path, error_msg_prefix="Loading image") 74 with fs.open(path, "rb") as f: 75 return np.array(Image.open(f)) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ome_zarr_converters_tools/models/_url_utils.py:53, in filesystem_for_url(url, error_msg_prefix) 51 url_type = find_url_type(url) 52 if url_type == UrlType.NOT_SUPPORTED: ---> 53 raise NotImplementedError( 54 f"{error_msg_prefix} for URL {url} " 55 f"with detected type {url_type} is not implemented yet." 56 ) 57 return fsspec.filesystem(url_type.value) NotImplementedError: Loading image for URL ../examples/hcs_plate/data/20200812-CardiomyocyteDifferentiation14-Cycle1_B03_T0001F001L01A01Z01C01.png with detected type UrlType.NOT_SUPPORTED is not implemented yet.
The Path value (e.g., CardiomyocytePlate.zarr/A/01/0) is the output path within the Zarr store -- plate name, well row/column, and acquisition index. All 6 tiles (3 FOVs x 2 Z-slices) were grouped into a single TiledImage because they belong to the same well and acquisition.
Step 4: Set up the plate structure¶
Before writing individual images, we need to create the HCS plate structure (plate metadata, wells, acquisitions)
in the output Zarr store. Use setup_ome_zarr_collection() for this.
import tempfile
from ome_zarr_converters_tools.models import OverwriteMode
from ome_zarr_converters_tools.pipelines._collection_setup import (
setup_ome_zarr_collection,
)
zarr_dir = tempfile.mkdtemp(prefix="tutorial_hcs_")
setup_ome_zarr_collection(
tiled_images=tiled_images,
collection_type="ImageInPlate",
zarr_dir=zarr_dir,
overwrite_mode=OverwriteMode.OVERWRITE,
)
print("Plate structure created.")
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[5], line 11 7 8 zarr_dir = tempfile.mkdtemp(prefix="tutorial_hcs_") 9 10 setup_ome_zarr_collection( ---> 11 tiled_images=tiled_images, 12 collection_type="ImageInPlate", 13 zarr_dir=zarr_dir, 14 overwrite_mode=OverwriteMode.OVERWRITE, NameError: name 'tiled_images' is not defined
Step 5: Build the registration pipeline and write OME-Zarr¶
The registration pipeline aligns tile positions (e.g., snapping to pixel grid, removing overlaps).
Then tiled_image_creation_pipeline() writes each TiledImage to the OME-Zarr dataset.
Note that zarr_url for each image is built from zarr_dir + tiled_image.path.
from ome_zarr_converters_tools.models import (
AlignmentCorrections,
TilingMode,
WriterMode,
)
from ome_zarr_converters_tools.pipelines import (
build_default_registration_pipeline,
tiled_image_creation_pipeline,
)
pipeline = build_default_registration_pipeline(
alignment_corrections=AlignmentCorrections(),
tiling_mode=TilingMode.AUTO,
)
for tiled_image in tiled_images:
zarr_url = f"{zarr_dir}/{tiled_image.path}"
omezarr = tiled_image_creation_pipeline(
zarr_url=zarr_url,
tiled_image=tiled_image,
registration_pipeline=pipeline,
converter_options=opts,
writer_mode=WriterMode.BY_FOV,
overwrite_mode=OverwriteMode.OVERWRITE,
resource=data_dir,
)
print(f"Written: {zarr_url}")
--------------------------------------------------------------------------- ImportError Traceback (most recent call last) Cell In[6], line 1 ----> 1 from ome_zarr_converters_tools.models import ( 2 AlignmentCorrections, 3 TilingMode, 4 WriterMode, ImportError: cannot import name 'AlignmentCorrections' from 'ome_zarr_converters_tools.models' (/opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ome_zarr_converters_tools/models/__init__.py)
from ngio import open_ome_zarr_plate
ome_zarr_plate = open_ome_zarr_plate(f"{zarr_dir}/CardiomyocytePlate.zarr")
print(f"Plate: {ome_zarr_plate}")
print(f"Images: {ome_zarr_plate.get_images()}")
ome_zarr_container = ome_zarr_plate.get_image(row="A", column=1, image_path="0")
image = ome_zarr_container.get_image()
print(f"Image shape: {image}")
--------------------------------------------------------------------------- FileNotFoundError Traceback (most recent call last) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ngio/utils/_zarr_utils.py:89, in open_group_wrapper(store, mode, zarr_format) 88 mode = mode if mode is not None else "a" ---> 89 group = zarr.open_group(store=store, mode=mode, zarr_format=zarr_format) 91 except FileExistsError as e: File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/api/synchronous.py:549, in open_group(store, mode, cache_attrs, synchronizer, path, chunk_store, storage_options, zarr_version, zarr_format, meta_array, attributes, use_consolidated) 493 """Open a group using file-mode-like semantics. 494 495 Parameters (...) 546 The new group. 547 """ 548 return Group( --> 549 sync( 550 async_api.open_group( 551 store=store, 552 mode=mode, 553 cache_attrs=cache_attrs, 554 synchronizer=synchronizer, 555 path=path, 556 chunk_store=chunk_store, 557 storage_options=storage_options, 558 zarr_version=zarr_version, 559 zarr_format=zarr_format, 560 meta_array=meta_array, 561 attributes=attributes, 562 use_consolidated=use_consolidated, 563 ) 564 ) 565 ) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/core/sync.py:159, in sync(coro, loop, timeout) 158 if isinstance(return_result, BaseException): --> 159 raise return_result 160 else: File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/core/sync.py:119, in _runner(coro) 118 try: --> 119 return await coro 120 except Exception as ex: File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/api/asynchronous.py:860, in open_group(store, mode, cache_attrs, synchronizer, path, chunk_store, storage_options, zarr_version, zarr_format, meta_array, attributes, use_consolidated) 858 warnings.warn("chunk_store is not yet implemented", ZarrRuntimeWarning, stacklevel=2) --> 860 store_path = await make_store_path(store, mode=mode, storage_options=storage_options, path=path) 861 if attributes is None: File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/storage/_common.py:459, in make_store_path(store_like, path, mode, storage_options) 458 else: --> 459 store = await make_store(store_like, mode=mode, storage_options=storage_options) 460 return await StorePath.open(store, path=path_normalized, mode=mode) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/storage/_common.py:388, in make_store(store_like, mode, storage_options) 386 else: 387 # Assume a filesystem path --> 388 return await make_store(Path(store_like), mode=mode, storage_options=storage_options) 390 elif _has_fsspec and isinstance(store_like, FSMap): File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/storage/_common.py:378, in make_store(store_like, mode, storage_options) 376 elif isinstance(store_like, Path): 377 # Create a new LocalStore --> 378 return await LocalStore.open(root=store_like, mode=mode, read_only=_read_only) 380 elif isinstance(store_like, str): 381 # Either an FSSpec URI or a local filesystem path File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/storage/_local.py:160, in LocalStore.open(cls, root, read_only, mode) 159 store = cls(root, read_only=read_only_creation) --> 160 await store._open() 162 # Set read_only state File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/zarr/storage/_local.py:172, in LocalStore._open(self, mode) 171 if not self.root.exists(): --> 172 raise FileNotFoundError(f"{self.root} does not exist") 173 return await super()._open() FileNotFoundError: /tmp/tutorial_hcs_r_xqcep0/CardiomyocytePlate.zarr does not exist The above exception was the direct cause of the following exception: NgioFileNotFoundError Traceback (most recent call last) Cell In[7], line 3 1 from ngio import open_ome_zarr_plate 2 ----> 3 ome_zarr_plate = open_ome_zarr_plate(f"{zarr_dir}/CardiomyocytePlate.zarr") 4 5 print(f"Plate: {ome_zarr_plate}") 6 print(f"Images: {ome_zarr_plate.get_images()}") File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ngio/hcs/_plate.py:1180, in open_ome_zarr_plate(store, cache, mode) 1167 def open_ome_zarr_plate( 1168 store: StoreOrGroup, 1169 cache: bool = False, 1170 mode: AccessModeLiteral = "r+", 1171 ) -> OmeZarrPlate: 1172 """Open an OME-Zarr plate. 1173 1174 Args: (...) 1178 access mode for the image. Defaults to "r+". 1179 """ -> 1180 group_handler = ZarrGroupHandler(store=store, cache=cache, mode=mode) 1181 return OmeZarrPlate(group_handler) File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ngio/utils/_zarr_utils.py:128, in ZarrGroupHandler.__init__(self, store, zarr_format, cache, mode) 125 if mode not in ["r", "r+", "w", "w-", "a", None]: 126 raise NgioValueError(f"Mode {mode} is not supported.") --> 128 group = open_group_wrapper(store=store, mode=mode, zarr_format=zarr_format) 129 self._group = group 130 self.use_cache = cache File /opt/hostedtoolcache/Python/3.13.12/x64/lib/python3.13/site-packages/ngio/utils/_zarr_utils.py:97, in open_group_wrapper(store, mode, zarr_format) 92 raise NgioFileExistsError( 93 f"A Zarr group already exists at {store}, consider setting overwrite=True." 94 ) from e 96 except FileNotFoundError as e: ---> 97 raise NgioFileNotFoundError(f"No Zarr group found at {store}") from e 99 except ContainsGroupError as e: 100 raise NgioFileExistsError( 101 f"A Zarr group already exists at {store}, consider setting overwrite=True." 102 ) from e NgioFileNotFoundError: No Zarr group found at /tmp/tutorial_hcs_r_xqcep0/CardiomyocytePlate.zarr
Cleanup¶
import shutil
shutil.rmtree(zarr_dir, ignore_errors=True)
print("Cleaned up tutorial output.")
Cleaned up tutorial output.