Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions hs2p/sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,10 @@ def process_slide(
for annotation in sampling_params.pixel_mapping.keys():
if sampling_params.tissue_percentage[annotation] is None:
continue
annotation_mask_dir = mask_visualize_dir / annotation
annotation_mask_dir.mkdir(exist_ok=True, parents=True)
tissue_mask_visu_path = None
if cfg.visualize and mask_visualize_dir is not None:
annotation_mask_dir = mask_visualize_dir / annotation
annotation_mask_dir.mkdir(exist_ok=True, parents=True)
tissue_mask_visu_path = Path(annotation_mask_dir, f"{wsi_name}.jpg")
coordinates, contour_indices, tile_level, resize_factor, tile_size_lv0 = sample_coordinates(
wsi_path=wsi_path,
Expand Down
2 changes: 1 addition & 1 deletion hs2p/wsi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def _precompute_tissue_mask(self):
cv2.drawContours(contour_mask, [self.cont], 0, 255, -1)

# Draw black filled holes on white filled contour
cv2.drawContours(contour_mask, self.holes, 0, 0, -1)
cv2.drawContours(contour_mask, self.holes, -1, 0, -1)

# Combine with the tissue mask
return cv2.bitwise_and(self.mask, contour_mask)
Expand Down
18 changes: 15 additions & 3 deletions hs2p/wsi/wsi.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,8 @@ def filter_black_and_white_tiles(
downsample = tile_spacing / self.get_level_spacing(0)
img_w, img_h = self.level_dimensions[tile_level]
filtered_keep_flags = []
error_count = 0
error_samples = []
for keep, coord in zip(keep_flags, coord_candidates):
if keep:
try:
Expand Down Expand Up @@ -604,9 +606,19 @@ def filter_black_and_white_tiles(
> filter_params.fraction_threshold
):
keep = 0
except Exception:
pass
except Exception as e:
error_count += 1
if len(error_samples) < 3:
error_samples.append(str(e))
filtered_keep_flags.append(keep)
if error_count > 0:
slide_id = getattr(self, "path", "<unknown-slide>")
sample_msg = "; ".join(error_samples)
warnings.warn(
f"Encountered {error_count} tile filtering error(s) on slide {slide_id}. "
f"Keeping affected tile(s). Sample error(s): {sample_msg}",
UserWarning,
)
return filtered_keep_flags
return keep_flags

Expand Down Expand Up @@ -1055,4 +1067,4 @@ def process_contour(
)

else:
return [], [], [], None, None
return [], [], [], None, None
43 changes: 43 additions & 0 deletions tests/test_filter_black_white_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import numpy as np
import pytest


wsi_mod = pytest.importorskip("hs2p.wsi.wsi")


def test_filter_black_white_tile_errors_warn_and_keep_tile():
wsi = object.__new__(wsi_mod.WholeSlideImage)
wsi.path = "fake-slide.tif"
wsi.level_dimensions = [(100, 100)]
wsi.get_level_spacing = lambda level: 1.0

def _raise(*args, **kwargs):
raise RuntimeError("read failure")

wsi.get_tile = _raise

filter_params = wsi_mod.FilterParameters(
ref_tile_size=16,
a_t=1,
a_h=1,
max_n_holes=1,
filter_white=True,
filter_black=False,
white_threshold=220,
black_threshold=25,
fraction_threshold=0.9,
)

keep_flags = [1]
coord_candidates = np.array([[0, 0]])

with pytest.warns(UserWarning, match="tile filtering"):
filtered = wsi.filter_black_and_white_tiles(
keep_flags=keep_flags,
coord_candidates=coord_candidates,
tile_size=32,
tile_level=0,
filter_params=filter_params,
)

assert filtered == [1]
46 changes: 46 additions & 0 deletions tests/test_sampling_process_slide.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from pathlib import Path
from types import SimpleNamespace

import pytest


sampling_mod = pytest.importorskip("hs2p.sampling")


def test_independent_sampling_no_visualization_does_not_crash(monkeypatch, tmp_path):
cfg = SimpleNamespace(
visualize=False,
output_dir=str(tmp_path),
tiling=SimpleNamespace(
backend="asap",
params=SimpleNamespace(spacing=0.5, tile_size=256),
seg_params=SimpleNamespace(),
filter_params=SimpleNamespace(),
visu_params=SimpleNamespace(downsample=32),
sampling_params=SimpleNamespace(independant_sampling=True),
),
)
sampling_params = sampling_mod.SamplingParameters(
pixel_mapping={"tumor": 1},
color_mapping=None,
tissue_percentage={"tumor": 0.1},
)

def _fake_sample_coordinates(**kwargs):
return ([(0, 0)], [0], 0, 1.0, 256)

monkeypatch.setattr(sampling_mod, "sample_coordinates", _fake_sample_coordinates)
monkeypatch.setattr(sampling_mod, "save_coordinates", lambda **kwargs: None)
monkeypatch.setattr(sampling_mod, "visualize_coordinates", lambda **kwargs: None)
monkeypatch.setattr(sampling_mod, "overlay_mask_on_slide", lambda **kwargs: None)

_, status_info = sampling_mod.process_slide(
wsi_path=Path("fake-wsi.tif"),
mask_path=Path("fake-mask.tif"),
cfg=cfg,
mask_visualize_dir=None,
sampling_visualize_dir=None,
sampling_params=sampling_params,
)

assert status_info["status"] == "success"
35 changes: 35 additions & 0 deletions tests/test_tissue_mask_holes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import numpy as np
import pytest


utils_mod = pytest.importorskip("hs2p.wsi.utils")


def _rect(x0: int, y0: int, x1: int, y1: int) -> np.ndarray:
return np.array(
[[[x0, y0]], [[x1, y0]], [[x1, y1]], [[x0, y1]]],
dtype=np.int32,
)


def test_precomputed_mask_subtracts_all_holes():
contour = _rect(2, 2, 17, 17)
holes = [_rect(4, 4, 6, 6), _rect(12, 12, 14, 14)]
tissue_mask = np.full((20, 20), 255, dtype=np.uint8)

checker = utils_mod.HasEnoughTissue(
contour=contour,
contour_holes=holes,
tissue_mask=tissue_mask,
target_tile_size=4,
tile_spacing=1.0,
resize_factor=1.0,
seg_spacing=1.0,
spacing_at_level_0=1.0,
pct=0.01,
)

mask = checker.precomputed_mask
assert mask[5, 5] == 0
assert mask[13, 13] == 0
assert mask[9, 9] == 1