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("../tests/data/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_*_space 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")],
xy_pixel_size=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_space="world",
start_y_space="world",
start_z_space="pixel",
start_t_space="pixel",
)
acq
AcquisitionDetails(start_x_space='world', start_y_space='world', start_z_space='pixel', start_t_space='pixel', length_x_space='pixel', length_y_space='pixel', length_z_space='pixel', length_t_space='pixel', xy_pixel_size=0.65, z_spacing=5.0, t_spacing=1.0, channels=[ChannelInfo(channel_label='DAPI', wavelength_id='405', color='#0000FF')], axes=['t', 'c', 'z', 'y', 'x'], data_type=<DataTypeEnum.AUTODETECT: 'autodetect'>, condition_table_path=None, stage_orientation=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_3', 'FOV_1', 'FOV_2'}
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_space='world', start_y_space='world', start_z_space='pixel', start_t_space='pixel', length_x_space='pixel', length_y_space='pixel', length_z_space='pixel', length_t_space='pixel', xy_pixel_size=0.65, z_spacing=5.0, t_spacing=1.0, channels=[ChannelInfo(channel_label='DAPI', wavelength_id='405', color='#0000FF')], axes=['t', 'c', 'z', 'y', 'x'], data_type=<DataTypeEnum.AUTODETECT: 'autodetect'>, condition_table_path=None, stage_orientation=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 pathlib import Path
from ome_zarr_converters_tools import ConverterOptions, tiles_aggregation_pipeline
# The DefaultImageLoader resolves file paths against `resource`, which must be an
# absolute path (or an s3:// URL). Resolve the example data dir to an absolute path.
data_dir = str(Path("../tests/data/hcs_plate/data").resolve())
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())}"
)
Number of TiledImages: 1 Path: CardiomyocytePlate.zarr/A/01/0, regions: 6, FOVs: 3
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.")
Plate structure created.
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 (
AutoTiling,
StagePositionCorrections,
WriterMode,
)
from ome_zarr_converters_tools.pipelines import (
build_default_registration_pipeline,
tiled_image_creation_pipeline,
)
pipeline = build_default_registration_pipeline(
alignment_corrections=StagePositionCorrections(),
tiling_strategy=AutoTiling(),
)
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}")
Written: /tmp/tutorial_hcs_2oypws41/CardiomyocytePlate.zarr/A/01/0
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}")
Plate: Plate([rows x columns] (1 x 1))
Images: {'A/01/0': OmeZarrContainer(levels=5, #tables=3)}
Image shape: Image(path=0, Dimensions(t: 1, c: 1, z: 2, y: 4320, x: 5120))
Cleanup¶
import shutil
shutil.rmtree(zarr_dir, ignore_errors=True)
print("Cleaned up tutorial output.")
Cleaned up tutorial output.