Skip to content

Commit

Permalink
Merge 071a100 into 036429b
Browse files Browse the repository at this point in the history
  • Loading branch information
alessiamarcolini committed Aug 6, 2020
2 parents 036429b + 071a100 commit 8f4ce9b
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 171 deletions.
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def ascii_bytes_from(path, *paths):
"Pillow",
"scikit-image",
"scipy",
"sparse",
"openslide-wrapper",
"typing_extensions",
]
Expand Down
14 changes: 14 additions & 0 deletions src/histolab/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class LevelError(Exception):
"""Raised when a requested level is not available"""

def __init__(self, *args) -> None:
if args:
self.message = args[0]
else:
self.message = None

def __str__(self):
if self.message:
return self.message
else:
return ""
28 changes: 19 additions & 9 deletions src/histolab/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
import numpy as np
import openslide
import PIL
import sparse

from .exceptions import LevelError
from .filters import image_filters as imf
from .filters import morphological_filters as mof
from .tile import Tile
Expand All @@ -42,7 +42,6 @@
polygon_to_mask_array,
region_coordinates,
regions_from_binary_mask,
resize_mask,
scale_coordinates,
)

Expand Down Expand Up @@ -97,20 +96,32 @@ def level_dimensions(self, level: int = 0) -> Tuple[int, int]:
try:
return self._wsi.level_dimensions[level]
except IndexError:
raise ValueError(
raise LevelError(
f"Level {level} not available. Number of available levels: "
f"{len(self._wsi.level_dimensions)}"
)

@lazyproperty
def levels(self) -> List[int]:
"""Return the slide's available levels
Returns
-------
List[int]
The levels available
"""
return list(range(len(self._wsi.level_dimensions)))

@lazyproperty
@lru_cache(maxsize=100)
def biggest_tissue_box_mask(self) -> sparse._coo.core.COO:
"""Return the binary mask of the box containing the max area of tissue.
def biggest_tissue_box_mask(self) -> np.ndarray:
"""Return the thumbnail binary mask of the box containing the max tissue area.
Returns
-------
mask: sparse._coo.core.COO
Binary mask of the box containing the max area of tissue.
mask: np.ndarray
Binary mask of the box containing the max area of tissue. The dimensions are
those of the thumbnail.
"""
thumb = self._wsi.get_thumbnail(self._thumbnail_size)
Expand All @@ -123,8 +134,7 @@ def biggest_tissue_box_mask(self) -> sparse._coo.core.COO:
thumb_bbox_mask = polygon_to_mask_array(
self._thumbnail_size, biggest_region_coordinates
)
thumb_bbox_mask_sparse = sparse.COO(thumb_bbox_mask)
return resize_mask(thumb_bbox_mask_sparse, self.dimensions)
return thumb_bbox_mask

def extract_tile(self, coords: CoordinatePair, level: int) -> Tile:
"""Extract a tile of the image at the selected level.
Expand Down
89 changes: 41 additions & 48 deletions src/histolab/tiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from typing import Tuple

import numpy as np
import sparse

from histolab.exceptions import LevelError

from .slide import Slide
from .tile import Tile
Expand All @@ -11,7 +12,6 @@
lru_cache,
region_coordinates,
regions_from_binary_mask,
resize_mask,
scale_coordinates,
)

Expand All @@ -28,10 +28,10 @@ class Tiler(Protocol):
tile_size: int

