Skip to content

SlideReader

histolytics.wsi.slide_reader.SlideReader

Source code in src/histolytics/wsi/slide_reader.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
class SlideReader:
    def __init__(
        self,
        path: Union[str, Path],
        backend: str = "OPENSLIDE",
    ) -> None:
        """Reader class for histological whole slide images.

        Parameters:
            path (str, Path):
                Path to slide image.
            backend (str):
                Backend to use for reading slide images. One of:

                - "OPENSLIDE": Uses OpenSlideReader.
                - "CUCIM": Uses CucimReader.
                - "BIOIO": Uses BioIOReader.

        Raises:
            FileNotFoundError: Path does not exist.
            ValueError: Backend name not recognised.
        """
        super().__init__()
        if backend not in AVAILABLE_BACKENDS:
            raise ValueError(f"Backend {backend} not recognised or not supported.")

        if backend == "OPENSLIDE":
            self._reader = OpenSlideReader(path=path)
        elif backend == "CUCIM":
            self._reader = CucimReader(path=path)
        elif backend == "BIOIO":
            self._reader = BioIOReader(path=path)

    @property
    def path(self) -> str:
        """Full slide filepath."""
        return self._reader.path

    @property
    def name(self) -> str:
        """Slide filename without an extension."""
        return self._reader.name

    @property
    def suffix(self) -> str:
        """Slide file-extension."""
        return self._reader.suffix

    @property
    def backend_name(self) -> str:
        """Name of the slide reader backend."""
        return self._reader.BACKEND_NAME

    @property
    def data_bounds(self) -> tuple[int, int, int, int]:
        """Data bounds defined by `xywh`-coordinates at `level=0`.

        Some image formats (eg. `.mrxs`) define a bounding box where image data resides,
        which may differ from the actual image dimensions. `HistoPrep` always uses the
        full image dimensions, but other software (such as `QuPath`) uses the image
        dimensions defined by this data bound.
        """
        return self._reader.data_bounds

    @property
    def dimensions(self) -> tuple[int, int]:
        """Image dimensions (height, width) at `level=0`."""
        return self._reader.dimensions

    @property
    def level_count(self) -> int:
        """Number of slide pyramid levels."""
        return self._reader.level_count

    @property
    def level_dimensions(self) -> dict[int, tuple[int, int]]:
        """Image dimensions (height, width) for each pyramid level."""
        return self._reader.level_dimensions

    @property
    def level_downsamples(self) -> dict[int, tuple[float, float]]:
        """Image downsample factors (height, width) for each pyramid level."""
        return self._reader.level_downsamples

    def read_level(self, level: int) -> np.ndarray:
        """Read full pyramid level data.

        Parameters:
            level (int):
                Slide pyramid level to read.

        Raises:
            ValueError: Invalid level argument.

        Returns:
            np.ndarray:
                Array containing image data from `level`.
        """
        return self._reader.read_level(level=level)

    def read_region(
        self, xywh: tuple[int, int, int, int], level: int = 0
    ) -> np.ndarray:
        """Read region based on `xywh`-coordinates.

        Parameters:
            xywh (tuple[int, int, int, int]):
                Coordinates for the region.
            level (int):
                Slide pyramid level to read from.

        Raises:
            ValueError: Invalid `level` argument.

        Returns:
            np.ndarray:
                Array containing image data from `xywh`-region.
        """
        return self._reader.read_region(xywh=xywh, level=level)

    def level_from_max_dimension(self, max_dimension: int = 4096) -> int:
        """Find pyramid level with *both* dimensions less or equal to `max_dimension`.
        If one isn't found, return the last pyramid level.

        Parameters:
            max_dimension (int):
                Maximum dimension for the level.

        Returns:
            int:
                Slide pyramid level.
        """
        for level, (level_h, level_w) in self.level_dimensions.items():
            if level_h <= max_dimension and level_w <= max_dimension:
                return level
        return list(self.level_dimensions.keys())[-1]

    def level_from_dimensions(self, dimensions: tuple[int, int]) -> int:
        """Find pyramid level which is closest to `dimensions`.

        Parameters:
            dimensions (tuple[int, int]):
                Height and width.

        Returns:
            int:
                Slide pyramid level.
        """
        height, width = dimensions
        available = []
        distances = []
        for level, (level_h, level_w) in self.level_dimensions.items():
            available.append(level)
            distances.append(abs(level_h - height) + abs(level_w - width))
        return available[distances.index(min(distances))]

    def get_tissue_mask(
        self,
        *,
        level: Optional[int] = None,
        threshold: Optional[int] = None,
        multiplier: float = 1.05,
        sigma: float = 0.0,
    ) -> tuple[int, np.ndarray]:
        """Detect tissue from slide pyramid level image.

        Parameters:
            level (int):
                Slide pyramid level to use for tissue detection. If None, uses the
                `level_from_max_dimension` method.
            threshold (int):
                Threshold for tissue detection. If set, will detect tissue by global
                thresholding. Otherwise Otsu's method is used to find a threshold.
            multiplier (float):
                Otsu's method finds an optimal threshold by minimizing the weighted
                within-class variance. This threshold is then multiplied with
                `multiplier`. Ignored if `threshold` is not None.
            sigma (float):
                Sigma for gaussian blurring.

        Raises:
            ValueError: Threshold not between 0 and 255.

        Returns:
            tuple[int, np.ndarray]:
                Threshold and tissue mask.
        """
        level = (
            self.level_from_max_dimension()
            if level is None
            else format_level(level, available=list(self.level_dimensions))
        )
        return get_tissue_mask(
            image=self.read_level(level),
            threshold=threshold,
            multiplier=multiplier,
            sigma=sigma,
        )

    def get_tile_coordinates(
        self,
        width: int,
        *,
        tissue_mask: Optional[np.ndarray],
        annotations: Optional[Polygon] = None,
        height: Optional[int] = None,
        overlap: float = 0.0,
        max_background: float = 0.95,
        out_of_bounds: bool = True,
    ) -> TileCoordinates:
        """Generate tile coordinates.

        Parameters:
            width (int):
                Width of a tile.
            tissue_mask (np.ndarray):
                Tissue mask for filtering tiles with too much background. If None,
                the filtering is disabled.
            annotations (Optional[Polygon]):
                Annotations to filter tiles by. If provided, only tiles that intersect
                with the annotations will be returned.
            height (int):
                Height of a tile. If None, will be set to `width`.
            overlap (float):
                Overlap between neighbouring tiles.
            max_background (float):
                Maximum proportion of background in tiles. Ignored if `tissue_mask`
                is None.
            out_of_bounds (bool):
                Keep tiles which contain regions outside of the image.

        Raises:
            ValueError: Height and/or width are smaller than 1.
            ValueError: Height and/or width is larger than dimensions.
            ValueError: Overlap is not in range [0, 1).

        Returns:
            TileCoordinates:
                `TileCoordinates` dataclass.
        """
        tile_coordinates = get_tile_coordinates(
            dimensions=self.dimensions,
            width=width,
            height=height,
            overlap=overlap,
            out_of_bounds=out_of_bounds,
        )
        if tissue_mask is not None:
            all_backgrounds = get_background_percentages(
                tile_coordinates=tile_coordinates,
                tissue_mask=tissue_mask,
                downsample=get_downsample(tissue_mask, self.dimensions),
            )
            filtered_coordinates = []
            for xywh, background in zip(tile_coordinates, all_backgrounds):
                if background <= max_background:
                    filtered_coordinates.append(xywh)
            tile_coordinates = filtered_coordinates

        if annotations is not None:
            # Convert tile coordinates to polygons
            tiles_gdf = gpd.GeoDataFrame(
                {
                    "geometry": [
                        box(x, y, x + w, y + h) for x, y, w, h in tile_coordinates
                    ]
                }
            )

            # Filter tiles that intersect with the annotation bbox
            filtered_tiles = tiles_gdf[tiles_gdf.intersects(annotations)]

            tile_coordinates = [
                (
                    int(poly.bounds[0]),
                    int(poly.bounds[1]),
                    int(poly.bounds[2] - poly.bounds[0]),
                    int(poly.bounds[3] - poly.bounds[1]),
                )
                for poly in filtered_tiles.geometry
            ]

        return TileCoordinates(
            coordinates=tile_coordinates,
            width=width,
            height=width if height is None else height,
            overlap=overlap,
            max_background=None if tissue_mask is None else max_background,
            tissue_mask=tissue_mask,
        )

    def get_spot_coordinates(
        self,
        tissue_mask: np.ndarray,
        *,
        min_area_pixel: int = 10,
        max_area_pixel: Optional[int] = None,
        min_area_relative: float = 0.2,
        max_area_relative: Optional[float] = 2.0,
    ) -> SpotCoordinates:
        """Generate tissue microarray spot coordinates.

        Parameters:
            tissue_mask:
                Tissue mask of the slide. It's recommended to increase `sigma` value when
                detecting tissue to remove non-TMA spots from the mask. Rest of the areas
                can be handled with the following arguments.
            min_area_pixel (int):
                Minimum pixel area for contours.
            max_area_pixel (int):
                Maximum pixel area for contours.
            min_area_relative (float):
                Relative minimum contour area, calculated from the median contour area
                after filtering contours with `[min,max]_pixel` arguments
                (`min_area_relative * median(contour_areas)`).
            max_area_relative (float):
                Relative maximum contour area, calculated from the median contour area
                after filtering contours with `[min,max]_pixel` arguments
                (`max_area_relative * median(contour_areas)`).

        Returns:
            SpotCoordinates:
                `SpotCoordinates` instance.
        """
        spot_mask = clean_tissue_mask(
            tissue_mask=tissue_mask,
            min_area_pixel=min_area_pixel,
            max_area_pixel=max_area_pixel,
            min_area_relative=min_area_relative,
            max_area_relative=max_area_relative,
        )
        # Dearray spots.
        spot_info = get_spot_coordinates(spot_mask)
        spot_coordinates = [  # upsample to level zero.
            _multiply_xywh(x, get_downsample(tissue_mask, self.dimensions))
            for x in spot_info.values()
        ]

        return SpotCoordinates(
            coordinates=spot_coordinates,
            spot_names=list(spot_info.keys()),
            tissue_mask=spot_mask,
        )

    def get_annotated_thumbnail(
        self,
        image: np.ndarray,
        coordinates: Iterator[tuple[int, int, int, int]],
        linewidth: int = 1,
    ) -> Image.Image:
        """Generate annotated thumbnail from coordinates.

        Parameters:
            image (np.ndarray):
                Input image.
            coordinates (Iterator[tuple[int, int, int, int]]):
                Coordinates to annotate.
            linewidth (int):
                Width of rectangle lines.

        Returns:
            PIL.Image.Image:
                Annotated thumbnail.
        """
        kwargs = {
            "image": image,
            "downsample": get_downsample(image, self.dimensions),
            "rectangle_width": linewidth,
        }
        if isinstance(coordinates, SpotCoordinates):
            text_items = [x.lstrip("spot_") for x in coordinates.spot_names]
            kwargs.update(
                {"coordinates": coordinates.coordinates, "text_items": text_items}
            )
        elif isinstance(coordinates, TileCoordinates):
            kwargs.update(
                {"coordinates": coordinates.coordinates, "highlight_first": True}
            )
        else:
            kwargs.update({"coordinates": coordinates})
        return get_annotated_image(**kwargs)

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(path={self.path}, "
            f"backend={self._reader.BACKEND_NAME})"
        )

