Skip to content

Commit 038f378

Browse files
Set scale for any coordinate transform (#287)
* fix docstring * generalize set-scale for multiscales * document cli option default behavior Co-authored-by: Talon Chandler <talonchandler@gmail.com> * formatting --------- Co-authored-by: Talon Chandler <talonchandler@gmail.com>
1 parent 3b53773 commit 038f378

4 files changed

Lines changed: 98 additions & 70 deletions

File tree

iohub/cli/cli.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,18 @@ def convert(input, output, grid_layout, chunks):
123123
type=float,
124124
help="New x scale",
125125
)
126+
@click.option(
127+
"--image",
128+
required=False,
129+
help="Image name to set scale for. Default is '0'",
130+
)
126131
def set_scale(
127132
input_position_dirpaths,
128133
t_scale=None,
129134
z_scale=None,
130135
y_scale=None,
131136
x_scale=None,
137+
image=None,
132138
):
133139
"""Update scale metadata in OME-Zarr datasets.
134140
@@ -138,6 +144,8 @@ def set_scale(
138144
139145
>> iohub set-scale -i input.zarr/*/*/* -z 2.0
140146
"""
147+
if image is None:
148+
image = "0"
141149
for input_position_dirpath in input_position_dirpaths:
142150
with open_ome_zarr(
143151
input_position_dirpath, layout="fov", mode="r+"
@@ -147,12 +155,7 @@ def set_scale(
147155
):
148156
if value is None:
149157
continue
150-
old_value = dataset.scale[dataset.get_axis_index(name)]
151-
print(
152-
f"Updating {input_position_dirpath} {name} scale from "
153-
f"{old_value} to {value}."
154-
)
155-
dataset.set_scale("0", name, value)
158+
dataset.set_scale(image, name, value)
156159

157160

158161
@cli.command(name="rename-wells")

iohub/ngff/nodes.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import math
1010
import os
1111
from copy import deepcopy
12+
from datetime import datetime
1213
from pathlib import Path
1314
from typing import TYPE_CHECKING, Generator, Literal, Sequence, Type
1415

@@ -1144,57 +1145,71 @@ def set_transform(
11441145
self.dump_meta()
11451146

11461147
def set_scale(
1147-
self,
1148-
image: str | Literal["*"],
1149-
axis_name: str,
1150-
new_scale: float,
1148+
self, image: str | Literal["*"], axis_name: str, new_scale: float
11511149
):
11521150
"""Set the scale for a named axis.
11531151
Either one image array or the whole FOV.
11541152
11551153
Parameters
11561154
----------
1157-
image : str | Literal[
1155+
image : str | Literal['*']
11581156
Name of one image array (e.g. "0") to transform,
11591157
or "*" for the whole FOV
11601158
axis_name : str
11611159
Name of the axis to set.
11621160
new_scale : float
11631161
Value of the new scale.
11641162
"""
1165-
if len(self.metadata.multiscales) > 1:
1166-
raise NotImplementedError(
1167-
"Cannot set scale for multi-resolution images."
1168-
)
1169-
11701163
if new_scale <= 0:
1171-
raise ValueError("New scale must be positive.")
1172-
1164+
raise ValueError(
1165+
f"New scale {axis_name}: {new_scale} is not positive!"
1166+
)
1167+
if image not in self and image != "*":
1168+
raise KeyError(f"Image {image} not found.")
11731169
axis_index = self.get_axis_index(axis_name)
1174-
1170+
# Update scale while preserving existing transforms
1171+
if image == "*":
1172+
transforms = (
1173+
self.metadata.multiscales[0].coordinate_transformations or []
1174+
)
1175+
else:
1176+
for dataset_meta in self.metadata.multiscales[0].datasets:
1177+
if dataset_meta.path == image:
1178+
transforms = dataset_meta.coordinate_transformations
1179+
break
11751180
# Append old scale to metadata
11761181
iohub_dict = {}
11771182
if "iohub" in self.zattrs:
11781183
iohub_dict = self.zattrs["iohub"]
1179-
iohub_dict.update({f"prior_{axis_name}_scale": self.scale[axis_index]})
1180-
self.zattrs["iohub"] = iohub_dict
1181-
1182-
# Update scale while preserving existing transforms
1183-
transforms = (
1184-
self.metadata.multiscales[0].datasets[0].coordinate_transformations
1184+
if "previous_transforms" not in iohub_dict:
1185+
iohub_dict["previous_transforms"] = []
1186+
iohub_dict["previous_transforms"].append(
1187+
{
1188+
"image": image,
1189+
"transforms": [
1190+
t.model_dump(**TO_DICT_SETTINGS) for t in transforms
1191+
],
1192+
"modified": datetime.now().isoformat(),
1193+
}
11851194
)
1195+
self.zattrs["iohub"] = iohub_dict
11861196
# Replace default identity transform with scale
1187-
if len(transforms) == 1 and transforms[0].type == "identity":
1197+
if transforms == [TransformationMeta(type="identity")]:
11881198
transforms = [TransformationMeta(type="scale", scale=[1] * 5)]
11891199
# Add scale transform if not present
11901200
if not any([transform.type == "scale" for transform in transforms]):
11911201
transforms.append(TransformationMeta(type="scale", scale=[1] * 5))
1192-
1202+
new_transforms = []
11931203
for transform in transforms:
11941204
if transform.type == "scale":
1205+
old_scale = transform.scale[axis_index]
11951206
transform.scale[axis_index] = new_scale
1196-
1197-
self.set_transform(image, transforms)
1207+
new_transforms.append(transform)
1208+
_logger.info(
1209+
f"Updating scale for axis {axis_name} "
1210+
f"from {old_scale} to {new_scale}."
1211+
)
1212+
self.set_transform(image, new_transforms)
11981213

11991214
def set_contrast_limits(self, channel_name: str, window: WindowDict):
12001215
"""Set the contrast limits for a channel.

tests/cli/test_cli.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def test_cli_convert_ome_tiff(grid_layout, tmpdir):
121121
assert "Converting" in result.output
122122

123123

124-
def test_cli_set_scale():
124+
def test_cli_set_scale(caplog):
125125
with _temp_copy(hcs_ref) as store_path:
126126
store_path = Path(store_path)
127127
position_path = Path(store_path) / "B" / "03" / "0"
@@ -149,23 +149,16 @@ def test_cli_set_scale():
149149
],
150150
)
151151
assert result_pos.exit_code == 0
152-
assert "Updating" in result_pos.output
153-
152+
assert any("Updating" in record.message for record in caplog.records)
154153
with open_ome_zarr(position_path, layout="fov") as output_dataset:
155154
assert tuple(output_dataset.scale[-3:]) == (random_z, 0.5, 0.5)
156155
assert output_dataset.scale != old_scale
157-
assert (
158-
output_dataset.zattrs["iohub"]["prior_x_scale"]
159-
== old_scale[-1]
160-
)
161-
assert (
162-
output_dataset.zattrs["iohub"]["prior_y_scale"]
163-
== old_scale[-2]
164-
)
165-
assert (
166-
output_dataset.zattrs["iohub"]["prior_z_scale"]
167-
== old_scale[-3]
168-
)
156+
for i, record in enumerate(
157+
output_dataset.zattrs["iohub"]["previous_transforms"]
158+
):
159+
for transform in record["transforms"]:
160+
if transform["type"] == "scale":
161+
assert transform["scale"][-3:][i] == old_scale[-3:][i]
169162

170163
# Test plate-expands-into-positions behavior
171164
runner = CliRunner()
@@ -181,7 +174,11 @@ def test_cli_set_scale():
181174
)
182175
with open_ome_zarr(position_path, layout="fov") as output_dataset:
183176
assert output_dataset.scale[-1] == 0.1
184-
assert output_dataset.zattrs["iohub"]["prior_x_scale"] == 0.5
177+
for transform in output_dataset.zattrs["iohub"][
178+
"previous_transforms"
179+
][-1]["transforms"]:
180+
if transform["type"] == "scale":
181+
assert transform["scale"][-1] == 0.5
185182

186183

187184
def test_cli_rename_wells_help():

tests/ngff/test_ngff.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -652,38 +652,51 @@ def test_set_transform_fov(ch_shape_dtype, arr_name):
652652
]
653653

654654

655-
@given(
656-
ch_shape_dtype=_channels_and_random_5d_shape_and_dtype(),
657-
)
658-
@settings(deadline=None)
659-
def test_set_scale(ch_shape_dtype):
655+
@pytest.mark.parametrize("image_name", ["0", "1", "a", "*"])
656+
def test_set_scale(image_name):
660657
"""Test `iohub.ngff.Position.set_scale()`"""
661-
channel_names, shape, dtype = ch_shape_dtype
662-
transform = [
663-
TransformationMeta(type="translation", translation=(1, 2, 3, 4, 5)),
664-
TransformationMeta(type="scale", scale=(5, 4, 3, 2, 1)),
665-
]
658+
translation = [float(t) for t in range(1, 6)]
659+
scale = [float(s) for s in range(5, 0, -1)]
660+
array_name = "0" if image_name == "*" else image_name
661+
new_scale = 10.0
666662
with TemporaryDirectory() as temp_dir:
667663
store_path = os.path.join(temp_dir, "ome.zarr")
668664
with open_ome_zarr(
669-
store_path, layout="fov", mode="w-", channel_names=channel_names
665+
store_path, layout="fov", mode="w-", channel_names=["a", "b"]
670666
) as dataset:
671-
dataset.create_zeros(name="0", shape=shape, dtype=dtype)
672-
dataset.set_transform(image="0", transform=transform)
673-
dataset.set_scale(image="0", axis_name="z", new_scale=10.0)
674-
assert dataset.scale[-3] == 10.0
675-
assert (
676-
dataset.metadata.multiscales[0]
677-
.datasets[0]
678-
.coordinate_transformations[0]
679-
.translation[-1]
680-
== 5
667+
dataset.create_zeros(
668+
name=array_name,
669+
shape=(1, 2, 4, 8, 16),
670+
dtype=int,
671+
transform=[
672+
TransformationMeta(
673+
type="translation", translation=translation
674+
),
675+
TransformationMeta(type="scale", scale=scale),
676+
],
681677
)
682-
683678
with pytest.raises(ValueError):
684-
dataset.set_scale(image="0", axis_name="z", new_scale=-1.0)
685-
686-
assert dataset.zattrs["iohub"]["prior_z_scale"] == 3.0
679+
dataset.set_scale(
680+
image=image_name, axis_name="z", new_scale=-1.0
681+
)
682+
with pytest.raises(KeyError):
683+
dataset.set_scale(
684+
image="nonexistent", axis_name="z", new_scale=9.0
685+
)
686+
assert dataset.scale[-3] == 3.0
687+
dataset.set_scale(
688+
image=image_name, axis_name="z", new_scale=new_scale
689+
)
690+
if image_name == "*":
691+
assert dataset.scale[-3] == new_scale * 3.0
692+
else:
693+
assert dataset.scale[-3] == new_scale
694+
assert dataset.get_effective_translation(array_name) == translation
695+
for tf in dataset.zattrs["iohub"]["previous_transforms"][0][
696+
"transforms"
697+
]:
698+
if tf["type"] == "scale":
699+
assert tf["scale"] == scale
687700

688701

689702
@given(channel_names=channel_names_st)

0 commit comments

Comments
 (0)