Creating empty images and metadata

Creating empty, OME-compliant image arrays and metadata

OMIO provides utility functions called create_empty_image, create_empty_metadata, and update_metadata_from_image to create empty, OME-compliant image arrays and metadata dictionaries based on user-defined specifications.

import omio as om
import numpy as np
import os

my_image, my_metadata = om.create_empty_image(return_metadata=True)
print(f"Created empty image with shape: {my_image.shape}, "
      f"dtype {my_image.dtype} and axes {my_metadata.get('axes', 'N/A')}."
>>>
Created empty image with shape: (1, 1, 1, 1, 1), dtype uint16 and axes TZCYX.

Without providing any arguments, create_empty_image creates a default empty image with shape (1, 1, 1, 512, 512) and dtype uint16. The axes are OME-compliant TZCYX. With the optional argument return_metadata=True (default is False), the associated metadata dictionary is also returned.

You can customize the created empty image by providing the desired shape and dtype as arguments:

my_image, my_metadata = om.create_empty_image(
    shape            = (5, 10, 512, 512),
    dtype            = np.uint16,
    return_metadata  = True)

print(f"Failed to create empty image. Type of my_image is {type(my_image)} "
      f"and of my_metadata {type(my_metadata)}.")
>>>
Failed to create empty image. Type of my_image is <class 'NoneType'> and of my_metadata <class 'NoneType'>.

The attempt above fails because the provided shape has only 4 dimensions instead of the required 5 dimensions for OME-compliant images. In such cases, OMIO will raise a warning and return None for both image and metadata.

my_image, my_metadata = om.create_empty_image(
    shape            = (5, 20, 2, 512, 512),
    dtype            = np.uint16,
    return_metadata  = True)

print(f"Created empty image with shape: {my_image.shape}, "
      f"dtype {my_image.dtype} and axes {my_metadata.get('axes', 'N/A')}.")
>>>
Created empty image with shape: (5, 20, 2, 512, 512), dtype uint16 and axes TZCYX.

You can now manipulate the created empty image as needed:

# for each timepoint, z-slice and channel, we fill the slice with an increasing integer value:
for t in range(my_image.shape[0]):
    for z in range(my_image.shape[1]):
        for c in range(my_image.shape[2]):
            my_image[t, z, c, :, :] = t * 100 + z * 10 + c

pathname_save = "example_data/custom_created_images/"
os.makedirs(pathname_save, exist_ok=True)

om.imwrite(
    os.path.join(pathname_save, "my_empty_image_filled.ome.tif"),
    my_image, my_metadata)

read_my_image, read_my_metadata = om.imread(
    os.path.join(pathname_save, "my_empty_image_filled.ome.tif"))

om.open_in_napari(read_my_image, read_my_metadata)
Custom created image opened in napari

When changing the image shape, for example by cropping or padding the stack,

my_cropped_image = my_image[:, 2:8, :, 100:400, 100:400]  # crop Z and spatial dimensions
print(f"Cropped image shape: {my_cropped_image.shape}")
>>>
Cropped image shape: (5, 6, 2, 300, 300)

you need to update the associated metadata dictionary accordingly. You can do so by manually updating the relevant metadata entries, or by using OMIO’s utility function update_metadata_from_image:

my_cropped_metadata = om.update_metadata_from_image(
    my_metadata,
    my_cropped_image)

print(f"Updated cropped image metadata axes: "
      f"{my_cropped_metadata.get('axes', 'N/A')} "
      f"with shape: {my_cropped_image.shape}.")

om.imwrite(
    os.path.join(pathname_save, "my_cropped_image.ome.tif"),
    my_cropped_image,
    my_cropped_metadata)

read_my_cropped_image, read_my_cropped_metadata = om.imread(
    os.path.join(pathname_save, "my_cropped_image.ome.tif"))

om.open_in_napari(
    read_my_cropped_image,
    read_my_cropped_metadata)
>>>
Updated cropped image metadata axes: TZCYX with shape: (5, 6, 2, 300, 300).
Custom created and cropped image opened in napari

Creating empty images as on-disk Zarr arrays

For larger synthetic or preallocated datasets, create_empty_image can create the empty image directly as an on-disk Zarr array. In this mode, OMIO stores the array inside .omio_cache below the provided zarr_store_path and records the cache paths in the returned metadata.

zarr_parent = "example_data/custom_created_images/"

my_zarr_image, my_zarr_metadata = om.create_empty_image(
    shape            = (5, 20, 2, 512, 512),
    dtype            = np.uint16,
    zarr_store       = "disk",
    zarr_store_path  = zarr_parent,
    zarr_store_name  = "my_empty_image_zarr",
    return_metadata  = True)

print(f"Created disk-backed Zarr image with shape: {my_zarr_image.shape}")
print(f"Zarr store path: {my_zarr_metadata['omio_zarr_store_path']}")
print(f"OMIO cache folder: {my_zarr_metadata['omio_cache_folder']}")

The returned object behaves like a writable Zarr array, so you can fill it plane by plane without keeping a second full copy in RAM:

for t in range(my_zarr_image.shape[0]):
    for z in range(my_zarr_image.shape[1]):
        for c in range(my_zarr_image.shape[2]):
            my_zarr_image[t, z, c, :, :] = t * 100 + z * 10 + c

When you no longer need the generated store, you can clean up the cache folder using the path recorded in the metadata:

om.cleanup_omio_cache(my_zarr_metadata["omio_cache_folder"], full_cleanup=True)