path property

path: str

Full slide filepath.

name property

name: str

Slide filename without an extension.

suffix property

suffix: str

Slide file-extension.

backend_name property

backend_name: str

Name of the slide reader backend.

data_bounds property

data_bounds: tuple[int, int, int, int]

Data bounds defined by xywh-coordinates at level=0.

Some image formats (eg. .mrxs) define a bounding box where image data resides, which may differ from the actual image dimensions. HistoPrep always uses the full image dimensions, but other software (such as QuPath) uses the image dimensions defined by this data bound.

dimensions property

dimensions: tuple[int, int]

Image dimensions (height, width) at level=0.

level_count property

level_count: int

Number of slide pyramid levels.

level_dimensions property

level_dimensions: dict[int, tuple[int, int]]

Image dimensions (height, width) for each pyramid level.

level_downsamples property

level_downsamples: dict[int, tuple[float, float]]

Image downsample factors (height, width) for each pyramid level.

__init__

__init__(path: Union[str, Path], backend: str = 'OPENSLIDE') -> None

Reader class for histological whole slide images.

Parameters:

Name Type Description Default
path (str, Path)

Path to slide image.

required
backend str

Backend to use for reading slide images. One of:

  • "OPENSLIDE": Uses OpenSlideReader.
  • "CUCIM": Uses CucimReader.
  • "BIOIO": Uses BioIOReader.
