From 2c8b560f0a67085393a0768945bb0de080852dfe Mon Sep 17 00:00:00 2001 From: Martin Helm Date: Tue, 17 Feb 2026 13:30:13 +0100 Subject: [PATCH 1/3] 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/3] 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/3] 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", [