Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature added: Get WSI at mpp #7574

Open
wants to merge 34 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f3e7d03
Added function get_img_at_mpp to class OpenSlideWSIReader of module w…
Mar 22, 2024
88002e8
Added get_img_at_mpp to class CuCIMWSIReader
Mar 22, 2024
a9fe772
Added function get_img_at_mpp to class TifffileWSIReader; changed res…
Mar 24, 2024
feac0dc
Small changes
Mar 24, 2024
8194026
Small changes
Mar 24, 2024
4df0b4b
Stein's Unbiased Risk Estimator (SURE) loss and Conjugate Gradient (#…
cxlcl Mar 22, 2024
d989c18
Renamed function to get_wsi_at_mpp
Mar 24, 2024
105f00b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 24, 2024
5db27c1
Reformatted wsi_reader.py
NikolasSchmitz Mar 25, 2024
18e82bd
auto updates (#7577)
monai-bot Mar 25, 2024
5bb531e
Fixed return type
NikolasSchmitz Mar 25, 2024
5214c56
Small fixes
NikolasSchmitz Mar 25, 2024
3f055a9
Remove nested error propagation on `ConfigComponent` instantiate (#7569)
surajpaib Mar 26, 2024
3264079
2872 implementation of mixup, cutmix and cutout (#7198)
juampatronics Mar 26, 2024
22ecc8c
Merge branch 'dev' into 4980-get-wsi-at-mpp
drbeh Apr 9, 2024
6fcc4a6
Updated function get_wsi_at_mpp; added function _resize_to_mpp_res to…
NikolasSchmitz Jul 31, 2024
4b0c9ba
Minor fixes: removed unnecessary comments
NikolasSchmitz Jul 31, 2024
d1a5e28
Merge branch 'dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Aug 2, 2024
66508e9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 2, 2024
441b462
Added function _compute_mpp_target_res to BaseWSIReader
NikolasSchmitz Aug 4, 2024
d73d739
Added new feature and merged updates from main repository
NikolasSchmitz Aug 4, 2024
feb6828
Added function _compute_mpp_target_res to BaseWSIReader
NikolasSchmitz Aug 4, 2024
5461801
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 4, 2024
e8c1544
Merge branch 'dev' into 4980-get-wsi-at-mpp
ericspod Aug 7, 2024
59683bc
Added a function _compute_mpp_tolerances which checks the mpp toleran…
NikolasSchmitz Aug 11, 2024
547442e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 11, 2024
3e337b0
Merge branch 'Project-MONAI:dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Aug 14, 2024
cc55b8a
Merge branch 'Project-MONAI:dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Aug 27, 2024
9eca8de
Merge branch 'Project-MONAI:dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Sep 10, 2024
a8bb436
Merge branch 'Project-MONAI:dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Sep 16, 2024
6094ffd
Merge branch 'Project-MONAI:dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Sep 21, 2024
c1dd7c3
Merge branch 'Project-MONAI:dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Nov 17, 2024
349c011
Merge branch 'Project-MONAI:dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Dec 3, 2024
ca6796b
Merge branch 'Project-MONAI:dev' into 4980-get-wsi-at-mpp
NikolasSchmitz Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 299 additions & 6 deletions monai/data/wsi_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,25 @@ def get_mpp(self, wsi, level: int) -> tuple[float, float]:
"""
return self.reader.get_mpp(wsi, level)

def get_wsi_at_mpp(self, wsi, mpp: tuple, atol: float = 0.00, rtol: float = 0.05) -> np.ndarray:
"""
Returns the representation of the whole slide image at a given micro-per-pixel (mpp) resolution.
The optional tolerance parameters are considered at the level whose mpp value is closest to the one provided by the user.
If the user-provided mpp is larger than the mpp of the closest level,
the image is downscaled to a resolution that matches the user-provided mpp.
Otherwise, if the closest level's resolution is not sufficient to meet the user's requested resolution,
the next lower level (which has a higher resolution) is chosen.
The image from this level is then down-scaled to achieve a resolution at the user-provided mpp value.

Args:
wsi: whole slide image object from WSIReader
mpp: the resolution in micron per pixel at which the representation of the whole slide image should be extracted.
atol: the acceptable absolute tolerance for resolution in micro per pixel.
rtol: the acceptable relative tolerance for resolution in micro per pixel.

"""
return self.reader.get_wsi_at_mpp(wsi, mpp, atol, rtol)

def get_power(self, wsi, level: int) -> float:
"""
Returns the micro-per-pixel resolution of the whole slide image at a given level.
Expand Down Expand Up @@ -744,6 +763,71 @@ def get_mpp(self, wsi, level: int) -> tuple[float, float]:

raise ValueError("`mpp` cannot be obtained for this file. Please use `level` instead.")

def get_wsi_at_mpp(self, wsi, mpp: tuple, atol: float = 0.00, rtol: float = 0.05) -> Any:
"""
Returns the representation of the whole slide image at a given micro-per-pixel (mpp) resolution.
The optional tolerance parameters are considered at the level whose mpp value is closest to the one provided by the user.
If the user-provided mpp is larger than the mpp of the closest level,
the image is downscaled to a resolution that matches the user-provided mpp.
Otherwise, if the closest level's resolution is not sufficient to meet the user's requested resolution,
the next lower level (which has a higher resolution) is chosen.
The image from this level is then down-scaled to achieve a resolution at the user-provided mpp value.

Args:
wsi: whole slide image object from WSIReader
mpp: the resolution in micron per pixel at which the representation of the whole slide image should be extracted.
atol: the acceptable absolute tolerance for resolution in micro per pixel.
rtol: the acceptable relative tolerance for resolution in micro per pixel.

"""

# cucim_resize, _ = optional_import("cucim.skimage.transform", name="resize")
cp, _ = optional_import("cupy")

user_mpp_x, user_mpp_y = mpp
mpp_list = [self.get_mpp(wsi, lvl) for lvl in range(wsi.resolutions["level_count"])]
closest_lvl = self._find_closest_level("mpp", mpp, mpp_list, 0, 5)
# -> Should not throw ValueError, instead just return the closest value; how to select tolerances?

# closest_lvl_dim = wsi.resolutions["level_dimensions"][closest_lvl]

# mpp_closest_lvl = mpp_list[closest_lvl]
mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_list[closest_lvl]

# Define tolerance intervals for x and y of closest level
lower_bound_x = mpp_closest_lvl_x * (1 - rtol) - atol
NikolasSchmitz marked this conversation as resolved.
Show resolved Hide resolved
upper_bound_x = mpp_closest_lvl_x * (1 + rtol) + atol
lower_bound_y = mpp_closest_lvl_y * (1 - rtol) - atol
upper_bound_y = mpp_closest_lvl_y * (1 + rtol) + atol

# Check if user-provided mpp_x and mpp_y fall within the tolerance intervals for closest level
within_tolerance_x = (user_mpp_x >= lower_bound_x) & (user_mpp_x <= upper_bound_x)
within_tolerance_y = (user_mpp_y >= lower_bound_y) & (user_mpp_y <= upper_bound_y)
within_tolerance = within_tolerance_x & within_tolerance_y

if within_tolerance:
# Take closest_level and continue with returning img at level
closest_lvl_wsi = wsi.read_region(
(0, 0), level=closest_lvl, size=wsi.resolutions["level_dimensions"][closest_lvl], num_workers=self.num_workers
)

else:
# If mpp_closest_level < mpp -> closest_level has higher res than img at mpp => downscale from closest_level to mpp
closest_level_is_bigger_x = mpp_closest_lvl_x < user_mpp_x
closest_level_is_bigger_y = mpp_closest_lvl_y < user_mpp_y
closest_level_is_bigger = closest_level_is_bigger_x & closest_level_is_bigger_y

if closest_level_is_bigger:
closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp)

else:
# Else: increase resolution (ie, decrement level) and then downsample
closest_lvl = closest_lvl - 1
closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp)

wsi_arr = cp.asnumpy(closest_lvl_wsi)
return wsi_arr

def get_power(self, wsi, level: int) -> float:
"""
Returns the objective power of the whole slide image at a given level.
Expand Down Expand Up @@ -827,6 +911,36 @@ def _get_patch(
patch = np.take(patch, [0, 1, 2], self.channel_dim)

return patch

def _resize_to_mpp_res(self, wsi, closest_lvl, mpp_list, user_mpp: tuple):
NikolasSchmitz marked this conversation as resolved.
Show resolved Hide resolved
"""
Resizes the whole slide image to the specified resolution in microns per pixel (mpp).

Args:
wsi: whole slide image object from WSIReader
user_mpp: the resolution in microns per pixel at which the whole slide image representation should be extracted.
closest_lvl: the wsi level that is closest to the user-provided mpp resolution.
mpp_list: list of mpp values for all levels of a whole slide image.

"""
cucim_resize, _ = optional_import("cucim.skimage.transform", name="resize")
cp, _ = optional_import("cupy")

mpp_closest_lvl = mpp_list[closest_lvl]
mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_closest_lvl

ds_factor_x = mpp_closest_lvl_x / user_mpp[0]
ds_factor_y = mpp_closest_lvl_y / user_mpp[1]

closest_lvl_dim = wsi.resolutions["level_dimensions"][closest_lvl]
closest_lvl_wsi = wsi.read_region((0, 0), level=closest_lvl, size=closest_lvl_dim, num_workers=self.num_workers)
wsi_arr = cp.array(closest_lvl_wsi)

target_res_x = int(np.round(closest_lvl_dim[1] * ds_factor_x))
target_res_y = int(np.round(closest_lvl_dim[0] * ds_factor_y))

closest_lvl_wsi = cucim_resize(wsi_arr, (target_res_x, target_res_y), order=1)
return closest_lvl_wsi


@require_pkg(pkg_name="openslide")
Expand Down Expand Up @@ -940,6 +1054,65 @@ def get_mpp(self, wsi, level: int) -> tuple[float, float]:

raise ValueError("`mpp` cannot be obtained for this file. Please use `level` instead.")

def get_wsi_at_mpp(self, wsi, mpp: tuple, atol: float = 0.00, rtol: float = 0.05) -> np.ndarray:
ericspod marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns the representation of the whole slide image at a given micro-per-pixel (mpp) resolution.
The optional tolerance parameters are considered at the level whose mpp value is closest to the one provided by the user.
If the user-provided mpp is larger than the mpp of the closest level,
the image is downscaled to a resolution that matches the user-provided mpp.
Otherwise, if the closest level's resolution is not sufficient to meet the user's requested resolution,
the next lower level (which has a higher resolution) is chosen.
The image from this level is then down-scaled to achieve a resolution at the user-provided mpp value.

Args:
wsi: whole slide image object from WSIReader
mpp: the resolution in micron per pixel at which the representation of the whole slide image should be extracted.
atol: the acceptable absolute tolerance for resolution in micro per pixel.
rtol: the acceptable relative tolerance for resolution in micro per pixel.

"""

user_mpp_x, user_mpp_y = mpp
mpp_list = [self.get_mpp(wsi, lvl) for lvl in range(wsi.level_count)]
closest_lvl = self._find_closest_level("mpp", mpp, mpp_list, 0, 5)
# -> Should not throw ValueError, instead just return the closest value; how to select tolerances?

mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_list[closest_lvl]

# Define tolerance intervals for x and y of closest level
lower_bound_x = mpp_closest_lvl_x * (1 - rtol) - atol
upper_bound_x = mpp_closest_lvl_x * (1 + rtol) + atol
lower_bound_y = mpp_closest_lvl_y * (1 - rtol) - atol
upper_bound_y = mpp_closest_lvl_y * (1 + rtol) + atol

# Check if user-provided mpp_x and mpp_y fall within the tolerance intervals for closest level
within_tolerance_x = (user_mpp_x >= lower_bound_x) & (user_mpp_x <= upper_bound_x)
within_tolerance_y = (user_mpp_y >= lower_bound_y) & (user_mpp_y <= upper_bound_y)
within_tolerance = within_tolerance_x & within_tolerance_y

if within_tolerance:
# Take closest_level and continue with returning img at level
closest_lvl_wsi = wsi.read_region(
(0, 0), level=closest_lvl, size=wsi.level_dimensions[closest_lvl]
)

else:
# If mpp_closest_level < mpp -> closest_level has higher res than img at mpp => downscale from closest_level to mpp
closest_level_is_bigger_x = mpp_closest_lvl_x < user_mpp_x
closest_level_is_bigger_y = mpp_closest_lvl_y < user_mpp_y
closest_level_is_bigger = closest_level_is_bigger_x & closest_level_is_bigger_y

if closest_level_is_bigger:
closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp)

