-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Add optional RankSEG decoding to AsDiscrete #8908
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 7 commits
45510fd
5b5dfff
c15c03a
3bc536f
5b2e387
b10b7e1
1866926
f08231b
3078f7e
1f1929b
fcee761
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 |
|---|---|---|
|
|
@@ -39,15 +39,19 @@ | |
| ) | ||
| from monai.transforms.utils_pytorch_numpy_unification import unravel_index | ||
| from monai.utils import ( | ||
| OptionalImportError, | ||
| TransformBackends, | ||
| convert_data_type, | ||
| convert_to_tensor, | ||
| ensure_tuple, | ||
| get_equivalent_dtype, | ||
| look_up_option, | ||
| optional_import, | ||
| ) | ||
| from monai.utils.type_conversion import convert_to_dst_type | ||
|
|
||
| rankseg_fn, has_rankseg = optional_import("rankseg.functional", name="rankseg") | ||
|
|
||
| __all__ = [ | ||
| "Activations", | ||
| "AsDiscrete", | ||
|
|
@@ -142,6 +146,7 @@ class AsDiscrete(Transform): | |
| Convert the input tensor/array into discrete values, possible operations are: | ||
|
|
||
| - `argmax`. | ||
| - `rankseg`. | ||
| - threshold input value to binary values. | ||
| - convert input value to One-Hot format (set ``to_one_hot=N``, `N` is the number of classes). | ||
| - round the value to the closest integer. | ||
|
|
@@ -155,6 +160,14 @@ class AsDiscrete(Transform): | |
| Defaults to ``None``. | ||
| rounding: if not None, round the data according to the specified option, | ||
| available options: ["torchrounding"]. | ||
| rankseg: whether to apply RankSEG decoding. Requires installing the optional ``rankseg`` package. | ||
| RankSEG is applied to a channel-first probability map for one image; ``dim`` identifies the | ||
| class/channel dimension and is moved to the front before decoding. For the common MONAI | ||
| post-processing input shape ``(C, *spatial)``, use the default ``dim=0``. | ||
| The output is a label map. With the default ``keepdim=True``, the output shape is ``(1, *spatial)``; | ||
| with ``keepdim=False``, it is ``(*spatial)``. The ``dim`` and ``keepdim`` shape handling is aligned | ||
| with ``argmax``. This option is incompatible with ``argmax=True``. | ||
| Defaults to ``False``. | ||
| kwargs: additional parameters to `torch.argmax`, `monai.networks.one_hot`. | ||
| currently ``dim``, ``keepdim``, ``dtype`` are supported, unrecognized parameters will be ignored. | ||
| These default to ``0``, ``True``, ``torch.float`` respectively. | ||
|
|
@@ -173,6 +186,12 @@ class AsDiscrete(Transform): | |
| >>> print(transform(np.array([[[0.0, 1.0]], [[2.0, 3.0]]]))) | ||
| # [[[0.0, 0.0]], [[1.0, 1.0]]] | ||
|
|
||
| RankSEG decoding requires the optional ``rankseg`` package: | ||
|
|
||
| >>> transform = AsDiscrete(rankseg=True) | ||
| >>> print(transform(np.array([[[0.3, 0.6]], [[0.7, 0.4]]]))) | ||
| # [[[1.0, 1.0]]] | ||
|
|
||
| """ | ||
|
|
||
| backend = [TransformBackends.TORCH] | ||
|
|
@@ -183,9 +202,13 @@ def __init__( | |
| to_onehot: int | None = None, | ||
| threshold: float | None = None, | ||
| rounding: str | None = None, | ||
| rankseg: bool = False, | ||
| **kwargs, | ||
| ) -> None: | ||
| if argmax and rankseg: | ||
| raise ValueError("`rankseg=True` is incompatible with `argmax=True`.") | ||
| self.argmax = argmax | ||
| self.rankseg = rankseg | ||
|
Member
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. If rankseg is True but the package isn't present, it would help to raise the 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. Thanks. I added the dependency check to |
||
| if isinstance(to_onehot, bool): # for backward compatibility | ||
| raise ValueError("`to_onehot=True/False` is deprecated, please use `to_onehot=num_classes` instead.") | ||
| self.to_onehot = to_onehot | ||
|
|
@@ -200,6 +223,7 @@ def __call__( | |
| to_onehot: int | None = None, | ||
| threshold: float | None = None, | ||
| rounding: str | None = None, | ||
| rankseg: bool | None = None, | ||
| ) -> NdarrayOrTensor: | ||
| """ | ||
| Args: | ||
|
|
@@ -211,6 +235,10 @@ def __call__( | |
| Defaults to ``self.to_onehot``. | ||
| threshold: if not None, threshold the float values to int number 0 or 1 with specified threshold value. | ||
| Defaults to ``self.threshold``. | ||
| rankseg: whether to apply RankSEG decoding. Requires installing the optional ``rankseg`` package. | ||
| Applies RankSEG to a channel-first probability map by default and uses the same ``dim`` and | ||
| ``keepdim`` shape handling as ``argmax``. This option is incompatible with ``argmax=True``. | ||
| Defaults to ``self.rankseg``. | ||
| rounding: if not None, round the data according to the specified option, | ||
| available options: ["torchrounding"]. | ||
|
|
||
|
|
@@ -220,9 +248,24 @@ def __call__( | |
| img = convert_to_tensor(img, track_meta=get_track_meta()) | ||
| img_t, *_ = convert_data_type(img, torch.Tensor) | ||
| argmax = self.argmax if argmax is None else argmax | ||
| rankseg = self.rankseg if rankseg is None else rankseg | ||
|
|
||
| if argmax and rankseg: | ||
| raise ValueError("`rankseg=True` is incompatible with `argmax=True`.") | ||
|
|
||
| if argmax: | ||
| img_t = torch.argmax(img_t, dim=self.kwargs.get("dim", 0), keepdim=self.kwargs.get("keepdim", True)) | ||
|
|
||
| if rankseg: | ||
| if not has_rankseg: | ||
| raise OptionalImportError("`rankseg=True` requires the `rankseg` package, but it is not installed.") | ||
| # Adjust shape to meet RankSEG's [B, C, *spatial] input requirement. | ||
| channel_dim = self.kwargs.get("dim", 0) % img_t.ndim | ||
| keepdim = self.kwargs.get("keepdim", True) | ||
| img_t = rankseg_fn(img_t.movedim(channel_dim, 0).unsqueeze(0)).squeeze(0) | ||
| if keepdim: | ||
| img_t = img_t.unsqueeze(channel_dim) | ||
|
|
||
| to_onehot = self.to_onehot if to_onehot is None else to_onehot | ||
| if to_onehot is not None: | ||
| if not isinstance(to_onehot, int): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,6 +166,7 @@ def __init__( | |
| to_onehot: Sequence[int | None] | int | None = None, | ||
| threshold: Sequence[float | None] | float | None = None, | ||
| rounding: Sequence[str | None] | str | None = None, | ||
| rankseg: Sequence[bool] | bool = False, | ||
| allow_missing_keys: bool = False, | ||
| **kwargs, | ||
| ) -> None: | ||
|
|
@@ -182,6 +183,10 @@ def __init__( | |
| rounding: if not None, round the data according to the specified option, | ||
| available options: ["torchrounding"]. it also can be a sequence of str or None, | ||
| each element corresponds to a key in ``keys``. | ||
| rankseg: whether to apply RankSEG decoding. Requires installing the optional ``rankseg`` package. | ||
|
Member
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. Similarly this is likely where new users will encounter this so a comment to see the description in 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. Thanks. I shortened the |
||
| RankSEG expects channel-first probability maps for one image. It also can be a sequence of bool, | ||
| each element corresponds to a key in ``keys``. Uses the same ``dim`` and ``keepdim`` shape handling | ||
| as ``argmax``. This option is incompatible with ``argmax=True``. | ||
| allow_missing_keys: don't raise exception if key is missing. | ||
| kwargs: additional parameters to ``AsDiscrete``. | ||
| ``dim``, ``keepdim``, ``dtype`` are supported, unrecognized parameters will be ignored. | ||
|
|
@@ -190,6 +195,9 @@ def __init__( | |
| """ | ||
| super().__init__(keys, allow_missing_keys) | ||
| self.argmax = ensure_tuple_rep(argmax, len(self.keys)) | ||
| self.rankseg = ensure_tuple_rep(rankseg, len(self.keys)) | ||
| if any(argmax_ and rankseg_ for argmax_, rankseg_ in zip(self.argmax, self.rankseg, strict=True)): | ||
| raise ValueError("`rankseg=True` is incompatible with `argmax=True`.") | ||
| self.to_onehot = [] | ||
| for flag in ensure_tuple_rep(to_onehot, len(self.keys)): | ||
| if isinstance(flag, bool): | ||
|
|
@@ -208,10 +216,12 @@ def __init__( | |
|
|
||
| def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, NdarrayOrTensor]: | ||
| d = dict(data) | ||
| for key, argmax, to_onehot, threshold, rounding in self.key_iterator( | ||
| d, self.argmax, self.to_onehot, self.threshold, self.rounding | ||
| for key, argmax, to_onehot, threshold, rounding, rankseg in self.key_iterator( | ||
| d, self.argmax, self.to_onehot, self.threshold, self.rounding, self.rankseg | ||
| ): | ||
| d[key] = self.converter(d[key], argmax, to_onehot, threshold, rounding) | ||
| d[key] = self.converter( | ||
| d[key], argmax=argmax, to_onehot=to_onehot, threshold=threshold, rounding=rounding, rankseg=rankseg | ||
| ) | ||
| return d | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably this is going to be the first place users will encounter any reference to RankSEG, there should be some description of what it does and what it's for. It would help to reduce the length of the existing text as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion. I shortened the
ranksegdocumentation and added a compact description clarifying that RankSEG is an inference-time decoder that converts class probability maps into segmentation label maps while maximizing the expected samplewise Dice or IoU.