Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make SlideSet iterable #177

Merged
merged 4 commits into from Nov 22, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 30 additions & 20 deletions src/histolab/slide.py
Expand Up @@ -25,7 +25,7 @@
import os
import pathlib
from functools import lru_cache
from typing import List, Tuple, Union
from typing import Iterator, List, Tuple, Union

import ntpath
import numpy as np
Expand Down Expand Up @@ -460,6 +460,30 @@ def __init__(
self._processed_path = processed_path
self._valid_extensions = valid_extensions

def __iter__(self) -> Iterator[Slide]:
"""Slides of the slideset

Returns
-------
generator of `Slide` objects.
"""
return iter(
[
Slide(os.path.join(self._slides_path, _path), self._processed_path)
for _path in os.listdir(self._slides_path)
if os.path.splitext(_path)[1] in self._valid_extensions
]
)

def __len__(self) -> int:
"""Total number of the slides of this Slideset

Returns
-------
int representing the number of the Slides.
alessiamarcolini marked this conversation as resolved.
Show resolved Hide resolved
"""
return len(list(self.__iter__()))

# ---public interface methods and properties---

def save_scaled_slides(self, scale_factor: int = 32, n: int = 0) -> None:
Expand All @@ -475,7 +499,7 @@ def save_scaled_slides(self, scale_factor: int = 32, n: int = 0) -> None:
"""
os.makedirs(self._processed_path, exist_ok=True)
n = self.total_slides if (n > self.total_slides or n == 0) else n
for slide in self.slides[:n]:
for slide in list(self.__iter__())[:n]:
slide.save_scaled_image(scale_factor)

def save_thumbnails(self, n: int = 0) -> None:
Expand All @@ -489,23 +513,9 @@ def save_thumbnails(self, n: int = 0) -> None:
"""
os.makedirs(self._processed_path, exist_ok=True)
n = self.total_slides if (n > self.total_slides or n == 0) else n
for slide in self.slides[:n]:
for slide in list(self.__iter__())[:n]:
slide.save_thumbnail()

@lazyproperty
def slides(self) -> List[Slide]:
alessiamarcolini marked this conversation as resolved.
Show resolved Hide resolved
"""Retrieve all the slides of the slideset

Returns
----------
slides: list, list of `Slide` objects
"""
return [
Slide(os.path.join(self._slides_path, _path), self._processed_path)
for _path in os.listdir(self._slides_path)
if os.path.splitext(_path)[1] in self._valid_extensions
]

@lazyproperty
def slides_stats(self) -> dict:
"""Retrieve statistic/graphs of slides files contained in the dataset.
Expand Down Expand Up @@ -536,7 +546,7 @@ def total_slides(self) -> int:
n: int
Number of slides.
"""
return len(self.slides)
return self.__len__()

# ---private interface methods and properties---

Expand Down Expand Up @@ -591,9 +601,9 @@ def _slides_dimensions(self) -> List[dict]:
"height": slide.dimensions[1],
"size": slide.dimensions[0] * slide.dimensions[1],
}
for slide in self.slides
for slide in list(self.__iter__())
]

@lazyproperty
def _slides_dimensions_list(self):
return [slide.dimensions for slide in self.slides]
return [slide.dimensions for slide in list(self.__iter__())]
69 changes: 29 additions & 40 deletions tests/unit/test_slide.py
Expand Up @@ -4,7 +4,6 @@
import math
import os
from collections import namedtuple
from unittest.mock import call

import numpy as np
import openslide
Expand Down Expand Up @@ -603,16 +602,15 @@ def it_constructs_from_args(self, request):
)
assert isinstance(slideset, SlideSet)

def it_can_constructs_slides(self, request, tmpdir):
def it_can_constructs_slides(self, request, tmpdir, Slide_):
tmp_path_ = tmpdir.mkdir("myslide")
slides_ = tuple(instance_mock(request, Slide) for _ in range(10))
_slides_ = property_mock(request, SlideSet, "slides")
_slides_.return_value = slides_
slides_ = method_mock(request, SlideSet, "__iter__")
slides_.return_value = [Slide_ for _ in range(10)]
slideset = SlideSet(tmp_path_, os.path.join(tmp_path_, "b"), [".svs"])

