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

WSIReader read by power and mpp #6244

Merged
merged 41 commits into from
Apr 11, 2023
Merged
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
008ed39
Implement read by magnification power and mpp
bhashemian Feb 9, 2023
622b19a
update defaults and comments
bhashemian Feb 14, 2023
10c39af
merge dev
bhashemian Mar 22, 2023
01e7687
update defaults and add test cases
bhashemian Mar 22, 2023
1814396
update error msg:
bhashemian Mar 22, 2023
8a3d66f
Merge branch 'dev' of github.com:Project-MONAI/MONAI into wsireader-mpp
bhashemian Mar 27, 2023
67ce0cb
formatting
bhashemian Mar 27, 2023
c5bf655
arrange params
bhashemian Mar 27, 2023
82da62e
micor docstring update
bhashemian Mar 27, 2023
008f9dc
update docstring
bhashemian Mar 27, 2023
cfd20e1
add defaults
bhashemian Mar 27, 2023
d2595ef
Merge branch 'dev' into wsireader-mpp
bhashemian Mar 28, 2023
b4c2dd1
update mpp, power and add several unittests to cover them
bhashemian Mar 29, 2023
8894dbd
Merge branch 'wsireader-mpp' of github.com:drbeh/MONAI into wsireader…
bhashemian Mar 29, 2023
3d2ad49
remove redundant check
bhashemian Mar 29, 2023
45145aa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 29, 2023
91da7d7
update unit tests with new wsi key
bhashemian Mar 29, 2023
93ae205
Merge branch 'wsireader-mpp' of github.com:drbeh/MONAI into wsireader…
bhashemian Mar 29, 2023
db809dc
update docsting and mmp for cucim
bhashemian Mar 29, 2023
20180dc
update docstring
bhashemian Mar 29, 2023
ac1e7d6
Merge branch 'dev' into wsireader-mpp
bhashemian Mar 30, 2023
d757ee8
address comments
bhashemian Mar 30, 2023
556d865
Merge branch 'wsireader-mpp' of github.com:drbeh/MONAI into wsireader…
bhashemian Mar 30, 2023
886163a
Merge branch 'dev' into wsireader-mpp
bhashemian Mar 30, 2023
5221ddc
update error msg
bhashemian Mar 30, 2023
9c52f65
make get_valid_level public and upate docstrings
bhashemian Mar 30, 2023
dc385ef
Merge branch 'dev' into wsireader-mpp
bhashemian Mar 31, 2023
6349376
Merge branch 'dev' into wsireader-mpp
Nic-Ma Apr 3, 2023
833f619
Merge branch 'dev' into wsireader-mpp
bhashemian Apr 3, 2023
3453d7c
Merge branch 'dev' of github.com:Project-MONAI/MONAI into wsireader-mpp
bhashemian Apr 3, 2023
14b6df4
address comments and implement ConverUnits
bhashemian Apr 3, 2023
4da71ef
Merge branch 'wsireader-mpp' of github.com:drbeh/MONAI into wsireader…
bhashemian Apr 3, 2023
914f358
merge dev
bhashemian Apr 3, 2023
0a46908
correct removed files
bhashemian Apr 3, 2023
1a1757e
Merge branch 'dev' into wsireader-mpp
bhashemian Apr 4, 2023
ad20310
Merge branch 'dev' into wsireader-mpp
bhashemian Apr 4, 2023
ce8c49a
simplify find closest level
bhashemian Apr 4, 2023
8094e03
Merge branch 'wsireader-mpp' of github.com:drbeh/MONAI into wsireader…
bhashemian Apr 4, 2023
9529e98
Merge branch 'dev' into wsireader-mpp
bhashemian Apr 5, 2023
3779bff
Merge branch 'dev' into wsireader-mpp
bhashemian Apr 6, 2023
d7e61ae
Merge branch 'dev' into wsireader-mpp
wyli Apr 11, 2023
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
Prev Previous commit
Next Next commit
address comments
Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com>
bhashemian committed Mar 30, 2023
commit d757ee836eaf5d4017430787c6e2d0a4696ee65a
137 changes: 87 additions & 50 deletions monai/data/wsi_reader.py
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ class BaseWSIReader(ImageReader):
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.
If none of them are provided here nor in `get_data`, `level=0` will be used.

Typical usage of a concrete implementation of this class is:

@@ -223,7 +223,7 @@ def get_downsample_ratio(self, wsi, level: int) -> float:

Args:
wsi: a whole slide image object loaded from a file
level: the level number where the size is calculated.
level: the level number where the downsample ratio is calculated.

"""
raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
@@ -239,8 +239,8 @@ 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 mpp 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.")
@@ -251,8 +251,8 @@ 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 objective power is calculated
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.")
@@ -332,15 +332,14 @@ def get_data(
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.

Notes:
Only one of resolution parameters, `level`, `mpp`, or `power`, should be provided.
If none of the are provided, it uses the defaults that are set during class instantiation.
If none of the are set here or during class instantiation, `level=0` will be used.
If none of them are provided, it uses the defaults that are set during class instantiation.
If none of them are set here nor during class instantiation, `level=0` will be used.
"""
if mode is None:
mode = self.mode
@@ -457,6 +456,11 @@ class WSIReader(BaseWSIReader):
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 nor in `get_data`, `level=0` will be used.

