Skip to content

Commit 50dadc7

Browse files
authored
Merge pull request #36 from fractal-analytics-platform/z_singleton
Add a Add Z singleton dimension task
2 parents f506717 + c11ede0 commit 50dadc7

File tree

7 files changed

+239
-2
lines changed

7 files changed

+239
-2
lines changed

src/fractal_helper_tasks/__FRACTAL_MANIFEST__.json

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"tags": [
1010
"Singleton time dimension"
1111
],
12-
"docs_info": "### Purpose\n- Removes a **singleton time (T) dimension** from an OME-Zarr image. \n- Creates a new OME-Zarr image with updated metadata and dimensions.\n- Optionally overwrites the input image if `overwrite_input` is set to True.\n\n### Outputs\n- A **new Zarr image** without the singleton T-dimension, stored with a configurable suffix. \n\n### Limitations\n- Only processes OME-Zarr images where the **T-axis is the first axis**. \n- Assumes the T-dimension is **singleton**; does not process non-singleton time axes. \n- Does not copy associated **label images** or **ROI tables** to the new Zarr structure. ",
12+
"docs_info": "### Purpose\n- Removes a **singleton time (T) dimension** from an OME-Zarr image. \n- Creates a new OME-Zarr image with updated metadata and dimensions.\n- Optionally overwrites the input image if `overwrite_input` is set to True.\n\n### Outputs\n- A **new Zarr image** without the singleton T-dimension, stored with a configurable suffix. \n\n### Limitations\n- Only processes OME-Zarr images where the **T-axis is the first axis**. \n- Assumes the T-dimension is **singleton**; does not process non-singleton time axes. \n- Does not copy associated **label images** to the new Zarr structure. ",
1313
"type": "parallel",
1414
"executable_parallel": "drop_t_dimension.py",
1515
"meta_parallel": {
@@ -205,6 +205,50 @@
205205
"title": "RechunkZarr"
206206
},
207207
"docs_link": "https://github.com/jluethi/fractal-helper-tasks"
208+
},
209+
{
210+
"name": "Add Z Singleton Dimension",
211+
"input_types": {
212+
"is_3D": false
213+
},
214+
"tags": [
215+
"Singleton Z dimension"
216+
],
217+
"docs_info": "### Purpose\n- Removes a **singleton time (T) dimension** from an OME-Zarr image. \n- Creates a new OME-Zarr image with updated metadata and dimensions.\n- Optionally overwrites the input image if `overwrite_input` is set to True.\n\n### Outputs\n- A **new Zarr image** without the singleton T-dimension, stored with a configurable suffix. \n\n### Limitations\n- Only processes OME-Zarr images where the **T-axis is the first axis**. \n- Assumes the T-dimension is **singleton**; does not process non-singleton time axes. \n- Does not copy associated **label images** to the new Zarr structure. ",
218+
"type": "parallel",
219+
"executable_parallel": "add_z_singleton.py",
220+
"meta_parallel": {
221+
"cpus_per_task": 1,
222+
"mem": 4000
223+
},
224+
"args_schema_parallel": {
225+
"additionalProperties": false,
226+
"properties": {
227+
"zarr_url": {
228+
"title": "Zarr Url",
229+
"type": "string",
230+
"description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
231+
},
232+
"suffix": {
233+
"default": "z_singleton",
234+
"title": "Suffix",
235+
"type": "string",
236+
"description": "Suffix to be used for the new Zarr image. If overwrite_input is True, this file is only temporary."
237+
},
238+
"overwrite_input": {
239+
"default": true,
240+
"title": "Overwrite Input",
241+
"type": "boolean",
242+
"description": "Whether the existing iamge should be overwritten with the new OME-Zarr with the Z singleton dimension."
243+
}
244+
},
245+
"required": [
246+
"zarr_url"
247+
],
248+
"type": "object",
249+
"title": "AddZSingleton"
250+
},
251+
"docs_link": "https://github.com/jluethi/fractal-helper-tasks"
208252
}
209253
],
210254
"has_args_schemas": true,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Copyright 2024 (C) BioVisionCenter, University of Zurich
2+
#
3+
# Original authors:
4+
# Joel Lüthi <[email protected]>
5+
#
6+
# This file is derived from a Fractal task core task developed by
7+
# Tommaso Comparin & Marco Franzon
8+
"""Task to remove singleton T dimension from an OME-Zarr."""
9+
10+
import logging
11+
import os
12+
import shutil
13+
from typing import Any
14+
15+
import dask.array as da
16+
import ngio
17+
from pydantic import validate_call
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
@validate_call
23+
def add_z_singleton(
24+
*,
25+
zarr_url: str,
26+
suffix: str = "z_singleton",
27+
overwrite_input: bool = True,
28+
) -> dict[str, Any]:
29+
"""Add a singleton Z dimension to a 2D OME-Zarr.
30+
31+
Args:
32+
zarr_url: Path or url to the individual OME-Zarr image to be processed.
33+
(standard argument for Fractal tasks, managed by Fractal server).
34+
suffix: Suffix to be used for the new Zarr image. If overwrite_input
35+
is True, this file is only temporary.
36+
overwrite_input: Whether the existing iamge should be overwritten with
37+
the new OME-Zarr with the Z singleton dimension.
38+
"""
39+
# Normalize zarr_url
40+
zarr_url_old = zarr_url.rstrip("/")
41+
zarr_url_new = f"{zarr_url_old}_{suffix}"
42+
43+
logger.info(f"{zarr_url_old=}")
44+
logger.info(f"{zarr_url_new=}")
45+
46+
old_ome_zarr = ngio.open_ome_zarr_container(zarr_url_old)
47+
old_ome_zarr_img = old_ome_zarr.get_image()
48+
if old_ome_zarr_img.has_axis("z"):
49+
raise ValueError(
50+
f"The Zarr image {zarr_url_old} already contains a Z axis. "
51+
"Thus, the add Z singleton dimension task can't be applied to it."
52+
)
53+
image = old_ome_zarr_img.get_array(mode="dask")
54+
axes_names = old_ome_zarr_img.meta.axes_mapper.on_disk_axes_names
55+
ndim = image.ndim
56+
insert_index = ndim - 2
57+
if insert_index < 0:
58+
raise ValueError(
59+
f"Cannot insert a Z axis at position {insert_index} in an array"
60+
f" with {ndim} dimensions."
61+
)
62+
# Insert singleton Z dimension
63+
image_with_z = da.expand_dims(image, axis=insert_index)
64+
logger.info(f"Original shape: {image.shape}, new shape: {image_with_z.shape}")
65+
axes_names_with_z = axes_names[:insert_index] + ["z"] + axes_names[insert_index:]
66+
67+
pixel_size = old_ome_zarr_img.pixel_size
68+
new_pixel_size = ngio.PixelSize(x=pixel_size.x, y=pixel_size.y, z=1.0)
69+
70+
chunk_sizes = old_ome_zarr_img.chunks
71+
new_chunk_sizes = chunk_sizes[:insert_index] + (1,) + chunk_sizes[insert_index:]
72+
73+
new_ome_zarr_container = old_ome_zarr.derive_image(
74+
store=zarr_url_new,
75+
shape=image_with_z.shape,
76+
chunks=new_chunk_sizes,
77+
dtype=old_ome_zarr_img.dtype,
78+
pixel_size=new_pixel_size,
79+
axes_names=axes_names_with_z,
80+
copy_tables=True,
81+
)
82+
new_image_container = new_ome_zarr_container.get_image()
83+
new_image_container.set_array(image_with_z)
84+
new_image_container.consolidate()
85+
86+
# TODO: Also handle copying over & adding Z dimension to label images?
87+
88+
if overwrite_input:
89+
image_list_update = dict(zarr_url=zarr_url_old, types=dict(has_t=False))
90+
os.rename(zarr_url_old, f"{zarr_url_old}_tmp")
91+
os.rename(zarr_url_new, zarr_url_old)
92+
shutil.rmtree(f"{zarr_url}_tmp")
93+
else:
94+
image_list_update = dict(
95+
zarr_url=zarr_url_new, origin=zarr_url_old, types=dict(has_t=False)
96+
)
97+
98+
return {"image_list_updates": [image_list_update]}
99+
100+
101+
if __name__ == "__main__":
102+
from fractal_task_tools.task_wrapper import run_fractal_task
103+
104+
run_fractal_task(
105+
task_function=add_z_singleton,
106+
logger_name=logger.name,
107+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
### Purpose
2+
- Creates a **singleton Z dimension** in a 2D OME-Zarr image. Useful when 2D images don't have a singleton Z dimension but downstream tasks require it.
3+
- Optionally overwrites the input image if `overwrite_input` is set to True.
4+
5+
### Outputs
6+
- A **new Zarr image** with the singleton Z dimension
7+
8+
### Limitations
9+
- Only processes 2D OME-Zarr images without a **Z-axis**.
10+
- Does not copy associated **label images** to the new Zarr structure.

src/fractal_helper_tasks/dev/docs_info/drop_t_dimension.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
### Limitations
1010
- Only processes OME-Zarr images where the **T-axis is the first axis**.
1111
- Assumes the T-dimension is **singleton**; does not process non-singleton time axes.
12-
- Does not copy associated **label images** or **ROI tables** to the new Zarr structure.
12+
- Does not copy associated **label images** to the new Zarr structure.

src/fractal_helper_tasks/dev/task_list.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,12 @@
3535
],
3636
docs_info="file:docs_info/rechunk_zarr.md",
3737
),
38+
ParallelTask(
39+
name="Add Z Singleton Dimension",
40+
executable="add_z_singleton.py",
41+
meta={"cpus_per_task": 1, "mem": 4000},
42+
input_types=dict(is_3D=False),
43+
tags=["Singleton Z dimension"],
44+
docs_info="file:docs_info/drop_t_dimension.md",
45+
),
3846
]

