Skip to content

SlideReader

Reader class for histological slide images.

Source code in histoslice/_reader.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 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
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
class SlideReader:
    """Reader class for histological slide images."""

    def __init__(
        self,
        path: Union[str, Path],
        mpp: Optional[tuple[float, float]] = None,
    ) -> None:
        """Initialize `SlideReader` instance.

        Args:
            path: Path to slide image.
            mpp: Override microns per pixel as (mpp_x, mpp_y). If None, attempts to
                extract from slide metadata. Defaults to None.

        Raises:
            FileNotFoundError: Path does not exist.
        """
        super().__init__()
        self._backend = _read_slide(path=path)
        self._mpp_override = mpp

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

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

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

    @property
    def backend_name(self) -> str:
        """Name of the slide reader backend."""
        return self._backend.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._backend.data_bounds

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

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

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

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

    @property
    def mpp(self) -> tuple[float, float] | None:
        """Microns per pixel (mpp_x, mpp_y) at level 0.

        Returns user-provided override if available, otherwise extracts from
        slide metadata. Returns None if not available.
        """
        if self._mpp_override is not None:
            return self._mpp_override
        return self._backend.mpp

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

        Args:
            level: Slide pyramid level to read.

        Raises:
            ValueError: Invalid level argument.

        Returns:
            Array containing image data from `level`.
        """
        return self._backend.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.

        Args:
            xywh: Coordinates for the region.
            level: Slide pyramid level to read from. Defaults to 0.

        Raises:
            ValueError: Invalid `level` argument.

        Returns:
            Array containing image data from `xywh`-region.
        """
        return self._backend.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.

        Args:
            max_dimension: Maximum dimension for the level. Defaults to 4096.

        Returns:
            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`.

        Args:
            dimensions: Height and width.

        Returns:
            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.

        Args:
            level: Slide pyramid level to use for tissue detection. If None, uses the
                `level_from_max_dimension` method. Defaults to None.
            threshold: Threshold for tissue detection. If set, will detect tissue by
                global thresholding. Otherwise Otsu's method is used to find a
                threshold. Defaults to None.
            multiplier: 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. Defaults to 1.0.
            sigma: Sigma for gaussian blurring. Defaults to 0.0.

        Raises:
            ValueError: Threshold not between 0 and 255.

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

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

        Args:
            tissue_mask: Tissue mask for filtering tiles with too much background. If
                None, the filtering is disabled.
            width: Width of a tile in pixels at target resolution.
            height: Height of a tile in pixels at target resolution. If None, will be
                set to `width`. Defaults to None.
            target_mpp: Target microns per pixel for normalization. If specified, tiles
                are extracted at the appropriate level to achieve this resolution. The
                output tiles will be `width` x `height` pixels representing a physical
                area of `width * target_mpp` x `height * target_mpp` microns.
                Defaults to None (use native slide resolution).
            overlap: Overlap between neighbouring tiles. Defaults to 0.0.
            max_background: Maximum proportion of background in tiles. Ignored if
                `tissue_mask` is None. Defaults to 0.95.
            out_of_bounds: Keep tiles which contain regions outside of the image.
                Defaults to True.

        Raises:
            ValueError: `target_mpp` specified but slide mpp not available.
            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` dataclass.
        """
        # Handle target_mpp parameter for resolution normalization
        if target_mpp is not None:
            slide_mpp = self.mpp
            if slide_mpp is None:
                raise ValueError(
                    "Target mpp specified but slide mpp not available. "
                    "Provide mpp to SlideReader constructor or omit target_mpp."
                )
            # Calculate scaling factor: target_mpp / slide_mpp
            # Physical size = width * target_mpp (e.g., 512px * 0.25mpp = 128µm)
            # At slide resolution: need (width * target_mpp) / slide_mpp pixels
            # Example: 512px at 0.25mpp target, slide at 0.5mpp → 256px needed
            avg_slide_mpp = (slide_mpp[0] + slide_mpp[1]) / 2.0
            scale = target_mpp / avg_slide_mpp

            # Scale width/height to extract at native resolution
            # These will represent the desired physical size
            width = int(round(width * scale))
            if height is not None:
                height = int(round(height * scale))

        tile_coordinates = F.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 = F.get_background_percentages(
                tile_coordinates=tile_coordinates,
                tissue_mask=tissue_mask,
                downsample=F.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
        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.

        Args:
            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: Minimum pixel area for contours. Defaults to 10.
            max_area_pixel: Maximum pixel area for contours. Defaults to None.
            min_area_relative: Relative minimum contour area, calculated from the median
                contour area after filtering contours with `[min,max]_pixel` arguments
                (`min_area_relative * median(contour_areas)`). Defaults to 0.2.
            max_area_relative: Relative maximum contour area, calculated from the median
                contour area after filtering contours with `[min,max]_pixel` arguments
                (`max_area_relative * median(contour_areas)`). Defaults to 2.0.

        Returns:
            `TMASpotCoordinates` instance.
        """
        spot_mask = F.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 = F.get_spot_coordinates(spot_mask)
        spot_coordinates = [  # upsample to level zero.
            _multiply_xywh(x, F.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.

        Args:
            image: Input image.
            coordinates: Coordinates to annotate.
            linewidth: Width of rectangle lines.

        Returns:
            Annotated thumbnail.
        """
        kwargs = {
            "image": image,
            "downsample": F.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 F.get_annotated_image(**kwargs)

    def yield_regions(
        self,
        coordinates: Iterator[tuple[int, int, int, int]],
        *,
        level: int = 0,
        transform: Optional[Callable[[np.ndarray], Any]] = None,
        num_workers: int = 1,
        return_exception: bool = False,
    ) -> Iterator[tuple[Union[np.ndarray, Exception, Any], tuple[int, int, int, int]]]:
        """Yield tile images and corresponding xywh coordinates.

        Args:
            coordinates: List of xywh-coordinates.
            level: Slide pyramid level for reading tile images. Defaults to 0.
            transform: Transform function for tile image. Defaults to None.
            num_workers: Number of worker processes. Defaults to 1.
            return_exception: Whether to return exception in case there is a failure to
                read region, instead of raising the exception. Defaults to False.

        Yields:
            Tuple of (possibly transformed) tile image and corresponding
            xywh-coordinate.
        """
        pool, iterable = prepare_worker_pool(
            worker_fn=functools.partial(
                _read_tile,
                level=level,
                transform=transform,
                return_exception=return_exception,
            ),
            reader=self,
            iterable_of_args=((x,) for x in coordinates),
            iterable_length=len(coordinates),
            num_workers=num_workers,
        )
        yield from zip(iterable, coordinates)
        close_pool(pool)

    def get_mean_and_std(
        self,
        coordinates: Iterator[tuple[int, int, int, int]],
        *,
        level: int = 0,
        max_samples: int = 1000,
        num_workers: int = 1,
        raise_exception: bool = True,
    ) -> tuple[tuple[float, ...], tuple[float, ...]]:
        """Calculate mean and std for each image channel.

        Args:
            coordinates: `TileCoordinates` instance or a list of xywh-coordinates.
            level: Slide pyramid level for reading tile images. Defaults to 0.
            max_samples: Maximum tiles to load. Defaults to 1000.
            num_workers: Number of worker processes for yielding tiles. Defaults to 1.
            raise_exception: Whether to raise an exception if there are problems with
                reading tile regions. Defaults to True.

        Returns:
            Tuples of mean and std values for each image channel.
        """
        if isinstance(coordinates, TileCoordinates):
            coordinates = coordinates.coordinates
        if len(coordinates) > max_samples:
            rng = np.random.default_rng()
            coordinates = rng.choice(
                coordinates, size=max_samples, replace=False
            ).tolist()
        iterable = self.yield_regions(
            coordinates=coordinates,
            level=level,
            num_workers=num_workers,
            return_exception=not raise_exception,
        )
        return F.get_mean_and_std_from_images(
            images=(tile for tile, __ in iterable if not isinstance(tile, Exception))
        )

    def save_regions(
        self,
        parent_dir: Union[str, Path],
        coordinates: Iterator[tuple[int, int, int, int]],
        *,
        level: int = 0,
        threshold: Optional[int] = None,
        tissue_mask: Optional[np.ndarray] = None,
        overwrite: bool = False,
        save_metrics: bool = False,
        save_masks: bool = False,
        save_thumbnails: bool = True,
        thumbnail_level: Optional[int] = None,
        image_format: str = "jpeg",
        quality: int = 80,
        num_workers: int = 1,
        raise_exception: bool = True,
        verbose: bool = True,
    ) -> tuple[pl.DataFrame, list[dict[str, object]]]:
        """Save regions from an iterable of xywh-coordinates.

        Args:
            parent_dir: Parent directory for output. All output is saved to
                `parent_dir/{self.name}/`.
            coordinates: Iterator of xywh-coordinates.
            level: Slide pyramid level for extracting xywh-regions. Defaults to 0.
            threshold: Tissue detection threshold. Required when either `save_masks` or
                `save_metrics` is True. Defaults to None.
            overwrite: Overwrite everything in `parent_dir/{slide_name}/` if it exists.
                Defaults to False.
            save_metrics: Save image metrics to metadata, requires that threshold is
                set. Defaults to False.
            save_masks: Save tissue masks as `png` images, requires that threshold is
                set. Defaults to False.
            save_thumbnails: Save slide thumbnail with and without region annotations.
                Defaults to True.
            tissue_mask: Optional tissue mask to use for thumbnail visualization.
                If None and coordinates contains a tissue mask, that mask is used.
            thumbnail_level: Slide pyramid level for thumbnail images. If None, uses the
                `level_from_max_dimension` method. Ignored when `save_thumbnails=False`.
                Defaults to None.
            image_format: File format for `Pillow` image writer. Defaults to "jpeg".
            quality: JPEG compression quality if `format="jpeg"`. Defaults to 80.
            num_workers: Number of data saving workers. Defaults to 1.
            raise_exception: Whether to raise an exception if there are problems with
                reading tile regions. Defaults to True.
            verbose: Enables `tqdm` progress bar. Defaults to True.

        Raises:
            ValueError: Invalid `level` argument.
            ValueError: Threshold is not between 0 and 255.

        Returns:
            Tuple of (metadata dataframe, failure reports).
        """
        if (save_metrics or save_masks) and threshold is None:
            raise ValueError(ERROR_NO_THRESHOLD)
        level = format_level(level, available=list(self.level_dimensions))
        parent_dir = parent_dir if isinstance(parent_dir, Path) else Path(parent_dir)
        output_dir = _prepare_output_dir(parent_dir / self.name, overwrite=overwrite)
        image_dir = "spots" if isinstance(coordinates, SpotCoordinates) else "tiles"
        # Save properties.
        if isinstance(coordinates, TileCoordinates):
            with (output_dir / "properties.json").open("w") as f:
                json.dump(
                    coordinates.get_properties(
                        level=level, level_downsample=self.level_downsamples[level]
                    ),
                    f,
                )
        actual_image_format = _resolve_image_format(image_format)
        # Save thumbnails.
        if save_thumbnails:
            if thumbnail_level is None:
                thumbnail_level = self.level_from_max_dimension()
            thumbnail = self.read_level(thumbnail_level)

            # Downscale thumbnail if too large to prevent JPEG size limits and reduce disk space
            thumbnail_small = F.downscale_for_thumbnail(thumbnail)
            if actual_image_format == "png":
                thumbnail_small = downscale_to_max_pixels(
                    thumbnail_small, max_pixels=300_000
                )

            _save_image(
                Image.fromarray(thumbnail_small),
                output_dir / f"thumbnail.{actual_image_format}",
                image_format=actual_image_format,
                quality=quality,
            )
            thumbnail_regions = self.get_annotated_thumbnail(
                thumbnail_small, coordinates
            )
            _save_image(
                thumbnail_regions,
                output_dir / f"thumbnail_{image_dir}.{actual_image_format}",
                image_format=actual_image_format,
                quality=quality,
            )
            coords_mask = None
            if isinstance(coordinates, (TileCoordinates, SpotCoordinates)):
                coords_mask = coordinates.tissue_mask
            mask_for_thumbnail = tissue_mask if tissue_mask is not None else coords_mask
            if mask_for_thumbnail is not None:
                # For tissue mask, scale it to match the thumbnail dimensions if needed
                original_tissue_mask = mask_for_thumbnail
                if thumbnail_small.shape[:2] != thumbnail.shape[:2]:
                    # If thumbnail was downscaled, apply the same downscaling to tissue mask
                    scale_h = thumbnail_small.shape[0] / thumbnail.shape[0]
                    scale_w = thumbnail_small.shape[1] / thumbnail.shape[1]
                    new_h = max(1, int(original_tissue_mask.shape[0] * scale_h))
                    new_w = max(1, int(original_tissue_mask.shape[1] * scale_w))
                    tissue_mask_resized = cv2.resize(
                        original_tissue_mask.astype(np.uint8),
                        (new_w, new_h),
                        interpolation=cv2.INTER_AREA,
                    )
                else:
                    tissue_mask_resized = original_tissue_mask

                _save_image(
                    Image.fromarray(255 - 255 * tissue_mask_resized),
                    output_dir / f"thumbnail_tissue.{actual_image_format}",
                    image_format=actual_image_format,
                    quality=quality,
                )
        metadata, failures = _save_regions(
            output_dir=output_dir,
            iterable=self.yield_regions(
                coordinates=coordinates,
                level=level,
                transform=functools.partial(
                    _load_region_data,
                    save_masks=save_masks,
                    save_metrics=save_metrics,
                    threshold=threshold,
                ),
                num_workers=num_workers,
                return_exception=not raise_exception,
            ),
            desc=self.name,
            total=len(coordinates),
            quality=quality,
            image_format=actual_image_format,
            image_dir=image_dir,
            file_prefixes=coordinates.spot_names
            if isinstance(coordinates, SpotCoordinates)
            else None,
            verbose=verbose,
        )
        metadata.write_parquet(output_dir / "metadata.parquet")
        if failures:
            (output_dir / "failures.json").write_text(json.dumps(failures, indent=2))
        return metadata, failures

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

backend_name property

Name of the slide reader backend.

data_bounds property

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

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

level_count property

Number of slide pyramid levels.

level_dimensions property

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

level_downsamples property

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

mpp property

Microns per pixel (mpp_x, mpp_y) at level 0.

Returns user-provided override if available, otherwise extracts from slide metadata. Returns None if not available.

name property

Slide filename without an extension.

path property

Full slide filepath.

suffix property

Slide file-extension.

__init__(path, mpp=None)

Initialize SlideReader instance.

Parameters:

Name Type Description Default
path Union[str, Path]

Path to slide image.

required
mpp Optional[tuple[float, float]]

Override microns per pixel as (mpp_x, mpp_y). If None, attempts to extract from slide metadata. Defaults to None.

None

Raises:

Type Description
FileNotFoundError

Path does not exist.

Source code in histoslice/_reader.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def __init__(
    self,
    path: Union[str, Path],
    mpp: Optional[tuple[float, float]] = None,
) -> None:
    """Initialize `SlideReader` instance.

    Args:
        path: Path to slide image.
        mpp: Override microns per pixel as (mpp_x, mpp_y). If None, attempts to
            extract from slide metadata. Defaults to None.

    Raises:
        FileNotFoundError: Path does not exist.
    """
    super().__init__()
    self._backend = _read_slide(path=path)
    self._mpp_override = mpp

get_annotated_thumbnail(image, coordinates, linewidth=1)

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

Annotated thumbnail.

Source code in histoslice/_reader.py
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
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.

    Args:
        image: Input image.
        coordinates: Coordinates to annotate.
        linewidth: Width of rectangle lines.

    Returns:
        Annotated thumbnail.
    """
    kwargs = {
        "image": image,
        "downsample": F.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 F.get_annotated_image(**kwargs)

get_mean_and_std(coordinates, *, level=0, max_samples=1000, num_workers=1, raise_exception=True)

Calculate mean and std for each image channel.

Parameters:

Name Type Description Default
coordinates Iterator[tuple[int, int, int, int]]

TileCoordinates instance or a list of xywh-coordinates.

required
level int

Slide pyramid level for reading tile images. Defaults to 0.

0
max_samples int

Maximum tiles to load. Defaults to 1000.

1000
num_workers int

Number of worker processes for yielding tiles. Defaults to 1.

1
raise_exception bool

Whether to raise an exception if there are problems with reading tile regions. Defaults to True.

True

Returns:

Type Description
tuple[tuple[float, ...], tuple[float, ...]]

Tuples of mean and std values for each image channel.

Source code in histoslice/_reader.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def get_mean_and_std(
    self,
    coordinates: Iterator[tuple[int, int, int, int]],
    *,
    level: int = 0,
    max_samples: int = 1000,
    num_workers: int = 1,
    raise_exception: bool = True,
) -> tuple[tuple[float, ...], tuple[float, ...]]:
    """Calculate mean and std for each image channel.

    Args:
        coordinates: `TileCoordinates` instance or a list of xywh-coordinates.
        level: Slide pyramid level for reading tile images. Defaults to 0.
        max_samples: Maximum tiles to load. Defaults to 1000.
        num_workers: Number of worker processes for yielding tiles. Defaults to 1.
        raise_exception: Whether to raise an exception if there are problems with
            reading tile regions. Defaults to True.

    Returns:
        Tuples of mean and std values for each image channel.
    """
    if isinstance(coordinates, TileCoordinates):
        coordinates = coordinates.coordinates
    if len(coordinates) > max_samples:
        rng = np.random.default_rng()
        coordinates = rng.choice(
            coordinates, size=max_samples, replace=False
        ).tolist()
    iterable = self.yield_regions(
        coordinates=coordinates,
        level=level,
        num_workers=num_workers,
        return_exception=not raise_exception,
    )
    return F.get_mean_and_std_from_images(
        images=(tile for tile, __ in iterable if not isinstance(tile, Exception))
    )

get_spot_coordinates(tissue_mask, *, min_area_pixel=10, max_area_pixel=None, min_area_relative=0.2, max_area_relative=2.0)

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. Defaults to 10.

10
max_area_pixel Optional[int]

Maximum pixel area for contours. Defaults to None.

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)). Defaults to 0.2.

0.2
max_area_relative Optional[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)). Defaults to 2.0.