@lru_cache(maxsize=100)
def box_mask(self, slide: Slide) -> sparse._coo.core.COO:
"""Return binary mask at level 0 of the box to consider for tiles extraction.
def box_mask_thumb(self, slide: Slide) -> np.ndarray:
"""Return binary mask, at thumbnail level, of the box for tiles extraction.
The mask pixels set to True will be the ones corresponding to the tissue box.
The mask pixels set to True correspond to the tissue box.
Parameters
----------
Expand All @@ -40,38 +40,12 @@ def box_mask(self, slide: Slide) -> sparse._coo.core.COO:
Returns
-------
sparse._coo.core.COO
Extraction mask at level 0
np.ndarray
Extraction mask at thumbnail level
"""

return slide.biggest_tissue_box_mask

@lru_cache(maxsize=100)
def box_mask_lvl(self, slide: Slide) -> sparse._coo.core.COO:
"""Return binary mask at target level of the box to consider for the extraction.
The mask pixels set to True will be the ones corresponding to the tissue box.
Parameters
----------
slide : Slide
The Slide from which to extract the extraction mask
Returns
-------
sparse._coo.core.COO
Extraction mask at target level
"""

box_mask_wsi = self.box_mask(slide)

if self.level != 0:
return resize_mask(
box_mask_wsi, target_dimensions=slide.level_dimensions(self.level),
)
else:
return box_mask_wsi

def _tile_filename(
self, tile_wsi_coords: CoordinatePair, tiles_counter: int
) -> str:
Expand Down Expand Up @@ -159,7 +133,7 @@ def level(self) -> int:
@level.setter
def level(self, level_: int):
if level_ < 0:
raise ValueError(f"Level cannot be negative ({level_})")
raise LevelError(f"Level cannot be negative ({level_})")
self._valid_level = level_

def extract(self, slide: Slide):
Expand All @@ -172,6 +146,12 @@ def extract(self, slide: Slide):
slide : Slide
Slide from which to extract the tiles
"""
if self.level not in slide.levels:
raise LevelError(
f"Level {self.level} not available. Number of available levels: "
f"{len(slide.levels)}"
)

grid_tiles = self._grid_tiles_generator(slide)

tiles_counter = 0
Expand All @@ -181,7 +161,7 @@ def extract(self, slide: Slide):
tile.save(tile_filename)
print(f"\t Tile {tiles_counter} saved: {tile_filename}")

print(f"{tiles_counter+1} Grid Tiles have been saved.")
print(f"{tiles_counter} Grid Tiles have been saved.")