'OPENSLIDE'

Raises:

Type Description
FileNotFoundError

Path does not exist.

ValueError

Backend name not recognised.

Source code in src/histolytics/wsi/slide_reader.py
def __init__(
    self,
    path: Union[str, Path],
    backend: str = "OPENSLIDE",
) -> None:
    """Reader class for histological whole slide images.

    Parameters:
        path (str, Path):
            Path to slide image.
        backend (str):
            Backend to use for reading slide images. One of:

            - "OPENSLIDE": Uses OpenSlideReader.
            - "CUCIM": Uses CucimReader.
            - "BIOIO": Uses BioIOReader.

    Raises:
        FileNotFoundError: Path does not exist.
        ValueError: Backend name not recognised.
    """
    super().__init__()
    if backend not in AVAILABLE_BACKENDS:
        raise ValueError(f"Backend {backend} not recognised or not supported.")

    if backend == "OPENSLIDE":
        self._reader = OpenSlideReader(path=path)
    elif backend == "CUCIM":
        self._reader = CucimReader(path=path)
    elif backend == "BIOIO":
        self._reader = BioIOReader(path=path)

read_level

read_level(level: int) -> np.ndarray

Read full pyramid level data.

Parameters:

Name Type Description Default
level int

Slide pyramid level to read.

