diff --git a/src/histolab/exceptions.py b/src/histolab/exceptions.py index 0c33bdc48..7b8bf817f 100644 --- a/src/histolab/exceptions.py +++ b/src/histolab/exceptions.py @@ -1,3 +1,22 @@ +# encoding: utf-8 + +# ------------------------------------------------------------------------ +# Copyright 2020 All Histolab Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------ + + class HistolabException(Exception): """Histolab custom exception main class""" diff --git a/src/histolab/scorer.py b/src/histolab/scorer.py index 70cf4b7a5..aaaaf3a9a 100644 --- a/src/histolab/scorer.py +++ b/src/histolab/scorer.py @@ -1,3 +1,21 @@ +# encoding: utf-8 + +# ------------------------------------------------------------------------ +# Copyright 2020 All Histolab Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------ + import operator from abc import abstractmethod @@ -16,7 +34,10 @@ @runtime_checkable class Scorer(Protocol): - """General scorer object""" + """General scorer object + + .. automethod:: __call__ + """ @abstractmethod def __call__(self, tile: Tile) -> float: @@ -24,7 +45,10 @@ def __call__(self, tile: Tile) -> float: class RandomScorer(Scorer): - """Implement a Scorer that returns a random float score between 0 and 1.""" + """Implement a Scorer that returns a random float score between 0 and 1. + + .. automethod:: __call__ + """ def __call__(self, tile: Tile) -> float: """Return the random score associated with the tile. @@ -51,6 +75,8 @@ class NucleiScorer(Scorer): .. math:: score = nuclei\_ratio \cdot tanh(tissue\_ratio) + + .. automethod:: __call__ """ def __call__(self, tile: Tile) -> float: diff --git a/src/histolab/tile.py b/src/histolab/tile.py index 7ef5c4d3d..815da8839 100644 --- a/src/histolab/tile.py +++ b/src/histolab/tile.py @@ -1,3 +1,21 @@ +# encoding: utf-8 + +# ------------------------------------------------------------------------ +# Copyright 2020 All Histolab Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------ + import os from pathlib import Path from typing import Callable, Union diff --git a/src/histolab/tiler.py b/src/histolab/tiler.py index 24404a9a8..096ab3300 100644 --- a/src/histolab/tiler.py +++ b/src/histolab/tiler.py @@ -1,3 +1,21 @@ +# encoding: utf-8 + +# ------------------------------------------------------------------------ +# Copyright 2020 All Histolab Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------ + import csv import os from abc import abstractmethod @@ -337,7 +355,7 @@ def __init__( check_tissue: bool = True, prefix: str = "", suffix: str = ".png", - max_iter: int = 1e4, + max_iter: int = int(1e4), ): super().__init__() @@ -387,7 +405,7 @@ def max_iter(self) -> int: return self._valid_max_iter @max_iter.setter - def max_iter(self, max_iter_: int = 1e4): + def max_iter(self, max_iter_: int = int(1e4)): if max_iter_ < self.n_tiles: raise ValueError( f"The maximum number of iterations ({max_iter_}) must be grater than or" diff --git a/src/histolab/types.py b/src/histolab/types.py index a7def11f9..b943133ab 100644 --- a/src/histolab/types.py +++ b/src/histolab/types.py @@ -20,3 +20,5 @@ CoordinatePair = namedtuple("CoordinatePair", ("x_ul", "y_ul", "x_br", "y_br")) Region = namedtuple("Region", ("index", "area", "bbox", "center")) + +CP = CoordinatePair diff --git a/src/histolab/util.py b/src/histolab/util.py index 5f408e0f0..2b6af61e2 100644 --- a/src/histolab/util.py +++ b/src/histolab/util.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------ + import functools import warnings from collections import deque diff --git a/tests/integration/test_tile.py b/tests/integration/test_tile.py index b62304720..ff9bf5f73 100644 --- a/tests/integration/test_tile.py +++ b/tests/integration/test_tile.py @@ -1,7 +1,7 @@ import pytest from histolab.tile import Tile -from histolab.types import CoordinatePair +from histolab.types import CP from ..fixtures import TILES @@ -21,7 +21,7 @@ class Describe_Tile: ), ) def it_knows_if_is_is_almost_white(self, tile_img, expected_result): - coords = CoordinatePair(0, 512, 0, 512) + coords = CP(0, 512, 0, 512) tile = Tile(tile_img, coords) is_almost_white = tile._is_almost_white diff --git a/tests/unit/filters/test_image_filters.py b/tests/unit/filters/test_image_filters.py index 80df91382..9cf4a266e 100644 --- a/tests/unit/filters/test_image_filters.py +++ b/tests/unit/filters/test_image_filters.py @@ -4,15 +4,14 @@ import numpy as np import PIL - from histolab.filters import image_filters as imf -from ...unitutil import NpArrayMock, PILImageMock, function_mock +from ...unitutil import PILIMG, NpArrayMock, function_mock class DescribeImageFilters: def it_calls_invert_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_invert = function_mock( request, "histolab.filters.image_filters_functional.invert" ) @@ -25,7 +24,7 @@ def it_calls_invert_filter_functional(self, request): assert type(invert(image)) == PIL.Image.Image def it_calls_pil_grayscale(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 grayscale_filter = function_mock(request, "PIL.ImageOps.grayscale") grayscale_filter.return_value = image grayscale = imf.RgbToGrayscale() @@ -36,7 +35,7 @@ def it_calls_pil_grayscale(self, request): assert type(grayscale(image)) == PIL.Image.Image def it_calls_rgb_to_hed_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_rgb_to_hed = function_mock( request, "histolab.filters.image_filters_functional.rgb_to_hed" ) @@ -49,7 +48,7 @@ def it_calls_rgb_to_hed_functional(self, request): assert type(rgb_to_hed(image)) == PIL.Image.Image def it_calls_hematoxylin_channel_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_hematoxylin_channel = function_mock( request, "histolab.filters.image_filters_functional.hematoxylin_channel" ) @@ -62,7 +61,7 @@ def it_calls_hematoxylin_channel_functional(self, request): assert type(hematoxylin_channel(image)) == PIL.Image.Image def it_calls_eosin_channel_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_eosin_channel = function_mock( request, "histolab.filters.image_filters_functional.eosin_channel" ) @@ -75,7 +74,7 @@ def it_calls_eosin_channel_functional(self, request): assert type(eosin_channel(image)) == PIL.Image.Image def it_calls_rgb_to_hsv_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_rgb_to_hsv = function_mock( request, "histolab.filters.image_filters_functional.rgb_to_hsv" ) @@ -88,7 +87,7 @@ def it_calls_rgb_to_hsv_functional(self, request): assert type(rgb_to_hsv(image)) == PIL.Image.Image def it_calls_stretch_contrast_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_stretch_contrast = function_mock( request, "histolab.filters.image_filters_functional.stretch_contrast" ) @@ -101,7 +100,7 @@ def it_calls_stretch_contrast_functional(self, request): assert type(stretch_contrast(image)) == PIL.Image.Image def it_calls_histogram_equalization_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_histogram_equalization = function_mock( request, "histolab.filters.image_filters_functional.histogram_equalization" ) @@ -114,7 +113,7 @@ def it_calls_histogram_equalization_functional(self, request): assert type(histogram_equalization(image)) == PIL.Image.Image def it_calls_adaptive_equalization_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_adaptive_equalization = function_mock( request, "histolab.filters.image_filters_functional.adaptive_equalization" ) @@ -127,7 +126,7 @@ def it_calls_adaptive_equalization_functional(self, request): assert type(adaptive_equalization(image)) == PIL.Image.Image def it_calls_local_equalization_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_local_equalization = function_mock( request, "histolab.filters.image_filters_functional.local_equalization" ) @@ -140,7 +139,7 @@ def it_calls_local_equalization_functional(self, request): assert type(local_equalization(image)) == PIL.Image.Image def it_calls_kmeans_segmentation_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_kmeans_segmentation = function_mock( request, "histolab.filters.image_filters_functional.kmeans_segmentation" ) @@ -153,7 +152,7 @@ def it_calls_kmeans_segmentation_functional(self, request): assert type(kmeans_segmentation(image)) == PIL.Image.Image def it_calls_rag_threshold_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_rag_threshold = function_mock( request, "histolab.filters.image_filters_functional.rag_threshold" ) @@ -166,7 +165,7 @@ def it_calls_rag_threshold_functional(self, request): assert type(rag_threshold(image)) == PIL.Image.Image def it_applies_hysteresis_threshold(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_hysteresis_threshold = function_mock( request, "histolab.filters.image_filters_functional.hysteresis_threshold" ) @@ -179,7 +178,7 @@ def it_applies_hysteresis_threshold(self, request): assert type(hysteresis_threshold(image)) == PIL.Image.Image def it_applies_hysteresis_threshold_mask_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_hysteresis_threshold_mask = function_mock( request, "histolab.filters.image_filters_functional.hysteresis_threshold_mask", @@ -193,7 +192,7 @@ def it_applies_hysteresis_threshold_mask_functional(self, request): assert type(hysteresis_threshold_mask(image)) == np.ndarray def it_calls_otsu_threshold_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_otsu_threshold = function_mock( request, "histolab.filters.image_filters_functional.otsu_threshold" ) @@ -206,7 +205,7 @@ def it_calls_otsu_threshold_functional(self, request): assert type(otsu_threshold(image)) == np.ndarray def it_calls_local_otsu_threshold_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_local_otsu_threshold = function_mock( request, "histolab.filters.image_filters_functional.local_otsu_threshold" ) @@ -219,7 +218,7 @@ def it_calls_local_otsu_threshold_functional(self, request): assert type(local_otsu_threshold(image)) == np.ndarray def it_calls_filter_entropy_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_filter_entropy = function_mock( request, "histolab.filters.image_filters_functional.filter_entropy" ) @@ -232,7 +231,7 @@ def it_calls_filter_entropy_functional(self, request): assert type(filter_entropy(image)) == np.ndarray def it_calls_canny_edges_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_canny_edges = function_mock( request, "histolab.filters.image_filters_functional.canny_edges" ) @@ -245,7 +244,7 @@ def it_calls_canny_edges_functional(self, request): assert type(canny_edges(image)) == np.ndarray def it_calls_grays_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_grays = function_mock( request, "histolab.filters.image_filters_functional.grays" ) @@ -258,7 +257,7 @@ def it_calls_grays_functional(self, request): assert type(grays(image)) == np.ndarray def it_calls_green_channel_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_green_channel_filter = function_mock( request, "histolab.filters.image_filters_functional.green_channel_filter" ) @@ -271,7 +270,7 @@ def it_calls_green_channel_filter_functional(self, request): assert type(green_channel_filter(image)) == np.ndarray def it_calls_red_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_red_filter = function_mock( request, "histolab.filters.image_filters_functional.red_filter" ) @@ -284,7 +283,7 @@ def it_calls_red_filter_functional(self, request): assert type(red_filter(image)) == np.ndarray def it_calls_red_pen_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_red_pen_filter = function_mock( request, "histolab.filters.image_filters_functional.red_pen_filter" ) @@ -297,7 +296,7 @@ def it_calls_red_pen_filter_functional(self, request): assert type(red_pen_filter(image)) == np.ndarray def it_calls_green_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_green_filter = function_mock( request, "histolab.filters.image_filters_functional.green_filter" ) @@ -310,7 +309,7 @@ def it_calls_green_filter_functional(self, request): assert type(green_filter(image)) == np.ndarray def it_calls_green_pen_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_green_pen_filter = function_mock( request, "histolab.filters.image_filters_functional.green_pen_filter" ) @@ -323,7 +322,7 @@ def it_calls_green_pen_filter_functional(self, request): assert type(green_pen_filter(image)) == np.ndarray def it_calls_blue_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_blue_filter = function_mock( request, "histolab.filters.image_filters_functional.blue_filter" ) @@ -336,7 +335,7 @@ def it_calls_blue_filter_functional(self, request): assert type(blue_filter(image)) == np.ndarray def it_calls_blue_pen_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_blue_pen_filter = function_mock( request, "histolab.filters.image_filters_functional.blue_pen_filter" ) @@ -349,7 +348,7 @@ def it_calls_blue_pen_filter_functional(self, request): assert type(blue_pen_filter(image)) == np.ndarray def it_calls_pen_marks_filter_functional(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_pen_marks = function_mock( request, "histolab.filters.image_filters_functional.pen_marks" ) @@ -373,7 +372,7 @@ def it_calls_np_to_pil(self, request): assert type(to_pil_image(array)) == PIL.Image.Image def it_calls_apply_mask_image(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 mask = NpArrayMock.ONES_500X500X4_BOOL util_apply_mask_image = function_mock(request, "histolab.util.apply_mask_image") util_apply_mask_image.return_value = PIL.Image.fromarray(np.array(image) * mask) @@ -385,7 +384,7 @@ def it_calls_apply_mask_image(self, request): assert type(util_apply_mask_image(image, mask)) == PIL.Image.Image def it_calls_lambda_filter(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image_np = np.array(image) fun_ = function_mock(request, "numpy.array") fun_.return_value = image_np @@ -397,7 +396,7 @@ def it_calls_lambda_filter(self, request): assert type(lambda_filter(image)) == np.ndarray def it_calls_yen_threshold(self, request): - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 F_yen_threshold = function_mock( request, "histolab.filters.image_filters_functional.yen_threshold" ) diff --git a/tests/unit/filters/test_morphological_fillters.py b/tests/unit/filters/test_morphological_fillters.py index 9922844e8..4e2003286 100644 --- a/tests/unit/filters/test_morphological_fillters.py +++ b/tests/unit/filters/test_morphological_fillters.py @@ -1,9 +1,9 @@ # encoding: utf-8 -import numpy as np import pytest -import skimage.morphology +import numpy as np +import skimage.morphology from histolab.filters import morphological_filters as mof from ...base import IMAGE1_RGB, IMAGE2_RGBA diff --git a/tests/unit/test_compositions.py b/tests/unit/test_compositions.py index af14a64f8..17abb3cca 100644 --- a/tests/unit/test_compositions.py +++ b/tests/unit/test_compositions.py @@ -1,6 +1,6 @@ -import numpy as np import pytest +import numpy as np from histolab.exceptions import FilterCompositionError from histolab.filters.compositions import ( FiltersComposition, @@ -63,7 +63,7 @@ def it_knows_tissue_areas_mask_slide_filters_composition( @pytest.mark.parametrize( "_cls, subclass", - ((Tile, _TileFiltersComposition), (Slide, _SlideFiltersComposition),), + ((Tile, _TileFiltersComposition), (Slide, _SlideFiltersComposition)), ) def it_can_dispatch_subclass_according_class_type(request, _cls, subclass): _init_ = initializer_mock(request, FiltersComposition) @@ -100,6 +100,9 @@ def it_raises_filtercompositionerror_if_class_is_none(request): assert str(err.value) == "cls_ parameter cannot be None" +# fixture components --------------------------------------------- + + @pytest.fixture def RgbToGrayscale_(request): return class_mock(request, "histolab.filters.image_filters.RgbToGrayscale") diff --git a/tests/unit/test_scorer.py b/tests/unit/test_scorer.py index 4d281ea1d..9d2082ba5 100644 --- a/tests/unit/test_scorer.py +++ b/tests/unit/test_scorer.py @@ -1,17 +1,10 @@ import numpy as np import PIL - from histolab import scorer from histolab.tile import Tile from ..base import COMPLEX_MASK -from ..unitutil import ( - PILImageMock, - function_mock, - instance_mock, - method_mock, - property_mock, -) +from ..unitutil import PILIMG, function_mock, instance_mock, method_mock, property_mock class DescribeScorers: @@ -22,11 +15,10 @@ def it_can_construct_randomscorer(self, request): score = random_scorer(tile) assert isinstance(random_scorer, scorer.RandomScorer) - assert isinstance(random_scorer, scorer.Scorer) assert type(score) == float def it_can_construct_nuclei_scorer(self, request): - image = PILImageMock.DIMS_10X10_RGB_RANDOM_COLOR + image = PILIMG.RGB_RANDOM_COLOR_10X10 tissue_ratio_ = property_mock(request, Tile, "tissue_ratio") tissue_ratio_.return_value = 0.7 apply_filters_ = method_mock(request, Tile, "apply_filters") @@ -50,6 +42,5 @@ def it_can_construct_nuclei_scorer(self, request): mask_difference_.call_args_list[0][0][1], COMPLEX_MASK ) assert isinstance(nuclei_scorer, scorer.NucleiScorer) - assert isinstance(nuclei_scorer, scorer.Scorer) assert type(score) == np.float64 assert score == 0 # to avoid float representation issues diff --git a/tests/unit/test_slide.py b/tests/unit/test_slide.py index bb47c7ccf..4d3b2ae06 100644 --- a/tests/unit/test_slide.py +++ b/tests/unit/test_slide.py @@ -6,22 +6,22 @@ from collections import namedtuple from unittest.mock import call +import pytest + import numpy as np import openslide import PIL -import pytest -from PIL import ImageShow - from histolab.exceptions import LevelError from histolab.filters.compositions import _SlideFiltersComposition from histolab.filters.image_filters import Compose from histolab.slide import Slide, SlideSet -from histolab.types import CoordinatePair, Region +from histolab.types import CP, Region from histolab.util import regions_from_binary_mask +from PIL import ImageShow from ..unitutil import ( ANY, - PILImageMock, + PILIMG, class_mock, dict_list_eq, function_mock, @@ -71,15 +71,69 @@ def or_it_has_wrong_processed_path(self, request): str(err.value) == "expected str, bytes or os.PathLike object, not NoneType" ) - def it_generates_the_correct_breadcumb(self, request, breadcumb_fixture): + @pytest.mark.parametrize( + "resampled_dims, dir_path, slide_path, proc_path, scale_factor, expected_path", ( - resampled_dims, - dir_path, - slide_path, - proc_path, - scale_factor, - expected_path, - ) = breadcumb_fixture + ( + (245, 123, 145, 99), + "/foo/bar/b/0/9", + "/foo/bar/myslide.svs", + "processed", + 64, + os.path.join("/foo/bar/b/0/9", "myslide-64x-245x123-145x99.png"), + ), + ( + (245, 123, 145, 99), + "/foo/bar/b/0/9", + "/foo/bar/myslide.svs", + "processed", + 32, + os.path.join("/foo/bar/b/0/9", "myslide-32x-245x123-145x99.png"), + ), + ( + (None, None, None, None), + "/foo/bar/b/0/9", + "/foo/bar/myslide.svs", + "processed", + 64, + os.path.join("/foo/bar/b/0/9", "myslide*.png"), + ), + ( + (None, 234, 192, None), + "/foo/bar/b/0/9", + "/foo/bar/myslide.svs", + "processed", + 64, + os.path.join("/foo/bar/b/0/9", "myslide-64x-Nonex234-192xNone.png"), + ), + ( + (123, 234, 192, None), + "/foo/bar/b/0/9", + "/foo/bar/myslide.svs", + "processed", + 64, + os.path.join("/foo/bar/b/0/9", "myslide-64x-123x234-192xNone.png"), + ), + ( + (None, None, 192, None), + "/foo/bar/b/0/9", + "/foo/bar/myslide.svs", + "processed", + 64, + os.path.join("/foo/bar/b/0/9", "myslide-64x-NonexNone-192xNone.png"), + ), + ), + ) + def it_generates_the_correct_breadcumb( + self, + request, + resampled_dims, + dir_path, + slide_path, + proc_path, + scale_factor, + expected_path, + ): _resampled_dimensions = method_mock(request, Slide, "_resampled_dimensions") _resampled_dimensions.return_value = resampled_dims slide = Slide(slide_path, proc_path) @@ -88,16 +142,44 @@ def it_generates_the_correct_breadcumb(self, request, breadcumb_fixture): assert _breadcumb == expected_path - def it_knows_its_name(self, slide_name_fixture): - _slide_path, expected_value = slide_name_fixture - slide = Slide(_slide_path, "processed/") + @pytest.mark.parametrize( + "slide_path, expected_value", + (("/foo/bar/myslide.svs", "myslide"), ("/foo/myslide.svs", "myslide")), + ) + def it_knows_its_name(self, slide_path, expected_value): + slide = Slide(slide_path, "processed/") name = slide.name assert name == expected_value - def it_knows_its_scaled_image_path(self, scaled_img_path_fixture, resampled_dims_): - slide_path, proc_path, slide_dims, expected_value = scaled_img_path_fixture + @pytest.mark.parametrize( + "slide_path, proc_path, slide_dims, expected_value", + ( + ( + "1.svs", + "1/p", + (345, 111, 333, 444), + os.path.join("1/p", "1-22x-345x111-333x444.png"), + ), + ( + "2.svs", + "2/p", + (345, 111, None, None), + os.path.join("2/p", "2-22x-345x111-NonexNone.png"), + ), + ( + "2.svs", + "2/p", + (345, 111, 123, 123), + os.path.join("2/p", "2-22x-345x111-123x123.png"), + ), + ("2.svs", "2/p", (None, None, None, None), os.path.join("2/p", "2*.png")), + ), + ) + def it_knows_its_scaled_image_path( + self, resampled_dims_, slide_path, proc_path, slide_dims, expected_value + ): resampled_dims_.return_value = slide_dims slide = Slide(slide_path, proc_path) @@ -121,7 +203,7 @@ def it_knows_its_thumbnails_path(self, resampled_dims_): def it_knows_its_dimensions(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -160,7 +242,7 @@ def but_it_raises_zero_division_error_when_scalefactor_is_0(self, dimensions_): def it_knows_its_resampled_array(self, tmpdir, resampled_dims_): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -173,7 +255,7 @@ def it_knows_its_resampled_array(self, tmpdir, resampled_dims_): def it_knows_its_thumbnail_size(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -184,7 +266,7 @@ def it_knows_its_thumbnail_size(self, tmpdir): def it_creates_a_correct_slide_object(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + image = PILIMG.RGBA_COLOR_50X50_155_0_0 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -216,7 +298,7 @@ def or_it_raises_an_PIL_exception(self, tmpdir): def it_can_resample_itself(self, tmpdir, resampled_dims_): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -245,7 +327,7 @@ def it_can_resample_itself(self, tmpdir, resampled_dims_): def it_resamples_with_the_correct_scale_factor(self, tmpdir, resampled_dims_): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -257,7 +339,7 @@ def it_resamples_with_the_correct_scale_factor(self, tmpdir, resampled_dims_): def it_can_save_scaled_image(self, tmpdir, resampled_dims_): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, os.path.join(tmp_path_, "processed")) @@ -272,7 +354,7 @@ def it_can_save_scaled_image(self, tmpdir, resampled_dims_): def it_can_save_thumbnail(self, tmpdir, resampled_dims_): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, os.path.join(tmp_path_, "processed")) @@ -344,7 +426,7 @@ def it_knows_its_biggest_tissue_box_mask( RemoveSmallObjects_, ): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -372,20 +454,21 @@ def it_knows_its_biggest_tissue_box_mask( region_coordinates_ = function_mock( request, "histolab.slide.region_coordinates" ) - region_coordinates_.return_values = CoordinatePair(0, 0, 2, 2) + region_coordinates_.return_values = CP(0, 0, 2, 2) polygon_to_mask_array_ = function_mock( request, "histolab.util.polygon_to_mask_array" ) - polygon_to_mask_array_( - (1000, 1000), CoordinatePair(0, 0, 2, 2) - ).return_value = [[True, True], [False, True]] + polygon_to_mask_array_((1000, 1000), CP(0, 0, 2, 2)).return_value = [ + [True, True], + [False, True], + ] biggest_mask_tissue_box = slide.biggest_tissue_box_mask region_coordinates_.assert_called_once_with(regions[0]) biggest_regions_.assert_called_once_with(slide, regions, n=1) polygon_to_mask_array_.assert_called_once_with( - (1000, 1000), CoordinatePair(x_ul=0, y_ul=0, x_br=2, y_br=2) + (1000, 1000), CP(x_ul=0, y_ul=0, x_br=2, y_br=2) ) np.testing.assert_almost_equal(biggest_mask_tissue_box, np.zeros((500, 500))) @@ -394,7 +477,7 @@ def it_knows_its_biggest_tissue_box_mask( ) def it_can_show_its_thumbnail(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -417,7 +500,7 @@ def but_it_raises_error_when_it_doesnt_exist(self): def it_knows_its_level_dimensions(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -428,7 +511,7 @@ def it_knows_its_level_dimensions(self, tmpdir): def but_it_raises_expection_when_level_does_not_exist(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -438,10 +521,17 @@ def but_it_raises_expection_when_level_does_not_exist(self, tmpdir): assert isinstance(err.value, LevelError) assert str(err.value) == "Level 3 not available. Number of available levels: 1" - def it_knows_if_coords_are_valid(self, valid_coords_fixture, tmpdir): - coords, expected_result = valid_coords_fixture + @pytest.mark.parametrize( + "coords, expected_result", + ( + (CP(0, 128, 0, 128), True), + (CP(800000, 90000, 8000010, 90010), False), + (CP(800000, 90000, -1, 90010), False), + ), + ) + def it_knows_if_coords_are_valid(self, coords, expected_result, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -453,7 +543,7 @@ def it_knows_if_coords_are_valid(self, valid_coords_fixture, tmpdir): def it_knows_its_levels(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -463,139 +553,6 @@ def it_knows_its_levels(self, tmpdir): assert type(levels) == list assert levels == [0] - # fixtures ------------------------------------------------------- - - @pytest.fixture(params=[("/a/b/mywsi.svs", ".svs"), ("/a/b/mywsi.34s", ".34s")]) - def slide_ext_fixture(self, request): - slide_path, expected_value = request.param - return slide_path, expected_value - - @pytest.fixture( - params=[ - ( - "/foo/bar/myslide.svs", - "/foo/bar/myslide/processed", - (345, 111, 333, 444), - os.path.join( - "/foo/bar/myslide/processed", "myslide-22x-345x111-333x444.png" - ), - ), - ( - "/foo/bar/myslide2.svs", - "/foo/bar/myslide/processed", - (345, 111, None, None), - os.path.join( - "/foo/bar/myslide/processed", "myslide2-22x-345x111-NonexNone.png" - ), - ), - ( - "/foo/bar/myslide2.svs", - "/foo/bar/myslide/processed", - (345, 111, 123, 123), - os.path.join( - "/foo/bar/myslide/processed", "myslide2-22x-345x111-123x123.png" - ), - ), - ( - "/foo/bar/myslide2.svs", - "/foo/bar/myslide/processed", - (None, None, None, None), - os.path.join("/foo/bar/myslide/processed", "myslide2*.png"), - ), - ] - ) - def scaled_img_path_fixture(self, request): - slide_path, proc_path, slide_dims, expected_value = request.param - return slide_path, proc_path, slide_dims, expected_value - - @pytest.fixture( - params=[ - ( - (245, 123, 145, 99), - "/foo/bar/b/0/9", - "/foo/bar/myslide.svs", - "processed", - 64, - os.path.join("/foo/bar/b/0/9", "myslide-64x-245x123-145x99.png"), - ), - ( - (245, 123, 145, 99), - "/foo/bar/b/0/9", - "/foo/bar/myslide.svs", - "processed", - 32, - os.path.join("/foo/bar/b/0/9", "myslide-32x-245x123-145x99.png"), - ), - ( - (None, None, None, None), - "/foo/bar/b/0/9", - "/foo/bar/myslide.svs", - "processed", - 64, - os.path.join("/foo/bar/b/0/9", "myslide*.png"), - ), - ( - (None, 234, 192, None), - "/foo/bar/b/0/9", - "/foo/bar/myslide.svs", - "processed", - 64, - os.path.join("/foo/bar/b/0/9", "myslide-64x-Nonex234-192xNone.png"), - ), - ( - (123, 234, 192, None), - "/foo/bar/b/0/9", - "/foo/bar/myslide.svs", - "processed", - 64, - os.path.join("/foo/bar/b/0/9", "myslide-64x-123x234-192xNone.png"), - ), - ( - (None, None, 192, None), - "/foo/bar/b/0/9", - "/foo/bar/myslide.svs", - "processed", - 64, - os.path.join("/foo/bar/b/0/9", "myslide-64x-NonexNone-192xNone.png"), - ), - ] - ) - def breadcumb_fixture(self, request): - ( - resampled_dims, - dir_path, - slide_path, - proc_path, - scale_factor, - expected_path, - ) = request.param - return ( - resampled_dims, - dir_path, - slide_path, - proc_path, - scale_factor, - expected_path, - ) - - @pytest.fixture( - params=[("/foo/bar/myslide.svs", "myslide"), ("/foo/myslide.svs", "myslide")] - ) - def slide_name_fixture(self, request): - slide_path, expceted_value = request.param - return slide_path, expceted_value - - @pytest.fixture( - params=[ - (CoordinatePair(0, 128, 0, 128), True), - (CoordinatePair(800000, 90000, 8000010, 90010), False), - (CoordinatePair(800000, 90000, -1, 90010), False), - ] - ) - def valid_coords_fixture(self, request): - coords, expected_result = request.param - return coords, expected_result - # fixture components --------------------------------------------- @pytest.fixture @@ -661,7 +618,7 @@ def it_can_constructs_slides(self, request, tmpdir): def it_knows_its_slides(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.svs"), "TIFF") slideset = SlideSet(tmp_path_, "proc", [".svs"]) @@ -708,9 +665,9 @@ def it_constructs_its_sequence_of_slides_to_help(self, request, Slide_, tmpdir): def it_knows_the_slides_dimensions(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.svs"), "TIFF") - image2 = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + image2 = PILIMG.RGBA_COLOR_50X50_155_0_0 image2.save(os.path.join(tmp_path_, "mywsi2.svs"), "TIFF") slideset = SlideSet(tmp_path_, "proc", [".svs"]) @@ -724,9 +681,9 @@ def it_knows_the_slides_dimensions(self, tmpdir): def it_knows_its_slides_dimensions_list(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.svs"), "TIFF") - image2 = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + image2 = PILIMG.RGBA_COLOR_50X50_155_0_0 image2.save(os.path.join(tmp_path_, "mywsi2.svs"), "TIFF") slideset = SlideSet(tmp_path_, "proc", [".svs"]) @@ -886,9 +843,9 @@ def it_can_save_thumbnails(self, request, tmpdir): def it_generates_slides_stats(self, total_slides_prop, tmpdir): total_slides_prop.return_value = 2 tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.svs"), "TIFF") - image2 = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + image2 = PILIMG.RGBA_COLOR_50X50_155_0_0 image2.save(os.path.join(tmp_path_, "mywsi2.svs"), "TIFF") slideset = SlideSet(tmp_path_, os.path.join("proc"), [".svs"]) diff --git a/tests/unit/test_tile.py b/tests/unit/test_tile.py index 37733e909..51f99aaaf 100644 --- a/tests/unit/test_tile.py +++ b/tests/unit/test_tile.py @@ -1,8 +1,8 @@ import os -import numpy as np import pytest +import numpy as np from histolab.filters.compositions import _TileFiltersComposition from histolab.filters.image_filters import Compose from histolab.tile import Tile @@ -11,7 +11,7 @@ from ..base import COMPLEX_MASK from ..unitutil import ( ANY, - PILImageMock, + PILIMG, class_mock, initializer_mock, method_mock, @@ -22,7 +22,7 @@ class Describe_Tile: def it_constructs_from_args(self, request): _init = initializer_mock(request, Tile) - _image = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + _image = PILIMG.RGBA_COLOR_50X50_155_0_0 _level = 0 _coords = CoordinatePair(0, 0, 50, 50) @@ -50,7 +50,7 @@ def it_knows_its_coords(self): assert coords == _coords def it_knows_its_image(self): - _image = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + _image = PILIMG.RGBA_COLOR_50X50_155_0_0 tile = Tile(_image, None, 0) image = tile.image @@ -66,7 +66,7 @@ def it_knows_its_level(self): def it_can_save_the_tile_image(self, tmpdir): tmp_path_ = os.path.join(tmpdir.mkdir("mydir"), "mytile.png") - _image = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + _image = PILIMG.RGBA_COLOR_50X50_155_0_0 tile = Tile(_image, None, 0) tile.save(tmp_path_) @@ -75,26 +75,36 @@ def it_can_save_the_tile_image(self, tmpdir): def and_it_can_save_the_tile_image_also_without_ext(self, tmpdir): tmp_path_ = os.path.join(tmpdir.mkdir("mydir"), "mytile") - _image = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + _image = PILIMG.RGBA_COLOR_50X50_155_0_0 tile = Tile(_image, None, 0) tile.save(tmp_path_) assert os.path.exists(tmp_path_ + ".png") + @pytest.mark.parametrize( + "almost_white, only_some_tissue, tissue_more_than_percent, expected_value", + ( + (False, True, True, True), + (False, False, False, False), + (True, False, False, False), + (False, True, False, False), + (False, False, True, False), + (True, True, True, False), + (True, False, True, False), + (True, True, False, False), + ), + ) def it_knows_if_it_has_enough_tissue( self, - has_enough_tissue_fixture, _is_almost_white, _has_only_some_tissue, _has_tissue_more_than_percent, + almost_white, + only_some_tissue, + tissue_more_than_percent, + expected_value, ): - ( - almost_white, - only_some_tissue, - tissue_more_than_percent, - expected_value, - ) = has_enough_tissue_fixture _is_almost_white.return_value = almost_white _has_only_some_tissue.return_value = only_some_tissue _has_tissue_more_than_percent.return_value = tissue_more_than_percent @@ -104,10 +114,26 @@ def it_knows_if_it_has_enough_tissue( assert has_enough_tissue == expected_value + @pytest.mark.parametrize( + "tissue_mask, percent, expected_value", + ( + ([[0, 1, 1, 0, 1], [0, 1, 1, 1, 1]], 80, False), + ([[0, 1, 1, 0, 1], [0, 1, 1, 1, 1]], 10, True), + ([[0, 1, 1, 0, 1], [0, 1, 1, 1, 1]], 100, False), + ([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], 100, False), + ([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], 10, True), + ([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], 80, True), + ([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], 60, True), + ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 60, False), + ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 10, False), + ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 3, False), + ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 80, False), + ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 100, False), + ), + ) def it_knows_if_has_tissue_more_than_percent( - self, request, has_tissue_more_than_percent_fixture + self, request, tissue_mask, percent, expected_value ): - tissue_mask, percent, expected_value = has_tissue_more_than_percent_fixture _tissue_mask = method_mock(request, Tile, "_tissue_mask") _tissue_mask.return_value = tissue_mask @@ -131,7 +157,7 @@ def it_calls_tile_tissue_mask_filters( _tissue_mask_filters.return_value = Compose( [RgbToGrayscale_, OtsuThreshold_, BinaryDilation_, BinaryFillHoles_] ) - image = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + image = PILIMG.RGBA_COLOR_50X50_155_0_0 tile = Tile(image, None, 0) tile._has_only_some_tissue() @@ -151,7 +177,7 @@ def it_knows_its_tissue_mask( filters = Compose( [RgbToGrayscale_, OtsuThreshold_, BinaryDilation_, BinaryFillHoles_] ) - image = PILImageMock.DIMS_50X50_RGBA_COLOR_155_0_0 + image = PILIMG.RGBA_COLOR_50X50_155_0_0 tile = Tile(image, None, 0) tissue_mask = tile._tissue_mask(filters) @@ -175,7 +201,7 @@ def it_knows_its_tissue_ratio( _tile_tissue_mask_filters.return_value = filters _call = method_mock(request, Compose, "__call__") _call.return_value = COMPLEX_MASK - image = PILImageMock.DIMS_10X10_RGB_RANDOM_COLOR + image = PILIMG.RGB_RANDOM_COLOR_10X10 tile = Tile(image, None, 0) tissue_ratio = tile.tissue_ratio @@ -186,8 +212,8 @@ def it_knows_its_tissue_ratio( assert tissue_ratio == 0.61 def it_knows_how_to_apply_filters_PIL(self, RgbToGrayscale_): - image_before = PILImageMock.DIMS_10X10_RGB_RANDOM_COLOR - image_after = PILImageMock.DIMS_10X10_GRAY_RANDOM + image_before = PILIMG.RGB_RANDOM_COLOR_10X10 + image_after = PILIMG.GRAY_RANDOM_10X10 RgbToGrayscale_.return_value = image_after tile = Tile(image_before, None, 0) @@ -200,8 +226,8 @@ def it_knows_how_to_apply_filters_PIL(self, RgbToGrayscale_): assert filtered_image.level == 0 def it_knows_how_to_apply_filters_np(self, OtsuThreshold_): - image_before = PILImageMock.DIMS_10X10_RGB_RANDOM_COLOR - image_after = PILImageMock.DIMS_10X10_GRAY_RANDOM + image_before = PILIMG.RGB_RANDOM_COLOR_10X10 + image_after = PILIMG.GRAY_RANDOM_10X10 OtsuThreshold_.return_value = np.array(image_after) tile = Tile(image_before, None, 0) @@ -213,54 +239,6 @@ def it_knows_how_to_apply_filters_np(self, OtsuThreshold_): assert filtered_image.coords is None assert filtered_image.level == 0 - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=[ - ([[0, 1, 1, 0, 1], [0, 1, 1, 1, 1]], 80, False), - ([[0, 1, 1, 0, 1], [0, 1, 1, 1, 1]], 10, True), - ([[0, 1, 1, 0, 1], [0, 1, 1, 1, 1]], 100, False), - ([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], 100, False), - ([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], 10, True), - ([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], 80, True), - ([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], 60, True), - ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 60, False), - ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 10, False), - ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 3, False), - ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 80, False), - ([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 100, False), - ] - ) - def has_tissue_more_than_percent_fixture(self, request): - tissue_mask, percent, expected_value = request.param - return tissue_mask, percent, expected_value - - @pytest.fixture( - params=[ - (False, True, True, True), - (False, False, False, False), - (True, False, False, False), - (False, True, False, False), - (False, False, True, False), - (True, True, True, False), - (True, False, True, False), - (True, True, False, False), - ] - ) - def has_enough_tissue_fixture(self, request): - ( - almost_white, - only_some_tissue, - tissue_more_than_percent, - expected_value, - ) = request.param - return ( - almost_white, - only_some_tissue, - tissue_more_than_percent, - expected_value, - ) - # fixture components --------------------------------------------- @pytest.fixture diff --git a/tests/unit/test_tiler.py b/tests/unit/test_tiler.py index ab26061de..a72f02793 100644 --- a/tests/unit/test_tiler.py +++ b/tests/unit/test_tiler.py @@ -2,20 +2,20 @@ import os from unittest.mock import call -import numpy as np import pytest +import numpy as np from histolab.exceptions import LevelError -from histolab.scorer import RandomScorer, Scorer +from histolab.scorer import RandomScorer from histolab.slide import Slide from histolab.tile import Tile from histolab.tiler import GridTiler, RandomTiler, ScoreTiler, Tiler -from histolab.types import CoordinatePair +from histolab.types import CP from ..unitutil import ( ANY, + PILIMG, NpArrayMock, - PILImageMock, function_mock, initializer_mock, instance_mock, @@ -28,22 +28,24 @@ class Describe_RandomTiler: def it_constructs_from_args(self, request): _init = initializer_mock(request, RandomTiler) - random_tiler = RandomTiler((512, 512), 10, 2, 7, True, "", ".png", 1e4) + random_tiler = RandomTiler((512, 512), 10, 2, 7, True, "", ".png", int(1e4)) - _init.assert_called_once_with(ANY, (512, 512), 10, 2, 7, True, "", ".png", 1e4) + _init.assert_called_once_with( + ANY, (512, 512), 10, 2, 7, True, "", ".png", int(1e4) + ) assert isinstance(random_tiler, RandomTiler) assert isinstance(random_tiler, Tiler) - def but_it_has_wrong_tile_size_value(self, request): + def but_it_has_wrong_tile_size_value(self): with pytest.raises(ValueError) as err: RandomTiler((512, -1), 10, 0) assert isinstance(err.value, ValueError) assert str(err.value) == "Tile size must be greater than 0 ((512, -1))" - def or_it_has_not_available_level_value(self, request, tmpdir): + def or_it_has_not_available_level_value(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGB_RANDOM_COLOR + image = PILIMG.RGB_RANDOM_COLOR_500X500 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -55,14 +57,14 @@ def or_it_has_not_available_level_value(self, request, tmpdir): assert isinstance(err.value, LevelError) assert str(err.value) == "Level 3 not available. Number of available levels: 1" - def or_it_has_negative_level_value(self, request): + def or_it_has_negative_level_value(self): with pytest.raises(LevelError) as err: RandomTiler((512, 512), 10, -1) assert isinstance(err.value, LevelError) assert str(err.value) == "Level cannot be negative (-1)" - def or_it_has_wrong_max_iter(self, request): + def or_it_has_wrong_max_iter(self): with pytest.raises(ValueError) as err: RandomTiler((512, 512), 10, 0, max_iter=3) @@ -73,9 +75,9 @@ def or_it_has_wrong_max_iter(self, request): "the maximum number of tiles (10)." ) - def or_it_has_wrong_seed(self, request, tmpdir): + def or_it_has_wrong_seed(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGB_RANDOM_COLOR + image = PILIMG.RGB_RANDOM_COLOR_500X500 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -88,7 +90,7 @@ def or_it_has_wrong_seed(self, request, tmpdir): assert str(err.value) == "Seed must be between 0 and 2**32 - 1" @pytest.mark.parametrize("tile_size", ((512, 512), (128, 128), (10, 10))) - def it_knows_its_tile_size(self, request, tile_size): + def it_knows_its_tile_size(self, tile_size): random_tiler = RandomTiler(tile_size, 10, 0) tile_size_ = random_tiler.tile_size @@ -97,7 +99,7 @@ def it_knows_its_tile_size(self, request, tile_size): assert tile_size_ == tile_size @pytest.mark.parametrize("max_iter", (1000, 10, 3000)) - def it_knows_its_max_iter(self, request, max_iter): + def it_knows_its_max_iter(self, max_iter): random_tiler = RandomTiler((128, 128), 10, 0, max_iter=max_iter) max_iter_ = random_tiler.max_iter @@ -105,22 +107,24 @@ def it_knows_its_max_iter(self, request, max_iter): assert type(max_iter_) == int assert max_iter_ == max_iter - def it_knows_its_tile_filename(self, request, tile_filename_fixture): + @pytest.mark.parametrize( + "level, prefix, suffix, tile_coords, tiles_counter, expected_filename", ( - tile_size, - n_tiles, - level, - seed, - check_tissue, - prefix, - suffix, - tile_coords, - tiles_counter, - expected_filename, - ) = tile_filename_fixture - random_tiler = RandomTiler( - tile_size, n_tiles, level, seed, check_tissue, prefix, suffix - ) + (3, "", ".png", CP(0, 512, 0, 512), 3, "tile_3_level3_0-512-0-512.png"), + ( + 0, + "folder/", + ".png", + CP(4, 127, 4, 127), + 10, + "folder/tile_10_level0_4-127-4-127.png", + ), + ), + ) + def it_knows_its_tile_filename( + self, level, prefix, suffix, tile_coords, tiles_counter, expected_filename + ): + random_tiler = RandomTiler((512, 512), 10, level, 7, True, prefix, suffix) _filename = random_tiler._tile_filename(tile_coords, tiles_counter) @@ -129,7 +133,7 @@ def it_knows_its_tile_filename(self, request, tile_filename_fixture): def it_can_generate_random_coordinates(self, request, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -149,7 +153,7 @@ def it_can_generate_random_coordinates(self, request, tmpdir): _box_mask_thumb.assert_called_once_with(random_tiler, slide) _tile_size.assert_has_calls([call((128, 128))]) _scale_coordinates.assert_called_once_with( - reference_coords=CoordinatePair(x_ul=0, y_ul=0, x_br=128, y_br=128), + reference_coords=CP(x_ul=0, y_ul=0, x_br=128, y_br=128), reference_size=(500, 500), target_size=(500, 500), ) @@ -163,7 +167,7 @@ def it_can_generate_random_coordinates(self, request, tmpdir): ) def it_knows_its_box_mask(self, request, tmpdir, check_tissue, expected_box): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -180,51 +184,51 @@ def it_knows_its_box_mask(self, request, tmpdir, check_tissue, expected_box): np.testing.assert_array_almost_equal(box_mask, expected_box) @pytest.mark.parametrize( - "coords1, coords2, check_tissue, has_enough_tissue, max_iter, expected_n_tiles", + "tile1, tile2, check_tissue, has_enough_tissue, max_iter, expected_value", ( ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), True, [True, True], 10, 2, ), ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), True, [True, False], 2, 1, ), ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(5900, 6000, 5900, 6000), # wrong coordinates + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(5900, 6000, 5900, 6000)), True, [True, True], 2, 2, ), ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), False, [True, True], 10, 2, ), ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), False, [False, False], 10, 2, ), ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), True, [False, False], 10, @@ -236,15 +240,15 @@ def it_can_generate_random_tiles( self, request, tmpdir, - coords1, - coords2, + tile1, + tile2, check_tissue, has_enough_tissue, max_iter, - expected_n_tiles, + expected_value, ): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -254,10 +258,8 @@ def it_can_generate_random_tiles( _random_tile_coordinates = method_mock( request, RandomTiler, "_random_tile_coordinates" ) - _random_tile_coordinates.side_effect = [coords1, coords2] * (max_iter // 2) - tile1 = Tile(image, coords1) - tile2 = Tile(image, coords2) - _extract_tile.side_effect = [tile1, tile2] * (max_iter // 2) + tiles = [tile1, tile2] + _extract_tile.side_effect = tiles * (max_iter // 2) random_tiler = RandomTiler( (10, 10), 2, level=0, max_iter=max_iter, check_tissue=check_tissue ) @@ -266,22 +268,20 @@ def it_can_generate_random_tiles( _random_tile_coordinates.assert_called_with(random_tiler, slide) assert _random_tile_coordinates.call_count <= random_tiler.max_iter - - _extract_tile.call_args_list == ([call(coords1, 0), call(coords2, 0)]) - assert len(generated_tiles) == expected_n_tiles - if expected_n_tiles == 2: - assert generated_tiles == [(tile1, coords1), (tile2, coords2)] + assert len(generated_tiles) == expected_value + for i, tile in enumerate(generated_tiles): + assert tile[0] == tiles[i] def it_can_extract_random_tiles(self, request, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, os.path.join(tmp_path_, "processed")) _random_tiles_generator = method_mock( request, RandomTiler, "_random_tiles_generator" ) - coords = CoordinatePair(0, 10, 0, 10) + coords = CP(0, 10, 0, 10) tile = Tile(image, coords) _random_tiles_generator.return_value = [(tile, coords), (tile, coords)] _tile_filename = method_mock(request, RandomTiler, "_tile_filename") @@ -303,62 +303,6 @@ def it_can_extract_random_tiles(self, request, tmpdir): os.path.join(tmp_path_, "processed", "tiles", "tile_1_level2_0-10-0-10.png") ) - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=( - ( - (512, 512), - 10, - 3, - 7, - True, - "", - ".png", - CoordinatePair(0, 512, 0, 512), - 3, - "tile_3_level3_0-512-0-512.png", - ), - ( - (512, 512), - 10, - 0, - 7, - True, - "folder/", - ".png", - CoordinatePair(4, 127, 4, 127), - 10, - "folder/tile_10_level0_4-127-4-127.png", - ), - ) - ) - def tile_filename_fixture(self, request): - ( - tile_size, - n_tiles, - level, - seed, - check_tissue, - prefix, - suffix, - tile_coords, - tiles_counter, - expected_filename, - ) = request.param - return ( - tile_size, - n_tiles, - level, - seed, - check_tissue, - prefix, - suffix, - tile_coords, - tiles_counter, - expected_filename, - ) - class Describe_GridTiler: def it_constructs_from_args(self, request): @@ -370,16 +314,16 @@ def it_constructs_from_args(self, request): assert isinstance(grid_tiler, GridTiler) assert isinstance(grid_tiler, Tiler) - def but_it_has_wrong_tile_size_value(self, request): + def but_it_has_wrong_tile_size_value(self): with pytest.raises(ValueError) as err: GridTiler((512, -1)) assert isinstance(err.value, ValueError) assert str(err.value) == "Tile size must be greater than 0 ((512, -1))" - def or_it_has_not_available_level_value(self, request, tmpdir): + def or_it_has_not_available_level_value(self, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGB_RANDOM_COLOR + image = PILIMG.RGB_RANDOM_COLOR_500X500 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -391,7 +335,7 @@ def or_it_has_not_available_level_value(self, request, tmpdir): assert isinstance(err.value, LevelError) assert str(err.value) == "Level 3 not available. Number of available levels: 1" - def or_it_has_negative_level_value(self, request): + def or_it_has_negative_level_value(self): with pytest.raises(LevelError) as err: GridTiler((512, 512), -1) @@ -399,29 +343,38 @@ def or_it_has_negative_level_value(self, request): assert str(err.value) == "Level cannot be negative (-1)" @pytest.mark.parametrize("tile_size", ((512, 512), (128, 128), (10, 10))) - def it_knows_its_tile_size(self, request, tile_size): - grid_tiler = GridTiler(tile_size, 10, 0) + def it_knows_its_tile_size(self, tile_size): + grid_tiler = GridTiler(tile_size, 10, True, 0) tile_size_ = grid_tiler.tile_size assert type(tile_size_) == tuple assert tile_size_ == tile_size - def it_knows_its_tile_filename(self, request, tile_filename_fixture): + @pytest.mark.parametrize( + "level, pixel_overlap, prefix, tile_coords, tiles_counter, expected_filename", ( - tile_size, - level, - check_tissue, - pixel_overlap, - prefix, - suffix, - tile_coords, - tiles_counter, - expected_filename, - ) = tile_filename_fixture - grid_tiler = GridTiler( - tile_size, level, check_tissue, pixel_overlap, prefix, suffix - ) + (3, 0, "", CP(0, 512, 0, 512), 3, "tile_3_level3_0-512-0-512.png"), + ( + 0, + 0, + "folder/", + CP(4, 127, 4, 127), + 10, + "folder/tile_10_level0_4-127-4-127.png", + ), + ), + ) + def it_knows_its_tile_filename( + self, + level, + pixel_overlap, + prefix, + tile_coords, + tiles_counter, + expected_filename, + ): + grid_tiler = GridTiler((512, 512), level, True, pixel_overlap, prefix, ".png") _filename = grid_tiler._tile_filename(tile_coords, tiles_counter) @@ -437,7 +390,7 @@ def it_knows_its_tile_filename(self, request, tile_filename_fixture): ) def it_knows_its_box_mask(self, request, tmpdir, check_tissue, expected_box): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -456,14 +409,14 @@ def it_knows_its_box_mask(self, request, tmpdir, check_tissue, expected_box): @pytest.mark.parametrize( "bbox_coordinates, pixel_overlap, expected_n_tiles_row", ( - (CoordinatePair(x_ul=0, y_ul=0, x_br=6060, y_br=1917), 0, 11), - (CoordinatePair(x_ul=0, y_ul=0, x_br=1921, y_br=2187), 0, 3), - (CoordinatePair(x_ul=0, y_ul=0, x_br=1921, y_br=2187), 128, 5), - (CoordinatePair(x_ul=0, y_ul=0, x_br=1921, y_br=2187), -128, 3), + (CP(x_ul=0, y_ul=0, x_br=6060, y_br=1917), 0, 11), + (CP(x_ul=0, y_ul=0, x_br=1921, y_br=2187), 0, 3), + (CP(x_ul=0, y_ul=0, x_br=1921, y_br=2187), 128, 5), + (CP(x_ul=0, y_ul=0, x_br=1921, y_br=2187), -128, 3), ), ) def it_can_calculate_n_tiles_row( - self, request, bbox_coordinates, pixel_overlap, expected_n_tiles_row + self, bbox_coordinates, pixel_overlap, expected_n_tiles_row ): grid_tiler = GridTiler((512, 512), 2, True, pixel_overlap) @@ -475,14 +428,14 @@ def it_can_calculate_n_tiles_row( @pytest.mark.parametrize( "bbox_coordinates, pixel_overlap, expected_n_tiles_column", ( - (CoordinatePair(x_ul=0, y_ul=0, x_br=6060, y_br=1917), 0, 3), - (CoordinatePair(x_ul=0, y_ul=0, x_br=6060, y_br=1917), -1, 3), - (CoordinatePair(x_ul=0, y_ul=0, x_br=1921, y_br=2187), 0, 4), - (CoordinatePair(x_ul=0, y_ul=0, x_br=1921, y_br=2187), 128, 5), + (CP(x_ul=0, y_ul=0, x_br=6060, y_br=1917), 0, 3), + (CP(x_ul=0, y_ul=0, x_br=6060, y_br=1917), -1, 3), + (CP(x_ul=0, y_ul=0, x_br=1921, y_br=2187), 0, 4), + (CP(x_ul=0, y_ul=0, x_br=1921, y_br=2187), 128, 5), ), ) def it_can_calculate_n_tiles_column( - self, request, bbox_coordinates, pixel_overlap, expected_n_tiles_column + self, bbox_coordinates, pixel_overlap, expected_n_tiles_column ): grid_tiler = GridTiler((512, 512), 2, True, pixel_overlap) @@ -491,16 +444,58 @@ def it_can_calculate_n_tiles_column( assert type(n_tiles_column) == int assert n_tiles_column == expected_n_tiles_column - def it_can_generate_grid_tiles(self, request, tmpdir, grid_tiles_fixture): + @pytest.mark.parametrize( + "tile1, tile2, check_tissue, has_enough_tissue, expected_n_tiles", ( - coords1, - coords2, - check_tissue, - has_enough_tissue, - expected_n_tiles, - ) = grid_tiles_fixture + ( + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + True, + [True, True], + 2, + ), + ( + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + False, + [True, True], + 2, + ), + ( + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + False, + [False, False], + 2, + ), + ( + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + True, + [False, False], + 0, + ), + ( + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + Tile(PILIMG.RGBA_COLOR_500X500_155_249_240, CP(0, 10, 0, 10)), + True, + [True, False], + 1, + ), + ), + ) + def it_can_generate_grid_tiles( + self, + request, + tmpdir, + tile1, + tile2, + check_tissue, + has_enough_tissue, + expected_n_tiles, + ): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -510,29 +505,24 @@ def it_can_generate_grid_tiles(self, request, tmpdir, grid_tiles_fixture): _grid_coordinates_generator = method_mock( request, GridTiler, "_grid_coordinates_generator" ) - _grid_coordinates_generator.return_value = [coords1, coords2] - tile1 = Tile(image, coords1) - tile2 = Tile(image, coords2) + _grid_coordinates_generator.return_value = [CP(0, 10, 0, 10), CP(0, 10, 0, 10)] _extract_tile.side_effect = [tile1, tile2] grid_tiler = GridTiler((10, 10), level=0, check_tissue=check_tissue) + tiles = [tile1, tile2] generated_tiles = list(grid_tiler._grid_tiles_generator(slide)) _grid_coordinates_generator.assert_called_once_with(grid_tiler, slide) assert _extract_tile.call_args_list == ( - [call(slide, coords1, 0), call(slide, coords2, 0)] + [call(slide, CP(0, 10, 0, 10), 0), call(slide, CP(0, 10, 0, 10), 0)] ) assert len(generated_tiles) == expected_n_tiles - if expected_n_tiles == 2: - assert generated_tiles == [(tile1, coords1), (tile2, coords2)] - if expected_n_tiles == 1: - assert generated_tiles == [(tile1, coords1)] - if expected_n_tiles == 0: - assert generated_tiles == [] + for i, tile in enumerate(generated_tiles): + assert tile[0] == tiles[i] def but_with_wrong_coordinates(self, request, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") @@ -541,8 +531,8 @@ def but_with_wrong_coordinates(self, request, tmpdir): _grid_coordinates_generator = method_mock( request, GridTiler, "_grid_coordinates_generator" ) - coords1 = CoordinatePair(600, 610, 600, 610) - coords2 = CoordinatePair(0, 10, 0, 10) + coords1 = CP(600, 610, 600, 610) + coords2 = CP(0, 10, 0, 10) _grid_coordinates_generator.return_value = [coords1, coords2] grid_tiler = GridTiler((10, 10), level=0, check_tissue=False) @@ -557,11 +547,11 @@ def but_with_wrong_coordinates(self, request, tmpdir): def and_doesnt_raise_error_with_wrong_coordinates(self, request, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, "processed") - coords = CoordinatePair(5800, 6000, 5800, 6000) + coords = CP(5800, 6000, 5800, 6000) _grid_coordinates_generator = method_mock( request, GridTiler, "_grid_coordinates_generator" ) @@ -574,12 +564,12 @@ def and_doesnt_raise_error_with_wrong_coordinates(self, request, tmpdir): def it_can_extract_grid_tiles(self, request, tmpdir): tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, os.path.join(tmp_path_, "processed")) _grid_tiles_generator = method_mock(request, GridTiler, "_grid_tiles_generator") - coords = CoordinatePair(0, 10, 0, 10) + coords = CP(0, 10, 0, 10) tile = Tile(image, coords) _grid_tiles_generator.return_value = [(tile, coords), (tile, coords)] _tile_filename = method_mock(request, GridTiler, "_tile_filename") @@ -604,107 +594,6 @@ def it_can_extract_grid_tiles(self, request, tmpdir): os.path.join(tmp_path_, "processed", "tiles", "tile_1_level2_0-10-0-10.png") ) - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=( - ( - (512, 512), - 3, - True, - 0, - "", - ".png", - CoordinatePair(0, 512, 0, 512), - 3, - "tile_3_level3_0-512-0-512.png", - ), - ( - (512, 512), - 0, - True, - 0, - "folder/", - ".png", - CoordinatePair(4, 127, 4, 127), - 10, - "folder/tile_10_level0_4-127-4-127.png", - ), - ) - ) - def tile_filename_fixture(self, request): - ( - tile_size, - level, - check_tissue, - pixel_overlap, - prefix, - suffix, - tile_coords, - tiles_counter, - expected_filename, - ) = request.param - return ( - tile_size, - level, - check_tissue, - pixel_overlap, - prefix, - suffix, - tile_coords, - tiles_counter, - expected_filename, - ) - - @pytest.fixture( - params=( - ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), - True, - [True, True], - 2, - ), - ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), - False, - [True, True], - 2, - ), - ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), - False, - [False, False], - 2, - ), - ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), - True, - [False, False], - 0, - ), - ( - CoordinatePair(0, 10, 0, 10), - CoordinatePair(0, 10, 0, 10), - True, - [True, False], - 1, - ), - ) - ) - def grid_tiles_fixture(self, request): - ( - coords1, - coords2, - check_tissue, - has_enough_tissue, - expected_n_tiles, - ) = request.param - return (coords1, coords2, check_tissue, has_enough_tissue, expected_n_tiles) - class Describe_ScoreTiler: def it_constructs_from_args(self, request): @@ -718,17 +607,16 @@ def it_constructs_from_args(self, request): assert isinstance(grid_tiler, GridTiler) assert isinstance(grid_tiler, Tiler) - def it_knows_its_scorer(self, request): + def it_knows_its_scorer(self): random_scorer = RandomScorer() score_tiler = ScoreTiler(random_scorer, (512, 512), 4, 0) scorer_ = score_tiler.scorer assert callable(scorer_) - assert isinstance(scorer_, Scorer) assert isinstance(scorer_, RandomScorer) - def it_knows_its_n_tiles(self, request): + def it_knows_its_n_tiles(self): n_tiles = 4 score_tiler = ScoreTiler(RandomScorer(), (512, 512), n_tiles, 0) @@ -739,8 +627,8 @@ def it_knows_its_n_tiles(self, request): def it_can_calculate_scores(self, request): slide = instance_mock(request, Slide) - coords = CoordinatePair(0, 10, 0, 10) - image = PILImageMock.DIMS_500X500_RGB_RANDOM_COLOR + coords = CP(0, 10, 0, 10) + image = PILIMG.RGB_RANDOM_COLOR_500X500 tile = Tile(image, coords) _grid_tiles_generator = method_mock( request, ScoreTiler, "_grid_tiles_generator" @@ -761,7 +649,7 @@ def it_can_calculate_scores(self, request): assert type(scores) == list assert type(scores[0]) == tuple assert type(scores[0][0]) == float - assert type(scores[0][1]) == CoordinatePair + assert type(scores[0][1]) == CP assert scores == [(0.5, coords), (0.7, coords)] def but_it_raises_runtimeerror_if_no_tiles_are_extracted(self, request): @@ -784,7 +672,7 @@ def but_it_raises_runtimeerror_if_no_tiles_are_extracted(self, request): ) def it_can_scale_scores(self): - coords = [CoordinatePair(0, 10 * i, 0, 10) for i in range(3)] + coords = [CP(0, 10 * i, 0, 10) for i in range(3)] scores = [0.3, 0.4, 0.7] scores_ = list(zip(scores, coords)) score_tiler = ScoreTiler(None, (10, 10), 2, 0) @@ -798,13 +686,56 @@ def it_can_scale_scores(self): assert round(score, 5) == round(expected_score, 5) assert coords_ == expected_coords - def it_can_calculate_highest_score_tiles( - self, request, highest_score_tiles_fixture - ): - n_tiles, expected_highest_score_tiles = highest_score_tiles_fixture + @pytest.mark.parametrize( + "n_tiles, expected_value", + ( + ( + 0, + ( + [ + (0.8, CP(0, 10, 0, 10)), + (0.7, CP(0, 10, 0, 10)), + (0.5, CP(0, 10, 0, 10)), + (0.2, CP(0, 10, 0, 10)), + (0.1, CP(0, 10, 0, 10)), + ], + [ + (1.0, CP(0, 10, 0, 10)), + (0.857142857142857, CP(0, 10, 0, 10)), + (0.5714285714285714, CP(0, 10, 0, 10)), + (0.14285714285714285, CP(0, 10, 0, 10)), + (0.0, CP(0, 10, 0, 10)), + ], + ), + ), + ( + 2, + ( + [(0.8, CP(0, 10, 0, 10)), (0.7, CP(0, 10, 0, 10))], + [(1.0, CP(0, 10, 0, 10)), (0.857142857142857, CP(0, 10, 0, 10))], + ), + ), + ( + 3, + ( + [ + (0.8, CP(0, 10, 0, 10)), + (0.7, CP(0, 10, 0, 10)), + (0.5, CP(0, 10, 0, 10)), + ], + [ + (1.0, CP(0, 10, 0, 10)), + (0.857142857142857, CP(0, 10, 0, 10)), + (0.5714285714285714, CP(0, 10, 0, 10)), + ], + ), + ), + ), + ) + def it_can_calculate_highest_score_tiles(self, request, n_tiles, expected_value): slide = instance_mock(request, Slide) _scores = method_mock(request, ScoreTiler, "_scores") - coords = CoordinatePair(0, 10, 0, 10) + coords = CP(0, 10, 0, 10) _scores.return_value = [ (0.7, coords), (0.5, coords), @@ -818,12 +749,12 @@ def it_can_calculate_highest_score_tiles( highest_score_tiles = score_tiler._highest_score_tiles(slide) _scores.assert_called_once_with(score_tiler, slide) - assert highest_score_tiles == expected_highest_score_tiles + assert highest_score_tiles == expected_value def but_it_raises_error_with_negative_n_tiles_value(self, request): slide = instance_mock(request, Slide) _scores = method_mock(request, ScoreTiler, "_scores") - coords = CoordinatePair(0, 10, 0, 10) + coords = CP(0, 10, 0, 10) _scores.return_value = [ (0.7, coords), (0.5, coords), @@ -843,12 +774,12 @@ def but_it_raises_error_with_negative_n_tiles_value(self, request): def it_can_extract_score_tiles(self, request, tmpdir): _extract_tile = method_mock(request, Slide, "extract_tile") tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, os.path.join(tmp_path_, "processed")) _highest_score_tiles = method_mock(request, ScoreTiler, "_highest_score_tiles") - coords = CoordinatePair(0, 10, 0, 10) + coords = CP(0, 10, 0, 10) tile = Tile(image, coords) _extract_tile.return_value = tile _highest_score_tiles.return_value = ( @@ -884,7 +815,7 @@ def it_can_extract_score_tiles(self, request, tmpdir): def it_can_save_report(self, request, tmpdir): tmp_path_ = tmpdir.mkdir("path") - coords = CoordinatePair(0, 10, 0, 10) + coords = CP(0, 10, 0, 10) highest_score_tiles = [(0.8, coords), (0.7, coords)] highest_scaled_score_tiles = [(0.1, coords), (0.0, coords)] filenames = ["tile0.png", "tile1.png"] @@ -912,12 +843,12 @@ def it_can_save_report(self, request, tmpdir): def it_can_extract_score_tiles_and_save_report(self, request, tmpdir): _extract_tile = method_mock(request, Slide, "extract_tile") tmp_path_ = tmpdir.mkdir("myslide") - image = PILImageMock.DIMS_500X500_RGBA_COLOR_155_249_240 + image = PILIMG.RGBA_COLOR_500X500_155_249_240 image.save(os.path.join(tmp_path_, "mywsi.png"), "PNG") slide_path = os.path.join(tmp_path_, "mywsi.png") slide = Slide(slide_path, os.path.join(tmp_path_, "processed")) _highest_score_tiles = method_mock(request, ScoreTiler, "_highest_score_tiles") - coords = CoordinatePair(0, 10, 0, 10) + coords = CP(0, 10, 0, 10) tile = Tile(image, coords) _extract_tile.return_value = tile _highest_score_tiles.return_value = ( @@ -956,60 +887,3 @@ def it_can_extract_score_tiles_and_save_report(self, request, tmpdir): [(0.8, coords), (0.7, coords)], [f"tile_{i}_level2_0-10-0-10.png" for i in range(2)], ) - - # fixtures ------------------------------------------------------- - - @pytest.fixture( - params=( - ( - 0, - ( - [ - (0.8, CoordinatePair(0, 10, 0, 10)), - (0.7, CoordinatePair(0, 10, 0, 10)), - (0.5, CoordinatePair(0, 10, 0, 10)), - (0.2, CoordinatePair(0, 10, 0, 10)), - (0.1, CoordinatePair(0, 10, 0, 10)), - ], - [ - (1.0, CoordinatePair(0, 10, 0, 10)), - (0.857142857142857, CoordinatePair(0, 10, 0, 10)), - (0.5714285714285714, CoordinatePair(0, 10, 0, 10)), - (0.14285714285714285, CoordinatePair(0, 10, 0, 10)), - (0.0, CoordinatePair(0, 10, 0, 10)), - ], - ), - ), - ( - 2, - ( - [ - (0.8, CoordinatePair(0, 10, 0, 10)), - (0.7, CoordinatePair(0, 10, 0, 10)), - ], - [ - (1.0, CoordinatePair(0, 10, 0, 10)), - (0.857142857142857, CoordinatePair(0, 10, 0, 10)), - ], - ), - ), - ( - 3, - ( - [ - (0.8, CoordinatePair(0, 10, 0, 10)), - (0.7, CoordinatePair(0, 10, 0, 10)), - (0.5, CoordinatePair(0, 10, 0, 10)), - ], - [ - (1.0, CoordinatePair(0, 10, 0, 10)), - (0.857142857142857, CoordinatePair(0, 10, 0, 10)), - (0.5714285714285714, CoordinatePair(0, 10, 0, 10)), - ], - ), - ), - ) - ) - def highest_score_tiles_fixture(self, request): - (n_tiles, expected_highest_score_tiles) = request.param - return (n_tiles, expected_highest_score_tiles) diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 2fe07d829..afff7b0e2 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -7,7 +7,7 @@ import pytest import numpy as np -from histolab.types import CoordinatePair, Region +from histolab.types import CP, Region from histolab.util import ( apply_mask_image, lazyproperty, @@ -39,8 +39,8 @@ @pytest.mark.parametrize( "ref_coords, ref_size, target_size, expected_value", ( - (CoordinatePair(0, 2, 4, 5), (10, 10), (5, 5), (0, 1, 2, 2)), - (CoordinatePair(90, 112, 124, 125), (100, 100), (95, 95), (85, 106, 117, 118)), + (CP(0, 2, 4, 5), (10, 10), (5, 5), (0, 1, 2, 2)), + (CP(90, 112, 124, 125), (100, 100), (95, 95), (85, 106, 117, 118)), ), ) def test_scale_coordinates(ref_coords, ref_size, target_size, expected_value): @@ -48,7 +48,7 @@ def test_scale_coordinates(ref_coords, ref_size, target_size, expected_value): scaled_coords = scale_coordinates(ref_coords, ref_size, target_size) - assert scaled_coords == CoordinatePair(x_ul, y_ul, x_br, y_br) + assert scaled_coords == CP(x_ul, y_ul, x_br, y_br) @pytest.mark.parametrize( @@ -140,9 +140,9 @@ def test_apply_mask_image(img, mask, expected_array): @pytest.mark.parametrize( "dims, vertices, expected_array", ( - ((5, 5), CoordinatePair(0, 3, 2, 5), "mask-arrays/polygon-to-mask-array-0325"), - ((5, 6), CoordinatePair(1, 0, 2, 0), "mask-arrays/polygon-to-mask-array-1020"), - ((5, 5), CoordinatePair(2, 1, 4, 3), "mask-arrays/polygon-to-mask-array-2143"), + ((5, 5), CP(0, 3, 2, 5), "mask-arrays/polygon-to-mask-array-0325"), + ((5, 6), CP(1, 0, 2, 0), "mask-arrays/polygon-to-mask-array-1020"), + ((5, 5), CP(2, 1, 4, 3), "mask-arrays/polygon-to-mask-array-2143"), ), ) def test_util_polygon_to_mask_array(dims, vertices, expected_array): @@ -157,7 +157,7 @@ def test_region_coordinates(): region = Region(index=0, area=14, bbox=(0, 1, 1, 2), center=(0.5, 0.5)) region_coords_ = region_coordinates(region) - assert region_coords_ == CoordinatePair(x_ul=1, y_ul=0, x_br=2, y_br=1) + assert region_coords_ == CP(x_ul=1, y_ul=0, x_br=2, y_br=1) class DescribeLazyPropertyDecorator: diff --git a/tests/unitutil.py b/tests/unitutil.py index faf472c7e..cead96d7d 100644 --- a/tests/unitutil.py +++ b/tests/unitutil.py @@ -94,22 +94,20 @@ def is_win32(): class PILImageMock: - DIMS_500X500_RGBA_COLOR_155_249_240 = PIL.Image.new( + RGBA_COLOR_500X500_155_249_240 = PIL.Image.new( "RGBA", size=(500, 500), color=(155, 249, 240) ) - DIMS_50X50_RGBA_COLOR_155_0_0 = PIL.Image.new( - "RGBA", size=(50, 50), color=(155, 0, 0) - ) + RGBA_COLOR_50X50_155_0_0 = PIL.Image.new("RGBA", size=(50, 50), color=(155, 0, 0)) - DIMS_500X500_RGB_RANDOM_COLOR = PIL.Image.fromarray( + RGB_RANDOM_COLOR_500X500 = PIL.Image.fromarray( (np.random.rand(500, 500, 3) * 255).astype("uint8") ).convert("RGB") - DIMS_10X10_RGB_RANDOM_COLOR = PIL.Image.fromarray( + RGB_RANDOM_COLOR_10X10 = PIL.Image.fromarray( (np.random.rand(10, 10, 3) * 255).astype("uint8") ).convert("RGB") - DIMS_10X10_GRAY_RANDOM = PIL.Image.fromarray( + GRAY_RANDOM_10X10 = PIL.Image.fromarray( (np.random.rand(10, 10) * 255).astype("uint8") ) @@ -119,3 +117,6 @@ class NpArrayMock: ONES_500X500X4_BOOL = np.ones([500, 500, 4], dtype="bool") ONES_500X500_BOOL = np.ones([500, 500], dtype="bool") RANDOM_500X500_BOOL = np.random.rand(500, 500) > 0.5 + + +PILIMG = PILImageMock