### Install WSInfer for development Source: https://wsinfer.readthedocs.io/en/latest/installing.html Clone the repository and install in editable mode with development dependencies. ```bash git clone https://github.com/SBU-BMI/wsinfer.git cd wsinfer python -m pip install --editable .[dev] pre-commit install ``` -------------------------------- ### Check WSInfer installation Source: https://wsinfer.readthedocs.io/en/latest/_sources/installing.rst.txt Displays the help menu to verify that the wsinfer command is correctly installed. ```bash wsinfer --help ``` -------------------------------- ### Developer installation Source: https://wsinfer.readthedocs.io/en/latest/_sources/installing.rst.txt Clones the repository and installs the package in editable mode with development dependencies. ```bash git clone https://github.com/SBU-BMI/wsinfer.git cd wsinfer python -m pip install --editable .[dev] pre-commit install ``` ```bash git clone https://github.com/SBU-BMI/wsinfer.git cd wsinfer python -m pip install --editable .[dev,openslide] wsinfer --help ``` -------------------------------- ### Install WSInfer for development with OpenSlide Source: https://wsinfer.readthedocs.io/en/latest/installing.html Clone the repository and install in editable mode with development and OpenSlide dependencies. ```bash git clone https://github.com/SBU-BMI/wsinfer.git cd wsinfer python -m pip install --editable .[dev,openslide] wsinfer --help ``` -------------------------------- ### Install WSInfer via pip Source: https://wsinfer.readthedocs.io/en/latest/_sources/installing.rst.txt Commands to install stable or unstable versions of WSInfer using pip. ```bash python -m pip install wsinfer ``` ```bash python -m pip install git+https://github.com/SBU-BMI/wsinfer ``` -------------------------------- ### Install WSInfer via conda Source: https://wsinfer.readthedocs.io/en/latest/_sources/installing.rst.txt Installs the latest stable version of WSInfer using conda or mamba. ```bash conda install -c conda-forge wsinfer ``` -------------------------------- ### Get help for WSInfer commands Source: https://wsinfer.readthedocs.io/en/latest/_sources/user_guide.rst.txt Use the --help flag to view available subcommands and options for the WSInfer CLI. ```bash wsinfer --help ``` ```bash wsinfer run --help ``` -------------------------------- ### Sample CSV output format Source: https://wsinfer.readthedocs.io/en/latest/_sources/user_guide.rst.txt Example of the first few rows of a model output CSV file, showing patch coordinates and probability scores. ```csv minx,miny,width,height,prob_Tumor 4200,27300,2100,2100,6.4415544e-05 4200,29400,2100,2100,9.763688e-05 4200,31500,2100,2100,0.03654445 ``` -------------------------------- ### Defining Generic Protocol Classes Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/wsi/index.rst.txt Example of defining a generic Protocol class. ```python class GenProto(Protocol[T]): def meth(self) -> T: ... ``` -------------------------------- ### Defining Protocol Classes Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/wsi/index.rst.txt Example of defining a Protocol class for structural subtyping. ```python class Proto(Protocol): def meth(self) -> int: ... ``` -------------------------------- ### Using Protocol Classes for Type Checking Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/wsi/index.rst.txt Example of using a Protocol class with static type checkers. ```python class C: def meth(self) -> int: return 0 def func(x: Proto) -> int: return x.meth() func(C()) # Passes static type check ``` -------------------------------- ### Check PyTorch CUDA version Source: https://wsinfer.readthedocs.io/en/latest/_sources/installing.rst.txt Verifies if a GPU-enabled version of PyTorch is installed by checking the CUDA version string. ```bash python -c 'import torch; print(torch.version.cuda)' ``` -------------------------------- ### Add Image and GeoJSON to QuPath Project Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/qupath.html Adds a whole slide image and its associated GeoJSON annotations to an existing QuPath project. Requires the paquo library to be installed and configured. ```python def add_image_and_geojson( qupath_proj: QuPathProject, *, image_path: Path | str, geojson_path: Path | str, ) -> None: with open(geojson_path) as f: try: geojson_features = json.load(f)["features"] except Exception as e: print(f"Unable to find features key:: {e}") entry = qupath_proj.add_image(image_path) if not isinstance(entry, QuPathProjectImageEntry): print("!!!!!!!!!!!!!!!!!!!!!") print( "Runtime error, please contact developer, explaining that the entry when" " adding an image returns a list of image entry objects." ) return try: hierarchy: QuPathPathObjectHierarchy = entry.hierarchy hierarchy.load_geojson(geojson_features) except Exception as e: print(f"Failed to run load_geojson function with error:: {e}") ``` -------------------------------- ### Initialize WSInfer module and versioning Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer.html This snippet handles the package versioning by attempting to import from _version or defaulting to an unknown version. ```python """WSInfer is a toolkit for fast patch-based inference on whole slide images.""" from __future__ import annotations try: from ._version import __version__ except ImportError: [docs] __version__ = "0.0.unknown" ``` -------------------------------- ### Backend Configuration and Initialization Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/wsi.html Initializes the slide backend based on available libraries, prioritizing TiffSlide over OpenSlide. ```python try: import openslide # Test that OpenSlide object exists. If it doesn't, an error will be thrown and # caught. For some reason, it is possible that openslide-python can be installed # but the OpenSlide object (and other openslide things) are not available. openslide.OpenSlide # noqa: B018 [docs] HAS_OPENSLIDE = True logger.debug("Imported openslide") except Exception as err: HAS_OPENSLIDE = False logger.debug(f"Unable to import openslide due to error: {err}") try: import tiffslide [docs] HAS_TIFFSLIDE = True logger.debug("Imported tiffslide") except Exception as err: HAS_TIFFSLIDE = False logger.debug(f"Unable to import tiffslide due to error: {err}") if not HAS_TIFFSLIDE and not HAS_OPENSLIDE: raise NoBackendException( "No backend is available. Please install openslide or tiffslide." ) ``` -------------------------------- ### Create QuPath Project from WSInfer Outputs Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/qupath.html Automates the creation of a new QuPath project by scanning a results directory for CSV, JSON, and SVS files. Validates the presence of QuPath via the paquo library before execution. ```python def make_qupath_project(wsi_dir: Path, results_dir: Path) -> None: if not HAS_PAQUO: print( """Cannot find QuPath. QuPath is required to use this functionality but it cannot be found. If QuPath is installed, please use define the environment variable PAQUO_QUPATH_DIR with the location of the QuPath installation. If QuPath is not installed, please install it from https://qupath.github.io/.""" ) sys.exit(1) else: print("Found QuPath successfully!") QUPATH_PROJECT_DIRECTORY = results_dir / "model-outputs-qupath" csv_files = list((results_dir / "model-outputs-csv").glob("*.csv")) slides_and_geojsons = [] for csv_file in csv_files: file_name = csv_file.stem json_file = results_dir / "model-outputs-geojson" / (file_name + ".json") image_file = wsi_dir / (file_name + ".svs") if json_file.exists() and image_file.exists(): matching_pair = (image_file, json_file) slides_and_geojsons.append(matching_pair) else: print(f"Skipping CSV: {csv_file.name} (No corresponding JSON)") with QuPathProject(QUPATH_PROJECT_DIRECTORY, mode="w") as qp: for image_path, geojson_path in slides_and_geojsons: try: add_image_and_geojson( qp, image_path=image_path, geojson_path=geojson_path ) except Exception as e: print(f"Failed to add image/geojson with error:: {e}") print("Successfully created QuPath Project!") ``` -------------------------------- ### Run WSInfer inference and utilities Source: https://wsinfer.readthedocs.io/en/latest/installing.html Common commands for listing models, running inference, and converting outputs. ```bash wsinfer-zoo ls ``` ```bash wsinfer run --wsi-dir slides/ --results-dir results/ --model breast-tumor-resnet34.tcga-brca ``` ```bash wsinfer togeojson results/ model-outputs-geojson/ ``` -------------------------------- ### Pull and run WSInfer with Apptainer Source: https://wsinfer.readthedocs.io/en/latest/_sources/user_guide.rst.txt Commands to pull the latest WSInfer container and execute inference using Apptainer. ```bash apptainer pull docker://kaczmarj/wsinfer:latest ``` ```bash apptainer run \ --nv \ --bind $(pwd) \ --env CUDA_VISIBLE_DEVICES=0 \ wsinfer_latest.sif run \ --wsi-dir slides/ \ --results-dir results/ \ --model breast-tumor-resnet34.tcga-brca ``` -------------------------------- ### Get patch coordinates within a polygon Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/patchlib/patch.html Calculates the top-left coordinates of patches that fall within a given shapely Polygon, using a specified patch size and overlap. ```python def get_patch_coordinates_within_polygon( slide_width: int, slide_height: int, patch_size: int, half_patch_size: int, polygon: Polygon, overlap: float = 0.0, ) -> npt.NDArray[np.int_]: """Get coordinates of patches within a polygon. Parameters ---------- slide_width : int The width of the slide in pixels at base resolution. slide_height : int The height of the slide in pixels at base resolution. patch_size : int The size of a patch in pixels. half_patch_size : int Half of the length of a patch in pixels. polygon : Polygon A shapely Polygon representing the presence of tissue. overlap : float The proportion of the patch_size to overlap. A value of 0.5 would have an overlap of 50%. A value of 0.2 would have an overlap of 20%. Negative values will add space between patches. A value of -1 would skip every other patch. Value must be in (-inf, 1). The default value of 0.0 produces non-overlapping patches. Returns ------- coordinates Array with shape (N, 2), where N is the number of tiles. Each row in this array contains the coordinates of the top-left of a tile: (minx, miny). """ if overlap >= 1: raise ValueError(f"overlap must be in (-inf, 1) but got {overlap}") # Handle potentially overlapping slides. step_size = round((1 - overlap) * patch_size) logger.info(f"Patches are {patch_size} px, with step size of {step_size} px.") # Make an array of Nx2, where each row is (x, y) centroid of the patch. tile_centroids_arr: npt.NDArray[np.int_] = np.array( list( itertools.product( range(0 + half_patch_size, slide_width, step_size), range(0 + half_patch_size, slide_height, step_size), ) ) ) tile_centroids_poly = [Point(c) for c in tile_centroids_arr] # Query which centroids are inside the polygon. tree = STRtree(tile_centroids_poly) centroid_indexes_in_polygon: npt.NDArray[np.int_] = tree.query( polygon, predicate="contains" ) # Sort so x and y are in ascending order (and y changes most rapidly). centroid_indexes_in_polygon.sort() tile_centroids_in_polygon = tile_centroids_arr[centroid_indexes_in_polygon] # Transform the centroids to the upper-left point (x, y). tile_minx_miny_in_polygon = tile_centroids_in_polygon - half_patch_size return tile_minx_miny_in_polygon ``` -------------------------------- ### Run WSInfer model Source: https://wsinfer.readthedocs.io/en/latest/user_guide.html Executes a patch classification model using a specified configuration file and input directory. ```bash wsinfer run --wsi-dir slides/ --results-dir results/ --model-path path/to/torchscript.pt --config config.json ``` -------------------------------- ### make_qupath_project(wsi_dir, results_dir) Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/qupath/index.rst.txt Creates a new QuPath project based on the provided whole slide image directory and results directory. ```APIDOC ## make_qupath_project ### Description Creates a new QuPath project based on the provided whole slide image directory and results directory. ### Signature `make_qupath_project(wsi_dir: pathlib.Path, results_dir: pathlib.Path) -> None` ### Parameters - **wsi_dir** (pathlib.Path) - Directory containing the whole slide images. - **results_dir** (pathlib.Path) - Directory containing the analysis results. ``` -------------------------------- ### Pull and run WSInfer with Docker Source: https://wsinfer.readthedocs.io/en/latest/_sources/user_guide.rst.txt Commands to pull the latest WSInfer image and execute inference using Docker, including necessary volume mounts and GPU configuration. ```bash docker pull kaczmarj/wsinfer:latest ``` ```bash docker run --rm -it \ --user $(id -u):$(id -g) \ --mount type=bind,source=$HOME,target=$HOME \ --mount type=bind,source=$(pwd),target=/work/ \ --gpus all \ --env CUDA_VISIBLE_DEVICES=0 \ --env HOME=$HOME \ --shm-size 512m \ kaczmarj/wsinfer:latest run \ --wsi-dir /work/slides/ \ --results-dir /work/results/ \ --model breast-tumor-resnet34.tcga-brca ``` -------------------------------- ### set_backend(name: str) -> None Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/wsi/index.rst.txt Sets the backend for WSI processing. ```APIDOC ## set_backend(name: str) -> None ### Description Sets the backend to be used for whole slide image operations. ### Parameters - **name** (str) - The name of the backend to use. ``` -------------------------------- ### jit_compile(model) Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/modellib/models/index.rst.txt JIT-compiles a model for inference. ```APIDOC ## jit_compile(model) ### Description JIT-compile a model for inference. A torchscript model may be JIT compiled here as well. ### Parameters - **model** (torch.nn.Module) - Required - The PyTorch module to compile. ### Returns - **torch.jit.ScriptModule | torch.nn.Module | Callable** - The compiled model. ``` -------------------------------- ### WSInfer Base Command Usage Source: https://wsinfer.readthedocs.io/en/latest/cli.html General syntax for invoking the WSInfer command-line tool. ```bash wsinfer [OPTIONS] COMMAND [ARGS]... ``` -------------------------------- ### Pull Docker image Source: https://wsinfer.readthedocs.io/en/latest/user_guide.html Download the latest WSInfer Docker image from the registry. ```bash docker pull kaczmarj/wsinfer:latest ``` -------------------------------- ### Run inference with Docker Source: https://wsinfer.readthedocs.io/en/latest/user_guide.html Execute inference in a Docker container, ensuring proper user permissions, home directory mounting, and GPU access. ```bash docker run --rm -it \ --user $(id -u):$(id -g) \ --mount type=bind,source=$HOME,target=$HOME \ --mount type=bind,source=$(pwd),target=/work/ \ --gpus all \ --env CUDA_VISIBLE_DEVICES=0 \ --env HOME=$HOME \ --shm-size 512m \ kaczmarj/wsinfer:latest run \ --wsi-dir /work/slides/ \ --results-dir /work/results/ \ --model breast-tumor-resnet34.tcga-brca ``` -------------------------------- ### Run inference with Apptainer Source: https://wsinfer.readthedocs.io/en/latest/user_guide.html Execute inference using the Apptainer container with GPU support and directory binding. ```bash apptainer run \ --nv \ --bind $(pwd) \ --env CUDA_VISIBLE_DEVICES=0 \ wsinfer_latest.sif run \ --wsi-dir slides/ \ --results-dir results/ \ --model breast-tumor-resnet34.tcga-brca ``` -------------------------------- ### Run Command Syntax Source: https://wsinfer.readthedocs.io/en/latest/cli.html Syntax for the run command to initiate inference on whole slide images. ```bash wsinfer run [OPTIONS] ``` -------------------------------- ### List available models Source: https://wsinfer.readthedocs.io/en/latest/index.html Displays the list of trained models currently available for use in the pipeline. ```bash wsinfer-zoo ls ``` -------------------------------- ### Validate WSInfer configuration Source: https://wsinfer.readthedocs.io/en/latest/user_guide.html Use this command to verify the structure and content of a JSON configuration file before running a model. ```bash wsinfer-zoo validate-config config.json ``` -------------------------------- ### JIT Compilation for Inference Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/modellib/models.html Utility to JIT-compile a PyTorch model for optimized inference, supporting both PyTorch 2.x compile and legacy TorchScript methods. ```python [docs] def jit_compile( model: torch.nn.Module, ) -> torch.jit.ScriptModule | torch.nn.Module | Callable: """JIT-compile a model for inference. A torchscript model may be JIT compiled here as well. """ noncompiled = model device = next(model.parameters()).device # Attempt to script. If it fails, return the original. test_input = torch.ones(1, 3, 224, 224).to(device) w = "Warning: could not JIT compile the model. Using non-compiled model instead." # PyTorch 2.x has torch.compile but it does not work when applied # to TorchScript models. if hasattr(torch, "compile") and not isinstance(model, torch.jit.ScriptModule): # Try to get the most optimized model. try: return torch.compile(model, fullgraph=True, mode="max-autotune") except Exception: pass try: return torch.compile(model, mode="max-autotune") except Exception: pass try: return torch.compile(model) except Exception: warnings.warn(w, stacklevel=1) return noncompiled # For pytorch 1.x, use torch.jit.script. else: try: mjit = torch.jit.script(model) with torch.no_grad(): mjit(test_input) except Exception: warnings.warn(w, stacklevel=1) return noncompiled # Now that we have scripted the model, try to optimize it further. If that # fails, return the scripted model. try: mjit_frozen = torch.jit.freeze(mjit) mjit_opt = torch.jit.optimize_for_inference(mjit_frozen) with torch.no_grad(): mjit_opt(test_input) return mjit_opt except Exception: return mjit # type: ignore ``` -------------------------------- ### Convert model outputs to Stony Brook format Source: https://wsinfer.readthedocs.io/en/latest/_sources/user_guide.rst.txt Command to convert WSInfer results into the QuIP-compatible format. ```bash wsinfer tosbu \ --wsi-dir slides/ \ --execution-id UNIQUE_ID_HERE \ --study-id STUDY_ID_HERE \ --make-color-text \ --num-processes 16 \ results/ \ results/model-outputs-sbubmi/ ``` -------------------------------- ### Create torchvision Compose from configuration Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/modellib/transforms.html Constructs a torchvision.transforms.Compose object by mapping configuration items to torchvision classes. ```python """PyTorch image classification transform.""" from __future__ import annotations from torchvision import transforms from wsinfer_zoo.client import TransformConfigurationItem # The subset of transforms known to the wsinfer config spec. # This can be expanded in the future as needs arise. _name_to_tv_cls = { "Resize": transforms.Resize, "ToTensor": transforms.ToTensor, "Normalize": transforms.Normalize, } [docs] def make_compose_from_transform_config( list_of_transforms: list[TransformConfigurationItem], ) -> transforms.Compose: """Create a torchvision Compose instance from configuration of transforms.""" all_t: list = [] for t in list_of_transforms: cls = _name_to_tv_cls[t.name] kwargs = t.arguments or {} all_t.append(cls(**kwargs)) return transforms.Compose(all_t) ``` -------------------------------- ### set_backend(name: str) -> None Source: https://wsinfer.readthedocs.io/en/latest/autoapi/wsinfer/wsi/index.html Sets the backend for whole slide image processing. ```APIDOC ## set_backend(name: str) ### Description Sets the backend to be used for whole slide image operations. ### Parameters - **name** (str) - Required - The name of the backend to set. ``` -------------------------------- ### Model and Module Utilities Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/modellib/models.html Functions for retrieving registered models and loading them as PyTorch modules. ```python from __future__ import annotations import dataclasses import warnings from typing import Callable import torch import wsinfer_zoo from wsinfer_zoo.client import HFModelTorchScript from wsinfer_zoo.client import Model @dataclasses.dataclass [docs] class LocalModelTorchScript(Model): ... [docs] def get_registered_model(name: str) -> HFModelTorchScript: registry = wsinfer_zoo.client.load_registry() model = registry.get_model_by_name(name=name) return model.load_model_torchscript() [docs] def get_pretrained_torch_module( model: HFModelTorchScript | LocalModelTorchScript, ) -> torch.nn.Module: """Get a PyTorch Module with weights loaded.""" mod: torch.nn.Module = torch.jit.load(model.model_path, map_location="cpu") if not isinstance(mod, torch.nn.Module): raise TypeError( "expected the loaded object to be a subclass of torch.nn.Module but got" f" {type(mod)}." ) return mod ``` -------------------------------- ### Run model inference Source: https://wsinfer.readthedocs.io/en/latest/_sources/user_guide.rst.txt Execute the inference workflow on a directory of whole slide images using a specified model. ```bash wsinfer run \ --wsi-dir slides/ \ --results-dir results/ \ --model breast-tumor-resnet34.tcga-brca \ ``` -------------------------------- ### Pull container images Source: https://wsinfer.readthedocs.io/en/latest/_sources/installing.rst.txt Commands to pull the WSInfer container image using Docker, Apptainer, or Singularity. ```bash docker pull kaczmarj/wsinfer ``` ```bash apptainer pull docker://kaczmarj/wsinfer ``` ```bash singularity pull docker://kaczmarj/wsinfer ``` -------------------------------- ### Validate and use custom model configuration Source: https://wsinfer.readthedocs.io/en/latest/_sources/user_guide.rst.txt Commands to validate a JSON configuration file and run inference using a custom TorchScript model. ```bash wsinfer-zoo validate-config config.json ``` ```bash wsinfer run --wsi-dir slides/ --results-dir results/ --model-path path/to/torchscript.pt --config config.json ``` -------------------------------- ### Set and Retrieve Slide Backend Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/wsi.html Functions to manually set the active backend and retrieve the corresponding class for slide operations. ```python [docs] def set_backend(name: str) -> None: global _BACKEND if name not in _allowed_backends: raise ValueError(f"Unknown backend: '{name}'") if name == "openslide" and not HAS_OPENSLIDE: raise BackendNotAvailable( "OpenSlide is not available. Please install the OpenSlide compiled" " library and the Python package 'openslide-python'." " See https://openslide.org/ for more information." ) elif name == "tiffslide": if not HAS_TIFFSLIDE: raise BackendNotAvailable( "TiffSlide is not available. Please install 'tiffslide'." ) logger.debug(f"Set backend to {name}") _BACKEND = name [docs] def get_wsi_cls() -> type[openslide.OpenSlide] | type[tiffslide.TiffSlide]: if _BACKEND not in _allowed_backends: raise ValueError( f"Unknown backend: '{_BACKEND}'. Please contact the developer!" ) if _BACKEND == "openslide": return openslide.OpenSlide # type: ignore elif _BACKEND == "tiffslide": return tiffslide.TiffSlide else: raise ValueError("Contact the developer, slide backend not known") ``` -------------------------------- ### Pull Apptainer container Source: https://wsinfer.readthedocs.io/en/latest/user_guide.html Download the latest WSInfer container image using Apptainer. ```bash apptainer pull docker://kaczmarj/wsinfer:latest ``` -------------------------------- ### Run model inference on WSI patches Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/modellib/run_inference.html Executes inference on a directory of whole slide images using a specified model. Requires that patching has already been performed in the results directory. ```python def run_inference( wsi_dir: str | Path, results_dir: str | Path, model_info: wsinfer_zoo.client.HFModelTorchScript | LocalModelTorchScript, batch_size: int = 32, num_workers: int = 0, speedup: bool = False, ) -> tuple[list[str], list[str]]: """Run model inference on a directory of whole slide images and save results to CSV. This assumes the patching has already been done and the results are stored in `results_dir`. An error will be raised otherwise. Output CSV files are written to `{results_dir}/model-outputs/`. Parameters ---------- wsi_dir : str or Path Directory containing whole slide images. This directory can *only* contain whole slide images. Otherwise, an error will be raised during model inference. results_dir : str or Path Directory containing results of patching. model_info : Instance of Weights including the model object and information about how to apply the model to new data. batch_size : int The batch size during the forward pass (default is 32). num_workers : int Number of workers for data loading (default is 0, meaning use a single thread). speedup : bool If True, JIT-compile the model. This has a startup cost but model inference should be faster (default False). Returns ------- A tuple of two lists of strings. The first list contains the slide IDs for which patching failed, and the second list contains the slide IDs for which model inference failed. """ # Make sure required directories exist. wsi_dir = Path(wsi_dir) if not wsi_dir.exists(): raise errors.WholeSlideImageDirectoryNotFound(f"directory not found: {wsi_dir}") wsi_paths = [p for p in wsi_dir.iterdir() if p.is_file()] if not wsi_paths: raise errors.WholeSlideImagesNotFound(wsi_dir) results_dir = Path(results_dir) if not results_dir.exists(): raise errors.ResultsDirectoryNotFound(results_dir) _validate_wsi_directory(wsi_dir) # Check patches directory. patch_dir = results_dir / "patches" if not patch_dir.exists(): raise errors.PatchDirectoryNotFound( "The 'patches' directory was not found in results directory. This can" " happen for a few reasons: 1) no tissue was detected in the slides," " 2) the physical spacing (MPP) could not be read from any of the slides" ", or 3) something else... Please read the logs above for potential errors." ) # Create the patch paths based on the whole slide image paths. In effect, only # create patch paths if the whole slide image patch exists. patch_paths = [patch_dir / p.with_suffix(".h5").name for p in wsi_paths] model_output_dir = results_dir / "model-outputs-csv" model_output_dir.mkdir(exist_ok=True) model = get_pretrained_torch_module(model=model_info) model.eval() # Set the device. # Use CPU if env var specifies. Prefer checking if this is false, because # if the var is set to almost anything (other than 0, False, f), then it # should be true. # This env var was introduced mainly for continuous integration tests that # failed on apple silicon in github actions. Forcing to cpu avoids failures. if os.getenv("WSINFER_FORCE_CPU", "0").lower() not in {"0", "f", "false"}: device = torch.device("cpu") elif torch.cuda.is_available(): device = torch.device("cuda") if torch.cuda.device_count() > 1: model = torch.nn.DataParallel(model) elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): device = torch.device("mps") else: device = torch.device("cpu") print(f'Using device "{device}"') model.to(device) ``` -------------------------------- ### Define WSInfer Custom Exceptions Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/errors.html Custom exception classes for handling specific errors like missing directories, unknown architectures, and backend issues. ```python """Exceptions used in WSInfer.""" from __future__ import annotations [docs] class WsinferException(Exception): """Base class for wsinfer exceptions.""" [docs] class UnknownArchitectureError(WsinferException): """Architecture is unknown and cannot be found.""" [docs] class WholeSlideImageDirectoryNotFound(WsinferException, FileNotFoundError): ... [docs] class DuplicateFilePrefixesFound(WsinferException): """A duplicate file prefix has been found. An example of duplicate file prefixes is files a.svs and a.tif. WSInfer relies on the stems as a unique ID, so we cannot allow duplicate stems. """ [docs] class WholeSlideImagesNotFound(WsinferException, FileNotFoundError): ... [docs] class ResultsDirectoryNotFound(WsinferException, FileNotFoundError): ... [docs] class PatchDirectoryNotFound(WsinferException, FileNotFoundError): ... [docs] class CannotReadSpacing(WsinferException): ... [docs] class NoBackendException(WsinferException): ... [docs] class BackendNotAvailable(WsinferException): """The requested backend is not available.""" ``` -------------------------------- ### WSInfer CLI operations Source: https://wsinfer.readthedocs.io/en/latest/_sources/installing.rst.txt Common command-line operations for listing models, running inference, and converting outputs. ```bash wsinfer --help ``` ```bash wsinfer-zoo ls ``` ```bash wsinfer run --wsi-dir slides/ --results-dir results/ --model breast-tumor-resnet34.tcga-brca ``` ```bash wsinfer togeojson results/ model-outputs-geojson/ ``` -------------------------------- ### Execute Model Inference on Whole Slide Images Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/modellib/run_inference.html This code block demonstrates the core inference loop, including model jit compilation, dataset initialization, batch processing, and saving probability results to CSV. ```python if speedup: if TYPE_CHECKING: model = type_cast(torch.nn.Module, jit_compile(model)) else: model = jit_compile(model) transform = make_compose_from_transform_config(model_info.config.transform) failed_patching = [p.stem for p in patch_paths if not p.exists()] failed_inference: list[str] = [] for i, (wsi_path, patch_path) in enumerate(zip(wsi_paths, patch_paths)): print(f"Slide {i+1} of {len(wsi_paths)}") print(f" Slide path: {wsi_path}") print(f" Patch path: {patch_path}") slide_csv_name = Path(wsi_path).with_suffix(".csv").name slide_csv = model_output_dir / slide_csv_name if slide_csv.exists(): print("Output CSV exists... skipping.") print(slide_csv) continue if not patch_path.exists(): print(f"Skipping because patch file not found: {patch_path}") continue try: dset = WholeSlideImagePatches( wsi_path=wsi_path, patch_path=patch_path, transform=transform, ) except Exception: failed_inference.append(wsi_dir.stem) continue # The worker_init_fn does not seem to be used when num_workers=0 # so we call it manually to finish setting up the dataset. if num_workers == 0: dset.worker_init() loader = torch.utils.data.DataLoader( dset, batch_size=batch_size, shuffle=False, num_workers=num_workers, worker_init_fn=dset.worker_init, ) # Store the coordinates and model probabiltiies of each patch in this slide. # This lets us know where the probabiltiies map to in the slide. slide_coords: list[npt.NDArray[np.integer]] = [] slide_probs: list[npt.NDArray[np.floating]] = [] for batch_imgs, batch_coords in tqdm.tqdm(loader): assert batch_imgs.shape[0] == batch_coords.shape[0], "length mismatch" with torch.no_grad(): logits: torch.Tensor = model(batch_imgs.to(device)).detach().cpu() # probs has shape (batch_size, num_classes) or (batch_size,) if len(logits.shape) > 1 and logits.shape[1] > 1: probs = torch.nn.functional.softmax(logits, dim=1) else: probs = torch.sigmoid(logits.squeeze(1)) # Cloning the tensor prevents memory accumulation and prevents # the error "RuntimeError: Too many open files". Jakub ran into this # error when running wsinfer on a slide in Windows Subsystem for Linux. slide_coords.append(batch_coords.clone().numpy()) slide_probs.append(probs.numpy()) slide_coords_arr = np.concatenate(slide_coords, axis=0) slide_df = pd.DataFrame( dict( minx=slide_coords_arr[:, 0], miny=slide_coords_arr[:, 1], width=slide_coords_arr[:, 2], height=slide_coords_arr[:, 3], ) ) slide_probs_arr = np.concatenate(slide_probs, axis=0) # Use 'prob-' prefix for all classes. This should make it clearer that the # column has probabilities for the class. It also makes it easier for us to # identify columns associated with probabilities. prob_colnames = [f"prob_{c}" for c in model_info.config.class_names] slide_df.loc[:, prob_colnames] = slide_probs_arr slide_df.to_csv(slide_csv, index=False) print("-" * 40) return failed_patching, failed_inference ``` -------------------------------- ### Verify PyTorch GPU availability Source: https://wsinfer.readthedocs.io/en/latest/_sources/installing.rst.txt Checks if PyTorch can detect the GPU by printing a boolean value. ```bash python -c 'import torch; print(torch.cuda.is_available())' ``` -------------------------------- ### Run model inference Source: https://wsinfer.readthedocs.io/en/latest/index.html Executes patch-based classification on whole slide images stored in a directory. ```bash wsinfer run \ --wsi-dir slides/ \ --results-dir results/ \ --model breast-tumor-resnet34.tcga-brca ``` -------------------------------- ### get_wsi_cls() -> type[openslide.OpenSlide] | type[tiffslide.TiffSlide] Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/wsi/index.rst.txt Retrieves the class type for the current WSI backend. ```APIDOC ## get_wsi_cls() -> type[openslide.OpenSlide] | type[tiffslide.TiffSlide] ### Description Returns the class type of the currently configured WSI backend. ``` -------------------------------- ### Iterate and process slides for patching Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/patchlib.html Iterates through a list of slide paths and applies segmentation and patching to each. Note that this implementation runs sequentially to maintain log integrity. ```python # NOTE: we could use multi-processing here but then the logs would get # discombobulated. for i, slide_path in enumerate(slide_paths): print(f"Slide {i+1} of {len(slide_paths)} ({(i+1)/len(slide_paths):.2%})") try: segment_and_patch_one_slide( slide_path=slide_path, save_dir=save_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, overlap=overlap, ) except Exception as e: logger.error(f"Failed to segment and patch slide\n{slide_path}", exc_info=e) return None ``` -------------------------------- ### Validate WSI directory Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/wsi.html Checks a directory for whole slide images and ensures that all files have unique prefixes to avoid ambiguity. ```python def _validate_wsi_directory(wsi_dir: str | Path) -> None: """Validate a directory of whole slide images.""" wsi_dir = Path(wsi_dir) maybe_slides = [p for p in wsi_dir.iterdir() if p.is_file()] uniq_stems = set(p.stem for p in maybe_slides) if len(uniq_stems) != len(maybe_slides): raise DuplicateFilePrefixesFound( "A slide with the same prefix but different extensions has been found" " (like slide.svs and slide.tif). Slides must have unique prefixes." ) ``` -------------------------------- ### get_pretrained_torch_module(model) Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/modellib/models/index.rst.txt Loads weights into a PyTorch module. ```APIDOC ## get_pretrained_torch_module(model) ### Description Get a PyTorch Module with weights loaded. ### Parameters - **model** (wsinfer_zoo.client.HFModelTorchScript | LocalModelTorchScript) - Required - The model configuration object. ### Returns - **torch.nn.Module** - The initialized PyTorch module. ``` -------------------------------- ### make_compose_from_transform_config Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/modellib/transforms/index.rst.txt Creates a torchvision.transforms.Compose instance based on a provided list of transform configurations. ```APIDOC ## make_compose_from_transform_config(list_of_transforms) ### Description Creates a torchvision Compose instance from a list of transform configuration items. ### Parameters - **list_of_transforms** (list[wsinfer_zoo.client.TransformConfigurationItem]) - Required - A list of configuration items defining the desired image transforms. ### Returns - **torchvision.transforms.Compose** - A composed transformation pipeline ready for use with PyTorch models. ``` -------------------------------- ### run_inference Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/modellib/run_inference/index.rst.txt Executes model inference on a directory of whole slide images, assuming patching has already been completed. ```APIDOC ## run_inference(wsi_dir, results_dir, model_info, batch_size=32, num_workers=0, speedup=False) ### Description Runs model inference on a directory of whole slide images and saves the results to CSV files in the model-outputs subdirectory of the results directory. This function requires that patching has already been performed in the specified results directory. ### Parameters - **wsi_dir** (str | pathlib.Path) - Required - Directory containing whole slide images. - **results_dir** (str | pathlib.Path) - Required - Directory containing results of patching. - **model_info** (wsinfer_zoo.client.HFModelTorchScript | wsinfer.modellib.models.LocalModelTorchScript) - Required - Instance of Weights including the model object and application information. - **batch_size** (int) - Optional - The batch size during the forward pass (default: 32). - **num_workers** (int) - Optional - Number of workers for data loading (default: 0). - **speedup** (bool) - Optional - If True, JIT-compiles the model for faster inference (default: False). ### Returns - **tuple[list[str], list[str]]** - A tuple containing two lists: the first list contains slide IDs where patching failed, and the second list contains slide IDs where model inference failed. ``` -------------------------------- ### Segment Tissue and Generate Patch Coordinates Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/patchlib.html This workflow segments tissue from a slide thumbnail, creates a polygon mask, identifies patch coordinates within that mask, and saves the results to disk. ```python # Segment tissue into a binary mask. if len(thumbsize) != 2: raise ValueError(f"Length of 'thumbsize' must be 2 but got {len(thumbsize)}") thumb: Image.Image = slide.get_thumbnail(thumbsize) if thumb.mode != "RGB": logger.warning(f"Converting mode of thumbnail from {thumb.mode} to RGB") thumb = thumb.convert("RGB") # thumb has ~12 MPP. thumb_mpp = (mpp * (np.array(slide.dimensions) / thumb.size)).mean() logger.info(f"Thumbnail has WxH {thumb.size} and MPP={thumb_mpp}") thumb_mpp_squared: float = thumb_mpp**2 # (pixels2 / micron2) * micron2 = pixels2 min_object_size_px: int = round(min_object_size_um2 / thumb_mpp_squared) min_hole_size_px: int = round(min_hole_size_um2 / thumb_mpp_squared) logger.info( f"Transformed minimum object size to {min_object_size_px} pixel area in" " thumbnail" ) logger.info( f"Transformed minimum hole size to {min_hole_size_px} pixel area in thumbnail" ) arr = segment_tissue( np.asarray(thumb), median_filter_size=median_filter_size, binary_threshold=binary_threshold, closing_kernel_size=closing_kernel_size, min_object_size_px=min_object_size_px, min_hole_size_px=min_hole_size_px, ) if not np.issubdtype(arr.dtype, np.bool_): raise TypeError( f"expected the segmentation array to be boolean dtype but got {arr.dtype}" ) # Create a polygon of the binary tissue mask. scale: tuple[float, float] = ( slide.dimensions[0] / thumb.size[0], slide.dimensions[1] / thumb.size[1], ) _res = get_multipolygon_from_binary_arr(arr.astype("uint8") * 255, scale=scale) if _res is None: logger.warning(f"No tissue was found in slide {slide_path}") return None polygon, contours, hierarchy = _res # Get the coordinates of patches inside the tissue polygon. slide_width, slide_height = slide.dimensions half_patch_size = round(patch_size / 2) # Nx4 --> N x (minx, miny, width, height) coords = get_patch_coordinates_within_polygon( slide_width=slide_width, slide_height=slide_height, patch_size=patch_size, half_patch_size=half_patch_size, polygon=polygon, overlap=overlap, ) logger.info(f"Found {len(coords)} patches within tissue") # Save coordinates to HDF5, if at least one patch was found. patch_path.parent.mkdir(exist_ok=True, parents=True) if coords.size > 0: logger.info(f"Writing patches to {patch_path}") save_hdf5( path=patch_path, coords=coords, patch_size=patch_size, patch_spacing_um_px=patch_spacing_um_px, compression="gzip", ) else: logger.warning(f"No patches found for slide {slide_path}") # Save thumbnail with drawn contours. logger.info(f"Writing tissue thumbnail with contours to disk: {mask_path}") mask_path.parent.mkdir(exist_ok=True, parents=True) img = draw_contours_on_thumbnail(thumb, contours=contours, hierarchy=hierarchy) img.thumbnail((1024, 1024), resample=Image.Resampling.LANCZOS) img.save(mask_path) return None ``` -------------------------------- ### WholeSlideImagePatches Source: https://wsinfer.readthedocs.io/en/latest/_sources/autoapi/wsinfer/modellib/data/index.rst.txt A PyTorch Dataset class that retrieves patches from a whole slide image. It requires the path to the WSI and a corresponding npy file containing patch coordinates. ```APIDOC ## class WholeSlideImagePatches(wsi_path, patch_path, transform=None) ### Description Dataset of one whole slide image. This object retrieves patches from a whole slide image on the fly. ### Parameters - **wsi_path** (str | pathlib.Path) - Required - Path to whole slide image file. - **patch_path** (str | pathlib.Path) - Required - Path to npy file with coordinates of input image. - **transform** (Callable[[PIL.Image.Image], torch.Tensor] | None) - Optional - A callable to modify a retrieved patch. The callable must accept a PIL.Image.Image instance and return a torch.Tensor. ### Methods - **worker_init(worker_id: int | None = None) -> None** - **__len__() -> int** - **__getitem__(idx: int) -> tuple[PIL.Image.Image | torch.Tensor, torch.Tensor]** ``` -------------------------------- ### WholeSlideImagePatches Dataset Class Source: https://wsinfer.readthedocs.io/en/latest/_modules/wsinfer/modellib/data.html A PyTorch Dataset that retrieves patches from a whole slide image on the fly. Requires a path to the WSI and a path to the HDF5 file containing patch coordinates. ```python class WholeSlideImagePatches(torch.utils.data.Dataset): """Dataset of one whole slide image. This object retrieves patches from a whole slide image on the fly. Parameters ---------- wsi_path : str, Path Path to whole slide image file. patch_path : str, Path Path to npy file with coordinates of input image. transform : callable, optional A callable to modify a retrieved patch. The callable must accept a PIL.Image.Image instance and return a torch.Tensor. """ def __init__( self, wsi_path: str | Path, patch_path: str | Path, transform: Callable[[Image.Image], torch.Tensor] | None = None, ): self.wsi_path = wsi_path self.patch_path = patch_path self.transform = transform assert Path(wsi_path).exists(), "wsi path not found" assert Path(patch_path).exists(), "patch path not found" self.patches = _read_patch_coords(self.patch_path) if self.patches.size == 0: raise ValueError(f"No patches were found in {self.patch_path}") assert self.patches.ndim == 2, "expected 2D array of patch coordinates" # x, y, width, height assert self.patches.shape[1] == 4, "expected second dimension to have len 4" def worker_init(self, worker_id: int | None = None) -> None: del worker_id wsi_reader = get_wsi_cls() self.slide = wsi_reader(self.wsi_path) def __len__(self) -> int: return self.patches.shape[0] def __getitem__(self, idx: int) -> tuple[Image.Image | torch.Tensor, torch.Tensor]: coords: Sequence[int] = self.patches[idx] assert len(coords) == 4, "expected 4 coords (minx, miny, width, height)" minx, miny, width, height = coords patch_im = self.slide.read_region( location=(minx, miny), level=0, size=(width, height) ) patch_im = patch_im.convert("RGB") if self.transform is not None: patch_im = self.transform(patch_im) return patch_im, torch.as_tensor([minx, miny, width, height]) ```