From 2c8b560f0a67085393a0768945bb0de080852dfe Mon Sep 17 00:00:00 2001 From: Martin Helm Date: Tue, 17 Feb 2026 13:30:13 +0100 Subject: [PATCH 1/7] Pad images with 0 on right and bottom, if images differ in dimensions --- .gitignore | 1 + src/spatialdata_io/readers/macsima.py | 61 ++++++++++++++++++++++++--- tests/test_macsima.py | 39 ++++++++++++++++- 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 35db05af..2d7d7f02 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ data/ tests/data uv.lock .asv/ +.venv/ diff --git a/src/spatialdata_io/readers/macsima.py b/src/spatialdata_io/readers/macsima.py index 2b4493f4..0139ff1d 100644 --- a/src/spatialdata_io/readers/macsima.py +++ b/src/spatialdata_io/readers/macsima.py @@ -125,12 +125,11 @@ def from_paths( ), ) imgs = [imread(img, **imread_kwargs) for img in valid_files] - for img, path in zip(imgs, valid_files, strict=True): - if img.shape[1:] != imgs[0].shape[1:]: - raise ValueError( - f"Images are not all the same size. Image {path} has shape {img.shape[1:]} while the first image " - f"{valid_files[0]} has shape {imgs[0].shape[1:]}" - ) + + # Pad images to same dimensions if necessary + if cls._check_for_differing_xy_dimensions(imgs): + imgs = cls._pad_images(imgs) + # create MultiChannelImage object with imgs and metadata output = cls(data=imgs, metadata=channel_metadata) return output @@ -220,6 +219,56 @@ def calc_scale_factors(self, default_scale_factor: int = 2) -> list[int]: def get_stack(self) -> da.Array: return da.stack(self.data, axis=0).squeeze(axis=1) + @staticmethod + def _check_for_differing_xy_dimensions(imgs: list[da.Array]) -> bool: + """Checks whether any of the images have differing extent in dimensions X and Y.""" + # Shape has order CYX + dims_x = [x.shape[2] for x in imgs] + dims_y = [x.shape[1] for x in imgs] + + dims_x_different = False if len(set(dims_x)) == 1 else True + dims_y_different = False if len(set(dims_y)) == 1 else True + + different_dimensions = any([dims_x_different, dims_y_different]) + + warnings.warn( + "Supplied images have different dimensions!", + UserWarning, + stacklevel=2, + ) + + return different_dimensions + + @staticmethod + def _pad_images(imgs: list[da.Array]) -> list[da.Array]: + """Pad all images to the same dimensions in X and Y with 0s.""" + dims_x_max = max([x.shape[2] for x in imgs]) + dims_y_max = max([x.shape[1] for x in imgs]) + + warnings.warn( + f"Padding images with 0s to same size of ({dims_y_max}, {dims_x_max})", + UserWarning, + stacklevel=2, + ) + + padded_imgs = [] + for img in imgs: + pad_y = dims_y_max - img.shape[1] + pad_x = dims_x_max - img.shape[2] + # Only pad if necessary + if (pad_y, pad_y) != (0, 0): + # Always pad to the right/bottom + pad_width = ( + (0, 0), + (0, pad_y), + (0, pad_x), + ) + + img = da.pad(img, pad_width, mode="constant", constant_values=0) + padded_imgs.append(img) + + return padded_imgs + def macsima( path: str | Path, diff --git a/tests/test_macsima.py b/tests/test_macsima.py index db701b52..0f31e933 100644 --- a/tests/test_macsima.py +++ b/tests/test_macsima.py @@ -93,7 +93,7 @@ def test_exception_on_no_valid_files(tmp_path: Path) -> None: # Write a tiff file without metadata height = 10 width = 10 - arr = np.zeros((height, width, 1), dtype=np.uint16) + arr = np.zeros((1, height, width), dtype=np.uint16) path_no_metadata = Path(tmp_path) / "tiff_no_metadata.tiff" imwrite(path_no_metadata, arr, metadata=None, description=None, software=None, datetime=None) @@ -101,6 +101,43 @@ def test_exception_on_no_valid_files(tmp_path: Path) -> None: macsima(tmp_path) +@pytest.mark.parametrize( + "dimensions,expected", + [ + (((10, 10), (10, 10)), False), + (((10, 10), (15, 10)), True), + (((10, 10), (10, 15)), True), + (((15, 10), (10, 15)), True), + ], +) +def test_check_differing_dimensions_works(dimensions: tuple[tuple[int, int], tuple[int, int]], expected: bool) -> None: + imgs = [] + for img_dim in dimensions: + arr = da.from_array(np.ones((1, img_dim[0], img_dim[1]), dtype=np.uint16)) + imgs.append(arr) + + if expected: + with pytest.warns(UserWarning, match="Supplied images have different dimensions!"): + assert MultiChannelImage._check_for_differing_xy_dimensions(imgs) == expected + else: + assert MultiChannelImage._check_for_differing_xy_dimensions(imgs) == expected + + +def test_padding_on_differing_dimensions() -> None: + heights = [10, 10, 15, 20] + widths = [10, 15, 10, 20] + + imgs = [] + for height, width in zip(heights, widths, strict=True): + arr = da.from_array(np.ones((1, height, width), dtype=np.uint16)) + imgs.append(arr) + + with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(20, 20\\)"): + imgs_padded = MultiChannelImage._pad_images(imgs) + for img in imgs_padded: + assert img.shape == (1, 20, 20) + + @skip_if_below_python_version() @pytest.mark.parametrize( "dataset,expected", From 4f93f86bc1026d95a9c36aed3393f24e44438f74 Mon Sep 17 00:00:00 2001 From: Martin Helm Date: Fri, 27 Feb 2026 11:39:29 +0100 Subject: [PATCH 2/7] Add missing autofluorescence entry. Skip empty folders when parsing multiple subfolders. --- src/spatialdata_io/readers/macsima.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/spatialdata_io/readers/macsima.py b/src/spatialdata_io/readers/macsima.py index 0139ff1d..5028a6ca 100644 --- a/src/spatialdata_io/readers/macsima.py +++ b/src/spatialdata_io/readers/macsima.py @@ -38,6 +38,7 @@ "B": "bleach", # v1 "AntigenCycle": "stain", # v0 "S": "stain", # v1 + "AF": "autofluorescence", } @@ -380,6 +381,9 @@ def macsima( for p in path.iterdir() if p.is_dir() and (not filter_folder_names or not any(f in p.name for f in filter_folder_names)) ]: + if not len(list(p.glob("*.tif*"))): + warnings.warn(f"No tif files found in {p}, skipping it!", UserWarning, stacklevel=2) + continue sdatas[p.stem] = parse_processed_folder( path=p, imread_kwargs=imread_kwargs, From b309f295e58f703b4c4076ba502b2eec9169795e Mon Sep 17 00:00:00 2001 From: Martin Helm Date: Fri, 27 Feb 2026 17:11:05 +0100 Subject: [PATCH 3/7] Add test for skipping empty subfolder --- tests/test_macsima.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_macsima.py b/tests/test_macsima.py index 0f31e933..bf198e4e 100644 --- a/tests/test_macsima.py +++ b/tests/test_macsima.py @@ -1,4 +1,5 @@ import math +import os import shutil from copy import deepcopy from pathlib import Path @@ -101,6 +102,16 @@ def test_exception_on_no_valid_files(tmp_path: Path) -> None: macsima(tmp_path) +def test_multiple_subfolder_parsing_skips_emtpy_folders(tmp_path: Path) -> None: + parent_folder = tmp_path / "test_folder" + shutil.copytree("./data/OMAP23_small", parent_folder / "OMAP23_small") + os.makedirs(parent_folder / "empty_folder") + + with pytest.warns(UserWarning, match="No tif files found in .* skipping it"): + sdata = macsima(parent_folder, parsing_style="processed_multiple_folders") + assert len(sdata.images.keys()) == 1 + + @pytest.mark.parametrize( "dimensions,expected", [ From 01f62961b74087db163c056f460d652e201602ed Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Mon, 4 May 2026 10:46:03 +0200 Subject: [PATCH 4/7] code style fixes; more docstrings --- src/spatialdata_io/readers/macsima.py | 15 ++++++++++++--- tests/test_macsima.py | 11 +++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/spatialdata_io/readers/macsima.py b/src/spatialdata_io/readers/macsima.py index 2593def3..d250fd3c 100644 --- a/src/spatialdata_io/readers/macsima.py +++ b/src/spatialdata_io/readers/macsima.py @@ -227,8 +227,8 @@ def _check_for_differing_xy_dimensions(imgs: list[da.Array]) -> bool: dims_x = [x.shape[2] for x in imgs] dims_y = [x.shape[1] for x in imgs] - dims_x_different = False if len(set(dims_x)) == 1 else True - dims_y_different = False if len(set(dims_y)) == 1 else True + dims_x_different = len(set(dims_x)) != 1 + dims_y_different = len(set(dims_y)) != 1 different_dimensions = any([dims_x_different, dims_y_different]) @@ -242,7 +242,11 @@ def _check_for_differing_xy_dimensions(imgs: list[da.Array]) -> bool: @staticmethod def _pad_images(imgs: list[da.Array]) -> list[da.Array]: - """Pad all images to the same dimensions in X and Y with 0s.""" + """Pad all images to the same dimensions in X and Y with 0s. + + Padding is added only away from the origin: on the right side for X and at the + bottom for Y, so the top-left corner of each image stays aligned. + """ dims_x_max = max([x.shape[2] for x in imgs]) dims_y_max = max([x.shape[1] for x in imgs]) @@ -260,8 +264,11 @@ def _pad_images(imgs: list[da.Array]) -> list[da.Array]: if (pad_y, pad_y) != (0, 0): # Always pad to the right/bottom pad_width = ( + # c axis: no pad (0, 0), + # y axis: no pad on the left, pad_y on the right (0, pad_y), + # x axis: no pad near the origin (top), pad_x on the bottom (0, pad_x), ) @@ -294,6 +301,8 @@ def macsima( This function reads images from a MACSima cyclic imaging experiment. MACSima data follows the OME-TIFF specificiation. All metadata is parsed from the OME metadata. The exact metadata schema can change between software versions of MACSiQView. As there is no public specification of the metadata fields used, please consider the provided test data sets as ground truth to guide development. + If images from different cycles differ in spatial dimensions, they are zero-padded on the right (X) and bottom (Y) to match + the largest dimensions, keeping the top-left origin aligned; a warning is emitted in that case. .. seealso:: diff --git a/tests/test_macsima.py b/tests/test_macsima.py index ca7af166..2557b5d2 100644 --- a/tests/test_macsima.py +++ b/tests/test_macsima.py @@ -1,3 +1,4 @@ +import contextlib import math import os import shutil @@ -127,10 +128,12 @@ def test_check_differing_dimensions_works(dimensions: tuple[tuple[int, int], tup arr = da.from_array(np.ones((1, img_dim[0], img_dim[1]), dtype=np.uint16)) imgs.append(arr) - if expected: - with pytest.warns(UserWarning, match="Supplied images have different dimensions!"): - assert MultiChannelImage._check_for_differing_xy_dimensions(imgs) == expected - else: + ctx = ( + pytest.warns(UserWarning, match="Supplied images have different dimensions!") + if expected + else contextlib.nullcontext() + ) + with ctx: assert MultiChannelImage._check_for_differing_xy_dimensions(imgs) == expected From 45f6096118114b93ff2789289f4b37355cd6c021 Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Mon, 4 May 2026 10:48:25 +0200 Subject: [PATCH 5/7] fix mistake in comments --- src/spatialdata_io/readers/macsima.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spatialdata_io/readers/macsima.py b/src/spatialdata_io/readers/macsima.py index d250fd3c..ac5c5c44 100644 --- a/src/spatialdata_io/readers/macsima.py +++ b/src/spatialdata_io/readers/macsima.py @@ -266,9 +266,9 @@ def _pad_images(imgs: list[da.Array]) -> list[da.Array]: pad_width = ( # c axis: no pad (0, 0), - # y axis: no pad on the left, pad_y on the right + # y axis: no pad near the origin (top), pad on the bottom (0, pad_y), - # x axis: no pad near the origin (top), pad_x on the bottom + # x axis: no pad near the origin (left), pad on the right (0, pad_x), ) From 90f7cf18e4d5c549804f026fc65e861ce08cc8ea Mon Sep 17 00:00:00 2001 From: Martin Helm Date: Fri, 8 May 2026 12:51:08 +0200 Subject: [PATCH 6/7] Obey translations stored in metadata for padding --- pyproject.toml | 2 + src/spatialdata_io/readers/macsima.py | 74 +++++++++--- tests/test_macsima.py | 166 +++++++++++++++++++++++--- 3 files changed, 208 insertions(+), 34 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d97dc276..5b94b545 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ dependencies = [ "tifffile>=2023.8.12", "ome-types", "xmltodict", + "napari-spatialdata>=0.7.1", + "napari[all]>=0.7.0", ] [project.optional-dependencies] diff --git a/src/spatialdata_io/readers/macsima.py b/src/spatialdata_io/readers/macsima.py index ac5c5c44..c3fe04d5 100644 --- a/src/spatialdata_io/readers/macsima.py +++ b/src/spatialdata_io/readers/macsima.py @@ -62,6 +62,8 @@ class ChannelMetadata: roi: int fluorophore: str exposure: float + translation_x: int + translation_y: int clone: str | None = None # For example DAPI doesnt have a clone @@ -104,6 +106,8 @@ def from_paths( fluorophore=metadata["fluorophore"], clone=metadata["clone"], exposure=metadata["exposure"], + translation_x=metadata["translation_x"], + translation_y=metadata["translation_y"], ) ) @@ -129,7 +133,7 @@ def from_paths( # Pad images to same dimensions if necessary if cls._check_for_differing_xy_dimensions(imgs): - imgs = cls._pad_images(imgs) + imgs = cls._pad_images(imgs, channel_metadata) # create MultiChannelImage object with imgs and metadata output = cls(data=imgs, metadata=channel_metadata) @@ -241,14 +245,25 @@ def _check_for_differing_xy_dimensions(imgs: list[da.Array]) -> bool: return different_dimensions @staticmethod - def _pad_images(imgs: list[da.Array]) -> list[da.Array]: + def _pad_images(imgs: list[da.Array], channel_metadata: list[ChannelMetadata]) -> list[da.Array]: """Pad all images to the same dimensions in X and Y with 0s. - Padding is added only away from the origin: on the right side for X and at the + Padding obeys translations stored in ome metadata. + If no translations are found, padding is added only away from the origin: + on the right side for X and at the bottom for Y, so the top-left corner of each image stays aligned. """ - dims_x_max = max([x.shape[2] for x in imgs]) - dims_y_max = max([x.shape[1] for x in imgs]) + min_translation_x = min(metadata.translation_x for metadata in channel_metadata) + min_translation_y = min(metadata.translation_y for metadata in channel_metadata) + normalized_translations_x = [metadata.translation_x - min_translation_x for metadata in channel_metadata] + normalized_translations_y = [metadata.translation_y - min_translation_y for metadata in channel_metadata] + + dims_x_max = max( + img.shape[2] + translation_x for img, translation_x in zip(imgs, normalized_translations_x, strict=True) + ) + dims_y_max = max( + img.shape[1] + translation_y for img, translation_y in zip(imgs, normalized_translations_y, strict=True) + ) warnings.warn( f"Padding images with 0s to same size of ({dims_y_max}, {dims_x_max})", @@ -257,19 +272,22 @@ def _pad_images(imgs: list[da.Array]) -> list[da.Array]: ) padded_imgs = [] - for img in imgs: - pad_y = dims_y_max - img.shape[1] - pad_x = dims_x_max - img.shape[2] + for img, translation_x, translation_y in zip( + imgs, normalized_translations_x, normalized_translations_y, strict=True + ): + pad_y_prepend = translation_y + pad_x_prepend = translation_x + # For appending, we check how much space is already covered by the original image, and we take into account how many pixels were prepended + # If there still is a difference, we append 0 to get to the same image size + pad_y_append = dims_y_max - img.shape[1] - pad_y_prepend + pad_x_append = dims_x_max - img.shape[2] - pad_x_prepend # Only pad if necessary - if (pad_y, pad_y) != (0, 0): - # Always pad to the right/bottom + if (pad_x_prepend, pad_x_append, pad_y_prepend, pad_y_append) != (0, 0, 0, 0): pad_width = ( # c axis: no pad (0, 0), - # y axis: no pad near the origin (top), pad on the bottom - (0, pad_y), - # x axis: no pad near the origin (left), pad on the right - (0, pad_x), + (pad_y_prepend, pad_y_append), + (pad_x_prepend, pad_x_append), ) img = da.pad(img, pad_width, mode="constant", constant_values=0) @@ -476,6 +494,25 @@ def _get_software_major_version(version: str) -> int: return major +def _get_translations(ome: OME) -> dict[str, int]: + try: + translations = { + "translation_x": ome.images[0].pixels.planes[0].position_x, + "translation_y": ome.images[0].pixels.planes[0].position_y, + } + # If the position attributes are not present the values will be None and we default to (0,0) + if any(v is None for v in translations.values()): + logger.debug(f"No translation found for {ome.images[0].name}, defaulting to (0, 0)") + translations = {"translation_x": 0, "translation_y": 0} + + # In case the ome is faulty, also default to (0,0) + except AttributeError: + logger.debug(f"No translation found for {ome.images[0].name}, defaulting to (0, 0)") + translations = {"translation_x": 0, "translation_y": 0} + + return translations + + def _parse_v0_ome_metadata(ome: OME) -> dict[str, Any]: """Parse Legacy Format of OME Metadata (software version 0.x.x).""" logger.debug("Parsing OME metadata expecting version 0 format") @@ -653,12 +690,16 @@ def _parse_ome_metadata(ome: OME) -> dict[str, Any]: major = _get_software_major_version(version_str) if major == 0: - return _parse_v0_ome_metadata(ome) + metadata = _parse_v0_ome_metadata(ome) elif major == 1: - return _parse_v1_ome_metadata(ome) + metadata = _parse_v1_ome_metadata(ome) else: raise ValueError("Unknown software version, cannot determine parser") + translations = _get_translations(ome) + metadata.update(translations) + return metadata + def parse_metadata(path: Path) -> dict[str, Any]: """Parse metadata for a file. @@ -821,6 +862,7 @@ def create_table(mci: MultiChannelImage) -> ad.AnnData: clones = mci.get_clones() exposures = mci.get_exposures() + # We dont add the translations, because they are usually not interesting for the user df = pd.DataFrame( { "name": names, diff --git a/tests/test_macsima.py b/tests/test_macsima.py index 2557b5d2..a0900b8b 100644 --- a/tests/test_macsima.py +++ b/tests/test_macsima.py @@ -14,7 +14,12 @@ from click.testing import CliRunner from ome_types import OME from ome_types.model import ( + Image, MapAnnotation, + Pixels, + Pixels_DimensionOrder, + PixelType, + Plane, Plate, Reagent, Screen, @@ -32,6 +37,7 @@ _collect_map_annotation_values, _get_software_major_version, _get_software_version, + _get_translations, _parse_ome_metadata, _parse_v0_ome_metadata, _parse_v1_ome_metadata, @@ -54,21 +60,25 @@ def make_ChannelMetadata( name: str, cycle: int, - fluorophore: str | None = None, - exposure: float | None = None, - imagetype: str | None = None, - well: str | None = None, - roi: int | None = None, + fluorophore: str = "", + exposure: float = 0.0, + imagetype: str = "StainCycle", + well: str = "A01", + roi: int = 0, + translation_x: int = 0, + translation_y: int = 0, ) -> ChannelMetadata: """Helper to construct ChannelMetadata with required defaults.""" return ChannelMetadata( name=name, cycle=cycle, - fluorophore=fluorophore or "", - exposure=exposure if exposure is not None else 0.0, - imagetype=imagetype or "StainCycle", - well=well or "A01", - roi=roi if roi is not None else 0, + fluorophore=fluorophore, + exposure=exposure, + imagetype=imagetype, + well=well, + translation_x=translation_x, + translation_y=translation_y, + roi=roi, ) @@ -138,6 +148,8 @@ def test_check_differing_dimensions_works(dimensions: tuple[tuple[int, int], tup def test_padding_on_differing_dimensions() -> None: + # Simple test where all translations are 0 + # Here we expect to pad to the largest element. heights = [10, 10, 15, 20] widths = [10, 15, 10, 20] @@ -146,11 +158,68 @@ def test_padding_on_differing_dimensions() -> None: arr = da.from_array(np.ones((1, height, width), dtype=np.uint16)) imgs.append(arr) + channel_metadata = [make_ChannelMetadata(name="test", cycle=1)] * 4 with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(20, 20\\)"): - imgs_padded = MultiChannelImage._pad_images(imgs) + imgs_padded = MultiChannelImage._pad_images(imgs, channel_metadata) for img in imgs_padded: assert img.shape == (1, 20, 20) + # More complex with non-zero translations + # First test that padding does the minimal padding necessary. + # To do this create images with very large, but identical translations. Since all of these should be normalized out we expect size 20x20 again. + heights = [10, 10, 15, 20] + widths = [10, 15, 10, 20] + + imgs = [] + for height, width in zip(heights, widths, strict=True): + arr = da.from_array(np.ones((1, height, width), dtype=np.uint16)) + imgs.append(arr) + channel_metadata = channel_metadata = [ + make_ChannelMetadata(name="test", cycle=1, translation_x=100, translation_y=100) + ] * 4 + with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(20, 20\\)"): + imgs_padded = MultiChannelImage._pad_images(imgs, channel_metadata) + for img in imgs_padded: + assert img.shape == (1, 20, 20) + + # Test with differing translations but same size. + # As we translate the first image by 2 in x and 3 in y, we expect a 13x12 image + heights = [10, 10] + widths = [10, 10] + + imgs = [] + for height, width in zip(heights, widths, strict=True): + arr = da.from_array(np.ones((1, height, width), dtype=np.uint16)) + imgs.append(arr) + channel_metadata = channel_metadata = [ + make_ChannelMetadata(name="test", cycle=1, translation_x=2, translation_y=3), + make_ChannelMetadata(name="test", cycle=1, translation_x=0, translation_y=0), + ] + with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(13, 12\\)"): + imgs_padded = MultiChannelImage._pad_images(imgs, channel_metadata) + for img in imgs_padded: + assert img.shape == (1, 13, 12) + + # Final test with differing image sizes, and translations that need to be normalized + # For the total size, we need to check the sum of each image dimension + normalized translation + # Here that would be image 2, with y = 15 + 5 - 3 = 17 (normalized to other image!) and x = 15 + 5 - 2 = 18 + + heights = [10, 15] + widths = [10, 15] + + imgs = [] + for height, width in zip(heights, widths, strict=True): + arr = da.from_array(np.ones((1, height, width), dtype=np.uint16)) + imgs.append(arr) + channel_metadata = channel_metadata = [ + make_ChannelMetadata(name="test", cycle=1, translation_x=2, translation_y=3), + make_ChannelMetadata(name="test", cycle=1, translation_x=5, translation_y=5), + ] + with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(17, 18\\)"): + imgs_padded = MultiChannelImage._pad_images(imgs, channel_metadata) + for img in imgs_padded: + assert img.shape == (1, 17, 18) + @skip_if_below_python_version() @pytest.mark.parametrize( @@ -696,15 +765,77 @@ def test_parse_v1_ome_metadata_handles_unknown_imagetypes() -> None: assert md["imagetype"] == "NOT A VALID TYPE" -def make_ome_with_version(version_value: str, extra_ma: dict[str, Any] | None = None) -> OME: - base = {"SoftwareVersion": version_value} +def test_get_translations_returns_correct_values() -> None: + ome = OME( + images=[ + Image( + pixels=Pixels( + dimension_order=Pixels_DimensionOrder("XYZCT"), + type=PixelType.UINT16, + size_x=1, + size_y=1, + size_z=1, + size_c=1, + size_t=1, + planes=[Plane(position_x=1, position_y=2, the_z=0, the_t=0, the_c=0)], + ) + ) + ] + ) + expected = {"translation_x": 1, "translation_y": 2} + + translations = _get_translations(ome) + assert translations == expected + + +def test_get_translations_defaults_to_0_on_missing_data() -> None: + ome = OME( + images=[ + Image( + pixels=Pixels( + dimension_order=Pixels_DimensionOrder("XYZCT"), + type=PixelType.UINT16, + size_x=1, + size_y=1, + size_z=1, + size_c=1, + size_t=1, + planes=[Plane(the_z=0, the_t=0, the_c=0)], + ) + ) + ], + ) + expected = {"translation_x": 0, "translation_y": 0} + + translations = _get_translations(ome) + assert translations == expected + + +def make_ome(extra_ma: dict[str, Any] | None = None) -> OME: + base = {} if extra_ma: base.update(extra_ma) - return OME(structured_annotations=StructuredAnnotations(map_annotations=[MapAnnotation(value=base)])) + return OME( + images=[ + Image( + pixels=Pixels( + dimension_order=Pixels_DimensionOrder("XYZCT"), + type=PixelType.UINT16, + size_x=1, + size_y=1, + size_z=1, + size_c=1, + size_t=1, + planes=[Plane(the_z=0, the_t=0, the_c=0)], + ) + ) + ], + structured_annotations=StructuredAnnotations(map_annotations=[MapAnnotation(value=base)]), + ) def test_parse_ome_metadata_dispatches_to_v0() -> None: - ome = make_ome_with_version("0.9.0") + ome = make_ome(extra_ma={"SoftwareVersion": "0.9.0"}) # enrich some so v0 parser has something to see ome.screens = [Screen(reagents=[Reagent(name="Marker0")])] @@ -717,15 +848,14 @@ def test_parse_ome_metadata_dispatches_to_v0() -> None: def test_parse_ome_metadata_dispatches_to_v1() -> None: - ome = make_ome_with_version("1.0.0", extra_ma={"Biomarker": "CD3"}) - + ome = make_ome(extra_ma={"SoftwareVersion": "1.0.0", "Biomarker": "CD3"}) md = _parse_ome_metadata(ome) assert md["name"] == "CD3" def test_parse_ome_metadata_unknown_major_raises() -> None: - ome = make_ome_with_version("2.0.0") + ome = make_ome(extra_ma={"SoftwareVersion": "2.0.0"}) with pytest.raises(ValueError, match="Unknown software version"): _parse_ome_metadata(ome) From b83ced1684a1f53b2a92239cdaecdf96c7d6a95a Mon Sep 17 00:00:00 2001 From: Martin Helm Date: Fri, 8 May 2026 13:00:47 +0200 Subject: [PATCH 7/7] Forgot to remove one skip decorator --- tests/test_macsima.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_macsima.py b/tests/test_macsima.py index 87425c8e..9be7a3c6 100644 --- a/tests/test_macsima.py +++ b/tests/test_macsima.py @@ -220,7 +220,6 @@ def test_padding_on_differing_dimensions() -> None: assert img.shape == (1, 17, 18) -@skip_if_below_python_version() @pytest.mark.parametrize( "dataset,expected", [