Skip to content

Commit

Permalink
Allow setting of mpp, modernize typing (#129)
Browse files Browse the repository at this point in the history
* **kwargs parameter needs to pass to all constructors
* Modernize typing
  • Loading branch information
jonasteuwen authored Jan 1, 2023
1 parent 6366c86 commit 3ff9a87
Show file tree
Hide file tree
Showing 20 changed files with 158 additions and 160 deletions.
4 changes: 2 additions & 2 deletions dlup/_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# coding=utf-8
# Copyright (c) dlup contributors
from typing import Optional
from __future__ import annotations


class DlupError(Exception):
pass


class UnsupportedSlideError(DlupError):
def __init__(self, msg: str, identifier: Optional[str] = None):
def __init__(self, msg: str, identifier: str | None = None):
msg = msg if identifier is None else f"slide '{identifier}': " + msg
super().__init__(self, msg)

Expand Down
26 changes: 13 additions & 13 deletions dlup/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import errno
import os
import pathlib
from typing import Callable, Optional, Tuple, Type, TypeVar, Union, cast
from typing import Callable, Type, TypeVar, Union, cast

import numpy as np # type: ignore
import openslide # type: ignore
Expand All @@ -25,14 +25,14 @@
from dlup.types import GenericFloatArray, GenericIntArray, GenericNumber, PathLike
from dlup.utils.image import check_if_mpp_is_valid

_Box = Tuple[GenericNumber, GenericNumber, GenericNumber, GenericNumber]
_Box = tuple[GenericNumber, GenericNumber, GenericNumber, GenericNumber]
_TSlideImage = TypeVar("_TSlideImage", bound="SlideImage")


class _SlideImageRegionView(RegionView):
"""Represents an image view tied to a slide image."""

def __init__(self, wsi: _TSlideImage, scaling: GenericNumber, boundary_mode: Optional[BoundaryMode] = None):
def __init__(self, wsi: _TSlideImage, scaling: GenericNumber, boundary_mode: BoundaryMode | None = None):
"""Initialize with a slide image object and the scaling level."""
# Always call the parent init
super().__init__(boundary_mode=boundary_mode)
Expand All @@ -45,7 +45,7 @@ def mpp(self) -> float:
return self._wsi.mpp / self._scaling

@property
def size(self) -> Tuple[int, ...]:
def size(self) -> tuple[int, ...]:
"""Size"""
return self._wsi.get_scaled_size(self._scaling)

Expand All @@ -56,7 +56,7 @@ def _read_region_impl(self, location: GenericFloatArray, size: GenericIntArray)
return self._wsi.read_region((x, y), self._scaling, (w, h))


def _clip2size(a: np.ndarray, size: Tuple[GenericNumber, GenericNumber]) -> np.ndarray:
def _clip2size(a: np.ndarray, size: tuple[GenericNumber, GenericNumber]) -> np.ndarray:
"""Clip values from 0 to size boundaries."""
return np.clip(a, (0, 0), size)

Expand Down Expand Up @@ -134,9 +134,9 @@ def from_file_path(
# @image_cache
def read_region(
self,
location: Union[np.ndarray, Tuple[GenericNumber, GenericNumber]],
location: Union[np.ndarray, tuple[GenericNumber, GenericNumber]],
scaling: float,
size: Union[np.ndarray, Tuple[int, int]],
size: Union[np.ndarray, tuple[int, int]],
) -> PIL.Image.Image:
"""Return a region at a specific scaling level of the pyramid.
Expand Down Expand Up @@ -244,7 +244,7 @@ def read_region(
size = cast(tuple[int, int], size)
return region.resize(size, resample=PIL.Image.Resampling.LANCZOS, box=box)

def get_scaled_size(self, scaling: GenericNumber) -> Tuple[int, ...]:
def get_scaled_size(self, scaling: GenericNumber) -> tuple[int, ...]:
"""Compute slide image size at specific scaling."""
size = np.array(self.size) * scaling
return tuple(size.astype(int))
Expand All @@ -263,7 +263,7 @@ def get_scaled_view(self, scaling: GenericNumber) -> _SlideImageRegionView:
"""Returns a RegionView at a specific level."""
return _SlideImageRegionView(self, scaling)

def get_thumbnail(self, size: Tuple[int, int] = (512, 512)) -> PIL.Image.Image:
def get_thumbnail(self, size: tuple[int, int] = (512, 512)) -> PIL.Image.Image:
"""Returns an RGB numpy thumbnail for the current slide.
Parameters
Expand All @@ -279,7 +279,7 @@ def thumbnail(self) -> PIL.Image.Image:
return self.get_thumbnail()

@property
def identifier(self) -> Optional[str]:
def identifier(self) -> str | None:
"""Returns a user-defined identifier."""
return self._identifier

Expand All @@ -289,12 +289,12 @@ def properties(self) -> dict:
return self._wsi.properties

@property
def vendor(self) -> Optional[str]:
def vendor(self) -> str | None:
"""Returns the scanner vendor."""
return self._wsi.vendor

@property
def size(self) -> Tuple[int, int]:
def size(self) -> tuple[int, int]:
"""Returns the highest resolution image size in pixels. Returns in (width, height)."""
return self._wsi.dimensions

Expand All @@ -304,7 +304,7 @@ def mpp(self) -> float:
return self._avg_native_mpp

@property
def magnification(self) -> Optional[float]:
def magnification(self) -> float | None:
"""Returns the objective power at which the WSI was sampled."""
return self._wsi.magnification

Expand Down
8 changes: 5 additions & 3 deletions dlup/_region.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# coding=utf-8
"""Defines the RegionView interface."""
from __future__ import annotations

from abc import ABC, abstractmethod
from enum import Enum
from typing import Iterable, Optional, Tuple, Union, cast
from typing import Iterable, Union, cast

import numpy as np
import PIL.Image
Expand Down Expand Up @@ -31,12 +33,12 @@ class RegionView(ABC):
subregions instead of a whole level.
"""

def __init__(self, boundary_mode: Optional[BoundaryMode] = None):
def __init__(self, boundary_mode: BoundaryMode | None = None):
self.boundary_mode = boundary_mode

@property
@abstractmethod
def size(self) -> Tuple[int, ...]:
def size(self) -> tuple[int, ...]:
"""Returns size of the region in U units."""
pass

Expand Down
40 changes: 19 additions & 21 deletions dlup/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import xml.etree.ElementTree as ET
from collections import defaultdict
from enum import Enum
from typing import Any, ClassVar, DefaultDict, Dict, Iterable, List, Optional, Tuple, Type, TypedDict, TypeVar, Union
from typing import Any, ClassVar, Dict, Iterable, Type, TypedDict, TypeVar, Union

import numpy as np
import shapely
Expand Down Expand Up @@ -57,9 +57,9 @@ class GeoJsonDict(TypedDict):
TypedDict for standard GeoJSON output
"""

id: Optional[str]
id: str | None
type: str
features: List[Dict[str, Union[str, Dict[str, str]]]]
features: list[Dict[str, Union[str, Dict[str, str]]]]


class Point(shapely.geometry.Point):
Expand All @@ -70,14 +70,14 @@ class Point(shapely.geometry.Point):
) # slots must be the same for assigning __class__ - https://stackoverflow.com/a/52140968
name: str # For documentation generation and static type checking

def __init__(self, coord: Union[shapely.geometry.Point, Tuple[float, float]], label: Optional[str] = None) -> None:
def __init__(self, coord: Union[shapely.geometry.Point, tuple[float, float]], label: str | None = None) -> None:
self._id_to_attrs[str(id(self))] = dict(label=label)

@property
def type(self):
return AnnotationType.POINT

def __new__(cls, coord: Tuple[float, float], *args, **kwargs) -> "Point":
def __new__(cls, coord: tuple[float, float], *args, **kwargs) -> "Point":
point = super().__new__(cls, coord)
point.__class__ = cls
return point
Expand All @@ -103,16 +103,14 @@ class Polygon(shapely.geometry.Polygon):
) # slots must be the same for assigning __class__ - https://stackoverflow.com/a/52140968
name: str # For documentation generation and static type checking

