Repository: Project-MONAI/MONAI

Issue:
WSIReader support for reading by existing power and mpp
In MONAI Pathology WG, we have discussed about the need for being able to read whole slide images at specific resolution (mpp or magnification) in addition to reading at a specific level.
As discussed in pathology working group, this feature request is part of #4980 that defines the need to read the existing objective power and magnification without interpolating if the requested power or mpp does not exist. We decided to address this part first as the interpolation part requires some discussion on the details.


Discussion:

Patch:
diff --git a/monai/data/wsi_reader.py b/monai/data/wsi_reader.py
--- a/monai/data/wsi_reader.py
+++ b/monai/data/wsi_reader.py
@@ -11,6 +11,7 @@
 
 from __future__ import annotations
 
+import warnings
 from abc import abstractmethod
 from collections.abc import Sequence
 from os.path import abspath
@@ -27,9 +28,11 @@
     dtype_numpy_to_torch,
     dtype_torch_to_numpy,
     ensure_tuple,
+    ensure_tuple_rep,
     optional_import,
     require_pkg,
 )
+from monai.utils.misc import ConvertUnits
 
 OpenSlide, _ = optional_import("openslide", name="OpenSlide")
 TiffFile, _ = optional_import("tifffile", name="TiffFile")
@@ -42,7 +45,13 @@ class BaseWSIReader(ImageReader):
     An abstract class that defines APIs to load patches from whole slide image files.
 
     Args:
-        level: the whole slide image level at which the image is extracted.
+        level: the whole slide image level at which the patches are extracted.
+        mpp: the resolution in micron per pixel at which the patches are extracted.
+        mpp_rtol: the acceptable relative tolerance for resolution in micro per pixel.
+        mpp_atol: the acceptable absolute tolerance for resolution in micro per pixel.
+        power: the objective power at which the patches are extracted.
+        power_rtol: the acceptable relative tolerance for objective power.
+        power_atol: the acceptable absolute tolerance for objective power.
         channel_dim: the desired dimension for color channel.
         dtype: the data type of output image.
         device: target device to put the extracted patch. Note that if device is "cuda"",
@@ -50,6 +59,11 @@ class BaseWSIReader(ImageReader):
         mode: the output image color mode, e.g., "RGB" or "RGBA".
         kwargs: additional args for the reader
 
+        Notes:
+            Only one of resolution parameters, `level`, `mpp`, or `power`, should be provided.
+            If such parameters are provided in `get_data` method, those will override the values provided here.
+            If none of them are provided here or in `get_data`, `level=0` will be used.
+
     Typical usage of a concrete implementation of this class is:
 
     .. code-block:: python
@@ -77,11 +91,17 @@ class BaseWSIReader(ImageReader):
 
     def __init__(
         self,
-        level: int,
-        channel_dim: int,
-        dtype: DtypeLike | torch.dtype,
-        device: torch.device | str | None,
-        mode: str,
+        level: int | None = None,
+        mpp: float | tuple[float, float] | None = None,
+        mpp_rtol: float = 0.05,
+        mpp_atol: float = 0.0,
+        power: int | None = None,
+        power_rtol: float = 0.05,
+        power_atol: float = 0.0,
+        channel_dim: int = 0,
+        dtype: DtypeLike | torch.dtype = np.uint8,
+        device: torch.device | str | None = None,
+        mode: str = "RGB",
         **kwargs,
     ):
         super().__init__()
