diff --git a/README.md b/README.md index 10f3bc6..8d32815 100755 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ PrePATH is a comprehensive preprocessing toolkit for whole slide images (WSI), built upon [CLAM](https://github.com/mahmoodlab/CLAM) and [ASlide](https://github.com/MrPeterJin/ASlide). -## TODO +## TODO + - H0-mini - OpenMidnight - TITAN (Slide level) @@ -31,6 +32,7 @@ PrePATH is a comprehensive preprocessing toolkit for whole slide images (WSI), b ## Installation ### Prerequisites + - Anaconda or Miniconda - `openslide-tools` (system dependency) @@ -49,6 +51,7 @@ wget https://github.com/birkhoffkiki/GPFM/releases/download/ckpt/GPFM.pth ``` **Notes:** + - ASlide should be installed as a Python package from [GitHub](https://github.com/MrPeterJin/ASlide) and is included in `requirements/gpfm.txt`. - Environment configurations for other foundation models should be referenced from their respective repositories. @@ -63,6 +66,10 @@ Extract coordinates of foreground patches from whole slide images: bash scripts/get_coors/SAL/sal.sh ``` +> ⚠ Note: The get_coord script provides an auto_size option to automatically adapt to different level-0 WSI magnifications, generating coordinates equivalent to a 256×256 FOV at 20× (see PrePATH/configs/resolution.py for customization). + +> ⚠ Note: The same option is also available for crop_image scripts. Both are disabled by default. If enabled (by appending the `--auto_size` sign), make sure to turn it on in both scripts to ensure consistent coordinate–crop alignment. + ### Step 2: Feature Extraction Extract patch-level features using the selected foundation model: @@ -106,7 +113,7 @@ models="resnet50 gpfm" | MUSK | `musk` | [HuggingFace](https://huggingface.co/xiangjx/musk) | | OmiCLIP | `omiclip` | [Github](https://github.com/GuangyuWangLab2021/Loki) | | PathoCLIP | `pathoclip` | [Github](https://github.com/wenchuan-zhang/patho-r1) | ---- +------------------------- ## Supported WSI Formats @@ -116,4 +123,3 @@ PrePATH supports the following whole slide image formats: - **SDPC** (.sdpc) - **TRON** (.tron) - All formats supported by OpenSlide (including .svs, .tiff, .ndpi, .vms, .vmu, .scn, .mrxs, .tif, .bif, and others) - diff --git a/create_patches_fp.py b/create_patches_fp.py index d72f168..eae1e9d 100644 --- a/create_patches_fp.py +++ b/create_patches_fp.py @@ -98,6 +98,7 @@ def seg_and_patch( stitch=False, patch=False, auto_skip=True, + auto_size=False, process_list=None, wsi_format="svs", ): @@ -152,20 +153,26 @@ def seg_and_patch( full_path = os.path.join(source, slide) try: WSI_object = wsi_slide_image(full_path) - mpp = WSI_object.mpp - if mpp is None: - # Fallback: assume 40x magnification (mpp=0.25) for files without MPP metadata - print(f"WARNING: MPP information not available for {slide}. Assuming 40x magnification (mpp=0.25).") - mpp = 0.25 - object_power = 40 + if auto_size: + mpp = WSI_object.mpp + if mpp is None: + # Fallback: assume 40x magnification (mpp=0.25) for files without MPP metadata + print(f"WARNING: MPP information not available for {slide}. Assuming 40x magnification (mpp=0.25).") + mpp = 0.25 + object_power = 40 + else: + # Convert mpp to magnification + object_power = int(round(10.0 / mpp)) + patch_size, step_size = adjust_size(object_power) + print("#" * 100) + print("levels:", WSI_object.wsi.level_dimensions) + print("mpp: {:.3f}, object_power: {}x, patch_size: {}, step_size: {} (auto-adjusted)".format(mpp, object_power, patch_size, step_size)) + print("#" * 100) else: - # Convert mpp to magnification - object_power = int(round(10.0 / mpp)) - patch_size, step_size = adjust_size(object_power) - print("#" * 100) - print("levels:", WSI_object.wsi.level_dimensions) - print("mpp: {:.3f}, object_power: {}x, patch_size: {}, step_size: {}".format(mpp, object_power, patch_size, step_size)) - print("#" * 100) + print("#" * 100) + print("levels:", WSI_object.wsi.level_dimensions) + print("Using fixed patch_size: {}, step_size: {}".format(patch_size, step_size)) + print("#" * 100) except Exception as e: print("Failed to reading:", full_path) print('Exception:', e) @@ -321,6 +328,7 @@ def mp_seg_and_patch( stitch=False, patch=False, auto_skip=True, + auto_size=False, process_list=None, wsi_format="svs", ): @@ -373,6 +381,29 @@ def func(i): full_path = os.path.join(source, slide) try: WSI_object = wsi_slide_image(full_path) + # Auto-adjust patch_size and step_size based on mpp if auto_size is enabled + current_patch_size = patch_size + current_step_size = step_size + if auto_size: + mpp = WSI_object.mpp + if mpp is None: + # Fallback: assume 40x magnification (mpp=0.25) for files without MPP metadata + print(f"WARNING: MPP information not available for {slide}. Assuming 40x magnification (mpp=0.25).") + mpp = 0.25 + object_power = 40 + else: + # Convert mpp to magnification + object_power = int(round(10.0 / mpp)) + current_patch_size, current_step_size = adjust_size(object_power) + print("#" * 100) + print("levels:", WSI_object.wsi.level_dimensions) + print("mpp: {:.3f}, object_power: {}x, patch_size: {}, step_size: {} (auto-adjusted)".format(mpp, object_power, current_patch_size, current_step_size)) + print("#" * 100) + else: + print("#" * 100) + print("levels:", WSI_object.wsi.level_dimensions) + print("Using fixed patch_size: {}, step_size: {}".format(current_patch_size, current_step_size)) + print("#" * 100) except: print("Failed to reading:", full_path) return @@ -472,7 +503,7 @@ def func(i): patch_time_elapsed = -1 # Default time if patch: - current_patch_params.update({"patch_level": patch_level, "patch_size": patch_size, "step_size": step_size, "save_path": patch_save_dir}) + current_patch_params.update({"patch_level": patch_level, "patch_size": current_patch_size, "step_size": current_step_size, "save_path": patch_save_dir}) file_path, patch_time_elapsed = patching( WSI_object=WSI_object, **current_patch_params, @@ -510,6 +541,7 @@ def func(i): parser.add_argument("--patch_level", type=int, default=0, help="downsample level at which to patch") parser.add_argument("--process_list", type=str, default=None, help="name of list of images to process with parameters (.csv)") parser.add_argument("--wsi_format", type=str, default="svs") +parser.add_argument("--auto_size", default=False, action="store_true", help="automatically adjust patch_size and step_size based on WSI magnification (mpp)") parser.add_argument("--use_mp", default=False, action="store_true") @@ -566,4 +598,4 @@ def func(i): fn = mp_seg_and_patch else: fn = seg_and_patch - seg_times, patch_times = fn(**directories, **parameters, patch_size=args.patch_size, step_size=args.step_size, seg=args.seg, use_default_params=False, save_mask=True, stitch=args.stitch, patch_level=args.patch_level, patch=args.patch, process_list=process_list, auto_skip=args.no_auto_skip, wsi_format=wsi_format) + seg_times, patch_times = fn(**directories, **parameters, patch_size=args.patch_size, step_size=args.step_size, seg=args.seg, use_default_params=False, save_mask=True, stitch=args.stitch, patch_level=args.patch_level, patch=args.patch, process_list=process_list, auto_skip=args.no_auto_skip, auto_size=args.auto_size, wsi_format=wsi_format)