diff --git a/setup.cfg b/setup.cfg index 437ab23..fcc2e9e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,9 +30,6 @@ classifiers = packages = find: python_requires = >= 3.7 install_requires = - # We require torch and torchvision but do not include them here because their - # installation varies by platform and hardware. - # See https://pytorch.org/get-started/locally/. click>=8.0,<9 h5py # OpenSlide and TIFF readers should handle all images we will encounter. @@ -43,6 +40,11 @@ install_requires = pillow pyyaml timm + # The installation fo torch and torchvision can differ by hardware. Users are + # advised to install torch and torchvision for their given hardware and then install + # wsinfer. See https://pytorch.org/get-started/locally/. + torch>=1.7 + torchvision tqdm [options.extras_require] diff --git a/tests/test_all.py b/tests/test_all.py index faf8d1e..db236b2 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -53,7 +53,6 @@ def tiff_image(tmp_path: Path) -> Path: def test_cli_list(tmp_path: Path): - from wsinfer.cli.cli import cli runner = CliRunner() @@ -967,6 +966,7 @@ def test_jit_compile(model_name: str, weights_name: str): def test_issue_89(): + """Do not fail if 'git' is not installed.""" from wsinfer.cli.infer import _get_info_for_save w = get_model_weights("resnet34", "TCGA-BRCA-v1") @@ -988,3 +988,34 @@ def test_issue_89(): assert d["runtime"]["git"] is None finally: os.environ["PATH"] = orig_path # reset path + + +def test_issue_94(tmp_path: Path, tiff_image: Path): + """Gracefully handle unreadable slides.""" + from wsinfer.cli.cli import cli + + # We have a valid tiff in 'tiff_image.parent'. We put in an unreadable file too. + badpath = tiff_image.parent / "bad.svs" + badpath.touch() + + runner = CliRunner() + results_dir = tmp_path / "inference" + result = runner.invoke( + cli, + [ + "run", + "--wsi-dir", + str(tiff_image.parent), + "--model", + "resnet34", + "--weights", + "TCGA-BRCA-v1", + "--results-dir", + str(results_dir), + ], + ) + # Important part is that we run through all of the files, despite the unreadble + # file. + assert result.exit_code == 0 + assert results_dir.joinpath("model-outputs").joinpath("purple.csv").exists() + assert not results_dir.joinpath("model-outputs").joinpath("bad.csv").exists() diff --git a/wsinfer/_patchlib/create_patches_fp.py b/wsinfer/_patchlib/create_patches_fp.py index 470c347..ddcfcbe 100644 --- a/wsinfer/_patchlib/create_patches_fp.py +++ b/wsinfer/_patchlib/create_patches_fp.py @@ -118,7 +118,6 @@ def seg_and_patch( process_list=None, patch_spacing=None, ): - slides = sorted(os.listdir(source)) slides = [slide for slide in slides if os.path.isfile(os.path.join(source, slide))] if process_list is None: @@ -160,7 +159,7 @@ def seg_and_patch( df.to_csv(os.path.join(save_dir, "process_list_autogen.csv"), index=False) idx = process_stack.index[i] slide = process_stack.loc[idx, "slide_id"] - print("\n\nprogress: {:.2f}, {}/{}".format(i / total, i, total)) + print("\n\nprogress: {:.1%}, {}/{}".format(i / total, i + 1, total)) print("processing {}".format(slide)) df.loc[idx, "process"] = 0 @@ -173,7 +172,12 @@ def seg_and_patch( # Inialize WSI full_path = os.path.join(source, slide) - WSI_object = WholeSlideImage(full_path) + # Some slide files might be malformed and unreadable. Skip them. + try: + WSI_object = WholeSlideImage(full_path) + except Exception: + print(f"Failed to load slide, skipping {full_path}") + continue if use_default_params: current_vis_params = vis_params.copy() diff --git a/wsinfer/_patchlib/utils/utils.py b/wsinfer/_patchlib/utils/utils.py index e3362ce..f6a03ef 100644 --- a/wsinfer/_patchlib/utils/utils.py +++ b/wsinfer/_patchlib/utils/utils.py @@ -182,7 +182,6 @@ def generate_split( all_val_ids.extend(val_ids) if custom_test_ids is None: # sample test split - test_ids = np.random.choice(remaining_ids, test_num[c], replace=False) remaining_ids = np.setdiff1d(remaining_ids, test_ids) all_test_ids.extend(test_ids) diff --git a/wsinfer/_patchlib/wsi_core/WholeSlideImage.py b/wsinfer/_patchlib/wsi_core/WholeSlideImage.py index 171ca2d..e89ab6d 100644 --- a/wsinfer/_patchlib/wsi_core/WholeSlideImage.py +++ b/wsinfer/_patchlib/wsi_core/WholeSlideImage.py @@ -28,7 +28,6 @@ class WholeSlideImage(object): def __init__(self, path): - """ Args: path (str): fullpath to WSI file @@ -242,7 +241,6 @@ def visWSI( seg_display=True, annot_display=True, ): - downsample = self.level_downsamples[vis_level] scale = [1 / downsample[0], 1 / downsample[1]] @@ -456,7 +454,6 @@ def _getPatchGenerator( count = 0 for y in range(start_y, stop_y, step_size_y): for x in range(start_x, stop_x, step_size_x): - if not self.isInContours( cont_check_fn, (x, y), diff --git a/wsinfer/_patchlib/wsi_core/wsi_utils.py b/wsinfer/_patchlib/wsi_core/wsi_utils.py index 3b3ac8b..2ca1cfa 100644 --- a/wsinfer/_patchlib/wsi_core/wsi_utils.py +++ b/wsinfer/_patchlib/wsi_core/wsi_utils.py @@ -204,7 +204,6 @@ def sample_rois( top_left=None, bot_right=None, ): - if len(scores.shape) == 2: scores = scores.flatten()