diff --git a/README.md b/README.md index 8297264..896e811 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ # LazySlide + +## Installation + +```bash +pip install git+ssh://git@github.com/rendeirolab/LazySlide.git +``` + ## Usage ```python import lazyslide as zs -slide = '' # Your SVS file +slide = 'https://github.com/camicroscope/Distro/raw/master/images/sample.svs' # Your SVS file wsi = zs.WSI(slide) wsi.plot_tissue() @@ -21,10 +28,9 @@ wsi.plot_tissue(contours=True) wsi.create_tiles(tile_px=256, mpp=.5) wsi.plot_tissue(tiles=True) - ``` -If you want to do feature extration +To do feature extraction: ```python import torch from torch.utils.data import DataLoader @@ -37,14 +43,13 @@ loader = DataLoader( resnet = torch.hub.load("pytorch/vision", "resnet50", weights="IMAGENET1K_V2") with torch.no_grad(): - for tile in loader(): + for tile in loader: tile_feature = resnet(tile) - ``` ### Developer Notes -To make pyvips works on Windows: +To make pyvips work on Windows: ```python import os @@ -52,4 +57,4 @@ vipsbin = '' os.environ['PATH'] = vipsbin + ';' + os.environ['PATH'] import pyvips as vips -``` \ No newline at end of file +``` diff --git a/lazyslide/utils.py b/lazyslide/utils.py index 5c165bb..aec0ba5 100644 --- a/lazyslide/utils.py +++ b/lazyslide/utils.py @@ -1,6 +1,10 @@ +from pathlib import Path from dataclasses import dataclass, field from itertools import tee from typing import Type +from urllib.parse import urlparse + +import requests from .readers.base import ReaderBase from .readers.vips import VipsReader @@ -17,11 +21,7 @@ def pairwise(iterable): def get_reader(reader="auto") -> Type[ReaderBase]: """Return an available backend""" - readers = { - "openslide": None, - "vips": None, - "cucim": None - } + readers = {"openslide": None, "vips": None, "cucim": None} try: import openslide @@ -53,6 +53,40 @@ def get_reader(reader="auto") -> Type[ReaderBase]: return readers[reader] +def is_url(s: str) -> bool: + try: + result = urlparse(s) + return all([result.scheme, result.netloc]) + except ValueError: + return False + + +def download_file(url: str, file_path: Path, chunk_size: int = 1024): + """Download a file in chunks""" + r = requests.get(url, stream=True) + with file_path.open("wb") as fd: + for chunk in r.iter_content(chunk_size=chunk_size): + fd.write(chunk) + + +def check_wsi_path(path: str | Path, allow_download: bool = True) -> Path: + import tempfile + + # Check path is URL or Path + if isinstance(path, str): + if is_url(path): + if not allow_download: + raise ValueError("Given a URL but not allowed to download.") + file_path = Path(tempfile.mkdtemp()) / path.split("/")[-1].split("?")[0] + download_file(path, file_path) + return file_path + elif Path(path).exists(): + return Path(path) + elif isinstance(path, Path): + if path.exists(): + return path + raise ValueError("Path must be a URL or Path to existing file.") + @dataclass class TileOps: level: int = 0 diff --git a/lazyslide/wsi.py b/lazyslide/wsi.py index e6b1415..a0df12e 100644 --- a/lazyslide/wsi.py +++ b/lazyslide/wsi.py @@ -186,8 +186,9 @@ def get_split_image_indices(image_height, image_width, min_side=20000): class WSI: + from .utils import check_wsi_path - def __init__(self, + self.image = check_wsi_path(image) image: Path | str, h5_file: Path | str = None, reader="auto", # openslide, vips, cucim @@ -220,6 +221,14 @@ def reader(self): def metadata(self): return self.reader.metadata + def move_wsi_file(self, new_path: Path) -> None: + new_path = Path(new_path) + if not new_path.exists(): + self.image.rename(new_path) + self.image = new_path + else: + raise FileExistsError(f"File {new_path} already exists.") + def create_mask(self, transform, name="user", level=-1, save=False): level = self.reader.translate_level(level) image = self.reader.get_level(level)