def __init__(
self, coord: Union[shapely.geometry.Polygon, Tuple[float, float]], label: Optional[str] = None
) -> None:
def __init__(self, coord: Union[shapely.geometry.Polygon, tuple[float, float]], label: str | None = None) -> None:
self._id_to_attrs[str(id(self))] = dict(label=label)

@property
def type(self):
return AnnotationType.POLYGON

def __new__(cls, coord: Tuple[float, float], *args, **kwargs) -> "Point":
def __new__(cls, coord: tuple[float, float], *args, **kwargs) -> "Point":
point = super().__new__(cls, coord)
point.__class__ = cls
return point
Expand Down Expand Up @@ -192,10 +190,10 @@ def append(self, sample):
def as_strtree(self) -> STRtree:
return STRtree(self._annotations)

def as_list(self) -> List:
def as_list(self) -> list:
return self._annotations

def as_json(self) -> List[Dict[str, Any]]:
def as_json(self) -> list[Dict[str, Any]]:
"""
Return the annotation as json format.
Expand Down Expand Up @@ -240,7 +238,7 @@ def __str__(self) -> str:
class WsiAnnotations:
"""Class to hold the annotations of all labels specific label for a whole slide image."""

def __init__(self, annotations: List[WsiSingleLabelAnnotation]):
def __init__(self, annotations: list[WsiSingleLabelAnnotation]):
self.available_labels = sorted([annotation.label for annotation in annotations])
if len(set(self.available_labels)) != len(self.available_labels):
raise ValueError(
Expand All @@ -253,7 +251,7 @@ def __init__(self, annotations: List[WsiSingleLabelAnnotation]):
# Now we have a dict of label: annotations.
self._annotation_trees = {label: self[label].as_strtree() for label in self.available_labels}

def filter(self, labels: Union[str, Union[List[str], Tuple[str]]]) -> None:
def filter(self, labels: Union[str, Union[list[str], tuple[str]]]) -> None:
"""
Filter annotations based on the given label list.
Expand All @@ -271,7 +269,7 @@ def filter(self, labels: Union[str, Union[List[str], Tuple[str]]]) -> None:
self._annotations = {k: v for k, v in self._annotations.items() if k in _labels}
self._annotation_trees = {k: v for k, v in self._annotation_trees.items() if k in _labels}

