Coverage for qubalab/images/openslide_server.py: 87%
61 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-31 11:24 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-31 11:24 +0000
1import numpy as np
2import dask.array as da
3from pathlib import Path
4from dataclasses import astuple
5import warnings
6try:
7 import openslide
8except ImportError as e:
9 warnings.warn(f'Unable to import OpenSlide, will try TiffSlide instead')
10 import tiffslide as openslide
11from .image_server import ImageServer
12from .metadata.image_metadata import ImageMetadata
13from .metadata.pixel_calibration import PixelCalibration, PixelLength
14from .metadata.image_shape import ImageShape
15from .region_2d import Region2D
18class OpenSlideServer(ImageServer):
19 """
20 An image server that relies on OpenSlide (https://openslide.org/) to read RGB images.
22 This server may only be able to detect the full resolution of a pyramidal image.
24 OpenSlide provides some properties to define a rectangle bounding the non-empty region of the slide
25 (see https://openslide.org/api/python/#standard-properties). If such properties are found, only this
26 rectangle will be read (but note that this behaviour was not properly tested).
27 """
29 def __init__(self, path: str, strip_alpha=True, single_channel=False, limit_bounds=True, **kwargs):
30 """
31 :param path: the local path to the image to open
32 :param strip_alpha: whether to strip the alpha channel from the image
33 :param single_channel: whether to keep only the first channel of the image
34 :param limit_bounds: whether to only consider a rectangle bounding the non-empty region of the slide,
35 if such rectangle is defined in the properties of the image
36 :param resize_method: the resampling method to use when resizing the image for downsampling. Bicubic by default
37 """
38 super().__init__(**kwargs)
39 self._reader = openslide.OpenSlide(path)
40 self._path = path
41 self._strip_alpha = strip_alpha
42 self._single_channel = single_channel
43 self._limit_bounds = limit_bounds
45 def close(self):
46 self._reader.close()
48 def _build_metadata(self) -> ImageMetadata:
49 n_channels = OpenSlideServer._get_n_channels(self._single_channel, self._strip_alpha)
51 full_bounds = (0, 0) + self._reader.dimensions
52 bounds = (self._reader.properties.get(openslide.PROPERTY_NAME_BOUNDS_X),
53 self._reader.properties.get(openslide.PROPERTY_NAME_BOUNDS_Y),
54 self._reader.properties.get(openslide.PROPERTY_NAME_BOUNDS_WIDTH),
55 self._reader.properties.get(openslide.PROPERTY_NAME_BOUNDS_HEIGHT))
56 if self._limit_bounds and not any(v is None for v in bounds):
57 self._bounds = bounds
58 w = self._bounds[3]
59 h = self._bounds[2]
60 shapes = tuple(ImageShape(x=int(w / d), y=int(h / d), c=n_channels) for d in self._reader.level_downsamples)
61 else:
62 self._bounds = full_bounds
63 shapes = tuple(ImageShape(x=d[0], y=d[1], c=n_channels) for d in self._reader.level_dimensions)
65 return ImageMetadata(
66 self._path,
67 Path(self._path).name,
68 shapes,
69 OpenSlideServer._get_pixel_calibration(
70 self._reader.properties.get(openslide.PROPERTY_NAME_MPP_X),
71 self._reader.properties.get(openslide.PROPERTY_NAME_MPP_Y)
72 ),
73 True,
74 np.uint8
75 )
77 def _read_block(self, level: int, region: Region2D) -> np.ndarray:
78 x, y, width, height, z, t = astuple(region)
79 assert z == 0, "OpenSlide can't read 3d images"
80 assert t == 0, "OpenSlide can't read time-varying images"
82 # x and y are provided in the coordinate space of the level, while openslide
83 # expect coordinates in the level 0 reference frame
84 x = int(x * self.metadata.downsamples[level])
85 y = int(y * self.metadata.downsamples[level])
87 # Map coordinates to bounding rectangle
88 x += self._bounds[0]
89 y += self._bounds[1]
91 image = self._reader.read_region((x, y), level, (width, height))
92 im = np.moveaxis(np.asarray(image), 2, 0)
93 image.close()
95 # Return image, stripping alpha/converting to single-channel if needed
96 return im[:self.metadata.n_channels, :, :]
98 @staticmethod
99 def _get_n_channels(single_channel: bool, strip_alpha: bool):
100 if single_channel:
101 return 1
102 elif strip_alpha:
103 return 3
104 else:
105 return 4
107 @staticmethod
108 def _get_pixel_calibration(pixel_width: str, pixel_height: str):
109 if pixel_width is not None and pixel_height is not None:
110 return PixelCalibration(
111 length_x=PixelLength.create_microns(float(pixel_width)),
112 length_y=PixelLength.create_microns(float(pixel_height))
113 )
114 else:
115 return PixelCalibration()