required

Raises:

Type Description
ValueError

Invalid level argument.

Returns:

Type Description
ndarray

np.ndarray: Array containing image data from level.

Source code in src/histolytics/wsi/slide_reader.py
def read_level(self, level: int) -> np.ndarray:
    """Read full pyramid level data.

    Parameters:
        level (int):
            Slide pyramid level to read.

    Raises:
        ValueError: Invalid level argument.

    Returns:
        np.ndarray:
            Array containing image data from `level`.
    """
    return self._reader.read_level(level=level)

read_region

read_region(xywh: tuple[int, int, int, int], level: int = 0) -> np.ndarray

Read region based on xywh-coordinates.

Parameters:

Name Type Description Default
xywh tuple[int, int, int, int]

Coordinates for the region.

required
level int

Slide pyramid level to read from.

0

Raises:

Type Description
ValueError

Invalid level argument.

Returns:

Type Description
ndarray

np.ndarray: Array containing image data from xywh-region.

Source code in src/histolytics/wsi/slide_reader.py
def read_region(
    self, xywh: tuple[int, int, int, int], level: int = 0
) -> np.ndarray:
    """Read region based on `xywh`-coordinates.

    Parameters:
        xywh (tuple[int, int, int, int]):
            Coordinates for the region.
        level (int):
            Slide pyramid level to read from.

    Raises:
        ValueError: Invalid `level` argument.

    Returns:
        np.ndarray:
            Array containing image data from `xywh`-region.
    """
    return self._reader.read_region(xywh=xywh, level=level)

level_from_max_dimension

level_from_max_dimension(max_dimension: int = 4096) -> int

Find pyramid level with both dimensions less or equal to max_dimension. If one isn't found, return the last pyramid level.

Parameters:

Name Type Description Default
max_dimension int

Maximum dimension for the level.

4096

Returns:

Name Type Description
int int

Slide pyramid level.

