Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ data/
tests/data
uv.lock
.asv/
.venv/
65 changes: 59 additions & 6 deletions src/spatialdata_io/readers/macsima.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"B": "bleach", # v1
"AntigenCycle": "stain", # v0
"S": "stain", # v1
"AF": "autofluorescence",
}


Expand Down Expand Up @@ -125,12 +126,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
Expand Down Expand Up @@ -220,6 +220,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,
Expand Down Expand Up @@ -331,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,
Expand Down
50 changes: 49 additions & 1 deletion tests/test_macsima.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import math
import os
import shutil
from copy import deepcopy
from pathlib import Path
Expand Down Expand Up @@ -93,14 +94,61 @@ 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)

with pytest.raises(ValueError, match="No valid files were found"):
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",
[
(((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",
Expand Down
Loading