@@ -67,45 +67,11 @@ def __init__(
6767 batch : bool = False ,
6868 weight : Sequence [float ] | float | int | torch .Tensor | None = None ,
6969 soft_label : bool = False ,
70+ ignore_index : int | None = None ,
7071 ) -> None :
7172 """
72- Args:
73- include_background: if False, channel index 0 (background category) is excluded from the calculation.
74- if the non-background segmentations are small compared to the total image size they can get overwhelmed
75- by the signal from the background so excluding it in such cases helps convergence.
76- to_onehot_y: whether to convert the ``target`` into the one-hot format,
77- using the number of classes inferred from `input` (``input.shape[1]``). Defaults to False.
78- sigmoid: if True, apply a sigmoid function to the prediction.
79- softmax: if True, apply a softmax function to the prediction.
80- other_act: callable function to execute other activation layers, Defaults to ``None``. for example:
81- ``other_act = torch.tanh``.
82- squared_pred: use squared versions of targets and predictions in the denominator or not.
83- jaccard: compute Jaccard Index (soft IoU) instead of dice or not.
84- reduction: {``"none"``, ``"mean"``, ``"sum"``}
85- Specifies the reduction to apply to the output. Defaults to ``"mean"``.
86-
87- - ``"none"``: no reduction will be applied.
88- - ``"mean"``: the sum of the output will be divided by the number of elements in the output.
89- - ``"sum"``: the output will be summed.
90-
91- smooth_nr: a small constant added to the numerator to avoid zero.
92- smooth_dr: a small constant added to the denominator to avoid nan.
93- batch: whether to sum the intersection and union areas over the batch dimension before the dividing.
94- Defaults to False, a Dice loss value is computed independently from each item in the batch
95- before any `reduction`.
96- weight: weights to apply to the voxels of each class. If None no weights are applied.
97- The input can be a single value (same weight for all classes), a sequence of values (the length
98- of the sequence should be the same as the number of classes. If not ``include_background``,
99- the number of classes should not include the background category class 0).
100- The value/values should be no less than 0. Defaults to None.
101- soft_label: whether the target contains non-binary values (soft labels) or not.
102- If True a soft label formulation of the loss will be used.
103-
104- Raises:
105- TypeError: When ``other_act`` is not an ``Optional[Callable]``.
106- ValueError: When more than 1 of [``sigmoid=True``, ``softmax=True``, ``other_act is not None``].
107- Incompatible values.
108-
73+ Args follow standard MONAI DiceLoss with the addition of:
74+ ignore_index: Specifies a target value that is ignored and does not contribute to the input gradient.
10975 """
11076 super ().__init__ (reduction = LossReduction (reduction ).value )
11177 if other_act is not None and not callable (other_act ):
@@ -126,29 +92,13 @@ def __init__(
12692 self .register_buffer ("class_weight" , weight )
12793 self .class_weight : None | torch .Tensor
12894 self .soft_label = soft_label
95+ self .ignore_index = ignore_index
12996
13097 def forward (self , input : torch .Tensor , target : torch .Tensor ) -> torch .Tensor :
13198 """
13299 Args:
133100 input: the shape should be BNH[WD], where N is the number of classes.
134101 target: the shape should be BNH[WD] or B1H[WD], where N is the number of classes.
135-
136- Raises:
137- AssertionError: When input and target (after one hot transform if set)
138- have different shapes.
139- ValueError: When ``self.reduction`` is not one of ["mean", "sum", "none"].
140-
141- Example:
142- >>> from monai.losses.dice import * # NOQA
143- >>> import torch
144- >>> from monai.losses.dice import DiceLoss
145- >>> B, C, H, W = 7, 5, 3, 2
146- >>> input = torch.rand(B, C, H, W)
147- >>> target_idx = torch.randint(low=0, high=C - 1, size=(B, H, W)).long()
148- >>> target = one_hot(target_idx[:, None, ...], num_classes=C)
149- >>> self = DiceLoss(reduction='none')
150- >>> loss = self(input, target)
151- >>> assert np.broadcast_shapes(loss.shape, input.shape) == input.shape
152102 """
153103 if self .sigmoid :
154104 input = torch .sigmoid (input )
@@ -163,6 +113,17 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
163113 if self .other_act is not None :
164114 input = self .other_act (input )
165115
116+ # --- UPDATED MASKING LOGIC ---
117+ valid_mask = None
118+ if self .ignore_index is not None :
119+ if target .shape [1 ] == 1 :
120+ # Target is already in index format (B1HW)
121+ valid_mask = (target != self .ignore_index ).to (input .dtype )
122+ else :
123+ # Target is in one-hot format (BNHW), extract indices using argmax
124+ target_idx = target .argmax (dim = 1 , keepdim = True )
125+ valid_mask = (target_idx != self .ignore_index ).to (input .dtype )
126+
166127 if self .to_onehot_y :
167128 if n_pred_ch == 1 :
168129 warnings .warn ("single channel prediction, `to_onehot_y=True` ignored." , stacklevel = 2 )
@@ -173,17 +134,21 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
173134 if n_pred_ch == 1 :
174135 warnings .warn ("single channel prediction, `include_background=False` ignored." , stacklevel = 2 )
175136 else :
176- # if skipping background, removing first channel
177137 target = target [:, 1 :]
178138 input = input [:, 1 :]
139+ # Background exclusion does not affect our spatial valid_mask (B1HW)
179140
180141 if target .shape != input .shape :
181142 raise AssertionError (f"ground truth has different shape ({ target .shape } ) from input ({ input .shape } )" )
182143
183- # reducing only spatial dimensions (not batch nor channels)
144+ # Apply spatial mask across all channels using broadcasting
145+ if valid_mask is not None :
146+ input = input * valid_mask
147+ target = target * valid_mask
148+ # ------------------------------
149+
184150 reduce_axis : list [int ] = torch .arange (2 , len (input .shape )).tolist ()
185151 if self .batch :
186- # reducing spatial dimensions and batch
187152 reduce_axis = [0 ] + reduce_axis
188153
189154 ord = 2 if self .squared_pred else 1
@@ -198,7 +163,6 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
198163
199164 num_of_classes = target .shape [1 ]
200165 if self .class_weight is not None and num_of_classes != 1 :
201- # make sure the lengths of weights are equal to the number of classes
202166 if self .class_weight .ndim == 0 :
203167 self .class_weight = torch .as_tensor ([self .class_weight ] * num_of_classes )
204168 else :
@@ -209,16 +173,13 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
209173 )
210174 if self .class_weight .min () < 0 :
211175 raise ValueError ("the value/values of the `weight` should be no less than 0." )
212- # apply class_weight to loss
213176 f = f * self .class_weight .to (f )
214177
215178 if self .reduction == LossReduction .MEAN .value :
216- f = torch .mean (f ) # the batch and channel average
179+ f = torch .mean (f )
217180 elif self .reduction == LossReduction .SUM .value :
218- f = torch .sum (f ) # sum over the batch and channel dims
181+ f = torch .sum (f )
219182 elif self .reduction == LossReduction .NONE .value :
220- # If we are not computing voxelwise loss components at least
221- # make sure a none reduction maintains a broadcastable shape
222183 broadcast_shape = list (f .shape [0 :2 ]) + [1 ] * (len (input .shape ) - 2 )
223184 f = f .view (broadcast_shape )
224185 else :
0 commit comments