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