2.0

Returns:

Type Description
SpotCoordinates

TMASpotCoordinates instance.

Source code in histoslice/_reader.py
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
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.

    Args:
        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: Minimum pixel area for contours. Defaults to 10.
        max_area_pixel: Maximum pixel area for contours. Defaults to None.
        min_area_relative: Relative minimum contour area, calculated from the median
            contour area after filtering contours with `[min,max]_pixel` arguments
            (`min_area_relative * median(contour_areas)`). Defaults to 0.2.
        max_area_relative: Relative maximum contour area, calculated from the median
            contour area after filtering contours with `[min,max]_pixel` arguments
            (`max_area_relative * median(contour_areas)`). Defaults to 2.0.

    Returns:
        `TMASpotCoordinates` instance.
    """
    spot_mask = F.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 = F.get_spot_coordinates(spot_mask)
    spot_coordinates = [  # upsample to level zero.
        _multiply_xywh(x, F.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_tile_coordinates(tissue_mask, width, *, height=None, target_mpp=None, overlap=0.0, max_background=0.95, out_of_bounds=True)

Generate tile coordinates.

Parameters:

Name Type Description Default
tissue_mask Optional[ndarray]

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

required
width int

Width of a tile in pixels at target resolution.

required
height Optional[int]

Height of a tile in pixels at target resolution. If None, will be set to width. Defaults to None.

None
target_mpp Optional[float]

Target microns per pixel for normalization. If specified, tiles are extracted at the appropriate level to achieve this resolution. The output tiles will be width x height pixels representing a physical area of width * target_mpp x height * target_mpp microns. Defaults to None (use native slide resolution).

None
overlap float

Overlap between neighbouring tiles. Defaults to 0.0.

0.0
max_background float

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

0.95
out_of_bounds bool

Keep tiles which contain regions outside of the image. Defaults to True.

True

Raises:

Type Description
ValueError

target_mpp specified but slide mpp not available.

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:

Type Description
TileCoordinates

TileCoordinates dataclass.

Source code in histoslice/_reader.py
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
def get_tile_coordinates(
    self,
    tissue_mask: Optional[np.ndarray],
    width: int,
    *,
    height: Optional[int] = None,
    target_mpp: Optional[float] = None,
    overlap: float = 0.0,
    max_background: float = 0.95,
    out_of_bounds: bool = True,
) -> TileCoordinates:
    """Generate tile coordinates.

    Args:
        tissue_mask: Tissue mask for filtering tiles with too much background. If
            None, the filtering is disabled.
        width: Width of a tile in pixels at target resolution.
        height: Height of a tile in pixels at target resolution. If None, will be
            set to `width`. Defaults to None.
        target_mpp: Target microns per pixel for normalization. If specified, tiles
            are extracted at the appropriate level to achieve this resolution. The
            output tiles will be `width` x `height` pixels representing a physical
            area of `width * target_mpp` x `height * target_mpp` microns.
            Defaults to None (use native slide resolution).
        overlap: Overlap between neighbouring tiles. Defaults to 0.0.
        max_background: Maximum proportion of background in tiles. Ignored if
            `tissue_mask` is None. Defaults to 0.95.
        out_of_bounds: Keep tiles which contain regions outside of the image.
            Defaults to True.

    Raises:
        ValueError: `target_mpp` specified but slide mpp not available.
        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` dataclass.
    """
    # Handle target_mpp parameter for resolution normalization
    if target_mpp is not None:
        slide_mpp = self.mpp
        if slide_mpp is None:
            raise ValueError(
                "Target mpp specified but slide mpp not available. "
                "Provide mpp to SlideReader constructor or omit target_mpp."
            )
        # Calculate scaling factor: target_mpp / slide_mpp
        # Physical size = width * target_mpp (e.g., 512px * 0.25mpp = 128µm)
        # At slide resolution: need (width * target_mpp) / slide_mpp pixels
        # Example: 512px at 0.25mpp target, slide at 0.5mpp → 256px needed
        avg_slide_mpp = (slide_mpp[0] + slide_mpp[1]) / 2.0
        scale = target_mpp / avg_slide_mpp

        # Scale width/height to extract at native resolution
        # These will represent the desired physical size
        width = int(round(width * scale))
        if height is not None:
            height = int(round(height * scale))

    tile_coordinates = F.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 = F.get_background_percentages(
            tile_coordinates=tile_coordinates,
            tissue_mask=tissue_mask,
            downsample=F.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
    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_tissue_mask(*, level=None, threshold=None, multiplier=1.05, sigma=0.0)

Detect tissue from slide pyramid level image.

Parameters:

Name Type Description Default
level Optional[int]

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

None
threshold Optional[int]

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

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. Defaults to 1.0.

1.05
sigma float

Sigma for gaussian blurring. Defaults to 0.0.

0.0

Raises:

Type Description
ValueError

Threshold not between 0 and 255.

Returns:

Type Description
tuple[int, ndarray]

Threshold and tissue mask.

Source code in histoslice/_reader.py
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
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.

    Args:
        level: Slide pyramid level to use for tissue detection. If None, uses the
            `level_from_max_dimension` method. Defaults to None.
        threshold: Threshold for tissue detection. If set, will detect tissue by
            global thresholding. Otherwise Otsu's method is used to find a
            threshold. Defaults to None.
        multiplier: 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. Defaults to 1.0.
        sigma: Sigma for gaussian blurring. Defaults to 0.0.

    Raises:
        ValueError: Threshold not between 0 and 255.

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

level_from_dimensions(dimensions)

Find pyramid level which is closest to dimensions.

Parameters:

Name Type Description Default
dimensions tuple[int, int]

Height and width.

required

Returns:

Type Description
int

Slide pyramid level.

Source code in histoslice/_reader.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def level_from_dimensions(self, dimensions: tuple[int, int]) -> int:
    """Find pyramid level which is closest to `dimensions`.

    Args:
        dimensions: Height and width.

    Returns:
        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))]

level_from_max_dimension(max_dimension=4096)

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. Defaults to 4096.

4096

Returns:

Type Description
int

Slide pyramid level.

Source code in histoslice/_reader.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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.

    Args:
        max_dimension: Maximum dimension for the level. Defaults to 4096.

    Returns:
        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]

read_level(level)

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

Array containing image data from level.

Source code in histoslice/_reader.py
113
114
115
116
117
118
119
120
121
122
123
124
125
def read_level(self, level: int) -> np.ndarray:
    """Read full pyramid level data.

    Args:
        level: Slide pyramid level to read.

    Raises:
        ValueError: Invalid level argument.

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

read_region(xywh, level=0)

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. Defaults to 0.

0

Raises:

Type Description
ValueError

Invalid level argument.

Returns:

Type Description
ndarray

Array containing image data from xywh-region.

Source code in histoslice/_reader.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def read_region(
    self, xywh: tuple[int, int, int, int], level: int = 0
) -> np.ndarray:
    """Read region based on `xywh`-coordinates.

    Args:
        xywh: Coordinates for the region.
        level: Slide pyramid level to read from. Defaults to 0.

    Raises:
        ValueError: Invalid `level` argument.

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

save_regions(parent_dir, coordinates, *, level=0, threshold=None, tissue_mask=None, overwrite=False, save_metrics=False, save_masks=False, save_thumbnails=True, thumbnail_level=None, image_format='jpeg', quality=80, num_workers=1, raise_exception=True, verbose=True)

Save regions from an iterable of xywh-coordinates.

Parameters:

Name Type Description Default
parent_dir Union[str, Path]

Parent directory for output. All output is saved to parent_dir/{self.name}/.

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

Iterator of xywh-coordinates.

required
level int

Slide pyramid level for extracting xywh-regions. Defaults to 0.

0
threshold Optional[int]

Tissue detection threshold. Required when either save_masks or save_metrics is True. Defaults to None.

None
overwrite bool

Overwrite everything in parent_dir/{slide_name}/ if it exists. Defaults to False.

False
save_metrics bool

Save image metrics to metadata, requires that threshold is set. Defaults to False.

False
save_masks bool

Save tissue masks as png images, requires that threshold is set. Defaults to False.

False
save_thumbnails bool

Save slide thumbnail with and without region annotations. Defaults to True.

True
tissue_mask Optional[ndarray]

Optional tissue mask to use for thumbnail visualization. If None and coordinates contains a tissue mask, that mask is used.

None
thumbnail_level Optional[int]

Slide pyramid level for thumbnail images. If None, uses the level_from_max_dimension method. Ignored when save_thumbnails=False. Defaults to None.

None
image_format str

File format for Pillow image writer. Defaults to "jpeg".

'jpeg'
quality int

JPEG compression quality if format="jpeg". Defaults to 80.

80
num_workers int

Number of data saving workers. Defaults to 1.

1
raise_exception bool

Whether to raise an exception if there are problems with reading tile regions. Defaults to True.

True
verbose bool

Enables tqdm progress bar. Defaults to True.

True

Raises:

Type Description
ValueError

Invalid level argument.

ValueError

Threshold is not between 0 and 255.

Returns:

Type Description
tuple[DataFrame, list[dict[str, object]]]

Tuple of (metadata dataframe, failure reports).

Source code in histoslice/_reader.py
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
def save_regions(
    self,
    parent_dir: Union[str, Path],
    coordinates: Iterator[tuple[int, int, int, int]],
    *,
    level: int = 0,
    threshold: Optional[int] = None,
    tissue_mask: Optional[np.ndarray] = None,
    overwrite: bool = False,
    save_metrics: bool = False,
    save_masks: bool = False,
    save_thumbnails: bool = True,
    thumbnail_level: Optional[int] = None,
    image_format: str = "jpeg",
    quality: int = 80,
    num_workers: int = 1,
    raise_exception: bool = True,
    verbose: bool = True,
) -> tuple[pl.DataFrame, list[dict[str, object]]]:
    """Save regions from an iterable of xywh-coordinates.

    Args:
        parent_dir: Parent directory for output. All output is saved to
            `parent_dir/{self.name}/`.
        coordinates: Iterator of xywh-coordinates.
        level: Slide pyramid level for extracting xywh-regions. Defaults to 0.
        threshold: Tissue detection threshold. Required when either `save_masks` or
            `save_metrics` is True. Defaults to None.
        overwrite: Overwrite everything in `parent_dir/{slide_name}/` if it exists.
            Defaults to False.
        save_metrics: Save image metrics to metadata, requires that threshold is
            set. Defaults to False.
        save_masks: Save tissue masks as `png` images, requires that threshold is
            set. Defaults to False.
        save_thumbnails: Save slide thumbnail with and without region annotations.
            Defaults to True.
        tissue_mask: Optional tissue mask to use for thumbnail visualization.
            If None and coordinates contains a tissue mask, that mask is used.
        thumbnail_level: Slide pyramid level for thumbnail images. If None, uses the
            `level_from_max_dimension` method. Ignored when `save_thumbnails=False`.
            Defaults to None.
        image_format: File format for `Pillow` image writer. Defaults to "jpeg".
        quality: JPEG compression quality if `format="jpeg"`. Defaults to 80.
        num_workers: Number of data saving workers. Defaults to 1.
        raise_exception: Whether to raise an exception if there are problems with
            reading tile regions. Defaults to True.
        verbose: Enables `tqdm` progress bar. Defaults to True.

    Raises:
        ValueError: Invalid `level` argument.
        ValueError: Threshold is not between 0 and 255.

    Returns:
        Tuple of (metadata dataframe, failure reports).
    """
    if (save_metrics or save_masks) and threshold is None:
        raise ValueError(ERROR_NO_THRESHOLD)
    level = format_level(level, available=list(self.level_dimensions))
    parent_dir = parent_dir if isinstance(parent_dir, Path) else Path(parent_dir)
    output_dir = _prepare_output_dir(parent_dir / self.name, overwrite=overwrite)
    image_dir = "spots" if isinstance(coordinates, SpotCoordinates) else "tiles"
    # Save properties.
    if isinstance(coordinates, TileCoordinates):
        with (output_dir / "properties.json").open("w") as f:
            json.dump(
                coordinates.get_properties(
                    level=level, level_downsample=self.level_downsamples[level]
                ),
                f,
            )
    actual_image_format = _resolve_image_format(image_format)
    # Save thumbnails.
    if save_thumbnails:
        if thumbnail_level is None:
            thumbnail_level = self.level_from_max_dimension()
        thumbnail = self.read_level(thumbnail_level)

        # Downscale thumbnail if too large to prevent JPEG size limits and reduce disk space
        thumbnail_small = F.downscale_for_thumbnail(thumbnail)
        if actual_image_format == "png":
            thumbnail_small = downscale_to_max_pixels(
                thumbnail_small, max_pixels=300_000
            )

        _save_image(
            Image.fromarray(thumbnail_small),
            output_dir / f"thumbnail.{actual_image_format}",
            image_format=actual_image_format,
            quality=quality,
        )
        thumbnail_regions = self.get_annotated_thumbnail(
            thumbnail_small, coordinates
        )
        _save_image(
            thumbnail_regions,
            output_dir / f"thumbnail_{image_dir}.{actual_image_format}",
            image_format=actual_image_format,
            quality=quality,
        )
        coords_mask = None
        if isinstance(coordinates, (TileCoordinates, SpotCoordinates)):
            coords_mask = coordinates.tissue_mask
        mask_for_thumbnail = tissue_mask if tissue_mask is not None else coords_mask
        if mask_for_thumbnail is not None:
            # For tissue mask, scale it to match the thumbnail dimensions if needed
            original_tissue_mask = mask_for_thumbnail
            if thumbnail_small.shape[:2] != thumbnail.shape[:2]:
                # If thumbnail was downscaled, apply the same downscaling to tissue mask
                scale_h = thumbnail_small.shape[0] / thumbnail.shape[0]
                scale_w = thumbnail_small.shape[1] / thumbnail.shape[1]
                new_h = max(1, int(original_tissue_mask.shape[0] * scale_h))
                new_w = max(1, int(original_tissue_mask.shape[1] * scale_w))
                tissue_mask_resized = cv2.resize(
                    original_tissue_mask.astype(np.uint8),
                    (new_w, new_h),
                    interpolation=cv2.INTER_AREA,
                )
            else:
                tissue_mask_resized = original_tissue_mask

            _save_image(
                Image.fromarray(255 - 255 * tissue_mask_resized),
                output_dir / f"thumbnail_tissue.{actual_image_format}",
                image_format=actual_image_format,
                quality=quality,
            )
    metadata, failures = _save_regions(
        output_dir=output_dir,
        iterable=self.yield_regions(
            coordinates=coordinates,
            level=level,
            transform=functools.partial(
                _load_region_data,
                save_masks=save_masks,
                save_metrics=save_metrics,
                threshold=threshold,
            ),
            num_workers=num_workers,
            return_exception=not raise_exception,
        ),
        desc=self.name,
        total=len(coordinates),
        quality=quality,
        image_format=actual_image_format,
        image_dir=image_dir,
        file_prefixes=coordinates.spot_names
        if isinstance(coordinates, SpotCoordinates)
        else None,
        verbose=verbose,
    )
    metadata.write_parquet(output_dir / "metadata.parquet")
    if failures:
        (output_dir / "failures.json").write_text(json.dumps(failures, indent=2))
    return metadata, failures

yield_regions(coordinates, *, level=0, transform=None, num_workers=1, return_exception=False)

Yield tile images and corresponding xywh coordinates.

Parameters:

Name Type Description Default
coordinates Iterator[tuple[int, int, int, int]]

List of xywh-coordinates.

required
level int

Slide pyramid level for reading tile images. Defaults to 0.

0
transform Optional[Callable[[ndarray], Any]]

Transform function for tile image. Defaults to None.

None
num_workers int

Number of worker processes. Defaults to 1.

1
return_exception bool

Whether to return exception in case there is a failure to read region, instead of raising the exception. Defaults to False.

False

Yields:

Type Description
Union[ndarray, Exception, Any]

Tuple of (possibly transformed) tile image and corresponding

tuple[int, int, int, int]

xywh-coordinate.

Source code in histoslice/_reader.py
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
def yield_regions(
    self,
    coordinates: Iterator[tuple[int, int, int, int]],
    *,
    level: int = 0,
    transform: Optional[Callable[[np.ndarray], Any]] = None,
    num_workers: int = 1,
    return_exception: bool = False,
) -> Iterator[tuple[Union[np.ndarray, Exception, Any], tuple[int, int, int, int]]]:
    """Yield tile images and corresponding xywh coordinates.

    Args:
        coordinates: List of xywh-coordinates.
        level: Slide pyramid level for reading tile images. Defaults to 0.
        transform: Transform function for tile image. Defaults to None.
        num_workers: Number of worker processes. Defaults to 1.
        return_exception: Whether to return exception in case there is a failure to
            read region, instead of raising the exception. Defaults to False.

    Yields:
        Tuple of (possibly transformed) tile image and corresponding
        xywh-coordinate.
    """
    pool, iterable = prepare_worker_pool(
        worker_fn=functools.partial(
            _read_tile,
            level=level,
            transform=transform,
            return_exception=return_exception,
        ),
        reader=self,
        iterable_of_args=((x,) for x in coordinates),
        iterable_length=len(coordinates),
        num_workers=num_workers,
    )
    yield from zip(iterable, coordinates)
    close_pool(pool)