Advanced: Programmatic Tile Construction¶
This tutorial shows how to build Tile objects programmatically instead of using a DataFrame.
This is useful when:
- Your image format requires a custom loader (e.g., proprietary microscope files)
- You need fine-grained control over tile construction
- You are integrating with a custom data source that doesn't map neatly to a table
The key idea: once you have a list of Tile objects, the rest of the pipeline
(aggregation, registration, writing) is identical to the DataFrame-based approach.
Dataset: We reuse images from the cardiomyocyte differentiation dataset.
Step 1: Implement a custom ImageLoader¶
Any custom loader must extend ImageLoaderInterface (a Pydantic model) and implement load_data().
The method receives an optional resource parameter (typically a base directory) and must return a NumPy array.
In [1]:
Copied!
import warnings
from typing import Any
import numpy as np
from PIL import Image
from ome_zarr_converters_tools.models._loader import ImageLoaderInterface
# Suppress warnings for cleaner documentation output.
# Do not use this in production code.
warnings.filterwarnings("ignore")
class PngLoader(ImageLoaderInterface):
"""Custom loader that loads a single PNG file."""
file_path: str
def load_data(self, resource: Any = None) -> np.ndarray:
"""Load the PNG file as a NumPy array."""
if resource is not None:
path = f"{resource}/{self.file_path}"
else:
path = self.file_path
return np.array(Image.open(path))
import warnings
from typing import Any
import numpy as np
from PIL import Image
from ome_zarr_converters_tools.models._loader import ImageLoaderInterface
# Suppress warnings for cleaner documentation output.
# Do not use this in production code.
warnings.filterwarnings("ignore")
class PngLoader(ImageLoaderInterface):
"""Custom loader that loads a single PNG file."""
file_path: str
def load_data(self, resource: Any = None) -> np.ndarray:
"""Load the PNG file as a NumPy array."""
if resource is not None:
path = f"{resource}/{self.file_path}"
else:
path = self.file_path
return np.array(Image.open(path))
Step 2: Build Tile objects manually¶
Each Tile needs:
- Position (
start_x,start_y,start_z,start_c,start_t) and size (length_x,length_y, ...) - A
collectiondefining the output structure (SingleImageorImageInPlate) - An
image_loaderthat knows how to load the raw data acquisition_detailswith pixel sizes and channel info
In [2]:
Copied!
from ome_zarr_converters_tools import (
AcquisitionDetails,
ChannelInfo,
SingleImage,
Tile,
)
acq = AcquisitionDetails(
channels=[ChannelInfo(channel_label="DAPI")],
pixelsize=0.65,
z_spacing=5.0,
t_spacing=1.0,
)
collection = SingleImage(image_path="manual_example")
# Build two tiles: FOV_1 with 2 Z-slices
tiles = [
Tile(
fov_name="FOV_1",
start_x=10.0,
start_y=10.0,
start_z=0.0,
length_x=2560,
length_y=2160,
length_z=1,
length_c=1,
length_t=1,
collection=collection,
image_loader=PngLoader(
file_path="20200812-CardiomyocyteDifferentiation14-Cycle1_B03_T0001F001L01A01Z01C01.png"
),
acquisition_details=acq,
),
Tile(
fov_name="FOV_1",
start_x=10.0,
start_y=10.0,
start_z=1.0,
length_x=2560,
length_y=2160,
length_z=1,
length_c=1,
length_t=1,
collection=collection,
image_loader=PngLoader(
file_path="20200812-CardiomyocyteDifferentiation14-Cycle1_B03_T0001F001L01A01Z02C01.png"
),
acquisition_details=acq,
),
]
print(f"Built {len(tiles)} tiles manually")
tiles[0]
from ome_zarr_converters_tools import (
AcquisitionDetails,
ChannelInfo,
SingleImage,
Tile,
)
acq = AcquisitionDetails(
channels=[ChannelInfo(channel_label="DAPI")],
pixelsize=0.65,
z_spacing=5.0,
t_spacing=1.0,
)
collection = SingleImage(image_path="manual_example")
# Build two tiles: FOV_1 with 2 Z-slices
tiles = [
Tile(
fov_name="FOV_1",
start_x=10.0,
start_y=10.0,
start_z=0.0,
length_x=2560,
length_y=2160,
length_z=1,
length_c=1,
length_t=1,
collection=collection,
image_loader=PngLoader(
file_path="20200812-CardiomyocyteDifferentiation14-Cycle1_B03_T0001F001L01A01Z01C01.png"
),
acquisition_details=acq,
),
Tile(
fov_name="FOV_1",
start_x=10.0,
start_y=10.0,
start_z=1.0,
length_x=2560,
length_y=2160,
length_z=1,
length_c=1,
length_t=1,
collection=collection,
image_loader=PngLoader(
file_path="20200812-CardiomyocyteDifferentiation14-Cycle1_B03_T0001F001L01A01Z02C01.png"
),
acquisition_details=acq,
),
]
print(f"Built {len(tiles)} tiles manually")
tiles[0]
Built 2 tiles manually
Out[2]:
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={}, collection=SingleImage(image_path='manual_example'), image_loader=PngLoader(file_path='20200812-CardiomyocyteDifferentiation14-Cycle1_B03_T0001F001L01A01Z01C01.png'), acquisition_details=AcquisitionDetails(start_x_coo='world', start_y_coo='world', start_z_coo='world', start_t_coo='world', 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=None, 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 3: Aggregate and write¶
From here, the pipeline is the same as the DataFrame-based approach:
aggregate into TiledImage objects, build a registration pipeline, and write to OME-Zarr.
In [3]:
Copied!
import tempfile
from ome_zarr_converters_tools import ConverterOptions, tiles_aggregation_pipeline
from ome_zarr_converters_tools.models import (
AlignmentCorrections,
OverwriteMode,
TilingMode,
WriterMode,
)
from ome_zarr_converters_tools.pipelines import (
build_default_registration_pipeline,
tiled_image_creation_pipeline,
)
data_dir = "../examples/hcs_plate/data"
opts = ConverterOptions()
# Aggregate
tiled_images = tiles_aggregation_pipeline(
tiles=tiles,
converter_options=opts,
resource=data_dir,
)
# Register and write
pipeline = build_default_registration_pipeline(
alignment_corrections=AlignmentCorrections(),
tiling_mode=TilingMode.AUTO,
)
zarr_dir = tempfile.mkdtemp(prefix="tutorial_advanced_")
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}")
import tempfile
from ome_zarr_converters_tools import ConverterOptions, tiles_aggregation_pipeline
from ome_zarr_converters_tools.models import (
AlignmentCorrections,
OverwriteMode,
TilingMode,
WriterMode,
)
from ome_zarr_converters_tools.pipelines import (
build_default_registration_pipeline,
tiled_image_creation_pipeline,
)
data_dir = "../examples/hcs_plate/data"
opts = ConverterOptions()
# Aggregate
tiled_images = tiles_aggregation_pipeline(
tiles=tiles,
converter_options=opts,
resource=data_dir,
)
# Register and write
pipeline = build_default_registration_pipeline(
alignment_corrections=AlignmentCorrections(),
tiling_mode=TilingMode.AUTO,
)
zarr_dir = tempfile.mkdtemp(prefix="tutorial_advanced_")
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[3], line 4 1 import tempfile 2 3 from ome_zarr_converters_tools import ConverterOptions, tiles_aggregation_pipeline ----> 4 from ome_zarr_converters_tools.models import ( 5 AlignmentCorrections, 6 OverwriteMode, 7 TilingMode, 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)
Step 4: Verify the result¶
In [4]:
Copied!
img = omezarr.get_image()
data = img.get_array()
print(f"Shape: {data.shape}")
print(f"Channels: {img.channel_labels}")
img = omezarr.get_image()
data = img.get_array()
print(f"Shape: {data.shape}")
print(f"Channels: {img.channel_labels}")
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[4], line 1 ----> 1 img = omezarr.get_image() 2 data = img.get_array() 3 4 print(f"Shape: {data.shape}") NameError: name 'omezarr' is not defined
Cleanup¶
In [5]:
Copied!
import shutil
shutil.rmtree(zarr_dir, ignore_errors=True)
print("Cleaned up tutorial output.")
import shutil
shutil.rmtree(zarr_dir, ignore_errors=True)
print("Cleaned up tutorial output.")
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[5], line 3 1 import shutil 2 ----> 3 shutil.rmtree(zarr_dir, ignore_errors=True) 4 print("Cleaned up tutorial output.") NameError: name 'zarr_dir' is not defined