Source code in src/histolytics/wsi/slide_reader.py
def level_from_max_dimension(self, max_dimension: int = 4096) -> int:
    """Find pyramid level with *both* dimensions less or equal to `max_dimension`.
    If one isn't found, return the last pyramid level.

    Parameters:
        max_dimension (int):
            Maximum dimension for the level.

    Returns:
        int:
            Slide pyramid level.
    """
    for level, (level_h, level_w) in self.level_dimensions.items():
        if level_h <= max_dimension and level_w <= max_dimension:
            return level
    return list(self.level_dimensions.keys())[-1]

level_from_dimensions

level_from_dimensions(dimensions: tuple[int, int]) -> int

Find pyramid level which is closest to dimensions.

Parameters:

Name Type Description Default
dimensions tuple[int, int]

Height and width.

required

Returns:

Name Type Description
int int

Slide pyramid level.

Source code in src/histolytics/wsi/slide_reader.py
def level_from_dimensions(self, dimensions: tuple[int, int]) -> int:
    """Find pyramid level which is closest to `dimensions`.

    Parameters:
        dimensions (tuple[int, int]):
            Height and width.

    Returns:
        int:
            Slide pyramid level.
    """
    height, width = dimensions
    available = []
    distances = []
    for level, (level_h, level_w) in self.level_dimensions.items():
        available.append(level)
        distances.append(abs(level_h - height) + abs(level_w - width))
    return available[distances.index(min(distances))]

get_tissue_mask

get_tissue_mask(*, level: Optional[int] = None, threshold: Optional[int] = None, multiplier: float = 1.05, sigma: float = 0.0) -> tuple[int, np.ndarray]

Detect tissue from slide pyramid level image.

Parameters:

Name Type Description Default
level int

Slide pyramid level to use for tissue detection. If None, uses the level_from_max_dimension method.

None
threshold int

Threshold for tissue detection. If set, will detect tissue by global thresholding. Otherwise Otsu's method is used to find a threshold.

None
multiplier float

Otsu's method finds an optimal threshold by minimizing the weighted within-class variance. This threshold is then multiplied with multiplier. Ignored if threshold is not None.

1.05
sigma float

Sigma for gaussian blurring.

0.0

Raises:

Type Description
ValueError

Threshold not between 0 and 255.

Returns:

Type Description
tuple[int, ndarray]

tuple[int, np.ndarray]: Threshold and tissue mask.

Source code in src/histolytics/wsi/slide_reader.py
def get_tissue_mask(
    self,
    *,
    level: Optional[int] = None,
    threshold: Optional[int] = None,
    multiplier: float = 1.05,
    sigma: float = 0.0,
) -> tuple[int, np.ndarray]:
    """Detect tissue from slide pyramid level image.

    Parameters:
        level (int):
            Slide pyramid level to use for tissue detection. If None, uses the
            `level_from_max_dimension` method.
        threshold (int):
            Threshold for tissue detection. If set, will detect tissue by global
            thresholding. Otherwise Otsu's method is used to find a threshold.
        multiplier (float):
            Otsu's method finds an optimal threshold by minimizing the weighted
            within-class variance. This threshold is then multiplied with
            `multiplier`. Ignored if `threshold` is not None.
        sigma (float):
            Sigma for gaussian blurring.

    Raises:
        ValueError: Threshold not between 0 and 255.

    Returns:
        tuple[int, np.ndarray]:
            Threshold and tissue mask.
    """
    level = (
        self.level_from_max_dimension()
        if level is None
        else format_level(level, available=list(self.level_dimensions))
    )
    return get_tissue_mask(
        image=self.read_level(level),
        threshold=threshold,
        multiplier=multiplier,
        sigma=sigma,
    )

get_tile_coordinates

get_tile_coordinates(width: int, *, tissue_mask: Optional[ndarray], annotations: Optional[Polygon] = None, height: Optional[int] = None, overlap: float = 0.0, max_background: float = 0.95, out_of_bounds: bool = True) -> TileCoordinates

Generate tile coordinates.

Parameters:

Name Type Description Default
width int

Width of a tile.

required
tissue_mask ndarray

Tissue mask for filtering tiles with too much background. If None, the filtering is disabled.

required
annotations Optional[Polygon]

Annotations to filter tiles by. If provided, only tiles that intersect with the annotations will be returned.

