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 15 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
bhashemian 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
293 changes: 292 additions & 1 deletion 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,101 @@ 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?

mpp_closest_lvl = mpp_list[closest_lvl]
closest_lvl_dim = wsi.resolutions["level_dimensions"][closest_lvl]

print(f"Closest Level: {closest_lvl} with MPP: {mpp_closest_lvl}")
mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_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
print(f"User-provided MPP lies within tolerance of level {closest_lvl}, returning wsi at this level.")
NikolasSchmitz marked this conversation as resolved.
Show resolved Hide resolved
closest_lvl_wsi = wsi.read_region(
(0, 0), level=closest_lvl, size=closest_lvl_dim, 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:
ds_factor_x = mpp_closest_lvl_x / user_mpp_x
ds_factor_y = mpp_closest_lvl_y / user_mpp_y

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=0)
print(f"Case 1: Downscaling using factor {(ds_factor_x, ds_factor_y)}")

else:
# Else: increase resolution (ie, decrement level) and then downsample
closest_lvl = closest_lvl - 1
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_x
ds_factor_y = mpp_closest_lvl_y / user_mpp_y

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=0)
NikolasSchmitz marked this conversation as resolved.
Show resolved Hide resolved
print(f"Case 2: Downscaling using factor {(ds_factor_x, ds_factor_y)}, now from level {closest_lvl}")

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 @@ -940,6 +1054,91 @@ 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.

"""

pil_image, _ = optional_import("PIL", name="Image")
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 = mpp_list[closest_lvl]
closest_lvl_dim = wsi.level_dimensions[closest_lvl]

print(f"Closest Level: {closest_lvl} with MPP: {mpp_closest_lvl}")
mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_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
print(f"User-provided MPP lies within tolerance of level {closest_lvl}, returning wsi at this level.")
closest_lvl_wsi = wsi.read_region((0, 0), level=closest_lvl, size=closest_lvl_dim)

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:
ds_factor_x = mpp_closest_lvl_x / user_mpp_x
ds_factor_y = mpp_closest_lvl_y / user_mpp_y

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)
print(f"Case 1: Downscaling using factor {(ds_factor_x, ds_factor_y)}")

else:
# Else: increase resolution (ie, decrement level) and then downsample
closest_lvl = closest_lvl - 1
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_x
ds_factor_y = mpp_closest_lvl_y / user_mpp_y

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)
print(f"Case 2: Downscaling using factor {(ds_factor_x, ds_factor_y)}, now from level {closest_lvl}")

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 @@ -1096,8 +1295,10 @@ 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:
if unit is not None: # Needs to be improved
unit = str(unit.value)[8:]
# unit = str(unit.value.name).lower() # TODO: Merge both methods

else:
warnings.warn("The resolution unit is missing. `micrometer` will be used as default.")
unit = "micrometer"
Expand All @@ -1110,6 +1311,96 @@ 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:
"""
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.

"""

pil_image, _ = optional_import("PIL", name="Image")
user_mpp_x, user_mpp_y = mpp
mpp_list = [self.get_mpp(wsi, lvl) for lvl in range(len(wsi.pages))]
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 = mpp_list[closest_lvl]

lvl_dims = [self.get_size(wsi, lvl) for lvl in range(len(wsi.pages))]
closest_lvl_dim = lvl_dims[closest_lvl]
closest_lvl_dim = (closest_lvl_dim[1], closest_lvl_dim[0])

print(f"Closest Level: {closest_lvl} with MPP: {mpp_closest_lvl}")
mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_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
print(f"User-provided MPP lies within tolerance of level {closest_lvl}, returning wsi at this level.")
closest_lvl_wsi = wsi.read_region((0, 0), level=closest_lvl, size=closest_lvl_dim)

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:
ds_factor_x = mpp_closest_lvl_x / user_mpp_x
ds_factor_y = mpp_closest_lvl_y / user_mpp_y

closest_lvl_wsi = pil_image.fromarray(wsi.pages[closest_lvl].asarray()) # Might be suboptimal

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)
print(f"Case 1: Downscaling using factor {(ds_factor_x, ds_factor_y)}")

else:
# Else: increase resolution (ie, decrement level) and then downsample
closest_lvl = closest_lvl - 1
mpp_closest_lvl = mpp_list[closest_lvl] # Update MPP
mpp_closest_lvl_x, mpp_closest_lvl_y = mpp_closest_lvl

ds_factor_x = mpp_closest_lvl_x / user_mpp_x
ds_factor_y = mpp_closest_lvl_y / user_mpp_y

closest_lvl_dim = lvl_dims[closest_lvl]
closest_lvl_dim = (closest_lvl_dim[1], closest_lvl_dim[0])

closest_lvl_wsi = pil_image.fromarray(wsi.pages[closest_lvl].asarray()) # Might be suboptimal

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)
print(f"Case 2: Downscaling using factor {(ds_factor_x, ds_factor_y)}, now from level {closest_lvl}")

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
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
Loading