-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Feature added: Get WSI at mpp #7574
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
base: dev
Are you sure you want to change the base?
Changes from all commits
f3e7d03
88002e8
a9fe772
feac0dc
8194026
4df0b4b
d989c18
105f00b
5db27c1
18e82bd
5bb531e
5214c56
3f055a9
3264079
22ecc8c
6fcc4a6
4b0c9ba
d1a5e28
66508e9
d4040dd
441b462
d73d739
feb6828
5461801
e8c1544
59683bc
547442e
3e337b0
cc55b8a
9eca8de
a8bb436
6094ffd
c1dd7c3
349c011
6ff912d
ca6796b
49f1663
52e0516
54774d9
2962016
3f87b10
801ca7f
1c5a26c
e73cff8
aa4f2de
c09c7e2
505e38b
df29f44
f70d0ef
61fc9bf
a834514
c92475d
5b988a2
5168a12
1437477
a4a78e3
b1ed4ff
6b18e9b
d74d4be
d6d0cc2
2f4388e
d0a4881
7545148
b01bf63
5c7822f
fd0d0cf
234f23f
2730abe
9d817e7
2afa6fb
8270658
23e4a74
787d30f
0d9f1dd
45182fa
a361fa2
832c14e
98d45b9
5059b14
d644371
922bbf1
4249b48
72f14e5
bb19273
bd6f87e
2553e3e
1c1ecd0
b3b1341
77de338
6169e75
1789735
01e60e0
165af63
4460f45
414078d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -431,6 +431,70 @@ def get_data( | |
| metadata[key] = [m[key] for m in metadata_list] | ||
| return _stack_images(patch_list, metadata), metadata | ||
|
|
||
| def _compute_mpp_target_res(self, closest_lvl, closest_lvl_dim, mpp_list, mpp: tuple): | ||
| """ | ||
| Computes the target dimensions for resizing a whole slide image | ||
| to match a user-specified resolution in microns per pixel (MPP). | ||
|
|
||
| Args: | ||
| closest_lvl: Whole slide image level closest to user-provided MPP resolution. | ||
| closest_lvl_dim: Dimensions (height, width) of the image at the closest level. | ||
| mpp_list: List of MPP values for all levels of the whole slide image. | ||
| mpp: The MPP resolution at which the whole slide image representation should be extracted. | ||
|
|
||
| Returns: | ||
| Tuple of (target_res_x, target_res_y) representing the target pixel dimensions. | ||
|
|
||
| """ | ||
| 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 / mpp[0] | ||
| ds_factor_y = mpp_closest_lvl_y / mpp[1] | ||
|
|
||
| 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)) | ||
|
|
||
| return target_res_x, target_res_y | ||
|
|
||
| def _compute_mpp_tolerances(self, closest_lvl, mpp_list, mpp, atol, rtol) -> bool: | ||
| """ | ||
| Determines if user-provided MPP values are within a specified tolerance of the closest | ||
| level's MPP and checks if the closest level has higher resolution than desired MPP. | ||
|
|
||
| Args: | ||
| closest_lvl: Whole slide image level closest to user-provided MPP resolution. | ||
| mpp_list: List of MPP values for all levels of the whole slide image. | ||
| mpp: The MPP resolution at which the whole slide image representation should be extracted. | ||
| atol: Absolute tolerance for MPP comparison. | ||
| rtol: Relative tolerance for MPP comparison. | ||
|
|
||
| Returns: | ||
| Tuple of (is_within_tolerance, closest_level_is_bigger) where first element indicates | ||
| if MPP is within tolerance and second indicates if closest level has higher resolution. | ||
|
|
||
| """ | ||
| user_mpp_x, user_mpp_y = mpp | ||
| 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 | ||
| is_within_tolerance_x = (user_mpp_x >= lower_bound_x) and (user_mpp_x <= upper_bound_x) | ||
| is_within_tolerance_y = (user_mpp_y >= lower_bound_y) and (user_mpp_y <= upper_bound_y) | ||
| is_within_tolerance = is_within_tolerance_x and is_within_tolerance_y | ||
|
|
||
| # 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 and closest_level_is_bigger_y | ||
|
|
||
| return is_within_tolerance, closest_level_is_bigger | ||
|
|
||
| def verify_suffix(self, filename: Sequence[PathLike] | PathLike) -> bool: | ||
| """ | ||
| Verify whether the specified file or files format is supported by WSI reader. | ||
|
|
@@ -603,6 +667,28 @@ 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. | ||
|
|
||
| Returns: | ||
| Numpy array containing the whole slide image at the requested MPP resolution. | ||
|
|
||
| """ | ||
| 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. | ||
|
|
@@ -744,6 +830,54 @@ 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. | ||
|
|
||
| Returns: | ||
| Cupy array containing the whole slide image at the requested MPP resolution. | ||
|
|
||
| """ | ||
| cp, _ = optional_import("cupy") | ||
|
|
||
| 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) | ||
|
|
||
| within_tolerance, closest_level_is_bigger = self._compute_mpp_tolerances(closest_lvl, mpp_list, mpp, atol, rtol) | ||
|
|
||
| if within_tolerance: | ||
| # If the image at the desired mpp resolution is within tolerances, return the image at closest_level. | ||
| closest_lvl_wsi = wsi.read_region( | ||
| (0, 0), level=closest_lvl, size=wsi.resolutions["level_dimensions"][closest_lvl], num_workers=self.num_workers | ||
| ) | ||
|
|
||
| elif closest_level_is_bigger: | ||
| # Otherwise, select the level closest to the desired mpp with a higher resolution and downsample it. | ||
| closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp) | ||
|
|
||
| else: | ||
| # If both checks fail, increase resolution (i.e., decrement level) and then downsample it. | ||
| if closest_lvl == 0: | ||
| 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 = cp.asnumpy(closest_lvl_wsi) | ||
| return wsi_arr | ||
|
Comment on lines
+833
to
+879
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle scalar mpp inputs. Passing a single float (supported elsewhere in this API surface) now breaks because Apply this diff (and replicate in the other backends): cp, _ = optional_import("cupy")
+ mpp = ensure_tuple_rep(mpp, 2)
+
mpp_list = [self.get_mpp(wsi, lvl) for lvl in range(wsi.resolutions["level_count"])]🤖 Prompt for AI Agents |
||
|
|
||
| def get_power(self, wsi, level: int) -> float: | ||
| """ | ||
| Returns the objective power of the whole slide image at a given level. | ||
|
|
@@ -828,6 +962,37 @@ def _get_patch( | |
|
|
||
| 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. | ||
|
|
||
| Returns: | ||
| Resized cupy image array at the target MPP resolution. | ||
|
|
||
| """ | ||
| cucim_resize, _ = optional_import("cucim.skimage.transform", name="resize") | ||
| cp, _ = optional_import("cupy") | ||
|
|
||
| closest_lvl_dim = wsi.resolutions["level_dimensions"][closest_lvl] | ||
|
|
||
| target_res_x, target_res_y = self._compute_mpp_target_res(closest_lvl, closest_lvl_dim, mpp_list, user_mpp) | ||
|
|
||
| wsi_arr = cp.array(wsi.read_region((0, 0), level=closest_lvl, size=closest_lvl_dim, num_workers=self.num_workers)) | ||
| closest_lvl_wsi = cucim_resize( | ||
| wsi_arr, | ||
| (target_res_x, target_res_y), | ||
| order=1, | ||
| preserve_range=True, | ||
| anti_aliasing=False).astype(cp.uint8) | ||
|
|
||
| return closest_lvl_wsi | ||
|
|
||
|
|
||
| @require_pkg(pkg_name="openslide") | ||
| class OpenSlideWSIReader(BaseWSIReader): | ||
|
|
@@ -940,6 +1105,53 @@ 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. | ||
|
|
||
| Returns: | ||
| Numpy array containing the whole slide image at the requested MPP resolution. | ||
|
|
||
| """ | ||
|
|
||
| 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) | ||
|
|
||
| within_tolerance, closest_level_is_bigger = self._compute_mpp_tolerances(closest_lvl, mpp_list, mpp, atol, rtol) | ||
|
|
||
| if within_tolerance: | ||
| # If the image at the desired mpp resolution is within tolerances, return the image at closest_level. | ||
| closest_lvl_wsi = wsi.read_region( | ||
| (0, 0), level=closest_lvl, size=wsi.level_dimensions[closest_lvl] | ||
| ) | ||
|
|
||
| elif closest_level_is_bigger: | ||
| # Otherwise, select the level closest to the desired mpp with a higher resolution and downsample it. | ||
| closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp) | ||
|
|
||
| else: | ||
| # If both checks fail, increase resolution (i.e., decrement level) and then downsample it. | ||
| if closest_lvl == 0: | ||
| 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: | ||
| """ | ||
| Returns the objective power of the whole slide image at a given level. | ||
|
|
@@ -1010,6 +1222,31 @@ def _get_patch( | |
|
|
||
| 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. | ||
|
|
||
| Returns: | ||
| PIL Image object resized to the target MPP resolution. | ||
|
|
||
| """ | ||
| pil_image, _ = optional_import("PIL", name="Image") | ||
|
|
||
| closest_lvl_dim = wsi.level_dimensions[closest_lvl] | ||
|
|
||
| target_res_x, target_res_y = self._compute_mpp_target_res(closest_lvl, closest_lvl_dim, mpp_list, user_mpp) | ||
|
|
||
| closest_lvl_wsi = wsi.read_region((0, 0), level=closest_lvl, size=closest_lvl_dim) | ||
| closest_lvl_wsi = closest_lvl_wsi.resize((target_res_y, target_res_x), pil_image.BILINEAR) # row, col order | ||
|
|
||
| return closest_lvl_wsi | ||
|
|
||
|
|
||
| @require_pkg(pkg_name="tifffile") | ||
| class TiffFileWSIReader(BaseWSIReader): | ||
|
|
@@ -1103,12 +1340,61 @@ def get_mpp(self, wsi, level: int) -> tuple[float, float]: | |
| 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] and 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.") | ||
|
|
||
| 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. | ||
|
|
||
| Returns: | ||
| Numpy array containing the whole slide image at the requested MPP resolution. | ||
|
|
||
| """ | ||
|
|
||
| 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) | ||
|
|
||
| within_tolerance, closest_level_is_bigger = self._compute_mpp_tolerances(closest_lvl, mpp_list, mpp, atol, rtol) | ||
|
|
||
| if within_tolerance: | ||
| # If the image at the desired mpp resolution is within tolerances, return the image at closest_level. | ||
| closest_lvl_wsi = wsi.read_region((0, 0), level=closest_lvl, size=self.get_size(wsi, closest_lvl)) | ||
|
|
||
| elif closest_level_is_bigger: | ||
| # Otherwise, select the level closest to the desired mpp with a higher resolution and downsample it. | ||
| closest_lvl_wsi = self._resize_to_mpp_res(wsi, closest_lvl, mpp_list, mpp) | ||
|
|
||
| else: | ||
| # If both checks fail, increase resolution (i.e., decrement level) and then downsample it. | ||
| if closest_lvl == 0: | ||
| 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) | ||
|
|
||
| raise ValueError("`mpp` cannot be obtained for this file. Please use `level` instead.") | ||
| wsi_arr = np.array(closest_lvl_wsi) | ||
| return wsi_arr | ||
|
|
||
| def get_power(self, wsi, level: int) -> float: | ||
| """ | ||
|
|
@@ -1154,7 +1440,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. | ||
|
|
@@ -1186,3 +1472,28 @@ 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. | ||
|
|
||
| Returns: | ||
| PIL Image object resized to the target MPP resolution. | ||
|
|
||
| """ | ||
| pil_image, _ = optional_import("PIL", name="Image") | ||
|
|
||
| closest_lvl_dim = self.get_size(wsi, closest_lvl) | ||
|
|
||
| target_res_x, target_res_y = self._compute_mpp_target_res(closest_lvl, closest_lvl_dim, mpp_list, user_mpp) | ||
|
|
||
| closest_lvl_wsi = pil_image.fromarray(wsi.pages[closest_lvl].asarray()) | ||
| closest_lvl_wsi = closest_lvl_wsi.resize((target_res_x, target_res_y), pil_image.BILINEAR) | ||
|
|
||
| return closest_lvl_wsi | ||
Uh oh!
There was an error while loading. Please reload this page.