else:
# Else: increase resolution (ie, decrement level) and then downsample
closest_lvl = closest_lvl - 1
closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp)

wsi_arr = np.array(closest_lvl_wsi)
return wsi_arr

def get_power(self, wsi, level: int) -> float:
"""
Returns the objective power of the whole slide image at a given level.
Expand Down Expand Up @@ -1009,6 +1182,34 @@ def _get_patch(
patch = np.moveaxis(patch, -1, self.channel_dim)

return patch

def _resize_to_mpp_res(self, wsi, closest_lvl, mpp_list, user_mpp: tuple):
"""
Resizes the whole slide image to the specified resolution in microns per pixel (mpp).

Args:
wsi: whole slide image object from WSIReader
user_mpp: the resolution in microns per pixel at which the whole slide image representation should be extracted.
closest_lvl: the wsi level that is closest to the user-provided mpp resolution.
mpp_list: list of mpp values for all levels of a whole slide image.

"""
pil_image, _ = optional_import("PIL", name="Image")

mpp_closest_lvl = mpp_list[closest_lvl]
mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_closest_lvl

ds_factor_x = mpp_closest_lvl_x / user_mpp[0]
ds_factor_y = mpp_closest_lvl_y / user_mpp[1]

closest_lvl_dim = wsi.level_dimensions[closest_lvl]
closest_lvl_wsi = wsi.read_region((0, 0), level=closest_lvl, size=closest_lvl_dim)

target_res_x = int(np.round(closest_lvl_dim[0] * ds_factor_x))
target_res_y = int(np.round(closest_lvl_dim[1] * ds_factor_y))

closest_lvl_wsi = closest_lvl_wsi.resize((target_res_x, target_res_y), pil_image.BILINEAR)
return closest_lvl_wsi


@require_pkg(pkg_name="tifffile")
Expand Down Expand Up @@ -1096,19 +1297,83 @@ def get_mpp(self, wsi, level: int) -> tuple[float, float]:
and wsi.pages[level].tags["YResolution"].value
):
unit = wsi.pages[level].tags.get("ResolutionUnit")
if unit is not None:
unit = str(unit.value)[8:]
if unit is not None: # Test with more tiff files
if isinstance(unit.value, int):
unit = str(unit.value.name).lower()
else:
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.

# 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])
if xres[0] & yres[0]:
return convert_to_micron(yres[1] / yres[0]), convert_to_micron(xres[1] / xres[0])
else:
raise ValueError("The `XResolution` and/or `YResolution` property of the image is zero, "
"which is needed to obtain `mpp` for this file. Please use `level` instead.")
raise ValueError("`mpp` cannot be obtained for this file. Please use `level` instead.")

raise ValueError("`mpp` cannot be obtained for this file. Please use `level` instead.")
def get_wsi_at_mpp(self, wsi, mpp: tuple, atol: float = 0.00, rtol: float = 0.05) -> np.ndarray:
"""
Returns the representation of the whole slide image at a given micro-per-pixel (mpp) resolution.
The optional tolerance parameters are considered at the level whose mpp value is closest to the one provided by the user.
If the user-provided mpp is larger than the mpp of the closest level,
the image is downscaled to a resolution that matches the user-provided mpp.
Otherwise, if the closest level's resolution is not sufficient to meet the user's requested resolution,
the next lower level (which has a higher resolution) is chosen.
The image from this level is then down-scaled to achieve a resolution at the user-provided mpp value.

