From 95e3b0609ea42d5b078150d23bc71624dac9e1bd Mon Sep 17 00:00:00 2001 From: kaczmarj Date: Thu, 22 Feb 2024 13:25:09 -0500 Subject: [PATCH] handle symlinked slides dirs fixes #214 --- tests/test_all.py | 25 +++++++++++++++++ wsinfer/cli/convert_csv_to_sbubmi.py | 10 +++---- wsinfer/cli/infer.py | 11 +++----- wsinfer/cli/patch.py | 40 ++++++++++++++-------------- wsinfer/patchlib/__init__.py | 4 +-- 5 files changed, 54 insertions(+), 36 deletions(-) diff --git a/tests/test_all.py b/tests/test_all.py index 8d175be..a6c47d0 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -532,3 +532,28 @@ def test_issue_203(tiff_image: Path) -> None: img = oslide.read_region((w, h), level=0, size=(256, 256)) assert img.size == (256, 256) assert np.allclose(np.array(img), 0) + + +def test_issue_214(tmp_path: Path, tiff_image: Path) -> None: + """Test that symlinked slides don't mess things up.""" + link = tmp_path / "forlinks" / "arbitrary-link-name.tiff" + link.parent.mkdir(parents=True) + link.symlink_to(tiff_image) + + runner = CliRunner() + results_dir = tmp_path / "inference" + result = runner.invoke( + cli, + [ + "run", + "--wsi-dir", + str(link.parent), + "--results-dir", + str(results_dir), + "--model", + "breast-tumor-resnet34.tcga-brca", + ], + ) + assert result.exit_code == 0 + assert (results_dir / "patches" / link.with_suffix(".h5").name).exists() + assert (results_dir / "model-outputs-csv" / link.with_suffix(".csv").name).exists() diff --git a/wsinfer/cli/convert_csv_to_sbubmi.py b/wsinfer/cli/convert_csv_to_sbubmi.py index d55df94..091aaef 100644 --- a/wsinfer/cli/convert_csv_to_sbubmi.py +++ b/wsinfer/cli/convert_csv_to_sbubmi.py @@ -249,21 +249,17 @@ def get_color(row: pd.Series) -> tuple[float, float, float]: @click.command() @click.argument( "results_dir", - type=click.Path( - exists=True, file_okay=False, dir_okay=True, path_type=Path, resolve_path=True - ), + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), ) @click.argument( "output", - type=click.Path(exists=False, path_type=Path, resolve_path=True), + type=click.Path(exists=False, path_type=Path), ) @click.option( "--wsi-dir", required=True, help="Directory with whole slide images.", - type=click.Path( - exists=True, file_okay=False, dir_okay=True, path_type=Path, resolve_path=True - ), + type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path), ) @click.option("--execution-id", required=True, help="Unique execution ID for this run.") @click.option("--study-id", required=True, help="Study ID, like TCGA-BRCA.") diff --git a/wsinfer/cli/infer.py b/wsinfer/cli/infer.py index 639452b..5c03c2e 100644 --- a/wsinfer/cli/infer.py +++ b/wsinfer/cli/infer.py @@ -188,7 +188,7 @@ def get_stdout(args: list[str]) -> str: @click.option( "-i", "--wsi-dir", - type=click.Path(exists=True, file_okay=False, path_type=Path, resolve_path=True), + type=click.Path(exists=True, file_okay=False, path_type=Path), required=True, help="Directory containing whole slide images. This directory can *only* contain" " whole slide images.", @@ -196,7 +196,7 @@ def get_stdout(args: list[str]) -> str: @click.option( "-o", "--results-dir", - type=click.Path(file_okay=False, path_type=Path, resolve_path=True), + type=click.Path(file_okay=False, path_type=Path), required=True, help="Directory to store results. If directory exists, will skip" " whole slides for which outputs exist.", @@ -212,7 +212,7 @@ def get_stdout(args: list[str]) -> str: @click.option( "-c", "--config", - type=click.Path(exists=True, dir_okay=False, path_type=Path, resolve_path=True), + type=click.Path(exists=True, dir_okay=False, path_type=Path), help=( "Path to configuration for the trained model. Use this option if the" " model weights are not registered in wsinfer. Mutually exclusive with" @@ -222,7 +222,7 @@ def get_stdout(args: list[str]) -> str: @click.option( "-p", "--model-path", - type=click.Path(exists=True, dir_okay=False, path_type=Path, resolve_path=True), + type=click.Path(exists=True, dir_okay=False, path_type=Path), help=( "Path to the pretrained model. Use only when --config is passed. Mutually " "exclusive with --model." @@ -349,9 +349,6 @@ def run( "--config and --model-path must both be set if one is set." ) - wsi_dir = wsi_dir.resolve() - results_dir = results_dir.resolve() - if not wsi_dir.exists(): raise FileNotFoundError(f"Whole slide image directory not found: {wsi_dir}") diff --git a/wsinfer/cli/patch.py b/wsinfer/cli/patch.py index 56d9983..d914bf3 100644 --- a/wsinfer/cli/patch.py +++ b/wsinfer/cli/patch.py @@ -11,7 +11,7 @@ @click.option( "-i", "--wsi-dir", - type=click.Path(exists=True, file_okay=False, path_type=Path, resolve_path=True), + type=click.Path(exists=True, file_okay=False, path_type=Path), required=True, help="Directory containing whole slide images. This directory can *only* contain" " whole slide images.", @@ -19,7 +19,7 @@ @click.option( "-o", "--results-dir", - type=click.Path(file_okay=False, path_type=Path, resolve_path=True), + type=click.Path(file_okay=False, path_type=Path), required=True, help="Directory to store patch results. If directory exists, will skip" " whole slides for which outputs exist.", @@ -32,7 +32,7 @@ help="Physical spacing of the patch in micrometers per pixel.", ) @click.option( - "--thumbsize", + "--seg-thumbsize", default=(2048, 2048), type=(int, int), help="The size of the slide thumbnail (in pixels) used for tissue segmentation." @@ -40,25 +40,25 @@ " max(thumbsize).", ) @click.option( - "--median-filter-size", + "--seg-median-filter-size", default=7, type=click.IntRange(min=3), help="The kernel size for median filtering. Must be greater than 1 and odd.", ) @click.option( - "--binary-threshold", + "--seg-binary-threshold", default=7, type=click.IntRange(min=1), help="The threshold for image binarization.", ) @click.option( - "--closing-kernel-size", + "--seg-closing-kernel-size", default=6, type=click.IntRange(min=1), help="The kernel size for binary closing (morphological operation).", ) @click.option( - "--min-object-size-um2", + "--seg-min-object-size-um2", default=200**2, type=click.FloatRange(min=0), help="The minimum size of an object to keep during tissue detection. If a" @@ -66,7 +66,7 @@ " The default is 200um x 200um. The units of this argument are microns squared.", ) @click.option( - "--min-hole-size-um2", + "--seg-min-hole-size-um2", default=190**2, type=click.FloatRange(min=0), help="The minimum size of a hole to keep as a hole. If a hole is smaller than this" @@ -78,12 +78,12 @@ def patch( results_dir: str, patch_size_px: int, patch_spacing_um_px: float, - thumbsize: tuple[int, int], - median_filter_size: int, - binary_threshold: int, - closing_kernel_size: int, - min_object_size_um2: float, - min_hole_size_um2: float, + seg_thumbsize: tuple[int, int], + seg_median_filter_size: int, + seg_binary_threshold: int, + seg_closing_kernel_size: int, + seg_min_object_size_um2: float, + seg_min_hole_size_um2: float, ) -> None: """Patch a directory of whole slide iamges.""" segment_and_patch_directory_of_slides( @@ -91,10 +91,10 @@ def patch( save_dir=results_dir, patch_size_px=patch_size_px, patch_spacing_um_px=patch_spacing_um_px, - thumbsize=thumbsize, - median_filter_size=median_filter_size, - binary_threshold=binary_threshold, - closing_kernel_size=closing_kernel_size, - min_object_size_um2=min_object_size_um2, - min_hole_size_um2=min_hole_size_um2, + thumbsize=seg_thumbsize, + median_filter_size=seg_median_filter_size, + binary_threshold=seg_binary_threshold, + closing_kernel_size=seg_closing_kernel_size, + min_object_size_um2=seg_min_object_size_um2, + min_hole_size_um2=seg_min_hole_size_um2, ) diff --git a/wsinfer/patchlib/__init__.py b/wsinfer/patchlib/__init__.py index 38776fd..10b7989 100644 --- a/wsinfer/patchlib/__init__.py +++ b/wsinfer/patchlib/__init__.py @@ -85,8 +85,8 @@ def segment_and_patch_one_slide( None """ - save_dir = Path(save_dir).resolve() - slide_path = Path(slide_path).resolve() + save_dir = Path(save_dir) + slide_path = Path(slide_path) slide_prefix = slide_path.stem logger.info(f"Segmenting and patching slide {slide_path}")