Skip to content
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
479cde8
:recycle: Add checks for input_path existence
blaginin Mar 27, 2023
87b43b8
:recycle: Refactor PatchPredictor
blaginin Mar 27, 2023
1daf19e
:adhesive_bandage: Add support for numpy array and WSIReader in datasets
blaginin Mar 27, 2023
ac0a9e8
:white_check_mark: Update tests for np.ndarray and WSIReader support …
blaginin Mar 27, 2023
59d4d97
Merge remote-tracking branch 'origin/develop' into feature-wsi-arguments
blaginin Mar 27, 2023
ac77bc0
Merge branch 'develop' into feature-wsi-arguments
blaginin Mar 27, 2023
7e9ac19
:recycle: Refactor code.
blaginin Mar 27, 2023
d711ed1
📌 Pin Pandas Version to `>=2.0.0`
shaneahmed Apr 5, 2023
42f8441
Merge branch 'develop' into feature-wsi-arguments
blaginin Apr 6, 2023
3aec8e0
:adhesive_bandage: add ignore_resolutions for compatibility
blaginin Apr 6, 2023
15790fa
:white_check_mark: Add tests for different types of input for `WSIPat…
blaginin Apr 6, 2023
50cc400
:recycle: removed the blank line
blaginin Apr 6, 2023
95f8faa
:white_check_mark: Fix tests for `PatchDataset`
blaginin Apr 6, 2023
bb063d7
:rewind: Undo _prepare_save_dir bugfix
blaginin Apr 6, 2023
338f517
Merge remote-tracking branch 'upstream/dev-update-pandas-dependency' …
blaginin Apr 6, 2023
48e3fdd
Merge branch 'develop' into feature-wsi-arguments
blaginin Apr 6, 2023
2823b4b
:white_check_mark: Add tests for ignore_resolutions mode
blaginin Apr 8, 2023
5500e00
:twisted_rightwards_arrows: Merge develop
blaginin Apr 12, 2023
d80034b
Merge branch 'develop' into feature-wsi-arguments
blaginin Apr 12, 2023
e09dbe2
Merge branch 'develop' into feature-wsi-arguments
blaginin May 2, 2023
5e9ae4f
:recycle: refactor patch predictor and related methods
blaginin May 2, 2023
5bcdc68
:recycle: add a blank line to `get_reader_by_filepath`
blaginin May 2, 2023
c31d078
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 2, 2023
74cd6fc
Merge branch 'develop' into feature-wsi-arguments
blaginin May 8, 2023
11674c4
:bug: fix bool dtype
blaginin May 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ opencv-python>=4.6.0
openslide-python>=1.2.0
pandas>=2.0.0
pillow>=9.3.0
pydicom>=2.3.1 # Used by wsidicom
pydicom>=2.3.1 # Used by wsidef test_store_reader_no_types(tmp_path, remote_sample):dicom
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wsidicom dependency.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now requirements.txt is identical to develop

pyyaml>=6.0
requests>=2.28.1
scikit-image>=0.20
Expand Down
23 changes: 23 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pathlib
import shutil
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Callable

import pytest
Expand Down Expand Up @@ -59,6 +60,28 @@ def __remote_sample(key: str) -> pathlib.Path:
return __remote_sample


@pytest.fixture(scope="session")
def blank_sample(tmp_path_factory: TempPathFactory):
"""Factory fixture for creating blank sample files."""

class BlankSample:
"""Sample file. Automatically deleted after use."""

def __init__(self, suffix: str):
self.suffix = suffix
self.file = None # will be set in __enter__

def __enter__(self) -> pathlib.Path:
folder = tmp_path_factory.mktemp("data")
self.file = NamedTemporaryFile(suffix=self.suffix, dir=folder, delete=True)
return pathlib.Path(self.file.name)

def __exit__(self, exc_type, exc_value, traceback):
self.file.close()

return BlankSample


@pytest.fixture(scope="session")
def sample_ndpi(remote_sample) -> pathlib.Path:
"""Sample pytest fixture for ndpi images.
Expand Down
169 changes: 155 additions & 14 deletions tests/models/test_patch_predictor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from tiatoolbox.utils import env_detection as toolbox_env
from tiatoolbox.utils.misc import download_data, imread, imwrite
from tiatoolbox.wsicore.wsireader import WSIReader
from tiatoolbox.wsicore.wsireader import VirtualWSIReader, WSIReader

ON_GPU = toolbox_env.has_gpu()

Expand Down Expand Up @@ -222,9 +222,9 @@ def test_wsi_patch_dataset(sample_wsi_dict, tmp_path):
mini_wsi_jpg = pathlib.Path(sample_wsi_dict["wsi2_4k_4k_jpg"])
mini_wsi_msk = pathlib.Path(sample_wsi_dict["wsi2_4k_4k_msk"])

def reuse_init(img_path=mini_wsi_svs, **kwargs):
def reuse_init(input_img=mini_wsi_svs, **kwargs):
"""Testing function."""
return WSIPatchDataset(img_path=img_path, **kwargs)
return WSIPatchDataset(input_img=input_img, **kwargs)

def reuse_init_wsi(**kwargs):
"""Testing function."""
Expand All @@ -249,20 +249,33 @@ def __getitem__(self, idx):
Proto() # skipcq

# invalid path input
with pytest.raises(ValueError, match=r".*`img_path` must be a valid file path.*"):
with pytest.raises(ValueError, match=r".*`input_img` path must exist.*"):
WSIPatchDataset(
img_path="aaaa",
input_img="aaaa",
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
auto_get_mask=False,
)

# invalid mask path input
with pytest.raises(ValueError, match=r".*`mask_path` must be a valid file path.*"):
with pytest.raises(ValueError, match=r".*`mask` must be a valid file path.*"):
WSIPatchDataset(
img_path=mini_wsi_svs,
mask_path="aaaa",
input_img=mini_wsi_svs,
mask="aaaa",
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
resolution=1.0,
units="mpp",
auto_get_mask=False,
)

# mask as not VirtualWSIReader
with pytest.raises(ValueError, match=r".*`mask` must be .* VirtualWSIReader.*"):
WSIPatchDataset(
input_img=mini_wsi_svs,
mask=WSIReader.open(mini_wsi_svs),
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
Expand All @@ -275,6 +288,10 @@ def __getitem__(self, idx):
with pytest.raises(ValueError, match="`X` is not supported."):
reuse_init(mode="X")

# invalid units
with pytest.raises(ValueError, match="`X` is not supported."):
reuse_init(units="X")

# invalid patch
with pytest.raises(ValueError, match="Invalid `patch_input_shape` value None."):
reuse_init()
Expand Down Expand Up @@ -346,8 +363,8 @@ def __getitem__(self, idx):
)
assert len(ds) > 0
ds = WSIPatchDataset(
img_path=mini_wsi_svs,
mask_path=mini_wsi_msk,
input_img=mini_wsi_svs,
mask=mini_wsi_msk,
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
Expand All @@ -361,8 +378,8 @@ def __getitem__(self, idx):
imwrite(negative_mask_path, negative_mask)
with pytest.raises(ValueError, match="No patch coordinates remain after filtering"):
ds = WSIPatchDataset(
img_path=mini_wsi_svs,
mask_path=negative_mask_path,
input_img=mini_wsi_svs,
mask=negative_mask_path,
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
Expand All @@ -374,7 +391,7 @@ def __getitem__(self, idx):
# * for tile
reader = WSIReader.open(mini_wsi_jpg)
tile_ds = WSIPatchDataset(
img_path=mini_wsi_jpg,
input_img=mini_wsi_jpg,
mode="tile",
patch_input_shape=patch_size,
stride_shape=stride_size,
Expand All @@ -395,6 +412,72 @@ def __getitem__(self, idx):
assert roi1.shape[1] == roi2.shape[1]
assert np.min(correlation) > 0.9, correlation

positive_mask = (negative_mask + 1).astype(bool)
# check mask as np array
with pytest.raises(ValueError, match=r".*`mask` must be binary.*"):
WSIPatchDataset(
input_img=mini_wsi_svs,
mask=np.array([[0, 0, 1]]),
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
auto_get_mask=False,
resolution=1.0,
units="mpp",
)
ds = WSIPatchDataset(
input_img=mini_wsi_svs,
mask=positive_mask,
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
auto_get_mask=False,
resolution=1.0,
units="mpp",
)

assert len(ds) > 0

# check mask VirtualWSIReader
with pytest.raises(ValueError, match=r".*`mask` must be binary.*"):
WSIPatchDataset(
input_img=mini_wsi_svs,
mask=VirtualWSIReader(np.array([[0, 0, 5]])),
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
auto_get_mask=False,
resolution=1.0,
units="mpp",
)

ds_from_fp = WSIPatchDataset(
input_img=mini_wsi_svs,
mask=VirtualWSIReader(positive_mask, mode="bool"),
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
auto_get_mask=False,
resolution=1.0,
units="mpp",
)

assert len(ds_from_fp) > 0

mini_wsi_svs_np = imread(mini_wsi_svs)
ds_from_np = WSIPatchDataset(
input_img=mini_wsi_svs_np,
mask=VirtualWSIReader(positive_mask, mode="bool"),
mode="wsi",
patch_input_shape=[512, 512],
stride_shape=[256, 256],
auto_get_mask=False,
resolution=1.0,
units="baseline",
)

assert len(ds_from_np) > 0


def test_patch_dataset_abc():
"""Test for ABC methods."""
Expand Down Expand Up @@ -489,6 +572,12 @@ def test_predictor_crash():
predictor.predict([1, 2, 3], masks=[1, 2], mode="wsi")
with pytest.raises(ValueError, match=r".*labels.*!=.*imgs.*"):
predictor.predict([1, 2, 3], labels=[1, 2], mode="patch")
# mask on patch are not supported
with pytest.raises(ValueError, match=r".*masks are not supported .* `patch`.*"):
predictor.predict(
[np.array([1, 2, 3])], masks=[np.array([1, 2, 3])], mode="patch"
)

# remove previously generated data
_rm_dir("output")

Expand Down Expand Up @@ -673,7 +762,7 @@ def test_patch_predictor_api(sample_patch1, sample_patch2, tmp_path):
# test prediction
predictor = PatchPredictor(model=model, batch_size=1, verbose=False)
output = predictor.predict(
inputs,
input_imgs=inputs,
return_probabilities=True,
labels=[1, "a"],
return_labels=True,
Expand Down Expand Up @@ -799,6 +888,58 @@ def test_wsi_predictor_api(sample_wsi_dict, tmp_path):
# remove previously generated data
_rm_dir("output")

# check that predictor can take in WSIReader object
svs_objects = [WSIReader.open(i) for i in [mini_wsi_svs, mini_wsi_svs]]
output = predictor.predict(
svs_objects,
masks=[mini_wsi_msk, mini_wsi_msk],
mode="wsi",
**kwargs,
)
assert str(mini_wsi_svs) in output
# remove previously generated data
_rm_dir(kwargs["save_dir"])

# check that predictor can take in ndarray object
img_objects = [
WSIReader.open(i).slide_thumbnail(1, "baseline")
for i in [mini_wsi_svs, mini_wsi_svs]
]

with pytest.raises(ValueError, match=".*Cannot determine scale.*"):
predictor.predict(
img_objects,
masks=[mini_wsi_msk, mini_wsi_msk],
mode="wsi",
**kwargs,
)
_rm_dir(kwargs["save_dir"])

_ = predictor.predict(
img_objects,
masks=[mini_wsi_msk, mini_wsi_msk],
mode="tile",
ignore_resolutions=True,
**kwargs,
)
_rm_dir(kwargs["save_dir"])

_kwargs = copy.deepcopy(kwargs)
_kwargs["units"] = "baseline"
_kwargs["resolution"] = 1.0

output = predictor.predict(
img_objects,
masks=[mini_wsi_msk, mini_wsi_msk],
mode="wsi",
**_kwargs,
)

assert len(output) == 2
assert 0 in output
assert 1 in output
_rm_dir(_kwargs["save_dir"])


def test_wsi_predictor_merge_predictions(sample_wsi_dict):
"""Test normal run of wsi predictor with merge predictions option."""
Expand Down
8 changes: 3 additions & 5 deletions tests/test_patch_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,9 @@ def test_get_patch_extractor(source_image, patch_extr_csv):


def test_points_patch_extractor_image_format(
sample_svs, sample_jp2, source_image, patch_extr_csv
sample_svs, sample_jp2, source_image, patch_extr_csv, blank_sample
):
"""Test PointsPatchExtractor returns the right object."""
file_parent_dir = pathlib.Path(__file__).parent
locations_list = pathlib.Path(patch_extr_csv)

points = patchextraction.get_patch_extractor(
Expand Down Expand Up @@ -131,10 +130,9 @@ def test_points_patch_extractor_image_format(

assert isinstance(points.wsi, OmnyxJP2WSIReader)

false_image = pathlib.Path(file_parent_dir.joinpath("data/source_image.test"))
with pytest.raises(FileNotSupported):
with blank_sample(".test") as false_image_path, pytest.raises(FileNotSupported):
_ = patchextraction.get_patch_extractor(
input_img=false_image,
input_img=false_image_path,
locations_list=locations_list,
method_name="point",
patch_size=(200, 200),
Expand Down
Loading