diff --git a/lazyslide/readers/vips.py b/lazyslide/readers/vips.py index 4928695..989cc2d 100644 --- a/lazyslide/readers/vips.py +++ b/lazyslide/readers/vips.py @@ -12,7 +12,6 @@ except Exception as _: pass - VIPS_FORMAT_TO_DTYPE = { "uchar": np.uint8, "char": np.int8, @@ -38,13 +37,27 @@ def vips2numpy( ) +def buffer2numpy( + buffer, +) -> np.ndarray: + """Converts a VIPS image into a numpy array""" + return np.ndarray( + buffer=buffer, + dtype=VIPS_FORMAT_TO_DTYPE[buffer.format], + shape=[buffer.height, buffer.width, buffer.bands], + ) + + class VipsReader(ReaderBase): def __init__( self, file: Union[Path, str], + caching: bool = True, ): self.file = file + self.caching = caching self.__level_vips_handler = {} # cache level handler + self.__region_vips_handler = {} # cache region handler _vips_img = self._get_vips_level(0) _vips_fields = set(_vips_img.get_fields()) @@ -61,47 +74,60 @@ def get_patch( top, width, height, - level: int = None, - downsample: float = None, + level: int = 0, fill=255, ): """Get a patch by x, y from top-left corner""" level = self.translate_level(level) - img = self._get_vips_level(level) - patch = self._get_vips_patch(img, left, top, width, height, fill=fill) - if downsample is not None: - if downsample != 1: - patch = patch.resize(1 / downsample) - patch = vips2numpy(patch) - return self._rgba_to_rgb(patch) + image = self._get_vips_level(level) + bg = [fill] + + crop_left, crop_top, crop_w, crop_h, pos = get_crop_left_top_width_height( + img_width=image.width, + img_height=image.height, + left=left, + top=top, + width=width, + height=height, + ) + patch = None + if self.caching: + cropped = self.__region_vips_handler[level].fetch( + crop_left, crop_top, crop_w, crop_h + ) + if pos is None: + return np.ndarray( + buffer=cropped, + dtype=VIPS_FORMAT_TO_DTYPE[image.format], + shape=[crop_h, crop_w, image.bands], + ) + else: + patch = vips.Image.new_from_buffer(cropped, "").gravity( + pos, width, height, background=bg + ) + else: + cropped = image.crop(crop_left, crop_top, crop_w, crop_h) + if pos is None: + patch = cropped + else: + patch = cropped.gravity(pos, width, height, background=bg) + + return vips2numpy(patch) # self._rgba_to_rgb(patch) def get_level(self, level): level = self.translate_level(level) img = self._get_vips_level(level) img = vips2numpy(img) - return self._rgba_to_rgb(img) + return img # self._rgba_to_rgb(img) def _get_vips_level(self, level=0): """Lazy load and load only one for all image level""" handler = self.__level_vips_handler.get(level) if handler is None: - handler = vips.Image.new_from_file(str(self.file), fail=True, level=level) + handler = vips.Image.new_from_file( + str(self.file), fail=True, level=level, rgb=True + ) self.__level_vips_handler[level] = handler + if self.caching: + self.__region_vips_handler[level] = vips.Region.new(handler) return handler - - @staticmethod - def _get_vips_patch(image, left, top, width, height, fill=255): - bg = [fill] - crop_left, crop_top, crop_w, crop_h, pos = get_crop_left_top_width_height( - img_width=image.width, - img_height=image.height, - left=left, - top=top, - width=width, - height=height, - ) - cropped = image.crop(crop_left, crop_top, crop_w, crop_h) - if pos is None: - return cropped - else: - return cropped.gravity(pos, width, height, background=bg) diff --git a/lazyslide/wsi.py b/lazyslide/wsi.py index 2f140c3..2e616a5 100644 --- a/lazyslide/wsi.py +++ b/lazyslide/wsi.py @@ -200,6 +200,7 @@ def __init__( image: Path | str, h5_file: Path | str = None, reader="auto", # openslide, vips, cucim + reader_options=None, ): from .utils import check_wsi_path @@ -208,6 +209,7 @@ def __init__( if h5_file is None: h5_file = self.image.with_suffix(".coords.h5") + self.reader_options = {} if reader_options is None else reader_options self.h5_file = H5File(h5_file) self._reader_class = get_reader(reader) self._reader = None @@ -226,7 +228,7 @@ def __repr__(self): @property def reader(self): if self._reader is None: - self._reader = self._reader_class(self.image) + self._reader = self._reader_class(self.image, **self.reader_options) return self._reader @property