Source code for tiatoolbox.wsicore.wsimeta

# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# The Original Code is Copyright (C) 2021, TIA Centre, University of Warwick
# All rights reserved.
# ***** END GPL LICENSE BLOCK *****

"""This module defines a dataclass which holds metadata about a WSI.

With this class, metadata is in a normalized consistent format
which is quite useful when working with many different WSI formats.
The raw metadata is also preserved and accessible via a dictionary. The
format of this dictionary may vary between WSI formats.

"""
import warnings
from pathlib import Path
from typing import Mapping, Optional, Sequence, Tuple

import numpy as np


[docs]class WSIMeta: """Whole slide image metadata class. Args: slide_dimensions (int, int): Tuple containing the width and height of the WSI. These are for the baseline (full resolution) image if the WSI is a pyramid or multi-resoltion. level_dimensions (list): A list of dimensions for each level of the pyramid or for each resolution in the WSI. objective_power (float, optional): The power of the objective lens used to create the image. level_count: (int, optional): The number of levels or resolutions in the WSI. If not given this is assigned len(level_dimensions). Defaults to None. level_downsamples (:obj:`list` of :obj:`float`): List of scale values which describe how many times smaller the current level is compared with the baseline. vendor (str, optional): Scanner vendor/manufacturer description. mpp (float, float, optional): Microns per pixel. file_path (Path, optional): Path to the corresponding WSI file. raw (dict, optional): Dictionary of unprocessed metadata extracted from the WSI format. For JPEG-2000 images this contains an xml object under the key "xml". Attributes: slide_dimensions (tuple(int)): Tuple containing the width and height of the WSI. These are for the baseline (full resolution) image if the WSI is a pyramid or multi-resoltion. Required. axes (str): Axes ordering of the image. This is most relevant for OME-TIFF images where the axes ordering can vary. For most images this with be "YXS" i.e. the image is store in the axis order of Y coordinates first, then X coordinates, and colour channels last. level_dimensions (list): A list of dimensions for each level of the pyramid or for each resolution in the WSI. Defaults to [slide_dimension]. objective_power (float): The magnification power of the objective lens used to scan the image. Not always present or accurate. Defaults to None. level_count: (int): The number of levels or resolutions in the WSI. If not given this is assigned len(level_dimensions). Defaults to len(level_dimensions). level_downsamples (:obj:`list` of :obj:`float`): List of scale values which describe how many times smaller the current level is compared with the baseline. Defaults to (1,). vendor (str): Scanner vendor/manufacturer description. mpp (float, float, optional): Microns per pixel. Derived from objective power and sensor size. Not always present or accurate. Defaults to None. file_path (Path): Path to the corresponding WSI file. Defaults to None. raw (dict): Dictionary of unprocessed metadata extracted from the WSI format. For JP2 images this contains an xml object under the key "xml". Defaults to empty dictionary. """ _valid_axes_characters = "YXSTZ" def __init__( self, slide_dimensions: Tuple[int, int], axes: str, level_dimensions: Optional[Sequence[Tuple[int, int]]] = None, objective_power: Optional[float] = None, level_count: Optional[int] = None, level_downsamples: Optional[Sequence[float]] = (1,), vendor: Optional[str] = None, mpp: Optional[Sequence[float]] = None, file_path: Optional[Path] = None, raw: Optional[Mapping[str, str]] = None, ): self.axes = axes self.objective_power = float(objective_power) if objective_power else None self.slide_dimensions = tuple([int(x) for x in slide_dimensions]) self.level_dimensions = ( tuple([(int(w), int(h)) for w, h in level_dimensions]) if level_dimensions is not None else [self.slide_dimensions] ) self.level_downsamples = ( [float(x) for x in level_downsamples] if level_downsamples is not None else None ) self.level_count = ( int(level_count) if level_count is not None else len(self.level_dimensions) ) self.vendor = str(vendor) self.mpp = np.array([float(x) for x in mpp]) if mpp is not None else None self.file_path = Path(file_path) if file_path is not None else None self.raw = raw if raw is not None else None self.validate()
[docs] def validate(self): """Validate passed values and cast to Python types. Metadata values are often given as strings and must be parsed/cast to the appropriate python type e.g. "3.14" to 3.14 etc. Returns: bool: True is validation passed, False otherwise. """ passed = True # Fatal conditions: Should return False if not True if len(set(self.axes) - set(self._valid_axes_characters)) > 0: warnings.warn( "Axes contains invalid characters. " f"Valid characters are '{self._valid_axes_characters}'." ) passed = False if self.level_count < 1: warnings.warn("Level count is not a positive integer") passed = False if self.level_dimensions is None: warnings.warn("level_dimensions is None") passed = False elif len(self.level_dimensions) != self.level_count: warnings.warn("Length of level dimensions != level count") passed = False if self.level_downsamples is None: warnings.warn("Level downsamples is None") passed = False elif len(self.level_downsamples) != self.level_count: warnings.warn("Length of level downsamples != level count") passed = False # Non-fatal conditions: Raise warning only, do not fail validation if self.raw is None: warnings.warn("Raw data is None") if all(x is None for x in [self.objective_power, self.mpp]): warnings.warn("Unknown scale (no objective_power or mpp)") return passed
[docs] def as_dict(self): """Convert WSIMeta to dictionary of Python types. Returns: dict: whole slide image meta data as dictionary """ if self.mpp is None: mpp = (self.mpp, self.mpp) else: mpp = tuple(self.mpp) param = { "objective_power": self.objective_power, "slide_dimensions": self.slide_dimensions, "level_count": self.level_count, "level_dimensions": self.level_dimensions, "level_downsamples": self.level_downsamples, "vendor": self.vendor, "mpp": mpp, "file_path": self.file_path, } return param