None
height int

Height of a tile. If None, will be set to width.

None
overlap float

Overlap between neighbouring tiles.

0.0
max_background float

Maximum proportion of background in tiles. Ignored if tissue_mask is None.

0.95
out_of_bounds bool

Keep tiles which contain regions outside of the image.

True

Raises:

Type Description
ValueError

Height and/or width are smaller than 1.

ValueError

Height and/or width is larger than dimensions.

ValueError

Overlap is not in range [0, 1).

Returns:

Name Type Description
TileCoordinates TileCoordinates

TileCoordinates dataclass.

Source code in src/histolytics/wsi/slide_reader.py
def get_tile_coordinates(
    self,
    width: int,
    *,
    tissue_mask: Optional[np.ndarray],
    annotations: Optional[Polygon] = None,
    height: Optional[int] = None,
    overlap: float = 0.0,
    max_background: float = 0.95,
    out_of_bounds: bool = True,
) -> TileCoordinates:
    """Generate tile coordinates.

    Parameters:
        width (int):
            Width of a tile.
        tissue_mask (np.ndarray):
            Tissue mask for filtering tiles with too much background. If None,
            the filtering is disabled.
        annotations (Optional[Polygon]):
            Annotations to filter tiles by. If provided, only tiles that intersect
            with the annotations will be returned.
        height (int):
            Height of a tile. If None, will be set to `width`.
        overlap (float):
            Overlap between neighbouring tiles.
        max_background (float):
            Maximum proportion of background in tiles. Ignored if `tissue_mask`
            is None.
        out_of_bounds (bool):
            Keep tiles which contain regions outside of the image.

    Raises:
        ValueError: Height and/or width are smaller than 1.
        ValueError: Height and/or width is larger than dimensions.
        ValueError: Overlap is not in range [0, 1).

    Returns:
        TileCoordinates:
            `TileCoordinates` dataclass.
    """
    tile_coordinates = get_tile_coordinates(
        dimensions=self.dimensions,
        width=width,
        height=height,
        overlap=overlap,
        out_of_bounds=out_of_bounds,
    )
    if tissue_mask is not None:
        all_backgrounds = get_background_percentages(
            tile_coordinates=tile_coordinates,
            tissue_mask=tissue_mask,
            downsample=get_downsample(tissue_mask, self.dimensions),
        )
        filtered_coordinates = []
        for xywh, background in zip(tile_coordinates, all_backgrounds):
            if background <= max_background:
                filtered_coordinates.append(xywh)
        tile_coordinates = filtered_coordinates

    if annotations is not None:
        # Convert tile coordinates to polygons
        tiles_gdf = gpd.GeoDataFrame(
            {
                "geometry": [
                    box(x, y, x + w, y + h) for x, y, w, h in tile_coordinates
                ]
            }
        )

        # Filter tiles that intersect with the annotation bbox
        filtered_tiles = tiles_gdf[tiles_gdf.intersects(annotations)]

        tile_coordinates = [
            (
                int(poly.bounds[0]),
                int(poly.bounds[1]),
                int(poly.bounds[2] - poly.bounds[0]),
                int(poly.bounds[3] - poly.bounds[1]),
            )
            for poly in filtered_tiles.geometry
        ]

    return TileCoordinates(
        coordinates=tile_coordinates,
        width=width,
        height=width if height is None else height,
        overlap=overlap,
        max_background=None if tissue_mask is None else max_background,
        tissue_mask=tissue_mask,
    )

get_spot_coordinates

get_spot_coordinates(tissue_mask: ndarray, *, min_area_pixel: int = 10, max_area_pixel: Optional[int] = None, min_area_relative: float = 0.2, max_area_relative: Optional[float] = 2.0) -> SpotCoordinates

Generate tissue microarray spot coordinates.

Parameters:

Name Type Description Default
tissue_mask ndarray

Tissue mask of the slide. It's recommended to increase sigma value when detecting tissue to remove non-TMA spots from the mask. Rest of the areas can be handled with the following arguments.

required
min_area_pixel int

Minimum pixel area for contours.

10
max_area_pixel int

Maximum pixel area for contours.