src/fractal_helper_tasks/drop_t_dimension.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def drop_t_dimension(
6767
dtype=old_ome_zarr_img.dtype,
6868
pixel_size=new_pixel_size,
6969
axes_names=axes_names,
70+
copy_tables=True,
7071
)
7172
new_image_container = new_ome_zarr_container.get_image()
7273
new_image_container.set_array(new_img)

tests/test_add_z_singleton.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Test drop t dimension task."""
2+
3+
from pathlib import Path
4+
5+
import ngio
6+
import numpy as np
7+
import pytest
8+
9+
from fractal_helper_tasks.add_z_singleton import (
10+
add_z_singleton,
11+
)
12+
13+
14+
@pytest.mark.parametrize(
15+
"orig_axes_names, target_axes_names, orig_dimensions, "
16+
"target_dimensions, overwrite_input",
17+
[
18+
("cyx", ["c", "z", "y", "x"], (5, 100, 100), (5, 1, 100, 100), True),
19+
("cyx", ["c", "z", "y", "x"], (5, 100, 100), (5, 1, 100, 100), False),
20+
("yx", ["z", "y", "x"], (100, 100), (1, 100, 100), True),
21+
(
22+
"tcyx",
23+
["t", "c", "z", "y", "x"],
24+
(3, 5, 100, 100),
25+
(3, 5, 1, 100, 100),
26+
True,
27+
),
28+
("tyx", ["t", "z", "y", "x"], (3, 100, 100), (3, 1, 100, 100), True),
29+
],
30+
)
31+
def test_add_singleton(
32+
tmp_path: Path,
33+
orig_axes_names: str,
34+
target_axes_names: list[str],
35+
orig_dimensions: tuple[int],
36+
target_dimensions: tuple[int],
37+
overwrite_input: bool,
38+
):
39+
zarr_url = str(tmp_path / "my_zarr.zarr")
40+
41+
ngio.create_ome_zarr_from_array(
42+
store=zarr_url,
43+
array=np.zeros(orig_dimensions),
44+
xy_pixelsize=0.5,
45+
axes_names=orig_axes_names,
46+
overwrite=True,
47+
)
48+
49+
suffix = "z_singleton"
50+
add_z_singleton(
51+
zarr_url=zarr_url,
52+
suffix=suffix,
53+
overwrite_input=overwrite_input,
54+
)
55+
56+
if overwrite_input:
57+
new_zarr_url = zarr_url
58+
else:
59+
new_zarr_url = f"{zarr_url}_{suffix}"
60+
61+
new_ome_zarr_container = ngio.open_ome_zarr_container(new_zarr_url)
62+
assert (
63+
new_ome_zarr_container.image_meta.axes_mapper.on_disk_axes_names
64+
== target_axes_names
65+
)
66+
assert new_ome_zarr_container.get_image().pixel_size.z == 1.0
67+
assert new_ome_zarr_container.get_image().shape == target_dimensions

0 commit comments

Comments
 (0)