@@ -91,6 +111,12 @@ def __init__(
         self.set_device(device)
         self.mode = mode
         self.kwargs = kwargs
+        self.mpp: tuple[float, float] | None = ensure_tuple_rep(mpp, 2) if mpp is not None else None  # type: ignore
+        self.power = power
+        self.mpp_rtol = mpp_rtol
+        self.mpp_atol = mpp_atol
+        self.power_rtol = power_rtol
+        self.power_atol = power_atol
         self.metadata: dict[Any, Any] = {}
 
     def set_dtype(self, dtype):
@@ -107,37 +133,106 @@ def set_device(self, device):
             raise ValueError(f"`device` must be `torch.device`, `str` or `None` but {type(device)} is given.")
 
     @abstractmethod
-    def get_size(self, wsi, level: int | None = None) -> tuple[int, int]:
+    def get_size(self, wsi, level: int) -> tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the size is calculated.
 
         """
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
 
+    def _find_closest_level(
+        self, name: str, value: tuple, value_at_levels: Sequence[tuple], atol: float, rtol: float
+    ) -> int:
+        """Find the level corresponding to the value of the quantity in the list of values at each level.
+        Args:
+            name: the name of the requested quantity
+            value: the value of requested quantity
+            value_at_levels: list of value of the quantity at each level
+            atol: the tolerance for the value
+            rtol: relative tolerance for the value
+        """
+        if value in value_at_levels:
+            return value_at_levels.index(value)
+
+        closest_value = min(value_at_levels, key=lambda a_value: sum([abs(x - y) for x, y in zip(a_value, value)]))  # type: ignore
+        for i in range(len(value)):
+            if abs(closest_value[i] - value[i]) > atol + rtol * abs(value[i]):
+                raise ValueError(
+                    f"The requested {name} < {value} > does not exist in this whole slide image "
+                    f"(with {name}_rtol={rtol} and {name}_atol={atol}). "
+                    f"Here is the list of available {name}: {value_at_levels}. "
+                    f"The closest matching available {name} is {closest_value}."
+                    f"Please consider changing the tolerances or use another {name}."
+                )
+        return value_at_levels.index(closest_value)
+
+    def get_valid_level(
+        self, wsi, level: int | None, mpp: float | tuple[float, float] | None, power: int | None
+    ) -> int:
+        """
+        Returns the level associated to the resolution parameters in the whole slide image.
+
+        Args:
+            wsi: a whole slide image object loaded from a file.
+            level: the level number.
+            mpp: the micron-per-pixel resolution.
+            power: the objective power.
+
+        """
+
+        # Try instance parameters if no resolution is provided
+        if mpp is None and power is None and level is None:
+            mpp = self.mpp
+            power = self.power
+            level = self.level
+
+        # Ensure that at most one resolution parameter is provided.
+        resolution = [val[0] for val in [("level", level), ("mpp", mpp), ("power", power)] if val[1] is not None]
+        if len(resolution) > 1:
+            raise ValueError(f"Only one of `level`, `mpp`, or `power` should be provided. {resolution} are provided.")
+
+        n_levels = self.get_level_count(wsi)
+
+        if mpp is not None:
+            mpp_ = ensure_tuple_rep(mpp, 2)
+            available_mpps = [self.get_mpp(wsi, level) for level in range(n_levels)]
+            level = self._find_closest_level("mpp", mpp_, available_mpps, self.mpp_atol, self.mpp_rtol)
+        elif power is not None:
+            power_ = ensure_tuple(power)
+            available_powers = [(self.get_power(wsi, level),) for level in range(n_levels)]
+            level = self._find_closest_level("power", power_, available_powers, self.power_atol, self.power_rtol)
+        else:
+            if level is None:
+                # Set the default value if no resolution parameter is provided.
+                level = 0
+            if level >= n_levels:
+                raise ValueError(f"The maximum level of this image is {n_levels-1} while level={level} is requested)!")
+
+        return level
+
     @abstractmethod
     def get_level_count(self, wsi) -> int:
         """
         Returns the number of levels in the whole slide image.
 
         Args:
-            wsi: a whole slide image object loaded from a file
+            wsi: a whole slide image object loaded from a file.
 
         """
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
 
     @abstractmethod
-    def get_downsample_ratio(self, wsi, level: int | None = None) -> float:
+    def get_downsample_ratio(self, wsi, level: int) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the downsample ratio is calculated.
 
         """
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
@@ -148,13 +243,25 @@ def get_file_path(self, wsi) -> str:
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
 
     @abstractmethod
-    def get_mpp(self, wsi, level: int | None = None) -> tuple[float, float]:
+    def get_mpp(self, wsi, level: int) -> tuple[float, float]:
         """
         Returns the micro-per-pixel resolution of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the mpp is calculated.
+
+        """
+        raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
+
+    @abstractmethod
+    def get_power(self, wsi, level: int) -> float:
+        """
+        Returns the objective power of the whole slide image at a given level.
+
+        Args:
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the objective power is calculated.
 
         """
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
@@ -171,9 +278,9 @@ def _get_patch(
             location: (top, left) tuple giving the top left pixel in the level 0 reference frame. Defaults to (0, 0).
             size: (height, width) tuple giving the patch size at the given level (`level`).
                 If None, it is set to the full image size at the given level.
-            level: the level number. Defaults to 0
-            dtype: the data type of output image
-            mode: the output image mode, 'RGB' or 'RGBA'
+            level: the level number.
+            dtype: the data type of output image.
+            mode: the output image mode, 'RGB' or 'RGBA'.
 
         """
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
@@ -185,12 +292,12 @@ def _get_metadata(
         Returns metadata of the extracted patch from the whole slide image.
 
         Args:
-            wsi: the whole slide image object, from which the patch is loaded
-            patch: extracted patch from whole slide image
+            wsi: the whole slide image object, from which the patch is loaded.
+            patch: extracted patch from whole slide image.
             location: (top, left) tuple giving the top left pixel in the level 0 reference frame. Defaults to (0, 0).
             size: (height, width) tuple giving the patch size at the given level (`level`).
                 If None, it is set to the full image size at the given level.
-            level: the level number. Defaults to 0
+            level: the level number.
 
         """
         if self.channel_dim >= len(patch.shape) or self.channel_dim < -len(patch.shape):
@@ -216,37 +323,44 @@ def get_data(
         location: tuple[int, int] = (0, 0),
         size: tuple[int, int] | None = None,
         level: int | None = None,
+        mpp: float | tuple[float, float] | None = None,
+        power: int | None = None,
         mode: str | None = None,
     ) -> tuple[np.ndarray, dict]:
         """
-        Verifies inputs, extracts patches from WSI image and generates metadata, and return them.
+        Verifies inputs, extracts patches from WSI image and generates metadata.
 
         Args:
-            wsi: a whole slide image object loaded from a file or a list of such objects
+            wsi: a whole slide image object loaded from a file or a list of such objects.
             location: (top, left) tuple giving the top left pixel in the level 0 reference frame. Defaults to (0, 0).
             size: (height, width) tuple giving the patch size at the given level (`level`).
                 If not provided or None, it is set to the full image size at the given level.
-            level: the level number. Defaults to 0
-            mode: the output image color mode, "RGB" or "RGBA". If not provided the default of "RGB" is used.
+            level: the whole slide image level at which the patches are extracted.
+            mpp: the resolution in micron per pixel at which the patches are extracted.
+            power: the objective power at which the patches are extracted.
+            dtype: the data type of output image.
+            mode: the output image mode, 'RGB' or 'RGBA'.
 
         Returns:
             a tuples, where the first element is an image patch [CxHxW] or stack of patches,
-                and second element is a dictionary of metadata
+                and second element is a dictionary of metadata.
+
+        Notes:
+            Only one of resolution parameters, `level`, `mpp`, or `power`, should be provided.
+            If none of them are provided, it uses the defaults that are set during class instantiation.
+            If none of them are set here or during class instantiation, `level=0` will be used.
         """
         if mode is None:
             mode = self.mode
         patch_list: list = []
         metadata_list: list = []
+
         # CuImage object is iterable, so ensure_tuple won't work on single object
-        if not isinstance(wsi, list):
-            wsi = [wsi]
+        if not isinstance(wsi, (list, tuple)):
+            wsi = (wsi,)
         for each_wsi in ensure_tuple(wsi):
-            # Verify magnification level
-            if level is None:
-                level = self.level
-            max_level = self.get_level_count(each_wsi) - 1
-            if level > max_level:
-                raise ValueError(f"The maximum level of this image is {max_level} while level={level} is requested)!")
+            # get the valid level based on resolution info
+            level = self.get_valid_level(each_wsi, level, mpp, power)
 
             # Verify location
             if location is None:
@@ -336,15 +450,26 @@ class WSIReader(BaseWSIReader):
 
     Args:
         backend: the name of backend whole slide image reader library, the default is cuCIM.
-        level: the level at which patches are extracted.
+        level: the whole slide image level at which the patches are extracted.
+        mpp: the resolution in micron per pixel at which the patches are extracted.
+        mpp_rtol: the acceptable relative tolerance for resolution in micro per pixel.
+        mpp_atol: the acceptable absolute tolerance for resolution in micro per pixel.
+        power: the objective power at which the patches are extracted.
+        power_rtol: the acceptable relative tolerance for objective power.
+        power_atol: the acceptable absolute tolerance for objective power.
         channel_dim: the desired dimension for color channel. Default to 0 (channel first).
         dtype: the data type of output image. Defaults to `np.uint8`.
-        mode: the output image color mode, "RGB" or "RGBA". Defaults to "RGB".
         device: target device to put the extracted patch. Note that if device is "cuda"",
             the output will be converted to torch tenor and sent to the gpu even if the dtype is numpy.
+        mode: the output image color mode, "RGB" or "RGBA". Defaults to "RGB".
         num_workers: number of workers for multi-thread image loading (cucim backend only).
         kwargs: additional arguments to be passed to the backend library
 
+        Notes:
+            Only one of resolution parameters, `level`, `mpp`, or `power`, should be provided.
+            If such parameters are provided in `get_data` method, those will override the values provided here.
+            If none of them are provided here or in `get_data`, `level=0` will be used.
+
     """
 
     supported_backends = ["cucim", "openslide", "tifffile"]
@@ -352,7 +477,13 @@ class WSIReader(BaseWSIReader):
     def __init__(
         self,
         backend="cucim",
-        level: int = 0,
+        level: int | None = None,
+        mpp: float | tuple[float, float] | None = None,
+        mpp_rtol: float = 0.05,
+        mpp_atol: float = 0.0,
+        power: int | None = None,
+        power_rtol: float = 0.05,
+        power_atol: float = 0.0,
         channel_dim: int = 0,
         dtype: DtypeLike | torch.dtype = np.uint8,
         device: torch.device | str | None = None,
@@ -363,15 +494,48 @@ def __init__(
         self.reader: CuCIMWSIReader | OpenSlideWSIReader | TiffFileWSIReader
         if self.backend == "cucim":
             self.reader = CuCIMWSIReader(
-                level=level, channel_dim=channel_dim, dtype=dtype, device=device, mode=mode, **kwargs
+                level=level,
+                mpp=mpp,
+                mpp_rtol=mpp_rtol,
+                mpp_atol=mpp_atol,
+                power=power,
+                power_rtol=power_rtol,
+                power_atol=power_atol,
+                channel_dim=channel_dim,
+                dtype=dtype,
+                device=device,
+                mode=mode,
+                **kwargs,
             )
         elif self.backend == "openslide":
             self.reader = OpenSlideWSIReader(
-                level=level, channel_dim=channel_dim, dtype=dtype, device=device, mode=mode, **kwargs
+                level=level,
+                mpp=mpp,
+                mpp_rtol=mpp_rtol,
+                mpp_atol=mpp_atol,
+                power=power,
+                power_rtol=power_rtol,
+                power_atol=power_atol,
+                channel_dim=channel_dim,
+                dtype=dtype,
+                device=device,
+                mode=mode,
+                **kwargs,
             )
         elif self.backend == "tifffile":
             self.reader = TiffFileWSIReader(
-                level=level, channel_dim=channel_dim, dtype=dtype, device=device, mode=mode, **kwargs
+                level=level,
+                mpp=mpp,
+                mpp_rtol=mpp_rtol,
+                mpp_atol=mpp_atol,
+                power=power,
+                power_rtol=power_rtol,
+                power_atol=power_atol,
+                channel_dim=channel_dim,
+                dtype=dtype,
+                device=device,
+                mode=mode,
+                **kwargs,
             )
         else:
             raise ValueError(
@@ -379,72 +543,77 @@ def __init__(
             )
         self.supported_suffixes = self.reader.supported_suffixes
         self.level = self.reader.level
+        self.mpp_rtol = self.reader.mpp_rtol
+        self.mpp_atol = self.reader.mpp_atol
+        self.power = self.reader.power
+        self.power_rtol = self.reader.power_rtol
+        self.power_atol = self.reader.power_atol
         self.channel_dim = self.reader.channel_dim
         self.dtype = self.reader.dtype
         self.device = self.reader.device
         self.mode = self.reader.mode
         self.kwargs = self.reader.kwargs
         self.metadata = self.reader.metadata
+        self.mpp = self.reader.mpp
 
     def get_level_count(self, wsi) -> int:
         """
         Returns the number of levels in the whole slide image.
 
         Args:
-            wsi: a whole slide image object loaded from a file
+            wsi: a whole slide image object loaded from a file.
 
         """
         return self.reader.get_level_count(wsi)
 
-    def get_size(self, wsi, level: int | None = None) -> tuple[int, int]:
+    def get_size(self, wsi, level: int) -> tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the size is calculated.
 
         """
-        if level is None:
-            level = self.level
-
         return self.reader.get_size(wsi, level)
 
-    def get_downsample_ratio(self, wsi, level: int | None = None) -> float:
+    def get_downsample_ratio(self, wsi, level: int) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the downsample ratio is calculated.
 
         """
-        if level is None:
-            level = self.level
-
         return self.reader.get_downsample_ratio(wsi, level)
 
     def get_file_path(self, wsi) -> str:
         """Return the file path for the WSI object"""
         return self.reader.get_file_path(wsi)
 
-    def get_mpp(self, wsi, level: int | None = None) -> tuple[float, float]:
+    def get_mpp(self, wsi, level: int) -> tuple[float, float]:
         """
         Returns the micro-per-pixel resolution of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the mpp is calculated.
 
         """
-        if level is None:
-            level = self.level
-
         return self.reader.get_mpp(wsi, level)
 
+    def get_power(self, wsi, level: int) -> float:
+        """
+        Returns the micro-per-pixel resolution of the whole slide image at a given level.
+
+        Args:
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the objective power is calculated.
+
+        """
+        return self.reader.get_power(wsi, level)
+
     def _get_patch(
         self, wsi, location: tuple[int, int], size: tuple[int, int], level: int, dtype: DtypeLike, mode: str
     ) -> np.ndarray:
@@ -452,13 +621,13 @@ def _get_patch(
         Extracts and returns a patch image form the whole slide image.
 
         Args:
-            wsi: a whole slide image object loaded from a file or a lis of such objects
+            wsi: a whole slide image object loaded from a file or a lis of such objects.
             location: (top, left) tuple giving the top left pixel in the level 0 reference frame. Defaults to (0, 0).
             size: (height, width) tuple giving the patch size at the given level (`level`).
                 If None, it is set to the full image size at the given level.
-            level: the level number. Defaults to 0
+            level: the level number.
             dtype: the data type of output image
-            mode: the output image mode, 'RGB' or 'RGBA'
+            mode: the output image mode, 'RGB' or 'RGBA'.
 
         """
         return self.reader._get_patch(wsi=wsi, location=location, size=size, level=level, dtype=dtype, mode=mode)
@@ -472,7 +641,7 @@ def read(self, data: Sequence[PathLike] | PathLike | np.ndarray, **kwargs):
             kwargs: additional args for the reader module (overrides `self.kwargs` for existing keys).
 
         Returns:
-            whole slide image object or list of such objects
+            whole slide image object or list of such objects.
 
         """
         return self.reader.read(data=data, **kwargs)
@@ -484,33 +653,34 @@ class CuCIMWSIReader(BaseWSIReader):
     Read whole slide images and extract patches using cuCIM library.
 
     Args:
-        level: the whole slide image level at which the image is extracted. (default=0)
-            This is overridden if the level argument is provided in `get_data`.
+        level: the whole slide image level at which the patches are extracted.
+        mpp: the resolution in micron per pixel at which the patches are extracted.
+        mpp_rtol: the acceptable relative tolerance for resolution in micro per pixel.
+        mpp_atol: the acceptable absolute tolerance for resolution in micro per pixel.
+        power: the objective power at which the patches are extracted.
+        power_rtol: the acceptable relative tolerance for objective power.
+        power_atol: the acceptable absolute tolerance for objective power.
         channel_dim: the desired dimension for color channel. Default to 0 (channel first).
         dtype: the data type of output image. Defaults to `np.uint8`.
         device: target device to put the extracted patch. Note that if device is "cuda"",
             the output will be converted to torch tenor and sent to the gpu even if the dtype is numpy.
         mode: the output image color mode, "RGB" or "RGBA". Defaults to "RGB".
-        num_workers: number of workers for multi-thread image loading
+        num_workers: number of workers for multi-thread image loading.
         kwargs: additional args for `cucim.CuImage` module:
             https://github.com/rapidsai/cucim/blob/main/cpp/include/cucim/cuimage.h
 
+        Notes:
+            Only one of resolution parameters, `level`, `mpp`, or `power`, should be provided.
+            If such parameters are provided in `get_data` method, those will override the values provided here.
+            If none of them are provided here or in `get_data`, `level=0` will be used.
+
     """
 
     supported_suffixes = ["tif", "tiff", "svs"]
     backend = "cucim"
 
-    def __init__(
-        self,
-        level: int = 0,
-        channel_dim: int = 0,
-        dtype: DtypeLike | torch.dtype = np.uint8,
-        device: torch.device | str | None = None,
-        mode: str = "RGB",
-        num_workers: int = 0,
-        **kwargs,
-    ):
-        super().__init__(level=level, channel_dim=channel_dim, dtype=dtype, device=device, mode=mode, **kwargs)
+    def __init__(self, num_workers: int = 0, **kwargs):
+        super().__init__(**kwargs)
         self.num_workers = num_workers
 
     @staticmethod
@@ -519,61 +689,80 @@ def get_level_count(wsi) -> int:
         Returns the number of levels in the whole slide image.
 
         Args:
-            wsi: a whole slide image object loaded from a file
+            wsi: a whole slide image object loaded from a file.
 
         """
         return wsi.resolutions["level_count"]  # type: ignore
 
-    def get_size(self, wsi, level: int | None = None) -> tuple[int, int]:
+    def get_size(self, wsi, level: int) -> tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the size is calculated.
 
         """
-        if level is None:
-            level = self.level
-
         return (wsi.resolutions["level_dimensions"][level][1], wsi.resolutions["level_dimensions"][level][0])
 
-    def get_downsample_ratio(self, wsi, level: int | None = None) -> float:
+    def get_downsample_ratio(self, wsi, level: int) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the downsample ratio is calculated.
 
         """
-        if level is None:
-            level = self.level
-
-        return wsi.resolutions["level_downsamples"][level]  # type: ignore
+        return float(wsi.resolutions["level_downsamples"][level])
 
     @staticmethod
     def get_file_path(wsi) -> str:
         """Return the file path for the WSI object"""
         return str(abspath(wsi.path))
 
-    def get_mpp(self, wsi, level: int | None = None) -> tuple[float, float]:
+    def get_mpp(self, wsi, level: int) -> tuple[float, float]:
         """
         Returns the micro-per-pixel resolution of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the mpp is calculated.
 
         """
-        if level is None:
-            level = self.level
+        downsample_ratio = self.get_downsample_ratio(wsi, level)
+
+        if "aperio" in wsi.metadata:
+            mpp_ = wsi.metadata["aperio"].get("MPP")
+            if mpp_:
+                return (downsample_ratio * float(mpp_),) * 2
+        if "cucim" in wsi.metadata:
+            mpp_ = wsi.metadata["cucim"].get("spacing")
+            if mpp_ and isinstance(mpp_, Sequence) and len(mpp_) >= 2:
+                if mpp_[0] and mpp_[1]:
+                    return (downsample_ratio * mpp_[1], downsample_ratio * mpp_[0])
+
+        raise ValueError("`mpp` cannot be obtained for this file. Please use `level` instead.")
+
+    def get_power(self, wsi, level: int) -> float:
+        """
+        Returns the objective power of the whole slide image at a given level.
 
-        factor = float(wsi.resolutions["level_downsamples"][level])
-        return (wsi.metadata["cucim"]["spacing"][1] * factor, wsi.metadata["cucim"]["spacing"][0] * factor)
+        Args:
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the objective power is calculated.
+
+        """
+        if "aperio" in wsi.metadata:
+            objective_power = wsi.metadata["aperio"].get("AppMag")
+            if objective_power:
+                downsample_ratio = self.get_downsample_ratio(wsi, level)
+                return float(objective_power) / downsample_ratio
+
+        raise ValueError(
+            "Currently, cuCIM backend can obtain the objective power only for Aperio images. "
+            "Please use `level` (or `mpp`) instead, or try OpenSlide backend."
+        )
 
     def read(self, data: Sequence[PathLike] | PathLike | np.ndarray, **kwargs):
         """
@@ -585,7 +774,7 @@ def read(self, data: Sequence[PathLike] | PathLike | np.ndarray, **kwargs):
                 For more details look at https://github.com/rapidsai/cucim/blob/main/cpp/include/cucim/cuimage.h
 
         Returns:
-            whole slide image object or list of such objects
+            whole slide image object or list of such objects.
 
         """
         cuimage_cls, _ = optional_import("cucim", name="CuImage")
@@ -607,13 +796,13 @@ def _get_patch(
         Extracts and returns a patch image form the whole slide image.
 
         Args:
-            wsi: a whole slide image object loaded from a file or a lis of such objects
+            wsi: a whole slide image object loaded from a file or a lis of such objects.
             location: (top, left) tuple giving the top left pixel in the level 0 reference frame. Defaults to (0, 0).
             size: (height, width) tuple giving the patch size at the given level (`level`).
                 If None, it is set to the full image size at the given level.
-            level: the level number. Defaults to 0
-            dtype: the data type of output image
-            mode: the output image mode, 'RGB' or 'RGBA'
+            level: the level number.
+            dtype: the data type of output image.
+            mode: the output image mode, 'RGB' or 'RGBA'.
 
         """
         # Extract a patch or the entire image
@@ -646,8 +835,13 @@ class OpenSlideWSIReader(BaseWSIReader):
     Read whole slide images and extract patches using OpenSlide library.
 
     Args:
-        level: the whole slide image level at which the image is extracted. (default=0)
-            This is overridden if the level argument is provided in `get_data`.
+        level: the whole slide image level at which the patches are extracted.
+        mpp: the resolution in micron per pixel at which the patches are extracted.
+        mpp_rtol: the acceptable relative tolerance for resolution in micro per pixel.
+        mpp_atol: the acceptable absolute tolerance for resolution in micro per pixel.
+        power: the objective power at which the patches are extracted.
+        power_rtol: the acceptable relative tolerance for objective power.
+        power_atol: the acceptable absolute tolerance for objective power.
         channel_dim: the desired dimension for color channel. Default to 0 (channel first).
         dtype: the data type of output image. Defaults to `np.uint8`.
         device: target device to put the extracted patch. Note that if device is "cuda"",
@@ -655,21 +849,18 @@ class OpenSlideWSIReader(BaseWSIReader):
         mode: the output image color mode, "RGB" or "RGBA". Defaults to "RGB".
         kwargs: additional args for `openslide.OpenSlide` module.
 
+        Notes:
+            Only one of resolution parameters, `level`, `mpp`, or `power`, should be provided.
+            If such parameters are provided in `get_data` method, those will override the values provided here.
+            If none of them are provided here or in `get_data`, `level=0` will be used.
+
     """
 
     supported_suffixes = ["tif", "tiff", "svs"]
     backend = "openslide"
 
-    def __init__(
-        self,
-        level: int = 0,
-        channel_dim: int = 0,
-        dtype: DtypeLike | torch.dtype = np.uint8,
-        device: torch.device | str | None = None,
-        mode: str = "RGB",
-        **kwargs,
-    ):
-        super().__init__(level=level, channel_dim=channel_dim, dtype=dtype, device=device, mode=mode, **kwargs)
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
 
     @staticmethod
     def get_level_count(wsi) -> int:
@@ -677,39 +868,31 @@ def get_level_count(wsi) -> int:
         Returns the number of levels in the whole slide image.
 
         Args:
-            wsi: a whole slide image object loaded from a file
+            wsi: a whole slide image object loaded from a file.
 
         """
         return wsi.level_count  # type: ignore
 
-    def get_size(self, wsi, level: int | None = None) -> tuple[int, int]:
+    def get_size(self, wsi, level: int) -> tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the size is calculated.
 
         """
-        if level is None:
-            level = self.level
-
         return (wsi.level_dimensions[level][1], wsi.level_dimensions[level][0])
 
-    def get_downsample_ratio(self, wsi, level: int | None = None) -> float:
+    def get_downsample_ratio(self, wsi, level: int) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the downsample ratio is calculated.
 
         """
-        if level is None:
-            level = self.level
-
         return wsi.level_downsamples[level]  # type: ignore
 
     @staticmethod
@@ -717,32 +900,61 @@ def get_file_path(wsi) -> str:
         """Return the file path for the WSI object"""
         return str(abspath(wsi._filename))
 
-    def get_mpp(self, wsi, level: int | None = None) -> tuple[float, float]:
+    def get_mpp(self, wsi, level: int) -> tuple[float, float]:
         """
         Returns the micro-per-pixel resolution of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the mpp is calculated.
+
+        """
+        downsample_ratio = self.get_downsample_ratio(wsi, level)
+        if (
+            "openslide.mpp-x" in wsi.properties
+            and "openslide.mpp-y" in wsi.properties
+            and wsi.properties["openslide.mpp-y"]
+            and wsi.properties["openslide.mpp-x"]
+        ):
+            return (
+                downsample_ratio * float(wsi.properties["openslide.mpp-y"]),
+                downsample_ratio * float(wsi.properties["openslide.mpp-x"]),
+            )
+
+        if (
+            "tiff.XResolution" in wsi.properties
+            and "tiff.YResolution" in wsi.properties
+            and wsi.properties["tiff.YResolution"]
+            and wsi.properties["tiff.XResolution"]
+        ):
+            unit = wsi.properties.get("tiff.ResolutionUnit")
+            if unit is None:
+                warnings.warn("The resolution unit is missing, `micrometer` will be used as default.")
+                unit = "micrometer"
+
+            convert_to_micron = ConvertUnits(unit, "micrometer")
+            return (
+                convert_to_micron(downsample_ratio / float(wsi.properties["tiff.YResolution"])),
+                convert_to_micron(downsample_ratio / float(wsi.properties["tiff.XResolution"])),
+            )
 
+        raise ValueError("`mpp` cannot be obtained for this file. Please use `level` instead.")
+
+    def get_power(self, wsi, level: int) -> float:
         """
-        if level is None:
-            level = self.level
-        unit = wsi.properties["tiff.ResolutionUnit"]
-        if unit == "centimeter":
-            factor = 10000.0
-        elif unit == "millimeter":
-            factor = 1000.0
-        elif unit == "micrometer":
-            factor = 1.0
-        elif unit == "inch":
-            factor = 25400.0
-        else:
-            raise ValueError(f"The resolution unit is not a valid tiff resolution: {unit}")
+        Returns the objective power of the whole slide image at a given level.
+
+        Args:
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the objective power is calculated.
+
+        """
+        objective_power = wsi.properties.get("openslide.objective-power")
+        if objective_power:
+            downsample_ratio = self.get_downsample_ratio(wsi, level)
+            return float(objective_power) / downsample_ratio
 
-        factor *= wsi.level_downsamples[level]
-        return (factor / float(wsi.properties["tiff.YResolution"]), factor / float(wsi.properties["tiff.XResolution"]))
+        raise ValueError("Objective `power` cannot be obtained for this file. Please use `level` (or `mpp`) instead.")
 
     def read(self, data: Sequence[PathLike] | PathLike | np.ndarray, **kwargs):
         """
@@ -753,7 +965,7 @@ def read(self, data: Sequence[PathLike] | PathLike | np.ndarray, **kwargs):
             kwargs: additional args that overrides `self.kwargs` for existing keys.
 
         Returns:
-            whole slide image object or list of such objects
+            whole slide image object or list of such objects.
 
         """
         wsi_list: list = []
@@ -778,9 +990,9 @@ def _get_patch(
             location: (top, left) tuple giving the top left pixel in the level 0 reference frame. Defaults to (0, 0).
             size: (height, width) tuple giving the patch size at the given level (`level`).
                 If None, it is set to the full image size at the given level.
-            level: the level number. Defaults to 0
-            dtype: the data type of output image
-            mode: the output image mode, 'RGB' or 'RGBA'
+            level: the level number.
+            dtype: the data type of output image.
+            mode: the output image mode, 'RGB' or 'RGBA'.
 
         """
         # Extract a patch or the entire image
@@ -805,8 +1017,10 @@ class TiffFileWSIReader(BaseWSIReader):
     Read whole slide images and extract patches using TiffFile library.
 
     Args:
-        level: the whole slide image level at which the image is extracted. (default=0)
-            This is overridden if the level argument is provided in `get_data`.
+        level: the whole slide image level at which the patches are extracted.
+        mpp: the resolution in micron per pixel at which the patches are extracted.
+        mpp_rtol: the acceptable relative tolerance for resolution in micro per pixel.
+        mpp_atol: the acceptable absolute tolerance for resolution in micro per pixel.
         channel_dim: the desired dimension for color channel. Default to 0 (channel first).
         dtype: the data type of output image. Defaults to `np.uint8`.
         device: target device to put the extracted patch. Note that if device is "cuda"",
@@ -814,21 +1028,19 @@ class TiffFileWSIReader(BaseWSIReader):
         mode: the output image color mode, "RGB" or "RGBA". Defaults to "RGB".
         kwargs: additional args for `tifffile.TiffFile` module.
 
+        Notes:
+            - Objective power cannot be obtained via TiffFile backend.
+            - Only one of resolution parameters, `level` or `mpp`, should be provided.
+                If such parameters are provided in `get_data` method, those will override the values provided here.
+                If none of them are provided here or in `get_data`, `level=0` will be used.
+
     """
 
     supported_suffixes = ["tif", "tiff", "svs"]
     backend = "tifffile"
 
-    def __init__(
-        self,
-        level: int = 0,
-        channel_dim: int = 0,
-        dtype: DtypeLike | torch.dtype = np.uint8,
-        device: torch.device | str | None = None,
-        mode: str = "RGB",
-        **kwargs,
-    ):
-        super().__init__(level=level, channel_dim=channel_dim, dtype=dtype, device=device, mode=mode, **kwargs)
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
 
     @staticmethod
     def get_level_count(wsi) -> int:
@@ -836,39 +1048,31 @@ def get_level_count(wsi) -> int:
         Returns the number of levels in the whole slide image.
 
         Args:
-            wsi: a whole slide image object loaded from a file
+            wsi: a whole slide image object loaded from a file.
 
         """
         return len(wsi.pages)
 
-    def get_size(self, wsi, level: int | None = None) -> tuple[int, int]:
+    def get_size(self, wsi, level: int) -> tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the size is calculated.
 
         """
-        if level is None:
-            level = self.level
-
         return (wsi.pages[level].imagelength, wsi.pages[level].imagewidth)
 
-    def get_downsample_ratio(self, wsi, level: int | None = None) -> float:
+    def get_downsample_ratio(self, wsi, level: int) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the downsample ratio is calculated.
 
         """
-        if level is None:
-            level = self.level
-
         return float(wsi.pages[0].imagelength) / float(wsi.pages[level].imagelength)
 
     @staticmethod
@@ -876,35 +1080,49 @@ def get_file_path(wsi) -> str:
         """Return the file path for the WSI object"""
         return str(abspath(wsi.filehandle.path))
 
-    def get_mpp(self, wsi, level: int | None = None) -> tuple[float, float]:
+    def get_mpp(self, wsi, level: int) -> tuple[float, float]:
         """
         Returns the micro-per-pixel resolution of the whole slide image at a given level.
 
         Args:
-            wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
-                will be used.
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the mpp is calculated.
+
+        """
+        if (
+            "XResolution" in wsi.pages[level].tags
+            and "YResolution" in wsi.pages[level].tags
+            and wsi.pages[level].tags["XResolution"].value
+            and wsi.pages[level].tags["YResolution"].value
+        ):
+            unit = wsi.pages[level].tags.get("ResolutionUnit")
+            if unit is not None:
+                unit = str(unit.value)[8:]
+            else:
+                warnings.warn("The resolution unit is missing. `micrometer` will be used as default.")
+                unit = "micrometer"
+
+            convert_to_micron = ConvertUnits(unit, "micrometer")
+            # Here x and y resolutions are rational numbers so each of them is represented by a tuple.
+            yres = wsi.pages[level].tags["YResolution"].value
+            xres = wsi.pages[level].tags["XResolution"].value
+            return convert_to_micron(yres[1] / yres[0]), convert_to_micron(xres[1] / xres[0])
 
+        raise ValueError("`mpp`  cannot be obtained for this file. Please use `level` instead.")
+
+    def get_power(self, wsi, level: int) -> float:
         """
-        if level is None:
-            level = self.level
+        Returns the objective power of the whole slide image at a given level.
 
-        unit = wsi.pages[level].tags["ResolutionUnit"].value
-        if unit == unit.CENTIMETER:
-            factor = 10000.0
-        elif unit == unit.MILLIMETER:
-            factor = 1000.0
-        elif unit == unit.MICROMETER:
-            factor = 1.0
-        elif unit == unit.INCH:
-            factor = 25400.0
-        else:
-            raise ValueError(f"The resolution unit is not a valid tiff resolution or missing: {unit.name}")
+        Args:
+            wsi: a whole slide image object loaded from a file.
+            level: the level number where the objective power is calculated.
 
-        # Here x and y resolutions are rational numbers so each of them is represented by a tuple.
-        yres = wsi.pages[level].tags["YResolution"].value
-        xres = wsi.pages[level].tags["XResolution"].value
-        return (factor * yres[1] / yres[0], factor * xres[1] / xres[0])
+        """
+        raise ValueError(
+            "Currently, TiffFile does not provide a general API to obtain objective power."
+            "Please use `level` (or `mpp`) instead, or try other backends."
+        )
 
     def read(self, data: Sequence[PathLike] | PathLike | np.ndarray, **kwargs):
         """
@@ -915,7 +1133,7 @@ def read(self, data: Sequence[PathLike] | PathLike | np.ndarray, **kwargs):
             kwargs: additional args that overrides `self.kwargs` for existing keys.
 
         Returns:
-            whole slide image object or list of such objects
+            whole slide image object or list of such objects.
 
         """
         wsi_list: list = []
@@ -940,9 +1158,9 @@ def _get_patch(
             location: (top, left) tuple giving the top left pixel in the level 0 reference frame. Defaults to (0, 0).
             size: (height, width) tuple giving the patch size at the given level (`level`).
                 If None, it is set to the full image size at the given level.
-            level: the level number. Defaults to 0
-            dtype: the data type of output image
-            mode: the output image mode, 'RGB' or 'RGBA'
+            level: the level number.
+            dtype: the data type of output image.
+            mode: the output image mode, 'RGB' or 'RGBA'.
 
         """
         # Load the entire image
diff --git a/monai/utils/misc.py b/monai/utils/misc.py
--- a/monai/utils/misc.py
+++ b/monai/utils/misc.py
@@ -23,6 +23,7 @@
 from ast import literal_eval
 from collections.abc import Callable, Iterable, Sequence
 from distutils.util import strtobool
+from math import log10
 from pathlib import Path
 from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
 
@@ -68,6 +69,9 @@
     "label_union",
     "path_to_uri",
     "pprint_edges",
+    "check_key_duplicates",
+    "CheckKeyDuplicatesYamlLoader",
+    "ConvertUnits",
 ]
 
 _seed = None
@@ -723,3 +727,76 @@ def construct_mapping(self, node, deep=False):
                     warnings.warn(f"Duplicate key: `{key}`")
             mapping.add(key)
         return super().construct_mapping(node, deep)
+
+
+class ConvertUnits:
+    """
+    Convert the values from input unit to the target unit
+
+    Args:
+        input_unit: the unit of the input quantity
+        target_unit: the unit of the target quantity
+
+    """
+
+    imperial_unit_of_length = {"inch": 0.0254, "foot": 0.3048, "yard": 0.9144, "mile": 1609.344}
+
+    unit_prefix = {
+        "peta": 15,
+        "tera": 12,
+        "giga": 9,
+        "mega": 6,
+        "kilo": 3,
+        "hecto": 2,
+        "deca": 1,
+        "deci": -1,
+        "centi": -2,
+        "milli": -3,
+        "micro": -6,
+        "nano": -9,
+        "pico": -12,
+        "femto": -15,
+    }
+    base_units = ["meter", "byte", "bit"]
+
+    def __init__(self, input_unit: str, target_unit: str) -> None:
+        self.input_unit, input_base = self._get_valid_unit_and_base(input_unit)
+        self.target_unit, target_base = self._get_valid_unit_and_base(target_unit)
+        if input_base == target_base:
+            self.unit_base = input_base
+        else:
+            raise ValueError(
+                "Both input and target units should be from the same quantity. "
+                f"Input quantity is {input_base} while target quantity is {target_base}"
+            )
+        self._calculate_conversion_factor()
+
+    def _get_valid_unit_and_base(self, unit):
+        unit = str(unit).lower()
+        if unit in self.imperial_unit_of_length:
+            return unit, "meter"
+        for base_unit in self.base_units:
+            if unit.endswith(base_unit):
+                return unit, base_unit
+        raise ValueError(f"Currently, it only supports length conversion but `{unit}` is given.")
+
+    def _get_unit_power(self, unit):
+        """Calculate the power of the unit factor with respect to the base unit"""
+        if unit in self.imperial_unit_of_length:
+            return log10(self.imperial_unit_of_length[unit])
+
+        prefix = unit[: len(self.unit_base)]
+        if prefix == "":
+            return 1.0
+        return self.unit_prefix[prefix]
+
+    def _calculate_conversion_factor(self):
+        """Calculate unit conversion factor with respect to the input unit"""
+        if self.input_unit == self.target_unit:
+            return 1.0
+        input_power = self._get_unit_power(self.input_unit)
+        target_power = self._get_unit_power(self.target_unit)
+        self.conversion_factor = 10 ** (input_power - target_power)
+
+    def __call__(self, value: int | float) -> Any:
+        return float(value) * self.conversion_factor


Test patch:
diff --git a/tests/test_masked_patch_wsi_dataset.py b/tests/test_masked_patch_wsi_dataset.py
--- a/tests/test_masked_patch_wsi_dataset.py
+++ b/tests/test_masked_patch_wsi_dataset.py
@@ -32,10 +32,9 @@
 _, has_codec = optional_import("imagecodecs")
 has_tiff = has_tiff and has_codec
 
-FILE_KEY = "wsi_img"
+FILE_KEY = "wsi_generic_tiff"
 FILE_URL = testing_data_config("images", FILE_KEY, "url")
-base_name, extension = os.path.basename(f"{FILE_URL}"), ".tiff"
-FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", "temp_" + base_name + extension)
+FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", f"temp_{FILE_KEY}.tiff")
 
 TEST_CASE_0 = [
     {"data": [{"image": FILE_PATH, WSIPatchKeys.LEVEL: 8, WSIPatchKeys.SIZE: (2, 2)}], "mask_level": 8},
diff --git a/tests/test_patch_wsi_dataset.py b/tests/test_patch_wsi_dataset.py
--- a/tests/test_patch_wsi_dataset.py
+++ b/tests/test_patch_wsi_dataset.py
@@ -32,10 +32,9 @@
 _, has_codec = optional_import("imagecodecs")
 has_tiff = has_tiff and has_codec
 
-FILE_KEY = "wsi_img"
+FILE_KEY = "wsi_generic_tiff"
 FILE_URL = testing_data_config("images", FILE_KEY, "url")
-base_name, extension = os.path.basename(f"{FILE_URL}"), ".tiff"
-FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", "temp_" + base_name + extension)
+FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", f"temp_{FILE_KEY}.tiff")
 
 TEST_CASE_0 = [
     {
diff --git a/tests/test_sliding_patch_wsi_dataset.py b/tests/test_sliding_patch_wsi_dataset.py
--- a/tests/test_sliding_patch_wsi_dataset.py
+++ b/tests/test_sliding_patch_wsi_dataset.py
@@ -32,10 +32,9 @@
 _, has_codec = optional_import("imagecodecs")
 has_tiff = has_tiff and has_codec
 
-FILE_KEY = "wsi_img"
+FILE_KEY = "wsi_generic_tiff"
 FILE_URL = testing_data_config("images", FILE_KEY, "url")
-base_name, extension = os.path.basename(f"{FILE_URL}"), ".tiff"
-FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", "temp_" + base_name + extension)
+FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", f"temp_{FILE_KEY}.tiff")
 
 FILE_PATH_SMALL_0 = os.path.join(os.path.dirname(__file__), "testing_data", "temp_wsi_inference_0.tiff")
 FILE_PATH_SMALL_1 = os.path.join(os.path.dirname(__file__), "testing_data", "temp_wsi_inference_1.tiff")
diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py
--- a/tests/test_wsireader.py
+++ b/tests/test_wsireader.py
@@ -35,95 +35,208 @@
 _, has_codec = optional_import("imagecodecs")
 has_tiff = has_tiff and has_codec
 
-FILE_KEY = "wsi_img"
-FILE_URL = testing_data_config("images", FILE_KEY, "url")
-base_name, extension = os.path.basename(f"{FILE_URL}"), ".tiff"
-FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", "temp_" + base_name + extension)
+WSI_GENERIC_TIFF_KEY = "wsi_generic_tiff"
+WSI_GENERIC_TIFF_PATH = os.path.join(os.path.dirname(__file__), "testing_data", f"temp_{WSI_GENERIC_TIFF_KEY}.tiff")
 
-HEIGHT = 32914
-WIDTH = 46000
+WSI_APERIO_SVS_KEY = "wsi_aperio_svs"
+WSI_APERIO_SVS_PATH = os.path.join(os.path.dirname(__file__), "testing_data", f"temp_{WSI_APERIO_SVS_KEY}.svs")
 
-TEST_CASE_WHOLE_0 = [FILE_PATH, 2, (3, HEIGHT // 4, WIDTH // 4)]
+WSI_GENERIC_TIFF_HEIGHT = 32914
+WSI_GENERIC_TIFF_WIDTH = 46000
 
-TEST_CASE_TRANSFORM_0 = [FILE_PATH, 4, (HEIGHT // 16, WIDTH // 16), (1, 3, HEIGHT // 16, WIDTH // 16)]
+TEST_CASE_WHOLE_0 = [WSI_GENERIC_TIFF_PATH, 2, (3, WSI_GENERIC_TIFF_HEIGHT // 4, WSI_GENERIC_TIFF_WIDTH // 4)]
+
+TEST_CASE_TRANSFORM_0 = [
+    WSI_GENERIC_TIFF_PATH,
+    4,
+    (WSI_GENERIC_TIFF_HEIGHT // 16, WSI_GENERIC_TIFF_WIDTH // 16),
+    (1, 3, WSI_GENERIC_TIFF_HEIGHT // 16, WSI_GENERIC_TIFF_WIDTH // 16),
+]
 
 # ----------------------------------------------------------------------------
 # Test cases for reading patches
 # ----------------------------------------------------------------------------
 
 TEST_CASE_0 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": None},
     {"location": (0, 0), "size": (2, 1)},
     np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.float64),
 ]
 
 TEST_CASE_1 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {},
-    {"location": (HEIGHT // 2, WIDTH // 2), "size": (2, 1), "level": 0},
+    {"location": (WSI_GENERIC_TIFF_HEIGHT // 2, WSI_GENERIC_TIFF_WIDTH // 2), "size": (2, 1), "level": 0},
     np.array([[[246], [246]], [[246], [246]], [[246], [246]]], dtype=np.uint8),
 ]
 
 TEST_CASE_2 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {},
     {"location": (0, 0), "size": (2, 1), "level": 8},
     np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.uint8),
 ]
 
 TEST_CASE_3 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"channel_dim": -1},
-    {"location": (HEIGHT // 2, WIDTH // 2), "size": (2, 1), "level": 0},
+    {"location": (WSI_GENERIC_TIFF_HEIGHT // 2, WSI_GENERIC_TIFF_WIDTH // 2), "size": (2, 1), "level": 0},
     np.moveaxis(np.array([[[246], [246]], [[246], [246]], [[246], [246]]], dtype=np.uint8), 0, -1),
 ]
 
 TEST_CASE_4 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"channel_dim": 2},
     {"location": (0, 0), "size": (2, 1), "level": 8},
     np.moveaxis(np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.uint8), 0, -1),
 ]
 
 TEST_CASE_5 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8},
     {"location": (0, 0), "size": (2, 1)},
     np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.uint8),
 ]
 
 TEST_CASE_6 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": np.int32},
     {"location": (0, 0), "size": (2, 1)},
     np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.int32),
 ]
 
 TEST_CASE_7 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": np.float32},
     {"location": (0, 0), "size": (2, 1)},
     np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.float32),
 ]
 
 TEST_CASE_8 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": torch.uint8},
     {"location": (0, 0), "size": (2, 1)},
     torch.tensor([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=torch.uint8),
 ]
 
 TEST_CASE_9 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": torch.float32},
     {"location": (0, 0), "size": (2, 1)},
     torch.tensor([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=torch.float32),
 ]
 
+# exact mpp in get_data
+TEST_CASE_10_MPP = [
+    WSI_GENERIC_TIFF_PATH,
+    {"mpp_atol": 0.0, "mpp_rtol": 0.0},
+    {"location": (WSI_GENERIC_TIFF_HEIGHT // 2, WSI_GENERIC_TIFF_WIDTH // 2), "size": (2, 1), "mpp": 1000},
+    np.array([[[246], [246]], [[246], [246]], [[246], [246]]], dtype=np.uint8),
+    {"level": 0},
+]
+
+# exact mpp as default
+TEST_CASE_11_MPP = [
+    WSI_GENERIC_TIFF_PATH,
+    {"mpp_atol": 0.0, "mpp_rtol": 0.0, "mpp": 1000},
+    {"location": (WSI_GENERIC_TIFF_HEIGHT // 2, WSI_GENERIC_TIFF_WIDTH // 2), "size": (2, 1)},
+    np.array([[[246], [246]], [[246], [246]], [[246], [246]]], dtype=np.uint8),
+    {"level": 0},
+]
+
+# exact mpp as default (Aperio SVS)
+TEST_CASE_12_MPP = [
+    WSI_APERIO_SVS_PATH,
+    {"mpp_atol": 0.0, "mpp_rtol": 0.0, "mpp": 0.499},
+    {"location": (0, 0), "size": (2, 1)},
+    np.array([[[239], [239]], [[239], [239]], [[239], [239]]], dtype=np.uint8),
+    {"level": 0},
+]
+# acceptable mpp within default tolerances
+TEST_CASE_13_MPP = [
+    WSI_GENERIC_TIFF_PATH,
+    {},
+    {"location": (0, 0), "size": (2, 1), "mpp": 256000},
+    np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.uint8),
+    {"level": 8},
+]
+
+# acceptable mpp within default tolerances (Aperio SVS)
+TEST_CASE_14_MPP = [
+    WSI_APERIO_SVS_PATH,
+    {"mpp": 8.0},
+    {"location": (0, 0), "size": (2, 1)},
+    np.array([[[238], [240]], [[239], [241]], [[240], [241]]], dtype=np.uint8),
+    {"level": 2},
+]
+
+# acceptable mpp within absolute tolerance (Aperio SVS)
+TEST_CASE_15_MPP = [
+    WSI_APERIO_SVS_PATH,
+    {"mpp": 7.0, "mpp_atol": 1.0, "mpp_rtol": 0.0},
+    {"location": (0, 0), "size": (2, 1)},
+    np.array([[[238], [240]], [[239], [241]], [[240], [241]]], dtype=np.uint8),
+    {"level": 2},
+]
+
+# acceptable mpp within relative tolerance (Aperio SVS)
+TEST_CASE_16_MPP = [
+    WSI_APERIO_SVS_PATH,
+    {"mpp": 7.8, "mpp_atol": 0.0, "mpp_rtol": 0.1},
+    {"location": (0, 0), "size": (2, 1)},
+    np.array([[[238], [240]], [[239], [241]], [[240], [241]]], dtype=np.uint8),
+    {"level": 2},
+]
+
+
+# exact power
+TEST_CASE_17_POWER = [
+    WSI_APERIO_SVS_PATH,
+    {"power_atol": 0.0, "power_rtol": 0.0},
+    {"location": (0, 0), "size": (2, 1), "power": 20},
+    np.array([[[239], [239]], [[239], [239]], [[239], [239]]], dtype=np.uint8),
+    {"level": 0},
+]
+
+# exact power
+TEST_CASE_18_POWER = [
+    WSI_APERIO_SVS_PATH,
+    {"power": 20, "power_atol": 0.0, "power_rtol": 0.0},
+    {"location": (0, 0), "size": (2, 1)},
+    np.array([[[239], [239]], [[239], [239]], [[239], [239]]], dtype=np.uint8),
+    {"level": 0},
+]
+
+# acceptable power within default tolerances (Aperio SVS)
+TEST_CASE_19_POWER = [
+    WSI_APERIO_SVS_PATH,
+    {},
+    {"location": (0, 0), "size": (2, 1), "power": 1.25},
+    np.array([[[238], [240]], [[239], [241]], [[240], [241]]], dtype=np.uint8),
+    {"level": 2},
+]
+
+# acceptable power within absolute tolerance (Aperio SVS)
+TEST_CASE_20_POWER = [
+    WSI_APERIO_SVS_PATH,
+    {"power_atol": 0.3, "power_rtol": 0.0},
+    {"location": (0, 0), "size": (2, 1), "power": 1.0},
+    np.array([[[238], [240]], [[239], [241]], [[240], [241]]], dtype=np.uint8),
+    {"level": 2},
+]
+
+# acceptable power within relative tolerance (Aperio SVS)
+TEST_CASE_21_POWER = [
+    WSI_APERIO_SVS_PATH,
+    {"power_atol": 0.0, "power_rtol": 0.3},
+    {"location": (0, 0), "size": (2, 1), "power": 1.0},
+    np.array([[[238], [240]], [[239], [241]], [[240], [241]]], dtype=np.uint8),
+    {"level": 2},
+]
 # device tests
 TEST_CASE_DEVICE_1 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": torch.float32, "device": "cpu"},
     {"location": (0, 0), "size": (2, 1)},
     torch.tensor([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=torch.float32),
@@ -131,7 +244,7 @@
 ]
 
 TEST_CASE_DEVICE_2 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": torch.float32, "device": "cuda"},
     {"location": (0, 0), "size": (2, 1)},
     torch.tensor([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=torch.float32),
@@ -139,7 +252,7 @@
 ]
 
 TEST_CASE_DEVICE_3 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": np.float32, "device": "cpu"},
     {"location": (0, 0), "size": (2, 1)},
     np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.float32),
@@ -147,7 +260,7 @@
 ]
 
 TEST_CASE_DEVICE_4 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "dtype": np.float32, "device": "cuda"},
     {"location": (0, 0), "size": (2, 1)},
     torch.tensor([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=torch.float32),
@@ -155,7 +268,7 @@
 ]
 
 TEST_CASE_DEVICE_5 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "device": "cuda"},
     {"location": (0, 0), "size": (2, 1)},
     torch.tensor([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=torch.uint8),
@@ -163,7 +276,7 @@
 ]
 
 TEST_CASE_DEVICE_6 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8},
     {"location": (0, 0), "size": (2, 1)},
     np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.uint8),
@@ -171,7 +284,7 @@
 ]
 
 TEST_CASE_DEVICE_7 = [
-    FILE_PATH,
+    WSI_GENERIC_TIFF_PATH,
     {"level": 8, "device": None},
     {"location": (0, 0), "size": (2, 1)},
     np.array([[[242], [242]], [[242], [242]], [[242], [242]]], dtype=np.uint8),
@@ -179,7 +292,7 @@
 ]
 
 TEST_CASE_MULTI_WSI = [
-    [FILE_PATH, FILE_PATH],
+    [WSI_GENERIC_TIFF_PATH, WSI_GENERIC_TIFF_PATH],
     {"location": (0, 0), "size": (2, 1), "level": 8},
     np.concatenate(
         [
@@ -199,7 +312,34 @@
 TEST_CASE_ERROR_2C = [np.ones((16, 16, 2), dtype=np.uint8)]  # two color channels
 TEST_CASE_ERROR_3D = [np.ones((16, 16, 16, 3), dtype=np.uint8)]  # 3D + color
 
-TEST_CASE_MPP_0 = [FILE_PATH, 0, (1000.0, 1000.0)]
+# mpp not within default
+TEST_CASE_ERROR_0_MPP = [
+    WSI_GENERIC_TIFF_PATH,
+    {},
+    {"location": (WSI_GENERIC_TIFF_HEIGHT // 2, WSI_GENERIC_TIFF_WIDTH // 2), "size": (2, 1), "mpp": 1200},
+    ValueError,
+]
+
+# mpp is not exact (no tolerance)
+TEST_CASE_ERROR_1_MPP = [
+    WSI_APERIO_SVS_PATH,
+    {"mpp_atol": 0.0, "mpp_rtol": 0.0},
+    {"location": (0, 0), "size": (2, 1), "mpp": 8.0},
+    ValueError,
+]
+
+# power not within default
+TEST_CASE_ERROR_2_POWER = [WSI_APERIO_SVS_PATH, {}, {"location": (0, 0), "size": (2, 1), "power": 40}, ValueError]
+
+# power is not exact (no tolerance)
+TEST_CASE_ERROR_3_POWER = [
+    WSI_APERIO_SVS_PATH,
+    {"power_atol": 0.0, "power_rtol": 0.0},
+    {"location": (0, 0), "size": (2, 1), "power": 1.25},
+    ValueError,
+]
+
+TEST_CASE_MPP_0 = [WSI_GENERIC_TIFF_PATH, 0, (1000.0, 1000.0)]
 
 
 def save_rgba_tiff(array: np.ndarray, filename: str, mode: str):
@@ -236,9 +376,18 @@ def save_gray_tiff(array: np.ndarray, filename: str):
 
 @skipUnless(has_cucim or has_osl or has_tiff, "Requires cucim, openslide, or tifffile!")
 def setUpModule():
-    hash_type = testing_data_config("images", FILE_KEY, "hash_type")
-    hash_val = testing_data_config("images", FILE_KEY, "hash_val")
-    download_url_or_skip_test(FILE_URL, FILE_PATH, hash_type=hash_type, hash_val=hash_val)
+    download_url_or_skip_test(
+        testing_data_config("images", WSI_GENERIC_TIFF_KEY, "url"),
+        WSI_GENERIC_TIFF_PATH,
+        hash_type=testing_data_config("images", WSI_GENERIC_TIFF_KEY, "hash_type"),
+        hash_val=testing_data_config("images", WSI_GENERIC_TIFF_KEY, "hash_val"),
+    )
+    download_url_or_skip_test(
+        testing_data_config("images", WSI_APERIO_SVS_KEY, "url"),
+        WSI_APERIO_SVS_PATH,
+        hash_type=testing_data_config("images", WSI_APERIO_SVS_KEY, "hash_type"),
+        hash_val=testing_data_config("images", WSI_APERIO_SVS_KEY, "hash_val"),
+    )
 
 
 class WSIReaderTests:
@@ -269,13 +418,28 @@ def test_read_whole_image(self, file_path, level, expected_shape):
                 TEST_CASE_7,
                 TEST_CASE_8,
                 TEST_CASE_9,
+                TEST_CASE_10_MPP,
+                TEST_CASE_11_MPP,
+                TEST_CASE_12_MPP,
+                TEST_CASE_13_MPP,
+                TEST_CASE_14_MPP,
+                TEST_CASE_15_MPP,
+                TEST_CASE_16_MPP,
+                TEST_CASE_17_POWER,
+                TEST_CASE_18_POWER,
+                TEST_CASE_19_POWER,
+                TEST_CASE_20_POWER,
+                TEST_CASE_21_POWER,
             ]
         )
-        def test_read_region(self, file_path, kwargs, patch_info, expected_img):
-            reader = WSIReader(self.backend, **kwargs)
-            level = patch_info.get("level", kwargs.get("level"))
-            if self.backend == "tifffile" and level < 2:
+        def test_read_region(self, file_path, reader_kwargs, patch_info, expected_img, *args):
+            reader = WSIReader(self.backend, **reader_kwargs)
+            level = patch_info.get("level", reader_kwargs.get("level"))
+            # Skip mpp, power tests for TiffFile backend
+            if self.backend == "tifffile" and (level is None or level < 2 or file_path == WSI_APERIO_SVS_PATH):
                 return
+            if level is None:
+                level = args[0].get("level")
             with reader.read(file_path) as img_obj:
                 # Read twice to check multiple calls
                 img, meta = reader.get_data(img_obj, **patch_info)
@@ -453,6 +617,15 @@ def test_read_region_device(self, file_path, kwargs, patch_info, expected_img, d
             assert_allclose(meta[WSIPatchKeys.SIZE], patch_info["size"], type_test=False)
             assert_allclose(meta[WSIPatchKeys.LOCATION], patch_info["location"], type_test=False)
 
+        @parameterized.expand(
+            [TEST_CASE_ERROR_0_MPP, TEST_CASE_ERROR_1_MPP, TEST_CASE_ERROR_2_POWER, TEST_CASE_ERROR_3_POWER]
+        )
+        def test_errors(self, file_path, reader_kwargs, patch_info, exception):
+            with self.assertRaises(exception):
+                reader = WSIReader(self.backend, **reader_kwargs)
+                with reader.read(file_path) as img_obj:
+                    reader.get_data(img_obj, **patch_info)
+
 
 @skipUnless(has_cucim, "Requires cucim")
 class TestCuCIM(WSIReaderTests.Tests):
diff --git a/tests/testing_data/data_config.json b/tests/testing_data/data_config.json
--- a/tests/testing_data/data_config.json
+++ b/tests/testing_data/data_config.json
@@ -1,10 +1,15 @@
 {
     "images": {
-        "wsi_img": {
+        "wsi_generic_tiff": {
             "url": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/CMU-1.tiff",
             "hash_type": "sha256",
             "hash_val": "73a7e89bc15576587c3d68e55d9bf92f09690280166240b48ff4b48230b13bcd"
         },
+        "wsi_aperio_svs": {
+            "url": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/Aperio-CMU-1.svs",
+            "hash_type": "sha256",
+            "hash_val": "00a3d54482cd707abf254fe69dccc8d06b8ff757a1663f1290c23418c480eb30"
+        },
         "favicon": {
             "url": "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/favicon.ico.zip",
             "hash_type": "sha256",