None
min_area_relative float

Relative minimum contour area, calculated from the median contour area after filtering contours with [min,max]_pixel arguments (min_area_relative * median(contour_areas)).

0.2
max_area_relative float

Relative maximum contour area, calculated from the median contour area after filtering contours with [min,max]_pixel arguments (max_area_relative * median(contour_areas)).

2.0

Returns:

Name Type Description
SpotCoordinates SpotCoordinates

SpotCoordinates instance.

Source code in src/histolytics/wsi/slide_reader.py
def get_spot_coordinates(
    self,
    tissue_mask: np.ndarray,
    *,
    min_area_pixel: int = 10,
    max_area_pixel: Optional[int] = None,
    min_area_relative: float = 0.2,
    max_area_relative: Optional[float] = 2.0,
) -> SpotCoordinates:
    """Generate tissue microarray spot coordinates.

    Parameters:
        tissue_mask:
            Tissue mask of the slide. It's recommended to increase `sigma` value when
            detecting tissue to remove non-TMA spots from the mask. Rest of the areas
            can be handled with the following arguments.
        min_area_pixel (int):
            Minimum pixel area for contours.
        max_area_pixel (int):
            Maximum pixel area for contours.
        min_area_relative (float):
            Relative minimum contour area, calculated from the median contour area
            after filtering contours with `[min,max]_pixel` arguments
            (`min_area_relative * median(contour_areas)`).
        max_area_relative (float):
            Relative maximum contour area, calculated from the median contour area
            after filtering contours with `[min,max]_pixel` arguments
            (`max_area_relative * median(contour_areas)`).

    Returns:
        SpotCoordinates:
            `SpotCoordinates` instance.
    """
    spot_mask = clean_tissue_mask(
        tissue_mask=tissue_mask,
        min_area_pixel=min_area_pixel,
        max_area_pixel=max_area_pixel,
        min_area_relative=min_area_relative,
        max_area_relative=max_area_relative,
    )
    # Dearray spots.
    spot_info = get_spot_coordinates(spot_mask)
    spot_coordinates = [  # upsample to level zero.
        _multiply_xywh(x, get_downsample(tissue_mask, self.dimensions))
        for x in spot_info.values()
    ]

    return SpotCoordinates(
        coordinates=spot_coordinates,
        spot_names=list(spot_info.keys()),
        tissue_mask=spot_mask,
    )

get_annotated_thumbnail

get_annotated_thumbnail(image: ndarray, coordinates: Iterator[tuple[int, int, int, int]], linewidth: int = 1) -> Image.Image

Generate annotated thumbnail from coordinates.

Parameters:

Name Type Description Default
image ndarray

Input image.

required
coordinates Iterator[tuple[int, int, int, int]]

Coordinates to annotate.

required
linewidth int

Width of rectangle lines.

1

Returns:

Type Description
Image

PIL.Image.Image: Annotated thumbnail.

Source code in src/histolytics/wsi/slide_reader.py
def get_annotated_thumbnail(
    self,
    image: np.ndarray,
    coordinates: Iterator[tuple[int, int, int, int]],
    linewidth: int = 1,
) -> Image.Image:
    """Generate annotated thumbnail from coordinates.

    Parameters:
        image (np.ndarray):
            Input image.
        coordinates (Iterator[tuple[int, int, int, int]]):
            Coordinates to annotate.
        linewidth (int):
            Width of rectangle lines.

    Returns:
        PIL.Image.Image:
            Annotated thumbnail.
    """
    kwargs = {
        "image": image,
        "downsample": get_downsample(image, self.dimensions),
        "rectangle_width": linewidth,
    }
    if isinstance(coordinates, SpotCoordinates):
        text_items = [x.lstrip("spot_") for x in coordinates.spot_names]
        kwargs.update(
            {"coordinates": coordinates.coordinates, "text_items": text_items}
        )
    elif isinstance(coordinates, TileCoordinates):
        kwargs.update(
            {"coordinates": coordinates.coordinates, "highlight_first": True}
        )
    else:
        kwargs.update({"coordinates": coordinates})
    return get_annotated_image(**kwargs)