"""

supported_backends = ["cucim", "openslide", "tifffile"]
@@ -570,7 +574,7 @@ def get_downsample_ratio(self, wsi, level: int) -> float:

Args:
wsi: a whole slide image object loaded from a file
level: the level number where the size is calculated.
level: the level number where the downsample ratio is calculated.

"""
return self.reader.get_downsample_ratio(wsi, level)
@@ -584,8 +588,8 @@ 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 mpp calculated.
wsi: a whole slide image object loaded from a file.
level: the level number where the mpp is calculated.

"""
return self.reader.get_mpp(wsi, level)
@@ -595,7 +599,7 @@ 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
wsi: a whole slide image object loaded from a file.
level: the level number where the objective power is calculated.

"""
@@ -656,6 +660,11 @@ class CuCIMWSIReader(BaseWSIReader):
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 nor in `get_data`, `level=0` will be used.

"""

supported_suffixes = ["tif", "tiff", "svs"]
@@ -692,8 +701,8 @@ 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.
wsi: a whole slide image object loaded from a file.
level: the level number where the downsample ratio is calculated.

"""
return float(wsi.resolutions["level_downsamples"][level])
@@ -708,35 +717,38 @@ 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 mpp is calculated.
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 "aperio" in wsi.metadata:
mpp_ = float(wsi.metadata["aperio"].get("MPP"))
mpp_ = wsi.metadata["aperio"].get("MPP")
if mpp_:
return (downsample_ratio * mpp_, downsample_ratio * 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])

return (
downsample_ratio * wsi.metadata["cucim"]["spacing"][1],
downsample_ratio * wsi.metadata["cucim"]["spacing"][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.

Args:
wsi: a whole slide image object loaded from a file
level: the level number where objective power is calculated.
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 = float(wsi.metadata["aperio"].get("AppMag"))
objective_power = wsi.metadata["aperio"].get("AppMag")
if objective_power:
downsample_ratio = self.get_downsample_ratio(wsi, level)
return objective_power / downsample_ratio
return float(objective_power) / downsample_ratio

raise ValueError(
"Objective power can only be obtained for Aperio images using CuCIM."
@@ -828,6 +840,11 @@ 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 nor in `get_data`, `level=0` will be used.

"""

supported_suffixes = ["tif", "tiff", "svs"]
@@ -864,7 +881,7 @@ def get_downsample_ratio(self, wsi, level: int) -> float:

Args:
wsi: a whole slide image object loaded from a file
level: the level number where the size is calculated.
level: the level number where the downsample ratio is calculated.

"""
return wsi.level_downsamples[level] # type: ignore
@@ -879,54 +896,64 @@ 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.

"""
downsample_ratio = self.get_downsample_ratio(wsi, level)
if "openslide.mpp-x" in wsi.properties and "openslide.mpp-y" in wsi.properties:
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:
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 == "centimeter":
unit_factor = 10000.0
factor = 10000.0
elif unit == "millimeter":
unit_factor = 1000.0
factor = 1000.0
elif unit == "micrometer":
unit_factor = 1.0
factor = 1.0
elif unit == "inch":
unit_factor = 25400.0
factor = 25400.0
else:
warnings.warn(
f"The resolution unit is not a valid tiff resolution or missing, unit={unit}."
" `micrometer` will be used as default."
)
unit_factor = 1.0
factor = 1.0

return (
unit_factor * downsample_ratio / float(wsi.properties["tiff.YResolution"]),
unit_factor * downsample_ratio / float(wsi.properties["tiff.XResolution"]),
factor * downsample_ratio / float(wsi.properties["tiff.YResolution"]),
factor * downsample_ratio / float(wsi.properties["tiff.XResolution"]),
)

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_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 objective power is calculated.
wsi: a whole slide image object loaded from a file.
level: the level number where the objective power is calculated.

"""
objective_power = float(wsi.properties.get("openslide.objective-power"))
objective_power = wsi.properties.get("openslide.objective-power")
if objective_power:
downsample_ratio = self.get_downsample_ratio(wsi, level)
return objective_power / downsample_ratio
return float(objective_power) / downsample_ratio

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

@@ -1005,6 +1032,11 @@ class TiffFileWSIReader(BaseWSIReader):
mode: the output image color mode, "RGB" or "RGBA". Defaults to "RGB".
kwargs: additional args for `tifffile.TiffFile` 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 nor in `get_data`, `level=0` will be used.

"""

supported_suffixes = ["tif", "tiff", "svs"]
@@ -1041,7 +1073,7 @@ def get_downsample_ratio(self, wsi, level: int) -> float:

Args:
wsi: a whole slide image object loaded from a file
level: the level number where the size is calculated.
level: the level number where the downsample ratio is calculated.

"""
return float(wsi.pages[0].imagelength) / float(wsi.pages[level].imagelength)
@@ -1056,11 +1088,16 @@ 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.

"""
if "XResolution" in wsi.pages[level].tags and "YResolution" in wsi.pages[level].tags:
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 = unit.value
@@ -1094,8 +1131,8 @@ 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 objective power is calculated.
wsi: a whole slide image object loaded from a file.
level: the level number where the objective power is calculated.

"""
raise ValueError(