--- a +++ b/slideflow/slide/qc/saver.py @@ -0,0 +1,101 @@ +"""Functions for saving/loading QC masks.""" + +import numpy as np +import slideflow as sf +from os.path import dirname, join, exists +from typing import Optional + +class Save: + + def __init__(self, dest: Optional[str] = None) -> None: + """QC function which saves the mask to a numpy file. + + When this QC method is applied to a slide, the current QC masks + (e.g., as applied by the Otsu or Gaussian filtering methods) are saved + to a numpy file. These saved masks can be loaded in the future + using :class:`slideflow.slide.qc.Load`. Saving/loading masks saves time + by allowing to avoid regenerating masks repeatedly. + + By default, masks are saved in the same folder as whole-slide images. + + .. code-block:: python + + from slideflow.slide import qc + + # Define a QC approach that auto-saves masks + qc = [ + qc.Otsu(), + qc.Save() + ] + P.extract_tiles(qc=qc) + + ... + # Auto-load previously saved masks + qc = [ + qc.Load() + ] + P.extract_tiles(qc=qc) + + Args: + dest (str, optional): Path in which to save the qc mask. + If None, will save in the same directory as the slide. + Defaults to None. + """ + self.dest = dest + + def __repr__(self): + return "Save(dest={!r})".format( + self.dest + ) + + def __call__(self, wsi: "sf.WSI") -> None: + """Save a QC mask for a given slide as a numpy file. + + Args: + wsi (sf.WSI): Whole-slide image. + + Returns: + None + """ + dest = self.dest if self.dest is not None else dirname(wsi.path) + mask = wsi.get_qc_mask(roi=False) + if mask: + np.savez(join(dest, wsi.name+'_qc.npz'), mask=mask) + return None + + +class Load: + + def __init__(self, source: Optional[str] = None) -> None: + """QC function which loads a saved numpy mask. + + Loads and applies a QC mask which was saved by + :class:`slideflow.slide.qc.Save` + + Args: + source (str, optional): Path to search for qc mask. + If None, will search in the same directory as the slide. + Defaults to None. + """ + self.source = source + + def __repr__(self): + return "Load(source={!r})".format( + self.source + ) + + def __call__(self, wsi: "sf.WSI") -> Optional[np.ndarray]: + """Load a QC mask for a given slide from a numpy file. + + Args: + wsi (sf.WSI): Whole-slide image. + + Returns: + Optional[np.ndarray]: Returns the QC mask if a {slide}_qc.npz file + was found, otherwise returns None. + """ + source = self.source if self.source is not None else dirname(wsi.path) + if exists(join(source, wsi.name+'_qc.npz')): + return np.load(join(source, wsi.name+'_qc.npz'))['mask'] + else: + return None \ No newline at end of file