From 9ce7c7328050a8659f6b269548ca301b594002c5 Mon Sep 17 00:00:00 2001 From: keviny2 Date: Thu, 10 Apr 2025 16:14:04 -0700 Subject: [PATCH 1/5] Implement img_raster in SpatialExperiment class --- src/spatialexperiment/SpatialExperiment.py | 37 ++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/spatialexperiment/SpatialExperiment.py b/src/spatialexperiment/SpatialExperiment.py index c289fe5..63afddd 100644 --- a/src/spatialexperiment/SpatialExperiment.py +++ b/src/spatialexperiment/SpatialExperiment.py @@ -959,8 +959,41 @@ def img_source( return img_sources def img_raster(self, sample_id=None, image_id=None): - # NOTE: this function seems redundant, might be an artifact of the different subclasses of SpatialImage in the R implementation? just call `get_img()` for now - self.get_img(sample_id=sample_id, image_id=image_id) + """Retrieve and load (if necessary) the images stored in the SpatialExperiment object. + + Args: + sample_id: + - `sample_id=True`: Matches all samples. + - `sample_id=None`: Matches the first sample. + - `sample_id=""`: Matches a sample by its id. + + image_id: + - `image_id=True`: Matches all images for the specified sample(s). + - `image_id=None`: Matches the first image for the sample(s). + - `image_id=""`: Matches image(s) by its(their) id. + + Returns: + The loaded image(s) for the matching criteria. Returns `None` if `img_data` is `None`. + When a single image matches, returns its loaded image. + When multiple images match, returns a list of loaded images. + + Raises: + ValueError: If no row matches the provided sample_id and image_id pair. + + Note: + See :py:meth:`~get_img` for detailed behavior regarding sample_id and image_id parameters. + """ + spis = self.get_img(sample_id=sample_id, image_id=image_id) + + if spis is None: + return None + + if isinstance(spis, VirtualSpatialImage): + return spis.img_raster() + + img_rasters = [spi.img_raster() for spi in spis] + + return img_rasters def rotate_img(self, sample_id=None, image_id=None, degrees=90): raise NotImplementedError() From fc382dd3bbd4bb58b5c485838b6dc9674e902ac7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 23:15:02 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spatialexperiment/SpatialExperiment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spatialexperiment/SpatialExperiment.py b/src/spatialexperiment/SpatialExperiment.py index 63afddd..72cb3bf 100644 --- a/src/spatialexperiment/SpatialExperiment.py +++ b/src/spatialexperiment/SpatialExperiment.py @@ -960,7 +960,7 @@ def img_source( def img_raster(self, sample_id=None, image_id=None): """Retrieve and load (if necessary) the images stored in the SpatialExperiment object. - + Args: sample_id: - `sample_id=True`: Matches all samples. @@ -987,7 +987,7 @@ def img_raster(self, sample_id=None, image_id=None): if spis is None: return None - + if isinstance(spis, VirtualSpatialImage): return spis.img_raster() From e2ce200b07267d0d995573693c7e37f2e31eafeb Mon Sep 17 00:00:00 2001 From: keviny2 Date: Fri, 11 Apr 2025 15:50:01 -0700 Subject: [PATCH 3/5] Add return types --- src/spatialexperiment/SpatialExperiment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spatialexperiment/SpatialExperiment.py b/src/spatialexperiment/SpatialExperiment.py index 63afddd..2beb200 100644 --- a/src/spatialexperiment/SpatialExperiment.py +++ b/src/spatialexperiment/SpatialExperiment.py @@ -919,7 +919,7 @@ def img_source( sample_id: Union[str, bool, None] = None, image_id: Union[str, bool, None] = None, path=False, - ): + ) -> Union[str, Path, None, List[Union[str, Path]]]: """Retrieve the source(s) for images stored in the SpatialExperiment object. Args: @@ -958,7 +958,7 @@ def img_source( return img_sources - def img_raster(self, sample_id=None, image_id=None): + def img_raster(self, sample_id=None, image_id=None) -> Union[Image.Image, List[Image.Image], None]: """Retrieve and load (if necessary) the images stored in the SpatialExperiment object. Args: From 102ffdd1e0581a42e186832c39c2fbc5c80d3867 Mon Sep 17 00:00:00 2001 From: keviny2 Date: Fri, 11 Apr 2025 15:50:09 -0700 Subject: [PATCH 4/5] Add tests for img_raster --- tests/test_img_raster.py | 149 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tests/test_img_raster.py diff --git a/tests/test_img_raster.py b/tests/test_img_raster.py new file mode 100644 index 0000000..2389e79 --- /dev/null +++ b/tests/test_img_raster.py @@ -0,0 +1,149 @@ +import pytest +from copy import deepcopy +from pathlib import Path +from PIL import Image +import numpy as np +from spatialexperiment import construct_spatial_image_class +from spatialexperiment.SpatialImage import ( + LoadedSpatialImage, + StoredSpatialImage, + RemoteSpatialImage +) + +def test_loaded_spatial_image_img_raster(): + image = Image.open("tests/images/sample_image2.png") + spi_loaded = construct_spatial_image_class(image, is_url=False) + raster = spi_loaded.img_raster() + + assert isinstance(spi_loaded, LoadedSpatialImage) + assert isinstance(raster, Image.Image) + + np_image = np.zeros((100, 100, 3), dtype=np.uint8) + spi_loaded_np = construct_spatial_image_class(np_image) + raster_np = spi_loaded_np.img_raster() + + assert isinstance(spi_loaded_np, LoadedSpatialImage) + assert isinstance(raster_np, Image.Image) + + +def test_stored_spatial_image_img_raster(): + image_path = "tests/images/sample_image1.jpg" + spi_stored = construct_spatial_image_class(image_path, is_url=False) + raster = spi_stored.img_raster() + + assert isinstance(spi_stored, StoredSpatialImage) + assert isinstance(raster, Image.Image) + + +def test_remote_spatial_image_img_raster(monkeypatch): + image_url = "https://example.com/test_image.jpg" + spi_remote = construct_spatial_image_class(image_url, is_url=True) + + # Mock the _download_image method to return an image + mock_path = "tests/images/sample_image1.jpg" + monkeypatch.setattr(spi_remote, "_download_image", lambda: mock_path) + + raster = spi_remote.img_raster() + + assert isinstance(spi_remote, RemoteSpatialImage) + assert isinstance(raster, Image.Image) + + # Test LRU cache works as expected + num_calls = 0 + + def mock_download(): + num_calls += 1 + return mock_path + + monkeypatch.setattr(spi_remote, "_download_image", mock_download) + + raster2 = spi_remote.img_raster() + assert num_calls == 0 + assert raster2 is raster + + +def test_img_raster_no_img_data(spe): + tspe = deepcopy(spe) + tspe.img_data = None + assert not tspe.img_raster() + + +def test_img_raster_no_matches(spe): + with pytest.raises(ValueError): + res = spe.img_raster(sample_id="foo", image_id="foo") + + +def test_img_raster_both_str(spe): + res = spe.img_raster(sample_id="sample_1", image_id="dice") + expected_raster = spe.get_img(sample_id="sample_1", image_id="dice").img_raster() + + assert isinstance(res, Image.Image) + assert res == expected_raster + + +def test_img_raster_both_true(spe): + res = spe.img_raster(sample_id=True, image_id=True) + images = spe.get_img(sample_id=True, image_id=True) + expected_rasters = [img.img_raster() for img in images] + + assert isinstance(res, list) + assert res == expected_rasters + + +def test_img_raster_both_none(spe): + res = spe.img_raster(sample_id=None, image_id=None) + expected_raster = spe.get_img(sample_id=None, image_id=None).img_raster() + + assert isinstance(res, Image.Image) + assert res == expected_raster + + +def test_img_raster_sample_str_image_true(spe): + res = spe.img_raster(sample_id="sample_1", image_id=True) + images = spe.get_img(sample_id="sample_1", image_id=True) + expected_rasters = [img.img_raster() for img in images] + + assert isinstance(res, list) + assert res == expected_rasters + + +def test_img_raster_sample_true_image_str(spe): + res = spe.img_raster(sample_id=True, image_id="desert") + expected_raster = spe.get_img(sample_id=True, image_id="desert").img_raster() + + assert isinstance(res, Image.Image) + assert res == expected_raster + + +def test_img_raster_sample_str_image_none(spe): + res = spe.img_raster(sample_id="sample_1", image_id=None) + expected_raster = spe.get_img(sample_id="sample_1", image_id=None).img_raster() + + assert isinstance(res, Image.Image) + assert res == expected_raster + + +def test_img_raster_sample_none_image_str(spe): + res = spe.img_raster(sample_id=None, image_id="aurora") + expected_raster = spe.get_img(sample_id=None, image_id="aurora").img_raster() + + assert isinstance(res, Image.Image) + assert res == expected_raster + + +def test_img_raster_sample_true_image_none(spe): + res = spe.img_raster(sample_id=True, image_id=None) + images = spe.get_img(sample_id=True, image_id=None) + expected_rasters = [img.img_raster() for img in images] + + assert isinstance(res, list) + assert res == expected_rasters + + +def test_img_raster_sample_none_image_true(spe): + res = spe.img_raster(sample_id=None, image_id=True) + images = spe.get_img(sample_id=None, image_id=True) + expected_rasters = [img.img_raster() for img in images] + + assert isinstance(res, list) + assert res == expected_rasters From 4285e8afef2d922e52cec718386300ea626492c7 Mon Sep 17 00:00:00 2001 From: keviny2 Date: Fri, 11 Apr 2025 15:52:10 -0700 Subject: [PATCH 5/5] Remove Path import --- tests/test_img_raster.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_img_raster.py b/tests/test_img_raster.py index 2389e79..c53d153 100644 --- a/tests/test_img_raster.py +++ b/tests/test_img_raster.py @@ -1,6 +1,5 @@ import pytest from copy import deepcopy -from pathlib import Path from PIL import Image import numpy as np from spatialexperiment import construct_spatial_image_class