Args:
wsi: whole slide image object from WSIReader
mpp: the resolution in micron per pixel at which the representation of the whole slide image should be extracted.
atol: the acceptable absolute tolerance for resolution in micro per pixel.
rtol: the acceptable relative tolerance for resolution in micro per pixel.

"""

user_mpp_x, user_mpp_y = mpp
mpp_list = [self.get_mpp(wsi, lvl) for lvl in range(len(wsi.pages))] # Fails for some Tifffiles
closest_lvl = self._find_closest_level("mpp", mpp, mpp_list, 0, 5)
# -> Should not throw ValueError, instead just return the closest value; how to select tolerances?

mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_list[closest_lvl]

# Define tolerance intervals for x and y of closest level
lower_bound_x = mpp_closest_lvl_x * (1 - rtol) - atol
upper_bound_x = mpp_closest_lvl_x * (1 + rtol) + atol
lower_bound_y = mpp_closest_lvl_y * (1 - rtol) - atol
upper_bound_y = mpp_closest_lvl_y * (1 + rtol) + atol

# Check if user-provided mpp_x and mpp_y fall within the tolerance intervals for closest level
within_tolerance_x = (user_mpp_x >= lower_bound_x) & (user_mpp_x <= upper_bound_x)
within_tolerance_y = (user_mpp_y >= lower_bound_y) & (user_mpp_y <= upper_bound_y)
within_tolerance = within_tolerance_x & within_tolerance_y

if within_tolerance:
# Take closest_level and continue with returning img at level
closest_lvl_wsi = wsi.read_region((0, 0), level=closest_lvl, size=self.get_size(wsi, closest_lvl))

else:
# If mpp_closest_level < mpp -> closest_level has higher res than img at mpp => downscale from closest_level to mpp
closest_level_is_bigger_x = mpp_closest_lvl_x < user_mpp_x
closest_level_is_bigger_y = mpp_closest_lvl_y < user_mpp_y
closest_level_is_bigger = closest_level_is_bigger_x & closest_level_is_bigger_y

if closest_level_is_bigger:
closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp)

else:
closest_lvl = closest_lvl - 1
closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp)

wsi_arr = np.array(closest_lvl_wsi)
return wsi_arr

def get_power(self, wsi, level: int) -> float:
"""
Expand Down Expand Up @@ -1154,7 +1419,7 @@ 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 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 None, it is set to the full image size at the given level.
Expand Down Expand Up @@ -1186,3 +1451,31 @@ def _get_patch(
patch = np.take(patch, [0, 1, 2], self.channel_dim)

return patch

def _resize_to_mpp_res(self, wsi, closest_lvl, mpp_list, user_mpp: tuple):
"""
Resizes the whole slide image to the specified resolution in microns per pixel (mpp).

Args:
wsi: whole slide image object from WSIReader
user_mpp: the resolution in microns per pixel at which the whole slide image representation should be extracted.
closest_lvl: the wsi level that is closest to the user-provided mpp resolution.
mpp_list: list of mpp values for all levels of a whole slide image.

"""
pil_image, _ = optional_import("PIL", name="Image")

mpp_closest_lvl = mpp_list[closest_lvl]
mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_closest_lvl

ds_factor_x = mpp_closest_lvl_x / user_mpp[0]
ds_factor_y = mpp_closest_lvl_y / user_mpp[1]

closest_lvl_dim = self.get_size(wsi, closest_lvl)
closest_lvl_wsi = pil_image.fromarray(wsi.pages[closest_lvl].asarray())

target_res_x = int(np.round(closest_lvl_dim[0] * ds_factor_x))
target_res_y = int(np.round(closest_lvl_dim[1] * ds_factor_y))

closest_lvl_wsi = closest_lvl_wsi.resize((target_res_x, target_res_y), pil_image.BILINEAR)
return closest_lvl_wsi
1 change: 0 additions & 1 deletion monai/transforms/regularization/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@


class Mixer(RandomizableTransform):

def __init__(self, batch_size: int, alpha: float = 1.0) -> None:
"""
Mixer is a base class providing the basic logic for the mixup-class of
Expand Down
1 change: 1 addition & 0 deletions tests/test_regularization.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def test_mixupd(self):


class TestCutMix(unittest.TestCase):

def setUp(self) -> None:
set_determinism(seed=0)

Expand Down