ez_wsi_dicomweb/magnification.py (150 lines of code) (raw):
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""A class that represents a WSI slide magnification."""
from __future__ import annotations
import dataclasses
import enum
import math
from typing import Dict
@enum.unique
class MagnificationLevel(enum.Enum):
"""Enums of all supported magnification levels for DICOM/WSI slides."""
UNKNOWN_MAGNIFICATION = 0
M_100X = 1 # 100x
M_80X = 2 # 80X
M_40X = 3 # 40x
M_20X = 4 # 20x
M_10X = 5 # 10x
M_5X = 6 # 5x
M_5X_DIV_2 = 7 # 2.5x = 5x / 2
M_5X_DIV_4 = 8 # 1.25x = 5x / 4
M_5X_DIV_8 = 9 # 0.625x = 5x / 8
M_5X_DIV_16 = 10 # 0.3125x = 5x / 16
M_5X_DIV_32 = 11 # 0.15625 = 5x / 32
M_5X_DIV_64 = 12 # 0.078125 = 5x / 64
M_5X_DIV_128 = 13 # 0.0390625 = 5x / 128
@dataclasses.dataclass(frozen=True)
class MagnificationProperties:
"""Class for storing all properties associated with a magnification.
Used to build a fast lookup table in
_MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES.
Attributes:
as_double: The floating point value of the magnification.
as_string: The string label of the magnification.
nominal_pixel_size: The nominal pixel size in mpp (micrometers per pixel).
"""
as_double: float
as_string: str
nominal_pixel_size: float
# A dict that maps all magnification levels to their properties.
_MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES: Dict[
MagnificationLevel, MagnificationProperties
] = {
MagnificationLevel.UNKNOWN_MAGNIFICATION: MagnificationProperties(
as_double=-1.0, as_string='unknown', nominal_pixel_size=0
),
MagnificationLevel.M_100X: MagnificationProperties(
as_double=100.0, as_string='100X', nominal_pixel_size=0.1
),
MagnificationLevel.M_80X: MagnificationProperties(
as_double=80.0, as_string='80X', nominal_pixel_size=0.125
),
MagnificationLevel.M_40X: MagnificationProperties(
as_double=40.0, as_string='40X', nominal_pixel_size=0.25
),
MagnificationLevel.M_20X: MagnificationProperties(
as_double=20.0, as_string='20X', nominal_pixel_size=0.5
),
MagnificationLevel.M_10X: MagnificationProperties(
as_double=10.0, as_string='10X', nominal_pixel_size=1.0
),
MagnificationLevel.M_5X: MagnificationProperties(
as_double=5.0, as_string='5X', nominal_pixel_size=2.0
),
MagnificationLevel.M_5X_DIV_2: MagnificationProperties(
as_double=2.5, as_string='2.5X', nominal_pixel_size=4.0
),
MagnificationLevel.M_5X_DIV_4: MagnificationProperties(
as_double=1.25, as_string='1.25X', nominal_pixel_size=8.0
),
MagnificationLevel.M_5X_DIV_8: MagnificationProperties(
as_double=0.625, as_string='0.625X', nominal_pixel_size=16
),
MagnificationLevel.M_5X_DIV_16: MagnificationProperties(
as_double=0.3125, as_string='0.3125X', nominal_pixel_size=32.0
),
MagnificationLevel.M_5X_DIV_32: MagnificationProperties(
as_double=0.15625, as_string='0.15625X', nominal_pixel_size=64.0
),
MagnificationLevel.M_5X_DIV_64: MagnificationProperties(
as_double=0.078125, as_string='0.078125X', nominal_pixel_size=128.0
),
MagnificationLevel.M_5X_DIV_128: MagnificationProperties(
as_double=0.0390625, as_string='0.0390625X', nominal_pixel_size=256.0
),
}
# The tolerance for matching a un-normalized pixel size value to a
# magnification level.
_MPP_SCALE_TOLERANCE = 0.2
class Magnification:
"""Represents a magnification level of a DICOM/WSI slide."""
def __init__(self, magnification_level: MagnificationLevel):
"""Constructor.
Args:
magnification_level: The magnification.
"""
self._magnification_level = magnification_level
@classmethod
def Unknown(cls):
"""Returns a Magnification object at UNKNOWN_MAGNIFICATION."""
return Magnification(MagnificationLevel.UNKNOWN_MAGNIFICATION)
@classmethod
def FromString(cls, mag_str: str):
"""Returns a Magnification object given a string potentially representing a magnification.
Args:
mag_str: The string value of the magnfication to search for.
"""
for (
level,
mag_level_props,
) in _MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES.items():
if mag_str == mag_level_props.as_string:
return Magnification(level)
return cls.Unknown()
@classmethod
def FromDouble(cls, mag_float: float):
"""Returns a Magnification by its floating value representation.
Args:
mag_float: The floating value of the magnfication to request.
"""
for (
mag_level,
mag_level_props,
) in _MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES.items():
if math.isclose(mag_float, mag_level_props.as_double):
return Magnification(mag_level)
return cls.Unknown()
@classmethod
def FromPixelSize(cls, mpp: float):
"""Returns a Magnification by a pixel size.
Args:
mpp: The pixel size, in micrometers per pixel, of the magnification to
request.
"""
for (
mag_level,
mag_level_props,
) in _MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES.items():
if mag_level == MagnificationLevel.UNKNOWN_MAGNIFICATION:
continue
scale = mpp / mag_level_props.nominal_pixel_size
if abs(scale - 1) < _MPP_SCALE_TOLERANCE:
return Magnification(mag_level)
return cls.Unknown()
def __eq__(self, other: object) -> bool:
if isinstance(other, Magnification):
return self._magnification_level == other._magnification_level
return False
@property
def nominal_pixel_size(self) -> float:
"""Returns the nominal pixel size of the magnification."""
return _MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES[
self._magnification_level
].nominal_pixel_size
@property
def magnification_level(self) -> MagnificationLevel:
"""Returns the enum value of the magnification level."""
return self._magnification_level
@property
def as_double(self) -> float:
"""Returns the floating value of the magnification level."""
return _MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES[
self._magnification_level
].as_double
@property
def as_string(self) -> str:
"""Returns the string value of the magnification level."""
return _MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES[
self._magnification_level
].as_string
@property
def is_unknown(self) -> bool:
"""Checks if the magnification is unknown."""
return self._magnification_level == MagnificationLevel.UNKNOWN_MAGNIFICATION
@property
def next_higher_magnification(self) -> Magnification:
"""Creates a Magnification object containing next higher magnification level.
Returns:
An object with next higher magnification, or UNKNOWN magnification if this
is the highest magnification level.
"""
return self.FromPixelSize(
_MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES[
self._magnification_level
].nominal_pixel_size
/ 2
)
@property
def next_lower_magnification(self) -> Magnification:
"""Creates a Magnification object containing next lower magnification level.
Returns:
An object with the next lower magnification, or UNKNOWN magnification if
this is the lowest magnification level.
"""
return self.FromPixelSize(
_MAGNIFICATION_LEVEL_TO_MAGNIFICATION_PROPERTIES[
self._magnification_level
].nominal_pixel_size
* 2
)