def _grid_coordinates_from_bbox_coordinates(
self, bbox_coordinates: CoordinatePair, slide: Slide
Expand Down Expand Up @@ -242,16 +222,21 @@ def _grid_coordinates_generator(self, slide: Slide) -> CoordinatePair:
Iterator[CoordinatePair]
Iterator of tiles' CoordinatePair
"""
box_mask_lvl = self.box_mask_lvl(slide)
box_mask_thumb = self.box_mask_thumb(slide)

regions = regions_from_binary_mask(box_mask_lvl.todense())
regions = regions_from_binary_mask(box_mask_thumb)
for region in regions: # at the moment there is only one region
bbox_coordinates = region_coordinates(region)
bbox_coordinates_thumb = region_coordinates(region)
bbox_coordinates = scale_coordinates(
bbox_coordinates_thumb,
box_mask_thumb.shape[::-1],
slide.level_dimensions(self.level),
)
yield from self._grid_coordinates_from_bbox_coordinates(
bbox_coordinates, slide
)

def _grid_tiles_generator(self, slide: Slide) -> (Tile, CoordinatePair):
def _grid_tiles_generator(self, slide: Slide) -> Tuple[Tile, CoordinatePair]:
"""Generator of tiles arranged in a grid.
Parameters
Expand Down Expand Up @@ -377,7 +362,7 @@ def level(self) -> int:
@level.setter
def level(self, level_: int):
if level_ < 0:
raise ValueError(f"Level cannot be negative ({level_})")
raise LevelError(f"Level cannot be negative ({level_})")
self._valid_level = level_

@property
Expand Down Expand Up @@ -427,24 +412,32 @@ def _random_tile_coordinates(self, slide: Slide) -> CoordinatePair:
CoordinatePair
Random tile Coordinates at level 0
"""
box_mask_lvl = self.box_mask_lvl(slide)
box_mask_thumb = self.box_mask_thumb(slide)
tile_w_lvl, tile_h_lvl = self.tile_size

x_ul_lvl = np.random.choice(sparse.where(box_mask_lvl)[1])
y_ul_lvl = np.random.choice(sparse.where(box_mask_lvl)[0])
x_ul_lvl = np.random.choice(np.where(box_mask_thumb)[1])
y_ul_lvl = np.random.choice(np.where(box_mask_thumb)[0])

# Scale tile dimensions to thumbnail dimensions
tile_w_thumb = (
tile_w_lvl * box_mask_thumb.shape[1] / slide.level_dimensions(self.level)[0]
)
tile_h_thumn = (
tile_h_lvl * box_mask_thumb.shape[0] / slide.level_dimensions(self.level)[1]
)

x_br_lvl = x_ul_lvl + tile_w_lvl
y_br_lvl = y_ul_lvl + tile_h_lvl
x_br_lvl = x_ul_lvl + tile_w_thumb
y_br_lvl = y_ul_lvl + tile_h_thumn

tile_wsi_coords = scale_coordinates(
reference_coords=CoordinatePair(x_ul_lvl, y_ul_lvl, x_br_lvl, y_br_lvl),
reference_size=slide.level_dimensions(level=self.level),
target_size=slide.level_dimensions(level=0),
reference_size=box_mask_thumb.shape[::-1],
target_size=slide.dimensions,
)

return tile_wsi_coords

def _random_tiles_generator(self, slide: Slide) -> (Tile, CoordinatePair):
def _random_tiles_generator(self, slide: Slide) -> Tuple[Tile, CoordinatePair]:
"""Generate Random Tiles within a slide box.
Stops if:
Expand Down
24 changes: 0 additions & 24 deletions src/histolab/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import numpy as np
import PIL
import PIL.ImageDraw
import sparse
from skimage.measure import label, regionprops

from .types import CoordinatePair, Region
Expand Down Expand Up @@ -182,29 +181,6 @@ def region_coordinates(region: Region) -> CoordinatePair:
return CoordinatePair(x_ul, y_ul, x_br, y_br)


def resize_mask(
input_mask: sparse._coo.core.COO, target_dimensions: Tuple[int, int]
) -> sparse._coo.core.COO:
"""Resize mask to ``target_dimensions``.
Parameters
----------
input_mask : sparse.COO
Input mask
target_dimensions : List[int, int]
Dimensions of the resized mask
Returns
-------
sparse._coo.core.COO
Resized mask
"""
input_mask_img = PIL.Image.fromarray(input_mask.todense())
resized_mask_img = input_mask_img.resize(target_dimensions)
resized_mask_arr = np.array(resized_mask_img).astype(bool)
return sparse.COO(resized_mask_arr)


def apply_mask_image(img: PIL.Image.Image, mask: np.ndarray) -> PIL.Image.Image:
"""Mask image with the provided binary mask.
Expand Down
4 changes: 0 additions & 4 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import numpy as np
import sparse

# ============== IMAGES ==============
IMAGE1_GRAY = np.array(
Expand Down Expand Up @@ -236,6 +235,3 @@
[True, True, False, False, False, True, True, False, True, False],
]
)

SPARSE_BASE_MASK = sparse.COO(BASE_MASK)
SPARSE_COMPLEX_MASK = sparse.COO(COMPLEX_MASK)
Binary file not shown.
Binary file not shown.
56 changes: 56 additions & 0 deletions tests/unit/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pytest

from histolab import exceptions as exp

from ..unitutil import ANY, initializer_mock


class DescribeExceptions(object):
@pytest.mark.parametrize("args", ("error", ["hello", "error"], None))
def it_can_construct_from_args_level_error(self, request, args):
_init = initializer_mock(request, exp.LevelError)

if type(args) == list:
level_error = exp.LevelError(*args)
else:
level_error = exp.LevelError(args)

if type(args) == list:
_init.assert_called_once_with(ANY, *args)
else:
_init.assert_called_once_with(ANY, args)
assert isinstance(level_error, exp.LevelError)
assert isinstance(level_error, Exception)

@pytest.mark.parametrize("args", ("error", ["hello", "error"], None))
def it_knows_its_message_level_error(self, request, args):
if type(args) == list:
level_error = exp.LevelError(*args)
else:
level_error = exp.LevelError(args)

message = level_error.message

if args:
if type(args) == str:
assert type(message) == str
assert message == args
if type(args) == list:
assert type(message) == str
assert message == list(args)[0]
else:
assert message is None

@pytest.mark.parametrize("args", ("error", ["hello", "error"], None))
def it_knows_its_str_level_error(self, request, args):
if type(args) == list:
level_error = exp.LevelError(*args)
else:
level_error = exp.LevelError(args)

s = str(level_error)

if not args:
assert s == ""
else:
assert s == level_error.message

0 comments on commit 8f4ce9b

Please sign in to comment.