slides = slideset.slides
slides = slideset.__iter__()

_slides_.assert_called_once_with()
slides_.assert_called_once_with(slideset)
assert len(slides) == 10

def it_knows_its_slides(self, tmpdir):
Expand All @@ -621,46 +619,37 @@ def it_knows_its_slides(self, tmpdir):
image.save(os.path.join(tmp_path_, "mywsi.svs"), "TIFF")
slideset = SlideSet(tmp_path_, "proc", [".svs"])

slides = slideset.slides

assert len(slides) == 1
assert len(slideset) == 1

slideset = SlideSet(None, "proc", [".svs"])

slides = slideset.slides

assert len(slides) == 0
assert len(slideset) == 0

with pytest.raises(FileNotFoundError) as err:
slideset = SlideSet("fake/path", "proc", [".svs"])
slides = slideset.slides
list(slideset)

assert isinstance(err.value, FileNotFoundError)
assert err.value.errno == errno.ENOENT

def it_constructs_its_sequence_of_slides_to_help(self, request, Slide_, tmpdir):
slides_path = tmpdir.mkdir("mypath")
for i in range(4):
open(os.path.join(slides_path, f"myfile{i}.svs"), "a")
slides_ = tuple(instance_mock(request, Slide) for _ in range(4))
Slide_.side_effect = iter(slides_)
slide_set = SlideSet(
slides_path=slides_path,
processed_path=os.path.join(slides_path, "processed"),
valid_extensions=[".svs"],
)
slides = tuple(slide_set.slides)
def it_constructs_its_sequence_of_slides_to_help(self, tmpdir):
tmp_path_ = tmpdir.mkdir("myslide")
image = PILIMG.RGBA_COLOR_500X500_155_249_240
image.save(os.path.join(tmp_path_, "mywsi.svs"), "TIFF")
image2 = PILIMG.RGBA_COLOR_50X50_155_0_0
image2.save(os.path.join(tmp_path_, "mywsi2.svs"), "TIFF")
slideset = SlideSet(tmp_path_, "proc", [".svs"])
expected_slides = [
Slide(os.path.join(tmp_path_, _path), "proc")
for _path in os.listdir(tmp_path_)
]

assert sorted(Slide_.call_args_list) == sorted(
[
call(
os.path.join(slides_path, f"myfile{i}.svs"),
os.path.join(slides_path, "processed"),
)
for i in range(4)
]
)
assert slides == slides_
slides = slideset.__iter__()

for i, slide in enumerate(slides):
np.testing.assert_array_almost_equal(
slide.resampled_array(), expected_slides[i].resampled_array()
)

def it_knows_the_slides_dimensions(self, tmpdir):
tmp_path_ = tmpdir.mkdir("myslide")
Expand Down Expand Up @@ -691,11 +680,11 @@ def it_knows_its_slides_dimensions_list(self, tmpdir):
assert sorted(_slides_dimensions_list) == sorted([(500, 500), (50, 50)])

def it_knows_its_total_slides(self, request, Slide_):
slides = property_mock(request, SlideSet, "slides")
slides = method_mock(request, SlideSet, "__iter__")
slides.return_value = [Slide_ for _ in range(4)]
slideset = SlideSet("the/path", "proc", [".svs"])

total_slides = slideset.total_slides
total_slides = len(slideset)

assert total_slides == 4

Expand Down Expand Up @@ -819,7 +808,7 @@ def it_can_save_scaled_slides(self, request, tmpdir):
slide2 = instance_mock(request, Slide)

slideset = SlideSet(tmp_path_, os.path.join(tmp_path_, "processed"), [])
slides = property_mock(request, SlideSet, "slides")
slides = method_mock(request, SlideSet, "__iter__")
slides.return_value = [slide1, slide2]
slideset.save_scaled_slides(32, 2)

Expand All @@ -832,7 +821,7 @@ def it_can_save_thumbnails(self, request, tmpdir):
slide2 = instance_mock(request, Slide)

slideset = SlideSet(tmp_path_, os.path.join(tmp_path_, "processed"), [])
slides = property_mock(request, SlideSet, "slides")
slides = method_mock(request, SlideSet, "__iter__")
slides.return_value = [slide1, slide2]
slideset.save_thumbnails(2)

Expand Down