def relabel(self, labels: Tuple[Tuple[str, str], ...]) -> None:
def relabel(self, labels: tuple[tuple[str, str], ...]) -> None:
"""
Rename labels in the class in-place.
Expand Down Expand Up @@ -347,7 +345,7 @@ def from_geojson(
data[_label].append(_)

# It is assume that a specific label can only be one type (point or polygon)
annotations: List[WsiSingleLabelAnnotation] = [
annotations: list[WsiSingleLabelAnnotation] = [
WsiSingleLabelAnnotation(label=k, type=data[k][0].type, coordinates=data[k]) for k in data.keys()
]

Expand Down Expand Up @@ -439,7 +437,7 @@ def from_asap_xml(
def __getitem__(self, label: str) -> WsiSingleLabelAnnotation:
return self._annotations[label]

def as_geojson(self, split_per_label=False) -> Union[GeoJsonDict, List[Tuple[str, GeoJsonDict]]]:
def as_geojson(self, split_per_label=False) -> Union[GeoJsonDict, list[tuple[str, GeoJsonDict]]]:
"""
Output the annotations as proper geojson.
Expand Down Expand Up @@ -493,10 +491,10 @@ def simplify(self, tolerance: float, *, preserve_topology: bool = True):

def read_region(
self,
coordinates: Union[np.ndarray, Tuple[GenericNumber, GenericNumber]],
coordinates: Union[np.ndarray, tuple[GenericNumber, GenericNumber]],
scaling: float,
region_size: Union[np.ndarray, Tuple[GenericNumber, GenericNumber]],
) -> List[Union[Polygon, Point]]:
region_size: Union[np.ndarray, tuple[GenericNumber, GenericNumber]],
) -> list[Union[Polygon, Point]]:
"""Reads the region of the annotations. API is the same as `dlup.SlideImage` so they can be used in conjunction.
The process is as follows:
Expand All @@ -520,7 +518,7 @@ def read_region(
Returns
-------
List[Tuple[str, ShapelyTypes]]
list[tuple[str, ShapelyTypes]]
List of tuples denoting the name of the annotation and a shapely object.
Examples
Expand Down Expand Up @@ -578,7 +576,7 @@ def read_region(

transformation_matrix = [scaling, 0, 0, scaling, -coordinates[0], -coordinates[1]]

output: List[Union[Polygon, Point]] = []
output: list[Union[Polygon, Point]] = []
for annotation_name, annotation in cropped_annotations:
annotation = shapely.affinity.affine_transform(annotation, transformation_matrix)
# It can occur that single polygon annotations result in being points after being intersected.
Expand Down
12 changes: 6 additions & 6 deletions dlup/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from enum import Enum
from functools import partial
from typing import Callable, Iterable, List, Tuple, Union
from typing import Callable, Iterable, Union

import numpy as np
import PIL.Image
Expand Down Expand Up @@ -89,7 +89,7 @@ def _fesi_common(image: np.ndarray) -> np.ndarray:
final_mask = mask.copy()
maximal_distance = distance.max()
global_max = distance.max()
seeds: List = []
seeds: list = []
while maximal_distance > 0:
start = np.unravel_index(distance.argmax(), distance.shape)
if (maximal_distance > 0.6 * global_max) or _is_close(seeds, start[::-1]):
Expand Down Expand Up @@ -217,7 +217,7 @@ def get_mask(slide: dlup.SlideImage, mask_func: Callable = improved_fesi, minima
def is_foreground(
slide_image: SlideImage,
background_mask: Union[np.ndarray, SlideImage, WsiAnnotations],
region: Tuple[float, float, int, int, float],
region: tuple[float, float, int, int, float],
threshold: float = 1.0,
) -> bool:

Expand All @@ -237,7 +237,7 @@ def is_foreground(
def is_foreground_polygon(
slide_image: SlideImage,
background_mask: WsiAnnotations,
region: Tuple[float, float, int, int, float],
region: tuple[float, float, int, int, float],
threshold: float = 1.0,
) -> bool:

Expand All @@ -257,7 +257,7 @@ def is_foreground_polygon(

def is_foreground_wsiannotations(
background_mask: SlideImage,
region: Tuple[float, float, int, int, float],
region: tuple[float, float, int, int, float],
threshold: float = 1.0,
) -> bool:

Expand All @@ -276,7 +276,7 @@ def is_foreground_wsiannotations(
def is_foreground_numpy(
slide_image: SlideImage,
background_mask: np.ndarray,
region: Tuple[float, float, int, int, float],
region: tuple[float, float, int, int, float],
threshold: float = 1.0,
) -> bool:

Expand Down
8 changes: 4 additions & 4 deletions dlup/cli/wsi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import pathlib
from multiprocessing import Pool
from typing import Tuple, cast
from typing import cast

from PIL import Image

Expand All @@ -21,15 +21,15 @@ def tiling(args: argparse.Namespace):
"""Perform the WSI tiling."""
input_file_path = args.slide_file_path
output_directory_path = args.output_directory_path
tile_size = cast(Tuple[int, int], (args.tile_size,) * 2)
tile_overlap = cast(Tuple[int, int], (args.tile_overlap,) * 2)
tile_size = cast(tuple[int, int], (args.tile_size,) * 2)
tile_overlap = cast(tuple[int, int], (args.tile_overlap,) * 2)

image = SlideImage.from_file_path(input_file_path)
mask = get_mask(slide=image, mask_func=AvailableMaskFunctions[args.mask_func])

# the nparray and PIL.Image.size height and width order are flipped is as it would be as a PIL.Image.
# Below [::-1] casts the thumbnail_size to the PIL.Image expected size
thumbnail_size = cast(Tuple[int, int], mask.shape[::-1])
thumbnail_size = cast(tuple[int, int], mask.shape[::-1])
thumbnail = image.get_thumbnail(thumbnail_size)

# Prepare output directory.
Expand Down
Loading

0 comments on commit 3ff9a87

Please sign in to comment.