From 2d449b4e24844e8e9979047ac0d4dc44ce96bd56 Mon Sep 17 00:00:00 2001 From: gasperp Date: Tue, 14 Mar 2023 17:22:51 +0000 Subject: [PATCH 01/23] added spacing to surface distances calculations --- monai/metrics/hausdorff_distance.py | 25 ++++++++++++++++++++----- monai/metrics/surface_dice.py | 16 +++++++++++++--- monai/metrics/surface_distance.py | 19 ++++++++++++++++--- monai/metrics/utils.py | 10 ++++++++-- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index bba9301dd7..ed6e83156d 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -76,7 +76,7 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor: # type: ignore[override] + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None) -> torch.Tensor: # type: ignore[override] """ Args: y_pred: input data to compute, typical segmentation model output. @@ -84,6 +84,7 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor should be binarized. y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. Raises: ValueError: when `y` is not a binarized tensor. @@ -95,6 +96,13 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor dims = y_pred.ndimension() if dims < 3: raise ValueError("y_pred should have at least three dimensions.") + + if spacing is not None: + # dims - 2 because we don't want to include the batch and channel dimension + assert ( + len(spacing) == y_pred.dim() - 2 + ), "spacing should have the same length as ``y_pred`` without batch and channel dimensions" + # compute (BxC) for each channel for each batch return compute_hausdorff_distance( y_pred=y_pred, @@ -103,6 +111,7 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor distance_metric=self.distance_metric, percentile=self.percentile, directed=self.directed, + spacing=spacing, ) def aggregate( @@ -133,6 +142,7 @@ def compute_hausdorff_distance( distance_metric: str = "euclidean", percentile: float | None = None, directed: bool = False, + spacing: float | list | np.ndarray | None = None, ) -> torch.Tensor: """ Compute the Hausdorff distance. @@ -151,6 +161,7 @@ def compute_hausdorff_distance( percentile of the Hausdorff Distance rather than the maximum result will be achieved. Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. """ if not include_background: @@ -170,23 +181,27 @@ def compute_hausdorff_distance( if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") - distance_1 = compute_percent_hausdorff_distance(edges_pred, edges_gt, distance_metric, percentile) + distance_1 = compute_percent_hausdorff_distance(edges_pred, edges_gt, distance_metric, percentile, spacing) if directed: hd[b, c] = distance_1 else: - distance_2 = compute_percent_hausdorff_distance(edges_gt, edges_pred, distance_metric, percentile) + distance_2 = compute_percent_hausdorff_distance(edges_gt, edges_pred, distance_metric, percentile, spacing) hd[b, c] = max(distance_1, distance_2) return convert_data_type(hd, output_type=torch.Tensor, device=y_pred.device, dtype=torch.float)[0] def compute_percent_hausdorff_distance( - edges_pred: np.ndarray, edges_gt: np.ndarray, distance_metric: str = "euclidean", percentile: float | None = None + edges_pred: np.ndarray, + edges_gt: np.ndarray, + distance_metric: str = "euclidean", + percentile: float | None = None, + spacing: float | list | np.ndarray | None = None, ) -> float: """ This function is used to compute the directed Hausdorff distance. """ - surface_distance = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric) + surface_distance = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing) # for both pred and gt do not have foreground if surface_distance.shape == (0,): diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index 12c47dec8d..3798c69e51 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -67,13 +67,14 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor: # type: ignore[override] + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None) -> torch.Tensor: # type: ignore[override] r""" Args: y_pred: Predicted segmentation, typically segmentation model output. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. y: Reference segmentation. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. Returns: Pytorch Tensor of shape [B,C], containing the NSD values :math:`\operatorname {NSD}_{b,c}` for each batch @@ -85,6 +86,7 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor class_thresholds=self.class_thresholds, include_background=self.include_background, distance_metric=self.distance_metric, + spacing=spacing, ) def aggregate( @@ -117,6 +119,7 @@ def compute_surface_dice( class_thresholds: list[float], include_background: bool = False, distance_metric: str = "euclidean", + spacing: float | list | np.ndarray | None = None, ) -> torch.Tensor: r""" This function computes the (Normalized) Surface Dice (NSD) between the two tensors `y_pred` (referred to as @@ -167,6 +170,7 @@ def compute_surface_dice( distance_metric: The metric used to compute surface distances. One of [``"euclidean"``, ``"chessboard"``, ``"taxicab"``]. Defaults to ``"euclidean"``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. Raises: ValueError: If `y_pred` and/or `y` are not PyTorch tensors. @@ -196,6 +200,12 @@ def compute_surface_dice( f"y_pred and y should have same shape, but instead, shapes are {y_pred.shape} (y_pred) and {y.shape} (y)." ) + if spacing is not None: + # dims - 2 because we don't want to include the batch and channel dimension + assert ( + len(spacing) == y_pred.dim() - 2 + ), "spacing should have the same length as ``y_pred`` without batch and channel dimensions" + if not torch.all(y_pred.byte() == y_pred) or not torch.all(y.byte() == y): raise ValueError("y_pred and y should be binarized tensors (e.g. torch.int64).") if torch.any(y_pred > 1) or torch.any(y > 1): @@ -226,8 +236,8 @@ def compute_surface_dice( if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") - distances_pred_gt = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric) - distances_gt_pred = get_surface_distance(edges_gt, edges_pred, distance_metric=distance_metric) + distances_pred_gt = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing) + distances_gt_pred = get_surface_distance(edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing) boundary_complete = len(distances_pred_gt) + len(distances_gt_pred) boundary_correct = np.sum(distances_pred_gt <= class_thresholds[c]) + np.sum( diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index f1b4979466..bc29df5729 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -69,7 +69,7 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor: # type: ignore[override] + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None) -> torch.Tensor: # type: ignore[override] """ Args: y_pred: input data to compute, typical segmentation model output. @@ -77,6 +77,7 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor should be binarized. y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. Raises: ValueError: when `y` is not a binarized tensor. @@ -86,6 +87,13 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor is_binary_tensor(y, "y") if y_pred.dim() < 3: raise ValueError("y_pred should have at least three dimensions.") + + if spacing is not None: + # dims - 2 because we don't want to include the batch and channel dimension + assert ( + len(spacing) == y_pred.dim() - 2 + ), "spacing should have the same length as ``y_pred`` without batch and channel dimensions" + # compute (BxC) for each channel for each batch return compute_average_surface_distance( y_pred=y_pred, @@ -93,6 +101,7 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor include_background=self.include_background, symmetric=self.symmetric, distance_metric=self.distance_metric, + spacing=spacing, ) def aggregate( @@ -122,6 +131,7 @@ def compute_average_surface_distance( include_background: bool = False, symmetric: bool = False, distance_metric: str = "euclidean", + spacing: float | list | np.ndarray | None = None, ) -> torch.Tensor: """ This function is used to compute the Average Surface Distance from `y_pred` to `y` @@ -142,6 +152,7 @@ def compute_average_surface_distance( `seg_pred` and `seg_gt`. Defaults to ``False``. distance_metric: : [``"euclidean"``, ``"chessboard"``, ``"taxicab"``] the metric used to compute surface distance. Defaults to ``"euclidean"``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. """ if not include_background: @@ -162,9 +173,11 @@ def compute_average_surface_distance( warnings.warn(f"the ground truth of class {c} is all 0, this may result in nan/inf distance.") if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") - surface_distance = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric) + surface_distance = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing) if symmetric: - surface_distance_2 = get_surface_distance(edges_gt, edges_pred, distance_metric=distance_metric) + surface_distance_2 = get_surface_distance( + edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing + ) surface_distance = np.concatenate([surface_distance, surface_distance_2]) asd[b, c] = np.nan if surface_distance.shape == (0,) else surface_distance.mean() diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index d0b5c28744..23f0aa455c 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -172,7 +172,12 @@ def get_mask_edges( return edges_pred, edges_gt -def get_surface_distance(seg_pred: np.ndarray, seg_gt: np.ndarray, distance_metric: str = "euclidean") -> np.ndarray: +def get_surface_distance( + seg_pred: np.ndarray, + seg_gt: np.ndarray, + distance_metric: str = "euclidean", + spacing: float | list | np.ndarray | None = None, +) -> np.ndarray: """ This function is used to compute the surface distances from `seg_pred` to `seg_gt`. @@ -185,6 +190,7 @@ def get_surface_distance(seg_pred: np.ndarray, seg_gt: np.ndarray, distance_metr - ``"euclidean"``, uses Exact Euclidean distance transform. - ``"chessboard"``, uses `chessboard` metric in chamfer type of transform. - ``"taxicab"``, uses `taxicab` metric in chamfer type of transform. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. Note: If seg_pred or seg_gt is all 0, may result in nan/inf distance. @@ -198,7 +204,7 @@ def get_surface_distance(seg_pred: np.ndarray, seg_gt: np.ndarray, distance_metr dis = np.inf * np.ones_like(seg_gt) return np.asarray(dis[seg_gt]) if distance_metric == "euclidean": - dis = distance_transform_edt(~seg_gt) + dis = distance_transform_edt(~seg_gt, sampling=spacing) elif distance_metric in {"chessboard", "taxicab"}: dis = distance_transform_cdt(~seg_gt, metric=distance_metric) else: From 26dd22cc41312e4df723af255d62c1493c6f18c0 Mon Sep 17 00:00:00 2001 From: gasperp Date: Tue, 14 Mar 2023 18:55:49 +0000 Subject: [PATCH 02/23] code formatting --- monai/metrics/hausdorff_distance.py | 8 ++++++-- monai/metrics/surface_dice.py | 15 ++++++++++++--- monai/metrics/surface_distance.py | 8 ++++++-- monai/metrics/utils.py | 4 +++- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index ed6e83156d..cd93335148 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -84,7 +84,9 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float should be binarized. y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to + the image dimensions; if a single number, this is used for all axes. If ``None``, + spacing of unity is used. Defaults to ``None``. Raises: ValueError: when `y` is not a binarized tensor. @@ -161,7 +163,9 @@ def compute_hausdorff_distance( percentile of the Hausdorff Distance rather than the maximum result will be achieved. Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal + to the image dimensions; if a single number, this is used for all axes. If ``None``, + spacing of unity is used. Defaults to ``None``. """ if not include_background: diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index 3798c69e51..76cab9687a 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -67,14 +67,21 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None) -> torch.Tensor: # type: ignore[override] + def _compute_tensor( + self, + y_pred: torch.Tensor, + y: torch.Tensor, + spacing: float | list | np.ndarray | None = None + ) -> torch.Tensor: r""" Args: y_pred: Predicted segmentation, typically segmentation model output. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. y: Reference segmentation. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, + must be of length equal to the image dimensions; if a single number, + this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. Returns: Pytorch Tensor of shape [B,C], containing the NSD values :math:`\operatorname {NSD}_{b,c}` for each batch @@ -170,7 +177,9 @@ def compute_surface_dice( distance_metric: The metric used to compute surface distances. One of [``"euclidean"``, ``"chessboard"``, ``"taxicab"``]. Defaults to ``"euclidean"``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the + image dimensions; if a single number, this is used for all axes. If ``None``, + spacing of unity is used. Defaults to ``None``. Raises: ValueError: If `y_pred` and/or `y` are not PyTorch tensors. diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index bc29df5729..1a051a9685 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -77,7 +77,9 @@ def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float should be binarized. y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to + the image dimensions; if a single number, this is used for all axes. If ``None``, + spacing of unity is used. Defaults to ``None``. Raises: ValueError: when `y` is not a binarized tensor. @@ -152,7 +154,9 @@ def compute_average_surface_distance( `seg_pred` and `seg_gt`. Defaults to ``False``. distance_metric: : [``"euclidean"``, ``"chessboard"``, ``"taxicab"``] the metric used to compute surface distance. Defaults to ``"euclidean"``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to + the image dimensions; if a single number, this is used for all axes. If ``None``, + spacing of unity is used. Defaults to ``None``. """ if not include_background: diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index 23f0aa455c..43067ece43 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -190,7 +190,9 @@ def get_surface_distance( - ``"euclidean"``, uses Exact Euclidean distance transform. - ``"chessboard"``, uses `chessboard` metric in chamfer type of transform. - ``"taxicab"``, uses `taxicab` metric in chamfer type of transform. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of + length equal to the image dimensions; if a single number, this is used for all axes. + If ``None``, spacing of unity is used. Defaults to ``None``. Note: If seg_pred or seg_gt is all 0, may result in nan/inf distance. From 753253cab89599bfb4b1167b3f36c20192fbe2c0 Mon Sep 17 00:00:00 2001 From: gasperp Date: Tue, 14 Mar 2023 19:09:07 +0000 Subject: [PATCH 03/23] deleted unnecessary whitespaces --- monai/metrics/hausdorff_distance.py | 9 +++++++-- monai/metrics/surface_dice.py | 6 +++--- monai/metrics/surface_distance.py | 7 ++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index cd93335148..1735bc6638 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -76,7 +76,12 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None) -> torch.Tensor: # type: ignore[override] + def _compute_tensor( + self, + y_pred: torch.Tensor, + y: torch.Tensor, + spacing: float | list | np.ndarray | None = None + ) -> torch.Tensor: """ Args: y_pred: input data to compute, typical segmentation model output. @@ -164,7 +169,7 @@ def compute_hausdorff_distance( Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal - to the image dimensions; if a single number, this is used for all axes. If ``None``, + to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. """ diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index 76cab9687a..01512a1673 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -68,9 +68,9 @@ def __init__( self.get_not_nans = get_not_nans def _compute_tensor( - self, - y_pred: torch.Tensor, - y: torch.Tensor, + self, + y_pred: torch.Tensor, + y: torch.Tensor, spacing: float | list | np.ndarray | None = None ) -> torch.Tensor: r""" diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index 1a051a9685..ec7ccb5be8 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -69,7 +69,12 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None) -> torch.Tensor: # type: ignore[override] + def _compute_tensor( + self, + y_pred: torch.Tensor, + y: torch.Tensor, + spacing: float | list | np.ndarray | None = None + ) -> torch.Tensor: """ Args: y_pred: input data to compute, typical segmentation model output. From 4b15604657df45c8ba752ae10ca8f4d94f2a0165 Mon Sep 17 00:00:00 2001 From: gasperp Date: Tue, 14 Mar 2023 21:10:59 +0000 Subject: [PATCH 04/23] formatted with black --- monai/metrics/hausdorff_distance.py | 5 +---- monai/metrics/surface_dice.py | 5 +---- monai/metrics/surface_distance.py | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 1735bc6638..29ee4afeb2 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -77,10 +77,7 @@ def __init__( self.get_not_nans = get_not_nans def _compute_tensor( - self, - y_pred: torch.Tensor, - y: torch.Tensor, - spacing: float | list | np.ndarray | None = None + self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None ) -> torch.Tensor: """ Args: diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index 01512a1673..c2a47e255c 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -68,10 +68,7 @@ def __init__( self.get_not_nans = get_not_nans def _compute_tensor( - self, - y_pred: torch.Tensor, - y: torch.Tensor, - spacing: float | list | np.ndarray | None = None + self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None ) -> torch.Tensor: r""" Args: diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index ec7ccb5be8..f5ea54b79b 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -70,10 +70,7 @@ def __init__( self.get_not_nans = get_not_nans def _compute_tensor( - self, - y_pred: torch.Tensor, - y: torch.Tensor, - spacing: float | list | np.ndarray | None = None + self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None ) -> torch.Tensor: """ Args: From 5d7bb441f3572befc16d14dd9908db6c555bd394 Mon Sep 17 00:00:00 2001 From: gasperp Date: Tue, 14 Mar 2023 21:24:26 +0000 Subject: [PATCH 05/23] minor --- monai/metrics/hausdorff_distance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 29ee4afeb2..22c7dcf3a9 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -165,7 +165,7 @@ def compute_hausdorff_distance( percentile of the Hausdorff Distance rather than the maximum result will be achieved. Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. """ From 65436a93f2d556201a0530bb67f9b2fb75835ce1 Mon Sep 17 00:00:00 2001 From: gasperp Date: Tue, 14 Mar 2023 21:24:49 +0000 Subject: [PATCH 06/23] DCO Remediation Commit for gasperp I, gasperp , hereby add my Signed-off-by to this commit: 2d449b4e24844e8e9979047ac0d4dc44ce96bd56 I, gasperp , hereby add my Signed-off-by to this commit: 26dd22cc41312e4df723af255d62c1493c6f18c0 I, gasperp , hereby add my Signed-off-by to this commit: 753253cab89599bfb4b1167b3f36c20192fbe2c0 Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 22c7dcf3a9..29ee4afeb2 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -165,7 +165,7 @@ def compute_hausdorff_distance( percentile of the Hausdorff Distance rather than the maximum result will be achieved. Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. """ From 6cf33bf3867adf269191348df03df1c328930275 Mon Sep 17 00:00:00 2001 From: gasperp Date: Tue, 14 Mar 2023 21:28:59 +0000 Subject: [PATCH 07/23] minor Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 29ee4afeb2..22c7dcf3a9 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -165,7 +165,7 @@ def compute_hausdorff_distance( percentile of the Hausdorff Distance rather than the maximum result will be achieved. Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. """ From 448a211d97aa3195b79edd0272993ba85c3f03f5 Mon Sep 17 00:00:00 2001 From: gasperp Date: Tue, 14 Mar 2023 21:29:23 +0000 Subject: [PATCH 08/23] DCO Remediation Commit for gasperp I, gasperp , hereby add my Signed-off-by to this commit: 4b15604657df45c8ba752ae10ca8f4d94f2a0165 I, gasperp , hereby add my Signed-off-by to this commit: 5d7bb441f3572befc16d14dd9908db6c555bd394 Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 22c7dcf3a9..29ee4afeb2 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -165,7 +165,7 @@ def compute_hausdorff_distance( percentile of the Hausdorff Distance rather than the maximum result will be achieved. Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal + spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. """ From bb53664afe1568d8ff2a3e27e4bb54d4aff9cdf1 Mon Sep 17 00:00:00 2001 From: gasperp Date: Wed, 15 Mar 2023 10:51:27 +0000 Subject: [PATCH 09/23] removed checker for length of spacing parameter Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 6 ------ monai/metrics/surface_dice.py | 6 ------ monai/metrics/surface_distance.py | 6 ------ 3 files changed, 18 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 29ee4afeb2..46582a4fe0 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -101,12 +101,6 @@ def _compute_tensor( if dims < 3: raise ValueError("y_pred should have at least three dimensions.") - if spacing is not None: - # dims - 2 because we don't want to include the batch and channel dimension - assert ( - len(spacing) == y_pred.dim() - 2 - ), "spacing should have the same length as ``y_pred`` without batch and channel dimensions" - # compute (BxC) for each channel for each batch return compute_hausdorff_distance( y_pred=y_pred, diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index c2a47e255c..b24533df3a 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -206,12 +206,6 @@ def compute_surface_dice( f"y_pred and y should have same shape, but instead, shapes are {y_pred.shape} (y_pred) and {y.shape} (y)." ) - if spacing is not None: - # dims - 2 because we don't want to include the batch and channel dimension - assert ( - len(spacing) == y_pred.dim() - 2 - ), "spacing should have the same length as ``y_pred`` without batch and channel dimensions" - if not torch.all(y_pred.byte() == y_pred) or not torch.all(y.byte() == y): raise ValueError("y_pred and y should be binarized tensors (e.g. torch.int64).") if torch.any(y_pred > 1) or torch.any(y > 1): diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index f5ea54b79b..568ab327d8 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -92,12 +92,6 @@ def _compute_tensor( if y_pred.dim() < 3: raise ValueError("y_pred should have at least three dimensions.") - if spacing is not None: - # dims - 2 because we don't want to include the batch and channel dimension - assert ( - len(spacing) == y_pred.dim() - 2 - ), "spacing should have the same length as ``y_pred`` without batch and channel dimensions" - # compute (BxC) for each channel for each batch return compute_average_surface_distance( y_pred=y_pred, From 5c8a4a3e71d90abae9d77c74f13101271b01157b Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 18:26:31 +0000 Subject: [PATCH 10/23] spacing parameter can now be passed to metric call via kwargs Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 47 +++++++++++++++++------- monai/metrics/metric.py | 23 +++++++----- monai/metrics/surface_dice.py | 53 ++++++++++++++++++++------- monai/metrics/surface_distance.py | 50 ++++++++++++++++++++------ monai/metrics/utils.py | 56 ++++++++++++++++++++++++++++- 5 files changed, 184 insertions(+), 45 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 46582a4fe0..e9a28778ad 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -12,6 +12,7 @@ from __future__ import annotations import warnings +from collections.abc import Sequence import numpy as np import torch @@ -22,6 +23,7 @@ get_surface_distance, ignore_background, is_binary_tensor, + prepare_spacing, ) from monai.utils import MetricReduction, convert_data_type @@ -77,8 +79,11 @@ def __init__( self.get_not_nans = get_not_nans def _compute_tensor( - self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None - ) -> torch.Tensor: + self, + y_pred: torch.Tensor, + y: torch.Tensor, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, + ) -> torch.Tensor: # type: ignore[override] """ Args: y_pred: input data to compute, typical segmentation model output. @@ -86,9 +91,15 @@ def _compute_tensor( should be binarized. y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to - the image dimensions; if a single number, this is used for all axes. If ``None``, - spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + Several input options are allowed: + - If a single number, isotropic spacing with that value is used for all images in the batch. + - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. + - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If + inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. + - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. Raises: ValueError: when `y` is not a binarized tensor. @@ -140,7 +151,7 @@ def compute_hausdorff_distance( distance_metric: str = "euclidean", percentile: float | None = None, directed: bool = False, - spacing: float | list | np.ndarray | None = None, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, ) -> torch.Tensor: """ Compute the Hausdorff distance. @@ -159,9 +170,15 @@ def compute_hausdorff_distance( percentile of the Hausdorff Distance rather than the maximum result will be achieved. Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal - to the image dimensions; if a single number, this is used for all axes. If ``None``, - spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + Several input options are allowed: + - If a single number, isotropic spacing with that value is used for all images in the batch. + - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. + - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If + inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. + - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. """ if not include_background: @@ -174,6 +191,10 @@ def compute_hausdorff_distance( batch_size, n_class = y_pred.shape[:2] hd = np.empty((batch_size, n_class)) + + img_dim = y_pred.ndim - 2 + spacing = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) + for b, c in np.ndindex(batch_size, n_class): (edges_pred, edges_gt) = get_mask_edges(y_pred[b, c], y[b, c]) if not np.any(edges_gt): @@ -181,11 +202,13 @@ def compute_hausdorff_distance( if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") - distance_1 = compute_percent_hausdorff_distance(edges_pred, edges_gt, distance_metric, percentile, spacing) + distance_1 = compute_percent_hausdorff_distance(edges_pred, edges_gt, distance_metric, percentile, spacing[b]) if directed: hd[b, c] = distance_1 else: - distance_2 = compute_percent_hausdorff_distance(edges_gt, edges_pred, distance_metric, percentile, spacing) + distance_2 = compute_percent_hausdorff_distance( + edges_gt, edges_pred, distance_metric, percentile, spacing[b] + ) hd[b, c] = max(distance_1, distance_2) return convert_data_type(hd, output_type=torch.Tensor, device=y_pred.device, dtype=torch.float)[0] @@ -195,7 +218,7 @@ def compute_percent_hausdorff_distance( edges_gt: np.ndarray, distance_metric: str = "euclidean", percentile: float | None = None, - spacing: float | list | np.ndarray | None = None, + spacing: int | float | list | np.ndarray | None = None, ) -> float: """ This function is used to compute the directed Hausdorff distance. diff --git a/monai/metrics/metric.py b/monai/metrics/metric.py index 608d914808..a6dc1a49a2 100644 --- a/monai/metrics/metric.py +++ b/monai/metrics/metric.py @@ -49,7 +49,7 @@ class IterationMetric(Metric): """ def __call__( - self, y_pred: TensorOrList, y: TensorOrList | None = None + self, y_pred: TensorOrList, y: TensorOrList | None = None, **kwargs: Any ) -> torch.Tensor | Sequence[torch.Tensor | Sequence[torch.Tensor]]: """ Execute basic computation for model prediction `y_pred` and ground truth `y` (optional). @@ -60,6 +60,7 @@ def __call__( or a `batch-first` Tensor. y: the ground truth to compute, must be a list of `channel-first` Tensor or a `batch-first` Tensor. + kwargs: additional parameters for specific metric computation logic (e.g. ``spacing`` for SurfaceDistanceMetric, etc.). Returns: The computed metric values at the iteration level. @@ -69,15 +70,15 @@ def __call__( """ # handling a list of channel-first data if isinstance(y_pred, (list, tuple)) or isinstance(y, (list, tuple)): - return self._compute_list(y_pred, y) + return self._compute_list(y_pred, y, **kwargs) # handling a single batch-first data if isinstance(y_pred, torch.Tensor): y_ = y.detach() if isinstance(y, torch.Tensor) else None - return self._compute_tensor(y_pred.detach(), y_) + return self._compute_tensor(y_pred.detach(), y_, **kwargs) raise ValueError("y_pred or y must be a list/tuple of `channel-first` Tensors or a `batch-first` Tensor.") def _compute_list( - self, y_pred: TensorOrList, y: TensorOrList | None = None + self, y_pred: TensorOrList, y: TensorOrList | None = None, **kwargs: Any ) -> torch.Tensor | list[torch.Tensor | Sequence[torch.Tensor]]: """ Execute the metric computation for `y_pred` and `y` in a list of "channel-first" tensors. @@ -93,9 +94,12 @@ def _compute_list( Note: subclass may enhance the operation to have multi-thread support. """ if y is not None: - ret = [self._compute_tensor(p.detach().unsqueeze(0), y_.detach().unsqueeze(0)) for p, y_ in zip(y_pred, y)] + ret = [ + self._compute_tensor(p.detach().unsqueeze(0), y_.detach().unsqueeze(0), **kwargs) + for p, y_ in zip(y_pred, y) + ] else: - ret = [self._compute_tensor(p_.detach().unsqueeze(0), None) for p_ in y_pred] + ret = [self._compute_tensor(p_.detach().unsqueeze(0), None, **kwargs) for p_ in y_pred] # concat the list of results (e.g. a batch of evaluation scores) if isinstance(ret[0], torch.Tensor): @@ -106,7 +110,7 @@ def _compute_list( return ret @abstractmethod - def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor | None = None) -> TensorOrList: + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor | None = None, **kwargs: Any) -> TensorOrList: """ Computation logic for `y_pred` and `y` of an iteration, the data should be "batch-first" Tensors. A subclass should implement its own computation logic. @@ -318,7 +322,7 @@ class CumulativeIterationMetric(Cumulative, IterationMetric): """ def __call__( - self, y_pred: TensorOrList, y: TensorOrList | None = None + self, y_pred: TensorOrList, y: TensorOrList | None = None, **kwargs: Any ) -> torch.Tensor | Sequence[torch.Tensor | Sequence[torch.Tensor]]: """ Execute basic computation for model prediction and ground truth. @@ -331,12 +335,13 @@ def __call__( or a `batch-first` Tensor. y: the ground truth to compute, must be a list of `channel-first` Tensor or a `batch-first` Tensor. + kwargs: additional parameters for specific metric computation logic (e.g. ``spacing`` for SurfaceDistanceMetric, etc.). Returns: The computed metric values at the iteration level. The output shape should be a `batch-first` tensor (BC[HWD]) or a list of `batch-first` tensors. """ - ret = super().__call__(y_pred=y_pred, y=y) + ret = super().__call__(y_pred=y_pred, y=y, **kwargs) if isinstance(ret, (tuple, list)): self.extend(*ret) else: diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index b24533df3a..00adffab1c 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -12,11 +12,18 @@ from __future__ import annotations import warnings +from collections.abc import Sequence import numpy as np import torch -from monai.metrics.utils import do_metric_reduction, get_mask_edges, get_surface_distance, ignore_background +from monai.metrics.utils import ( + do_metric_reduction, + get_mask_edges, + get_surface_distance, + ignore_background, + prepare_spacing, +) from monai.utils import MetricReduction, convert_data_type from .metric import CumulativeIterationMetric @@ -68,17 +75,26 @@ def __init__( self.get_not_nans = get_not_nans def _compute_tensor( - self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None - ) -> torch.Tensor: + self, + y_pred: torch.Tensor, + y: torch.Tensor, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, + ) -> torch.Tensor: # type: ignore[override] r""" Args: y_pred: Predicted segmentation, typically segmentation model output. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. y: Reference segmentation. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, - must be of length equal to the image dimensions; if a single number, - this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + Several input options are allowed: + - If a single number, isotropic spacing with that value is used for all images in the batch. + - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. + - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If + inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. + - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. Returns: Pytorch Tensor of shape [B,C], containing the NSD values :math:`\operatorname {NSD}_{b,c}` for each batch @@ -123,7 +139,7 @@ def compute_surface_dice( class_thresholds: list[float], include_background: bool = False, distance_metric: str = "euclidean", - spacing: float | list | np.ndarray | None = None, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, ) -> torch.Tensor: r""" This function computes the (Normalized) Surface Dice (NSD) between the two tensors `y_pred` (referred to as @@ -174,9 +190,15 @@ def compute_surface_dice( distance_metric: The metric used to compute surface distances. One of [``"euclidean"``, ``"chessboard"``, ``"taxicab"``]. Defaults to ``"euclidean"``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the - image dimensions; if a single number, this is used for all axes. If ``None``, - spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + Several input options are allowed: + - If a single number, isotropic spacing with that value is used for all images in the batch. + - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. + - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If + inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. + - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. Raises: ValueError: If `y_pred` and/or `y` are not PyTorch tensors. @@ -229,6 +251,9 @@ def compute_surface_dice( nsd = np.empty((batch_size, n_class)) + img_dim = y_pred.ndim - 2 + spacing = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) + for b, c in np.ndindex(batch_size, n_class): (edges_pred, edges_gt) = get_mask_edges(y_pred[b, c], y[b, c], crop=False) if not np.any(edges_gt): @@ -236,8 +261,12 @@ def compute_surface_dice( if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") - distances_pred_gt = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing) - distances_gt_pred = get_surface_distance(edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing) + distances_pred_gt = get_surface_distance( + edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing[b] + ) + distances_gt_pred = get_surface_distance( + edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing[b] + ) boundary_complete = len(distances_pred_gt) + len(distances_gt_pred) boundary_correct = np.sum(distances_pred_gt <= class_thresholds[c]) + np.sum( diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index 568ab327d8..7b185ee38d 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -12,6 +12,7 @@ from __future__ import annotations import warnings +from collections.abc import Sequence import numpy as np import torch @@ -22,6 +23,7 @@ get_surface_distance, ignore_background, is_binary_tensor, + prepare_spacing, ) from monai.utils import MetricReduction, convert_data_type @@ -70,8 +72,11 @@ def __init__( self.get_not_nans = get_not_nans def _compute_tensor( - self, y_pred: torch.Tensor, y: torch.Tensor, spacing: float | list | np.ndarray | None = None - ) -> torch.Tensor: + self, + y_pred: torch.Tensor, + y: torch.Tensor, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, + ) -> torch.Tensor: # type: ignore[override] """ Args: y_pred: input data to compute, typical segmentation model output. @@ -79,9 +84,15 @@ def _compute_tensor( should be binarized. y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to - the image dimensions; if a single number, this is used for all axes. If ``None``, - spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + Several input options are allowed: + - If a single number, isotropic spacing with that value is used for all images in the batch. + - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. + - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If + inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. + - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. Raises: ValueError: when `y` is not a binarized tensor. @@ -129,7 +140,7 @@ def compute_average_surface_distance( include_background: bool = False, symmetric: bool = False, distance_metric: str = "euclidean", - spacing: float | list | np.ndarray | None = None, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, ) -> torch.Tensor: """ This function is used to compute the Average Surface Distance from `y_pred` to `y` @@ -150,9 +161,15 @@ def compute_average_surface_distance( `seg_pred` and `seg_gt`. Defaults to ``False``. distance_metric: : [``"euclidean"``, ``"chessboard"``, ``"taxicab"``] the metric used to compute surface distance. Defaults to ``"euclidean"``. - spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to - the image dimensions; if a single number, this is used for all axes. If ``None``, - spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + Several input options are allowed: + - If a single number, isotropic spacing with that value is used for all images in the batch. + - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. + - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If + inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. + - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. """ if not include_background: @@ -167,16 +184,27 @@ def compute_average_surface_distance( batch_size, n_class = y_pred.shape[:2] asd = np.empty((batch_size, n_class)) + img_dim = y_pred.ndim - 2 + spacing = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) + for b, c in np.ndindex(batch_size, n_class): (edges_pred, edges_gt) = get_mask_edges(y_pred[b, c], y[b, c]) if not np.any(edges_gt): warnings.warn(f"the ground truth of class {c} is all 0, this may result in nan/inf distance.") if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") - surface_distance = get_surface_distance(edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing) + surface_distance = get_surface_distance( + edges_pred, + edges_gt, + distance_metric=distance_metric, + spacing=spacing[b], + ) if symmetric: surface_distance_2 = get_surface_distance( - edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing + edges_gt, + edges_pred, + distance_metric=distance_metric, + spacing=spacing[b], ) surface_distance = np.concatenate([surface_distance, surface_distance_2]) asd[b, c] = np.nan if surface_distance.shape == (0,) else surface_distance.mean() diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index 43067ece43..ac16373848 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -13,6 +13,7 @@ import warnings from typing import Any +from collections.abc import Sequence import numpy as np import torch @@ -176,7 +177,7 @@ def get_surface_distance( seg_pred: np.ndarray, seg_gt: np.ndarray, distance_metric: str = "euclidean", - spacing: float | list | np.ndarray | None = None, + spacing: int | float | np.ndarray | Sequence[int | float] | None = None, ) -> np.ndarray: """ This function is used to compute the surface distances from `seg_pred` to `seg_gt`. @@ -193,6 +194,11 @@ def get_surface_distance( spacing: spacing of pixel (or voxel) along each axis. If a sequence, must be of length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. + spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + Several input options are allowed: + - If a single number, isotropic spacing with that value is used. + - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. + - If ``None``, spacing of unity is used. Defaults to ``None``. Note: If seg_pred or seg_gt is all 0, may result in nan/inf distance. @@ -269,3 +275,51 @@ def remap_instance_id(pred: torch.Tensor, by_size: bool = False) -> torch.Tensor for idx, instance_id in enumerate(pred_id): new_pred[pred == instance_id] = idx + 1 return new_pred + + +def prepare_spacing( + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None, + batch_size: int, + img_dim: int, +): + """ + This function is used to prepare the `spacing` parameter to include batch dimension for the computation of + surface distance, hausdorff distance or surface dice. + + An example with batch_size = 4 and img_dim = 3: + input spacing = None -> output spacing = [None, None, None, None] + input spacing = 0.8 -> output spacing = [0.8, 0.8, 0.8, 0.8] + input spacing = [0.8, 0.5, 0.9] -> output spacing = [[0.8, 0.5, 0.9], [0.8, 0.5, 0.9], [0.8, 0.5, 0.9], [0.8, 0.5, 0.9]] + input spacing = [0.8, 0.7, 1.2, 0.8] -> output spacing = [0.8, 0.7, 1.2, 0.8] (same as input) + + Args: + spacing: can be a float, a sequence of length `img_dim`, or a sequence with length `batch_size` + that includes floats or sequences of length `img_dim`. + + Raises: + AssertionError: when `spacing` is a sequence of sequence, where the outer sequence length does not + equal `batch_size` or inner sequence length does not equal `img_dim`. + + Returns: + spacing: a sequence with length `batch_size` that includes integers, floats or sequences of length `img_dim`. + """ + if spacing is None or isinstance(spacing, (int, float)): + spacing = [spacing] * batch_size + elif isinstance(spacing, (tuple, list, np.ndarray)): + if isinstance(spacing[0], (tuple, list, np.ndarray)): + assert ( + len(spacing) == batch_size + ), "if spacing is a sequence of sequences, the outer sequence should have same length as batch size." + assert ( + len(spacing[0]) == 1 or len(spacing[0]) == img_dim + ), "each element of spacing list should either have length of one of same as image dim." + else: + assert ( + len(spacing) == img_dim + ), "if spacing is a sequence of numbers, it should have same length as image dim." + spacing = [spacing] * batch_size + else: + raise AssertionError( + "spacing should either be an integer, float, a sequence of numbers or a sequence of sequences." + ) + return spacing From f963ea0e7e6a042e80b153a62eee98cb47babc20 Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 18:27:20 +0000 Subject: [PATCH 11/23] added tests that include spacing parameter Signed-off-by: gasperp --- tests/test_hausdorff_distance.py | 77 +++++++++++++++++++++++----- tests/test_surface_dice.py | 61 ++++++++++++++++++++++ tests/test_surface_distance.py | 87 ++++++++++++++++++++++++++------ 3 files changed, 197 insertions(+), 28 deletions(-) diff --git a/tests/test_hausdorff_distance.py b/tests/test_hausdorff_distance.py index 40f5b187d0..46c2fc95a2 100644 --- a/tests/test_hausdorff_distance.py +++ b/tests/test_hausdorff_distance.py @@ -23,7 +23,10 @@ def create_spherical_seg_3d( - radius: float = 20.0, centre: tuple[int, int, int] = (49, 49, 49), im_shape: tuple[int, int, int] = (99, 99, 99) + radius: float = 20.0, + centre: tuple[int, int, int] = (49, 49, 49), + im_shape: tuple[int, int, int] = (99, 99, 99), + im_spacing: tuple[float, float, float] = (1.0, 1.0, 1.0), ) -> np.ndarray: """ Return a 3D image with a sphere inside. Voxel values will be @@ -32,16 +35,23 @@ def create_spherical_seg_3d( Args: radius: radius of sphere (in terms of number of voxels, can be partial) centre: location of sphere centre. - im_shape: shape of image to create + im_shape: shape of image to create. + im_spacing: spacing of image to create. See also: :py:meth:`~create_test_image_3d` """ # Create image image = np.zeros(im_shape, dtype=np.int32) - spy, spx, spz = np.ogrid[ - -centre[0] : im_shape[0] - centre[0], -centre[1] : im_shape[1] - centre[1], -centre[2] : im_shape[2] - centre[2] - ] + spy, spx, spz = np.ogrid[: im_shape[0], : im_shape[1], : im_shape[2]] + spy = spy.astype(float) * im_spacing[0] + spx = spx.astype(float) * im_spacing[1] + spz = spz.astype(float) * im_spacing[2] + + spy -= centre[0] + spx -= centre[1] + spz -= centre[2] + circle = (spx * spx + spy * spy + spz * spz) <= radius * radius image[circle] = 1 @@ -49,12 +59,22 @@ def create_spherical_seg_3d( return image +test_spacing = (0.85, 1.2, 0.9) TEST_CASES = [ - [[create_spherical_seg_3d(), create_spherical_seg_3d(), 1], [0, 0, 0, 0, 0, 0]], + [ + [ + create_spherical_seg_3d(), + create_spherical_seg_3d(), + None, + 1, + ], + [0, 0, 0, 0, 0, 0], + ], [ [ create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), create_spherical_seg_3d(radius=20, centre=(19, 19, 19)), + None, ], [1.7320508075688772, 1.7320508075688772, 1, 1, 3, 3], ], @@ -62,6 +82,7 @@ def create_spherical_seg_3d( [ create_spherical_seg_3d(radius=33, centre=(19, 33, 22)), create_spherical_seg_3d(radius=33, centre=(20, 33, 22)), + None, ], [1, 1, 1, 1, 1, 1], ], @@ -69,6 +90,7 @@ def create_spherical_seg_3d( [ create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + None, ], [20.09975124224178, 20.223748416156685, 15, 20, 24, 35], ], @@ -77,6 +99,7 @@ def create_spherical_seg_3d( # pred does not have foreground (but gt has), the metric should be inf np.zeros([99, 99, 99]), create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + None, ], [np.inf, np.inf, np.inf, np.inf, np.inf, np.inf], ], @@ -85,6 +108,7 @@ def create_spherical_seg_3d( # gt does not have foreground (but pred has), the metric should be inf create_spherical_seg_3d(), np.zeros([99, 99, 99]), + None, ], [np.inf, np.inf, np.inf, np.inf, np.inf, np.inf], ], @@ -92,20 +116,47 @@ def create_spherical_seg_3d( [ create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + None, 95, ], [19.924858845171276, 20.09975124224178, 14, 18, 22, 33], ], + [ + [ + create_spherical_seg_3d(radius=20, centre=(20, 20, 20), im_spacing=test_spacing), + create_spherical_seg_3d(radius=20, centre=(19, 19, 19), im_spacing=test_spacing), + test_spacing, + ], + [2.0808651447296143, 2.2671568, 2, 2, 3, 4], + # np.sqrt((np.array(test_spacing)**2).sum()) + ], + [ + [ + create_spherical_seg_3d(radius=15, centre=(20, 33, 22), im_spacing=test_spacing), + create_spherical_seg_3d(radius=30, centre=(20, 33, 22), im_spacing=test_spacing), + test_spacing, + ], + [15.439640998840332, 15.62594, 11, 17, 20, 28], + ], ] TEST_CASES_NANS = [ + [ + [ + # both pred and gt do not have foreground, spacing is None, metric and not_nans should be 0 + np.zeros([99, 99, 99]), + np.zeros([99, 99, 99]), + None, + ] + ], [ [ # both pred and gt do not have foreground, metric and not_nans should be 0 np.zeros([99, 99, 99]), np.zeros([99, 99, 99]), + test_spacing, ] - ] + ], ] @@ -113,10 +164,10 @@ class TestHausdorffDistance(unittest.TestCase): @parameterized.expand(TEST_CASES) def test_value(self, input_data, expected_value): percentile = None - if len(input_data) == 3: - [seg_1, seg_2, percentile] = input_data + if len(input_data) == 4: + [seg_1, seg_2, spacing, percentile] = input_data else: - [seg_1, seg_2] = input_data + [seg_1, seg_2, spacing] = input_data ct = 0 seg_1 = torch.tensor(seg_1, device=_device) seg_2 = torch.tensor(seg_2, device=_device) @@ -129,7 +180,7 @@ def test_value(self, input_data, expected_value): batch, n_class = 2, 3 batch_seg_1 = seg_1.unsqueeze(0).unsqueeze(0).repeat([batch, n_class, 1, 1, 1]) batch_seg_2 = seg_2.unsqueeze(0).unsqueeze(0).repeat([batch, n_class, 1, 1, 1]) - hd_metric(batch_seg_1, batch_seg_2) + hd_metric(batch_seg_1, batch_seg_2, spacing=spacing) result = hd_metric.aggregate(reduction="mean") expected_value_curr = expected_value[ct] np.testing.assert_allclose(expected_value_curr, result.cpu(), rtol=1e-7) @@ -138,13 +189,13 @@ def test_value(self, input_data, expected_value): @parameterized.expand(TEST_CASES_NANS) def test_nans(self, input_data): - [seg_1, seg_2] = input_data + [seg_1, seg_2, spacing] = input_data seg_1 = torch.tensor(seg_1) seg_2 = torch.tensor(seg_2) hd_metric = HausdorffDistanceMetric(include_background=False, get_not_nans=True) batch_seg_1 = seg_1.unsqueeze(0).unsqueeze(0) batch_seg_2 = seg_2.unsqueeze(0).unsqueeze(0) - hd_metric(batch_seg_1, batch_seg_2) + hd_metric(batch_seg_1, batch_seg_2, spacing=spacing) result, not_nans = hd_metric.aggregate() np.testing.assert_allclose(0, result, rtol=1e-7) np.testing.assert_allclose(0, not_nans, rtol=1e-7) diff --git a/tests/test_surface_dice.py b/tests/test_surface_dice.py index 3ee54e5903..15e6245619 100644 --- a/tests/test_surface_dice.py +++ b/tests/test_surface_dice.py @@ -23,6 +23,67 @@ class TestAllSurfaceDiceMetrics(unittest.TestCase): + def test_tolerance_euclidean_distance_with_spacing(self): + batch_size = 2 + n_class = 2 + test_spacing = (0.85, 1.2) + predictions = torch.zeros((batch_size, 480, 640), dtype=torch.int64, device=_device) + labels = torch.zeros((batch_size, 480, 640), dtype=torch.int64, device=_device) + predictions[0, :, 50:] = 1 + labels[0, :, 60:] = 1 # 10 px shift + predictions_hot = F.one_hot(predictions, num_classes=n_class).permute(0, 3, 1, 2) + labels_hot = F.one_hot(labels, num_classes=n_class).permute(0, 3, 1, 2) + + sd0 = SurfaceDiceMetric(class_thresholds=[0, 0], include_background=True) + res0 = sd0(predictions_hot, labels_hot, spacing=test_spacing) + agg0 = sd0.aggregate() # aggregation: nanmean across image then nanmean across batch + sd0_nans = SurfaceDiceMetric(class_thresholds=[0, 0], include_background=True, get_not_nans=True) + res0_nans = sd0_nans(predictions_hot, labels_hot) + agg0_nans, not_nans = sd0_nans.aggregate() + + np.testing.assert_array_equal(res0.cpu(), res0_nans.cpu()) + np.testing.assert_equal(res0.device, predictions.device) + np.testing.assert_array_equal(agg0.cpu(), agg0_nans.cpu()) + np.testing.assert_equal(agg0.device, predictions.device) + + res1 = SurfaceDiceMetric(class_thresholds=[1, 1], include_background=True)( + predictions_hot, labels_hot, spacing=test_spacing + ) + res9 = SurfaceDiceMetric(class_thresholds=[9, 9], include_background=True)( + predictions_hot, labels_hot, spacing=test_spacing + ) + res10 = SurfaceDiceMetric(class_thresholds=[10, 10], include_background=True)( + predictions_hot, labels_hot, spacing=test_spacing + ) + res11 = SurfaceDiceMetric(class_thresholds=[11, 11], include_background=True)( + predictions_hot, labels_hot, spacing=test_spacing + ) + # because spacing is (0.85, 1.2) and we moved 10 pixels in the columns direction, + # everything with tolerance 12 or more should be the same as tolerance 12 (surface dice is 1.0) + res12 = SurfaceDiceMetric(class_thresholds=[12, 12], include_background=True)( + predictions_hot, labels_hot, spacing=test_spacing + ) + res13 = SurfaceDiceMetric(class_thresholds=[13, 13], include_background=True)( + predictions_hot, labels_hot, spacing=test_spacing + ) + + for res in [res0, res9, res10, res11, res12, res13]: + assert res.shape == torch.Size([2, 2]) + + assert res0[0, 0] < res1[0, 0] < res9[0, 0] < res10[0, 0] < res11[0, 0] + assert res0[0, 1] < res1[0, 1] < res9[0, 1] < res10[0, 1] < res11[0, 1] + np.testing.assert_array_equal(res12.cpu(), res13.cpu()) + + expected_res0 = np.zeros((batch_size, n_class)) + expected_res0[0, 1] = 1 - (478 + 480 + 9 * 2) / (480 * 4 + 588 * 2 + 578 * 2) + expected_res0[0, 0] = 1 - (478 + 480 + 9 * 2) / (480 * 4 + 48 * 2 + 58 * 2) + expected_res0[1, 0] = 1 + expected_res0[1, 1] = np.nan + for b, c in np.ndindex(batch_size, n_class): + np.testing.assert_allclose(expected_res0[b, c], res0[b, c].cpu()) + np.testing.assert_array_equal(agg0.cpu(), np.nanmean(np.nanmean(expected_res0, axis=1), axis=0)) + np.testing.assert_equal(not_nans.cpu(), torch.tensor(2)) + def test_tolerance_euclidean_distance(self): batch_size = 2 n_class = 2 diff --git a/tests/test_surface_distance.py b/tests/test_surface_distance.py index f2e2ea7144..c7a148073d 100644 --- a/tests/test_surface_distance.py +++ b/tests/test_surface_distance.py @@ -23,7 +23,10 @@ def create_spherical_seg_3d( - radius: float = 20.0, centre: tuple[int, int, int] = (49, 49, 49), im_shape: tuple[int, int, int] = (99, 99, 99) + radius: float = 20.0, + centre: tuple[int, int, int] = (49, 49, 49), + im_shape: tuple[int, int, int] = (99, 99, 99), + im_spacing: tuple[float, float, float] = (1.0, 1.0, 1.0), ) -> np.ndarray: """ Return a 3D image with a sphere inside. Voxel values will be @@ -32,16 +35,23 @@ def create_spherical_seg_3d( Args: radius: radius of sphere (in terms of number of voxels, can be partial) centre: location of sphere centre. - im_shape: shape of image to create + im_shape: shape of image to create. + im_spacing: spacing of image to create. See also: :py:meth:`~create_test_image_3d` """ # Create image image = np.zeros(im_shape, dtype=np.int32) - spy, spx, spz = np.ogrid[ - -centre[0] : im_shape[0] - centre[0], -centre[1] : im_shape[1] - centre[1], -centre[2] : im_shape[2] - centre[2] - ] + spy, spx, spz = np.ogrid[: im_shape[0], : im_shape[1], : im_shape[2]] + spy -= centre[0] + spx -= centre[1] + spz -= centre[2] + + spy = spy.astype(float) * im_spacing[0] + spx = spx.astype(float) * im_spacing[1] + spz = spz.astype(float) * im_spacing[2] + circle = (spx * spx + spy * spy + spz * spz) <= radius * radius image[circle] = 1 @@ -49,8 +59,17 @@ def create_spherical_seg_3d( return image +test_spacing = (0.85, 1.2, 0.9) TEST_CASES = [ - [[create_spherical_seg_3d(), create_spherical_seg_3d()], [0, 0]], + [ + [ + create_spherical_seg_3d(), + create_spherical_seg_3d(), + "euclidean", + None, + ], + [0, 0], + ], [ [ create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), @@ -63,6 +82,8 @@ def create_spherical_seg_3d( [ create_spherical_seg_3d(radius=33, centre=(19, 33, 22)), create_spherical_seg_3d(radius=33, centre=(20, 33, 22)), + "euclidean", + None, ], [0.350217, 0.3483278807706289], ], @@ -70,6 +91,8 @@ def create_spherical_seg_3d( [ create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + "euclidean", + None, ], [15.117741, 12.040033513150455], ], @@ -89,18 +112,51 @@ def create_spherical_seg_3d( ], [20.214613, 12.432687531048186], ], - [[np.zeros([99, 99, 99]), create_spherical_seg_3d(radius=40, centre=(20, 33, 22))], [np.inf, np.inf]], - [[create_spherical_seg_3d(), np.zeros([99, 99, 99]), "taxicab"], [np.inf, np.inf]], + [ + [ + np.zeros([99, 99, 99]), + create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + "euclidean", + None, + ], + [np.inf, np.inf], + ], + [ + [ + create_spherical_seg_3d(), + np.zeros([99, 99, 99]), + "taxicab", + ], + [np.inf, np.inf], + ], + [ + [ + create_spherical_seg_3d(radius=33, centre=(42, 45, 52), im_spacing=test_spacing), + create_spherical_seg_3d(radius=33, centre=(43, 45, 52), im_spacing=test_spacing), + "euclidean", + test_spacing, + ], + [0.431094, 0.431094], + ], ] TEST_CASES_NANS = [ [ [ - # both pred and gt do not have foreground, metric and not_nans should be 0 + # both pred and gt do not have foreground, spacing is None, metric and not_nans should be 0 np.zeros([99, 99, 99]), np.zeros([99, 99, 99]), + None, ] - ] + ], + [ + [ + # both pred and gt do not have foreground, spacing is not None, metric and not_nans should be 0 + np.zeros([99, 99, 99]), + np.zeros([99, 99, 99]), + test_spacing, + ] + ], ] @@ -109,9 +165,10 @@ class TestAllSurfaceMetrics(unittest.TestCase): def test_value(self, input_data, expected_value): if len(input_data) == 3: [seg_1, seg_2, metric] = input_data + spacing = None else: - [seg_1, seg_2] = input_data - metric = "euclidean" + [seg_1, seg_2, metric, spacing] = input_data + ct = 0 seg_1 = torch.tensor(seg_1, device=_device) seg_2 = torch.tensor(seg_2, device=_device) @@ -121,7 +178,7 @@ def test_value(self, input_data, expected_value): batch, n_class = 2, 3 batch_seg_1 = seg_1.unsqueeze(0).unsqueeze(0).repeat([batch, n_class, 1, 1, 1]) batch_seg_2 = seg_2.unsqueeze(0).unsqueeze(0).repeat([batch, n_class, 1, 1, 1]) - sur_metric(batch_seg_1, batch_seg_2) + sur_metric(batch_seg_1, batch_seg_2, spacing=spacing) result = sur_metric.aggregate() expected_value_curr = expected_value[ct] np.testing.assert_allclose(expected_value_curr, result.cpu(), rtol=1e-5) @@ -130,14 +187,14 @@ def test_value(self, input_data, expected_value): @parameterized.expand(TEST_CASES_NANS) def test_nans(self, input_data): - [seg_1, seg_2] = input_data + [seg_1, seg_2, spacing] = input_data seg_1 = torch.tensor(seg_1) seg_2 = torch.tensor(seg_2) sur_metric = SurfaceDistanceMetric(include_background=False, get_not_nans=True) # test list of channel-first Tensor batch_seg_1 = [seg_1.unsqueeze(0)] batch_seg_2 = [seg_2.unsqueeze(0)] - sur_metric(batch_seg_1, batch_seg_2) + sur_metric(batch_seg_1, batch_seg_2, spacing=spacing) result, not_nans = sur_metric.aggregate(reduction="mean") np.testing.assert_allclose(0, result, rtol=1e-5) np.testing.assert_allclose(0, not_nans, rtol=1e-5) From 99a73dfa9c736007d19b0342c46f9c249dac8f17 Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 18:58:44 +0000 Subject: [PATCH 12/23] fixed isort error Signed-off-by: gasperp --- monai/metrics/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index ac16373848..288b1ff1a4 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -12,8 +12,8 @@ from __future__ import annotations import warnings -from typing import Any from collections.abc import Sequence +from typing import Any import numpy as np import torch From 66383aaeb498f860fd905b4e3812c3e9514a3af0 Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 18:59:27 +0000 Subject: [PATCH 13/23] minor fix Signed-off-by: gasperp --- tests/test_surface_distance.py | 145 +++++++++++++++++---------------- 1 file changed, 73 insertions(+), 72 deletions(-) diff --git a/tests/test_surface_distance.py b/tests/test_surface_distance.py index c7a148073d..291eeca148 100644 --- a/tests/test_surface_distance.py +++ b/tests/test_surface_distance.py @@ -44,14 +44,15 @@ def create_spherical_seg_3d( # Create image image = np.zeros(im_shape, dtype=np.int32) spy, spx, spz = np.ogrid[: im_shape[0], : im_shape[1], : im_shape[2]] - spy -= centre[0] - spx -= centre[1] - spz -= centre[2] spy = spy.astype(float) * im_spacing[0] spx = spx.astype(float) * im_spacing[1] spz = spz.astype(float) * im_spacing[2] + spy -= centre[0] + spx -= centre[1] + spz -= centre[2] + circle = (spx * spx + spy * spy + spz * spz) <= radius * radius image[circle] = 1 @@ -61,74 +62,74 @@ def create_spherical_seg_3d( test_spacing = (0.85, 1.2, 0.9) TEST_CASES = [ - [ - [ - create_spherical_seg_3d(), - create_spherical_seg_3d(), - "euclidean", - None, - ], - [0, 0], - ], - [ - [ - create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), - create_spherical_seg_3d(radius=20, centre=(19, 19, 19)), - "taxicab", - ], - [1.0380029806259314, 1.0380029806259314], - ], - [ - [ - create_spherical_seg_3d(radius=33, centre=(19, 33, 22)), - create_spherical_seg_3d(radius=33, centre=(20, 33, 22)), - "euclidean", - None, - ], - [0.350217, 0.3483278807706289], - ], - [ - [ - create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), - create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - "euclidean", - None, - ], - [15.117741, 12.040033513150455], - ], - [ - [ - create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), - create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - "chessboard", - ], - [11.492719, 9.605067064083457], - ], - [ - [ - create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), - create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - "taxicab", - ], - [20.214613, 12.432687531048186], - ], - [ - [ - np.zeros([99, 99, 99]), - create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - "euclidean", - None, - ], - [np.inf, np.inf], - ], - [ - [ - create_spherical_seg_3d(), - np.zeros([99, 99, 99]), - "taxicab", - ], - [np.inf, np.inf], - ], + # [ + # [ + # create_spherical_seg_3d(), + # create_spherical_seg_3d(), + # "euclidean", + # None, + # ], + # [0, 0], + # ], + # [ + # [ + # create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), + # create_spherical_seg_3d(radius=20, centre=(19, 19, 19)), + # "taxicab", + # ], + # [1.0380029806259314, 1.0380029806259314], + # ], + # [ + # [ + # create_spherical_seg_3d(radius=33, centre=(19, 33, 22)), + # create_spherical_seg_3d(radius=33, centre=(20, 33, 22)), + # "euclidean", + # None, + # ], + # [0.350217, 0.3483278807706289], + # ], + # [ + # [ + # create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), + # create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + # "euclidean", + # None, + # ], + # [15.117741, 12.040033513150455], + # ], + # [ + # [ + # create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), + # create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + # "chessboard", + # ], + # [11.492719, 9.605067064083457], + # ], + # [ + # [ + # create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), + # create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + # "taxicab", + # ], + # [20.214613, 12.432687531048186], + # ], + # [ + # [ + # np.zeros([99, 99, 99]), + # create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + # "euclidean", + # None, + # ], + # [np.inf, np.inf], + # ], + # [ + # [ + # create_spherical_seg_3d(), + # np.zeros([99, 99, 99]), + # "taxicab", + # ], + # [np.inf, np.inf], + # ], [ [ create_spherical_seg_3d(radius=33, centre=(42, 45, 52), im_spacing=test_spacing), @@ -136,7 +137,7 @@ def create_spherical_seg_3d( "euclidean", test_spacing, ], - [0.431094, 0.431094], + [0.4951, 0.4951], ], ] From f30e355ff0bf184219fd6aeab37080a04ebb53f2 Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 19:01:33 +0000 Subject: [PATCH 14/23] minor Signed-off-by: gasperp --- tests/test_hausdorff_distance.py | 1 - tests/test_surface_distance.py | 136 +++++++++++++++---------------- 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/tests/test_hausdorff_distance.py b/tests/test_hausdorff_distance.py index 46c2fc95a2..70153250aa 100644 --- a/tests/test_hausdorff_distance.py +++ b/tests/test_hausdorff_distance.py @@ -128,7 +128,6 @@ def create_spherical_seg_3d( test_spacing, ], [2.0808651447296143, 2.2671568, 2, 2, 3, 4], - # np.sqrt((np.array(test_spacing)**2).sum()) ], [ [ diff --git a/tests/test_surface_distance.py b/tests/test_surface_distance.py index 291eeca148..0d09dbd0ce 100644 --- a/tests/test_surface_distance.py +++ b/tests/test_surface_distance.py @@ -62,74 +62,74 @@ def create_spherical_seg_3d( test_spacing = (0.85, 1.2, 0.9) TEST_CASES = [ - # [ - # [ - # create_spherical_seg_3d(), - # create_spherical_seg_3d(), - # "euclidean", - # None, - # ], - # [0, 0], - # ], - # [ - # [ - # create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), - # create_spherical_seg_3d(radius=20, centre=(19, 19, 19)), - # "taxicab", - # ], - # [1.0380029806259314, 1.0380029806259314], - # ], - # [ - # [ - # create_spherical_seg_3d(radius=33, centre=(19, 33, 22)), - # create_spherical_seg_3d(radius=33, centre=(20, 33, 22)), - # "euclidean", - # None, - # ], - # [0.350217, 0.3483278807706289], - # ], - # [ - # [ - # create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), - # create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - # "euclidean", - # None, - # ], - # [15.117741, 12.040033513150455], - # ], - # [ - # [ - # create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), - # create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - # "chessboard", - # ], - # [11.492719, 9.605067064083457], - # ], - # [ - # [ - # create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), - # create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - # "taxicab", - # ], - # [20.214613, 12.432687531048186], - # ], - # [ - # [ - # np.zeros([99, 99, 99]), - # create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - # "euclidean", - # None, - # ], - # [np.inf, np.inf], - # ], - # [ - # [ - # create_spherical_seg_3d(), - # np.zeros([99, 99, 99]), - # "taxicab", - # ], - # [np.inf, np.inf], - # ], + [ + [ + create_spherical_seg_3d(), + create_spherical_seg_3d(), + "euclidean", + None, + ], + [0, 0], + ], + [ + [ + create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), + create_spherical_seg_3d(radius=20, centre=(19, 19, 19)), + "taxicab", + ], + [1.0380029806259314, 1.0380029806259314], + ], + [ + [ + create_spherical_seg_3d(radius=33, centre=(19, 33, 22)), + create_spherical_seg_3d(radius=33, centre=(20, 33, 22)), + "euclidean", + None, + ], + [0.350217, 0.3483278807706289], + ], + [ + [ + create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), + create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + "euclidean", + None, + ], + [15.117741, 12.040033513150455], + ], + [ + [ + create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), + create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + "chessboard", + ], + [11.492719, 9.605067064083457], + ], + [ + [ + create_spherical_seg_3d(radius=20, centre=(20, 33, 22)), + create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + "taxicab", + ], + [20.214613, 12.432687531048186], + ], + [ + [ + np.zeros([99, 99, 99]), + create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), + "euclidean", + None, + ], + [np.inf, np.inf], + ], + [ + [ + create_spherical_seg_3d(), + np.zeros([99, 99, 99]), + "taxicab", + ], + [np.inf, np.inf], + ], [ [ create_spherical_seg_3d(radius=33, centre=(42, 45, 52), im_spacing=test_spacing), From 20b9c02dbf2445a8dec069041100c50d67f9faa0 Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 19:21:02 +0000 Subject: [PATCH 15/23] fixed formatting Signed-off-by: gasperp --- monai/metrics/surface_distance.py | 10 ++-------- tests/test_hausdorff_distance.py | 10 +--------- tests/test_surface_distance.py | 26 +++----------------------- 3 files changed, 6 insertions(+), 40 deletions(-) diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index 7b185ee38d..a8ee05208b 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -194,17 +194,11 @@ def compute_average_surface_distance( if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") surface_distance = get_surface_distance( - edges_pred, - edges_gt, - distance_metric=distance_metric, - spacing=spacing[b], + edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing[b] ) if symmetric: surface_distance_2 = get_surface_distance( - edges_gt, - edges_pred, - distance_metric=distance_metric, - spacing=spacing[b], + edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing[b] ) surface_distance = np.concatenate([surface_distance, surface_distance_2]) asd[b, c] = np.nan if surface_distance.shape == (0,) else surface_distance.mean() diff --git a/tests/test_hausdorff_distance.py b/tests/test_hausdorff_distance.py index 70153250aa..a50b27b79e 100644 --- a/tests/test_hausdorff_distance.py +++ b/tests/test_hausdorff_distance.py @@ -61,15 +61,7 @@ def create_spherical_seg_3d( test_spacing = (0.85, 1.2, 0.9) TEST_CASES = [ - [ - [ - create_spherical_seg_3d(), - create_spherical_seg_3d(), - None, - 1, - ], - [0, 0, 0, 0, 0, 0], - ], + [[create_spherical_seg_3d(), create_spherical_seg_3d(), None, 1], [0, 0, 0, 0, 0, 0]], [ [ create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), diff --git a/tests/test_surface_distance.py b/tests/test_surface_distance.py index 0d09dbd0ce..81ddee107b 100644 --- a/tests/test_surface_distance.py +++ b/tests/test_surface_distance.py @@ -62,15 +62,7 @@ def create_spherical_seg_3d( test_spacing = (0.85, 1.2, 0.9) TEST_CASES = [ - [ - [ - create_spherical_seg_3d(), - create_spherical_seg_3d(), - "euclidean", - None, - ], - [0, 0], - ], + [[create_spherical_seg_3d(), create_spherical_seg_3d(), "euclidean", None], [0, 0]], [ [ create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), @@ -114,22 +106,10 @@ def create_spherical_seg_3d( [20.214613, 12.432687531048186], ], [ - [ - np.zeros([99, 99, 99]), - create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), - "euclidean", - None, - ], - [np.inf, np.inf], - ], - [ - [ - create_spherical_seg_3d(), - np.zeros([99, 99, 99]), - "taxicab", - ], + [np.zeros([99, 99, 99]), create_spherical_seg_3d(radius=40, centre=(20, 33, 22)), "euclidean", None], [np.inf, np.inf], ], + [[create_spherical_seg_3d(), np.zeros([99, 99, 99]), "taxicab"], [np.inf, np.inf]], [ [ create_spherical_seg_3d(radius=33, centre=(42, 45, 52), im_spacing=test_spacing), From aa2196ede3c99f7f0bcce456544fd37d4e7d6590 Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 19:50:30 +0000 Subject: [PATCH 16/23] minor Signed-off-by: gasperp --- monai/metrics/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index 288b1ff1a4..47eb2e2554 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -281,7 +281,7 @@ def prepare_spacing( spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None, batch_size: int, img_dim: int, -): +) -> Sequence[int | float | np.ndarray | Sequence[float]]: """ This function is used to prepare the `spacing` parameter to include batch dimension for the computation of surface distance, hausdorff distance or surface dice. From ed604046101f4749d627673340abbb953bab190c Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 20:11:19 +0000 Subject: [PATCH 17/23] fixed docs issue Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 28 ++++++++++++---------------- monai/metrics/surface_dice.py | 28 ++++++++++++---------------- monai/metrics/surface_distance.py | 28 ++++++++++++---------------- 3 files changed, 36 insertions(+), 48 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 147e722bbc..f57b4327f7 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -91,14 +91,12 @@ def _compute_tensor( y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. - Several input options are allowed: - - If a single number, isotropic spacing with that value is used for all images in the batch. - - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. - This spacing will be used for all images in the batch. - - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If - inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, - else the inner sequence length must be equal to the image dimensions. - - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. + If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, + the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + If a sequence of sequences, the length of the outer sequence must be equal to the batch size. + If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used + for all images in batch. Defaults to ``None``. Raises: ValueError: when `y_pred` has less than three dimensions. @@ -166,14 +164,12 @@ def compute_hausdorff_distance( Defaults to ``None``. directed: whether to calculate directed Hausdorff distance. Defaults to ``False``. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. - Several input options are allowed: - - If a single number, isotropic spacing with that value is used for all images in the batch. - - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. - This spacing will be used for all images in the batch. - - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If - inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, - else the inner sequence length must be equal to the image dimensions. - - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. + If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, + the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + If a sequence of sequences, the length of the outer sequence must be equal to the batch size. + If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used + for all images in batch. Defaults to ``None``. """ if not include_background: diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index 00adffab1c..40040aa9cb 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -87,14 +87,12 @@ def _compute_tensor( y: Reference segmentation. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. - Several input options are allowed: - - If a single number, isotropic spacing with that value is used for all images in the batch. - - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. - This spacing will be used for all images in the batch. - - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If - inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, - else the inner sequence length must be equal to the image dimensions. - - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. + If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, + the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + If a sequence of sequences, the length of the outer sequence must be equal to the batch size. + If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used + for all images in batch. Defaults to ``None``. Returns: Pytorch Tensor of shape [B,C], containing the NSD values :math:`\operatorname {NSD}_{b,c}` for each batch @@ -191,14 +189,12 @@ def compute_surface_dice( One of [``"euclidean"``, ``"chessboard"``, ``"taxicab"``]. Defaults to ``"euclidean"``. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. - Several input options are allowed: - - If a single number, isotropic spacing with that value is used for all images in the batch. - - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. - This spacing will be used for all images in the batch. - - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If - inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, - else the inner sequence length must be equal to the image dimensions. - - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. + If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, + the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + If a sequence of sequences, the length of the outer sequence must be equal to the batch size. + If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used + for all images in batch. Defaults to ``None``. Raises: ValueError: If `y_pred` and/or `y` are not PyTorch tensors. diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index 3c3cdae335..7fdece2049 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -84,14 +84,12 @@ def _compute_tensor( y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. - Several input options are allowed: - - If a single number, isotropic spacing with that value is used for all images in the batch. - - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. - This spacing will be used for all images in the batch. - - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If - inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, - else the inner sequence length must be equal to the image dimensions. - - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. + If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, + the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + If a sequence of sequences, the length of the outer sequence must be equal to the batch size. + If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used + for all images in batch. Defaults to ``None``. Raises: ValueError: when `y_pred` has less than three dimensions. @@ -158,14 +156,12 @@ def compute_average_surface_distance( distance_metric: : [``"euclidean"``, ``"chessboard"``, ``"taxicab"``] the metric used to compute surface distance. Defaults to ``"euclidean"``. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. - Several input options are allowed: - - If a single number, isotropic spacing with that value is used for all images in the batch. - - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. - This spacing will be used for all images in the batch. - - If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If - inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, - else the inner sequence length must be equal to the image dimensions. - - If ``None``, spacing of unity is used for all images in batch. Defaults to ``None``. + If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, + the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + If a sequence of sequences, the length of the outer sequence must be equal to the batch size. + If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, + else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used + for all images in batch. Defaults to ``None``. """ if not include_background: From 378d023f44feedb57621219f516e9574275fa718 Mon Sep 17 00:00:00 2001 From: gasperp Date: Mon, 27 Mar 2023 20:33:38 +0000 Subject: [PATCH 18/23] fixed line too long issue Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 3 ++- monai/metrics/surface_dice.py | 3 ++- monai/metrics/surface_distance.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index f57b4327f7..b4635e3f6f 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -92,7 +92,8 @@ def _compute_tensor( The values should be binarized. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, - the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index 40040aa9cb..ebbabbd112 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -88,7 +88,8 @@ def _compute_tensor( It must be a one-hot encoded, batch-first tensor [B,C,H,W]. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, - the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index 7fdece2049..64de1d6e73 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -85,7 +85,8 @@ def _compute_tensor( The values should be binarized. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, - the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. + the length of the sequence must be equal to the image dimensions. + This spacing will be used for all images in the batch. If a sequence of sequences, the length of the outer sequence must be equal to the batch size. If inner sequence has length 1, isotropic spacing with that value is used for all images in the batch, else the inner sequence length must be equal to the image dimensions. If ``None``, spacing of unity is used From 5f71c716a7ab037b9d8207772dd016c311bc2770 Mon Sep 17 00:00:00 2001 From: gasperp Date: Wed, 12 Apr 2023 09:40:32 +0000 Subject: [PATCH 19/23] fixed mypy issues Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 24 ++++++++--------- monai/metrics/loss_metric.py | 4 ++- monai/metrics/surface_dice.py | 22 +++++++-------- monai/metrics/surface_distance.py | 22 +++++++-------- monai/metrics/utils.py | 42 ++++++++++++++++++----------- 5 files changed, 61 insertions(+), 53 deletions(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index b4635e3f6f..c73c32ebf4 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -13,6 +13,7 @@ import warnings from collections.abc import Sequence +from typing import Any import numpy as np import torch @@ -77,12 +78,7 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor( - self, - y_pred: torch.Tensor, - y: torch.Tensor, - spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, - ) -> torch.Tensor: # type: ignore[override] + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, **kwargs: Any) -> torch.Tensor: # type: ignore[override] """ Args: y_pred: input data to compute, typical segmentation model output. @@ -90,7 +86,9 @@ def _compute_tensor( should be binarized. y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. - spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + kwargs: additional parameters, e.g. ``spacing`` should be passed to correctly compute the metric. + ``spacing``: spacing of pixel (or voxel). This parameter is relevant only + if ``distance_metric`` is set to ``"euclidean"``. If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. @@ -114,7 +112,7 @@ def _compute_tensor( distance_metric=self.distance_metric, percentile=self.percentile, directed=self.directed, - spacing=spacing, + spacing=kwargs.get("spacing"), ) def aggregate( @@ -145,7 +143,7 @@ def compute_hausdorff_distance( distance_metric: str = "euclidean", percentile: float | None = None, directed: bool = False, - spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[int | float]] | None = None, ) -> torch.Tensor: """ Compute the Hausdorff distance. @@ -185,7 +183,7 @@ def compute_hausdorff_distance( hd = np.empty((batch_size, n_class)) img_dim = y_pred.ndim - 2 - spacing = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) + spacing_list = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) for b, c in np.ndindex(batch_size, n_class): (edges_pred, edges_gt) = get_mask_edges(y_pred[b, c], y[b, c]) @@ -194,12 +192,12 @@ def compute_hausdorff_distance( if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") - distance_1 = compute_percent_hausdorff_distance(edges_pred, edges_gt, distance_metric, percentile, spacing[b]) + distance_1 = compute_percent_hausdorff_distance(edges_pred, edges_gt, distance_metric, percentile, spacing_list[b]) if directed: hd[b, c] = distance_1 else: distance_2 = compute_percent_hausdorff_distance( - edges_gt, edges_pred, distance_metric, percentile, spacing[b] + edges_gt, edges_pred, distance_metric, percentile, spacing_list[b] ) hd[b, c] = max(distance_1, distance_2) return convert_data_type(hd, output_type=torch.Tensor, device=y_pred.device, dtype=torch.float)[0] @@ -210,7 +208,7 @@ def compute_percent_hausdorff_distance( edges_gt: np.ndarray, distance_metric: str = "euclidean", percentile: float | None = None, - spacing: int | float | list | np.ndarray | None = None, + spacing: int | float | np.ndarray | Sequence[int | float] | None = None, ) -> float: """ This function is used to compute the directed Hausdorff distance. diff --git a/monai/metrics/loss_metric.py b/monai/metrics/loss_metric.py index 2cc9755e36..3136f42f4a 100644 --- a/monai/metrics/loss_metric.py +++ b/monai/metrics/loss_metric.py @@ -11,6 +11,8 @@ from __future__ import annotations +from typing import Any + import torch from torch.nn.modules.loss import _Loss @@ -92,7 +94,7 @@ def aggregate( f, not_nans = do_metric_reduction(data, reduction or self.reduction) return (f, not_nans) if self.get_not_nans else f - def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor | None = None) -> TensorOrList: + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor | None = None, **kwargs: Any) -> TensorOrList: """ Input `y_pred` is compared with ground truth `y`. Both `y_pred` and `y` are expected to be a batch-first Tensor (BC[HWD]). diff --git a/monai/metrics/surface_dice.py b/monai/metrics/surface_dice.py index ebbabbd112..ad0aeb332b 100644 --- a/monai/metrics/surface_dice.py +++ b/monai/metrics/surface_dice.py @@ -13,6 +13,7 @@ import warnings from collections.abc import Sequence +from typing import Any import numpy as np import torch @@ -74,19 +75,16 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor( - self, - y_pred: torch.Tensor, - y: torch.Tensor, - spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, - ) -> torch.Tensor: # type: ignore[override] + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, **kwargs: Any) -> torch.Tensor: # type: ignore[override] r""" Args: y_pred: Predicted segmentation, typically segmentation model output. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. y: Reference segmentation. It must be a one-hot encoded, batch-first tensor [B,C,H,W]. - spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + kwargs: additional parameters, e.g. ``spacing`` should be passed to correctly compute the metric. + ``spacing``: spacing of pixel (or voxel). This parameter is relevant only + if ``distance_metric`` is set to ``"euclidean"``. If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. @@ -105,7 +103,7 @@ def _compute_tensor( class_thresholds=self.class_thresholds, include_background=self.include_background, distance_metric=self.distance_metric, - spacing=spacing, + spacing=kwargs.get("spacing"), ) def aggregate( @@ -138,7 +136,7 @@ def compute_surface_dice( class_thresholds: list[float], include_background: bool = False, distance_metric: str = "euclidean", - spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[int | float]] | None = None, ) -> torch.Tensor: r""" This function computes the (Normalized) Surface Dice (NSD) between the two tensors `y_pred` (referred to as @@ -249,7 +247,7 @@ def compute_surface_dice( nsd = np.empty((batch_size, n_class)) img_dim = y_pred.ndim - 2 - spacing = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) + spacing_list = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) for b, c in np.ndindex(batch_size, n_class): (edges_pred, edges_gt) = get_mask_edges(y_pred[b, c], y[b, c], crop=False) @@ -259,10 +257,10 @@ def compute_surface_dice( warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") distances_pred_gt = get_surface_distance( - edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing[b] + edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing_list[b] ) distances_gt_pred = get_surface_distance( - edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing[b] + edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing_list[b] ) boundary_complete = len(distances_pred_gt) + len(distances_gt_pred) diff --git a/monai/metrics/surface_distance.py b/monai/metrics/surface_distance.py index 64de1d6e73..cb3fa89190 100644 --- a/monai/metrics/surface_distance.py +++ b/monai/metrics/surface_distance.py @@ -13,6 +13,7 @@ import warnings from collections.abc import Sequence +from typing import Any import numpy as np import torch @@ -70,12 +71,7 @@ def __init__( self.reduction = reduction self.get_not_nans = get_not_nans - def _compute_tensor( - self, - y_pred: torch.Tensor, - y: torch.Tensor, - spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, - ) -> torch.Tensor: # type: ignore[override] + def _compute_tensor(self, y_pred: torch.Tensor, y: torch.Tensor, **kwargs: Any) -> torch.Tensor: # type: ignore[override] """ Args: y_pred: input data to compute, typical segmentation model output. @@ -83,7 +79,9 @@ def _compute_tensor( should be binarized. y: ground truth to compute the distance. It must be one-hot format and first dim is batch. The values should be binarized. - spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. + kwargs: additional parameters, e.g. ``spacing`` should be passed to correctly compute the metric. + ``spacing``: spacing of pixel (or voxel). This parameter is relevant only + if ``distance_metric`` is set to ``"euclidean"``. If a single number, isotropic spacing with that value is used for all images in the batch. If a sequence of numbers, the length of the sequence must be equal to the image dimensions. This spacing will be used for all images in the batch. @@ -105,7 +103,7 @@ def _compute_tensor( include_background=self.include_background, symmetric=self.symmetric, distance_metric=self.distance_metric, - spacing=spacing, + spacing=kwargs.get("spacing"), ) def aggregate( @@ -135,7 +133,7 @@ def compute_average_surface_distance( include_background: bool = False, symmetric: bool = False, distance_metric: str = "euclidean", - spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None = None, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[int | float]] | None = None, ) -> torch.Tensor: """ This function is used to compute the Average Surface Distance from `y_pred` to `y` @@ -178,7 +176,7 @@ def compute_average_surface_distance( asd = np.empty((batch_size, n_class)) img_dim = y_pred.ndim - 2 - spacing = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) + spacing_list = prepare_spacing(spacing=spacing, batch_size=batch_size, img_dim=img_dim) for b, c in np.ndindex(batch_size, n_class): (edges_pred, edges_gt) = get_mask_edges(y_pred[b, c], y[b, c]) @@ -187,11 +185,11 @@ def compute_average_surface_distance( if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") surface_distance = get_surface_distance( - edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing[b] + edges_pred, edges_gt, distance_metric=distance_metric, spacing=spacing_list[b] ) if symmetric: surface_distance_2 = get_surface_distance( - edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing[b] + edges_gt, edges_pred, distance_metric=distance_metric, spacing=spacing_list[b] ) surface_distance = np.concatenate([surface_distance, surface_distance_2]) asd[b, c] = np.nan if surface_distance.shape == (0,) else surface_distance.mean() diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index 47eb2e2554..ab416e0b5a 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -13,7 +13,7 @@ import warnings from collections.abc import Sequence -from typing import Any +from typing import Any, cast, Union import numpy as np import torch @@ -278,10 +278,10 @@ def remap_instance_id(pred: torch.Tensor, by_size: bool = False) -> torch.Tensor def prepare_spacing( - spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[float]] | None, + spacing: int | float | np.ndarray | Sequence[int | float | np.ndarray | Sequence[int | float]] | None, batch_size: int, img_dim: int, -) -> Sequence[int | float | np.ndarray | Sequence[float]]: +) -> Sequence[None | int | float | np.ndarray | Sequence[int | float]]: """ This function is used to prepare the `spacing` parameter to include batch dimension for the computation of surface distance, hausdorff distance or surface dice. @@ -292,6 +292,9 @@ def prepare_spacing( input spacing = [0.8, 0.5, 0.9] -> output spacing = [[0.8, 0.5, 0.9], [0.8, 0.5, 0.9], [0.8, 0.5, 0.9], [0.8, 0.5, 0.9]] input spacing = [0.8, 0.7, 1.2, 0.8] -> output spacing = [0.8, 0.7, 1.2, 0.8] (same as input) + An example with batch_size = 3 and img_dim = 3: + input spacing = [0.8, 0.5, 0.9] -> output spacing = [[0.8, 0.5, 0.9], [0.8, 0.5, 0.9], [0.8, 0.5, 0.9], [0.8, 0.5, 0.9]] + Args: spacing: can be a float, a sequence of length `img_dim`, or a sequence with length `batch_size` that includes floats or sequences of length `img_dim`. @@ -304,22 +307,31 @@ def prepare_spacing( spacing: a sequence with length `batch_size` that includes integers, floats or sequences of length `img_dim`. """ if spacing is None or isinstance(spacing, (int, float)): - spacing = [spacing] * batch_size - elif isinstance(spacing, (tuple, list, np.ndarray)): - if isinstance(spacing[0], (tuple, list, np.ndarray)): + return list([spacing] * batch_size) + elif isinstance(spacing, (Sequence, np.ndarray)): + assert all( + [isinstance(s, type(spacing[0])) for s in list(spacing)] + ), "if `spacing` is a sequence, its elements should be of same type." + + if isinstance(spacing[0], (Sequence, np.ndarray)): assert ( len(spacing) == batch_size - ), "if spacing is a sequence of sequences, the outer sequence should have same length as batch size." - assert ( - len(spacing[0]) == 1 or len(spacing[0]) == img_dim - ), "each element of spacing list should either have length of one of same as image dim." - else: + ), "if `spacing` is a sequence of sequences, the outer sequence should have same length as batch size." + assert all( + [len(s) == img_dim for s in list(spacing)] + ), "each element of `spacing` list should either have same length as image dim." + assert all( + [isinstance(i, (int, float)) for s in list(spacing) for i in list(s)] + ), "if `spacing` is a sequence of sequences or 2D np.ndarray, the elements should be integers or floats." + return list(spacing) + elif isinstance(spacing[0], (int, float)): assert ( len(spacing) == img_dim - ), "if spacing is a sequence of numbers, it should have same length as image dim." - spacing = [spacing] * batch_size + ), "if `spacing` is a sequence of numbers, it should have same length as image dim." + return cast(Sequence[Union[np.ndarray, Sequence[Union[int, float]]]], [spacing for _ in range(batch_size)]) + else: + raise AssertionError(f"`spacing` is a sequence of elements with unsupported type: {type(spacing[0])}") else: raise AssertionError( - "spacing should either be an integer, float, a sequence of numbers or a sequence of sequences." + "`spacing` should either be an integer, float, a sequence of numbers or a sequence of sequences." ) - return spacing From 280cd9e01e8bfe9b4346c831a7df92df418ccf36 Mon Sep 17 00:00:00 2001 From: gasperp Date: Wed, 12 Apr 2023 09:44:47 +0000 Subject: [PATCH 20/23] minor isort fix Signed-off-by: gasperp --- monai/metrics/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index ab416e0b5a..be435cd89b 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -13,7 +13,7 @@ import warnings from collections.abc import Sequence -from typing import Any, cast, Union +from typing import Any, Union, cast import numpy as np import torch From 658259aaf535ee9f054d2d7ece758ca282d8468e Mon Sep 17 00:00:00 2001 From: gasperp Date: Wed, 12 Apr 2023 09:46:50 +0000 Subject: [PATCH 21/23] minor black fix Signed-off-by: gasperp --- monai/metrics/hausdorff_distance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index c73c32ebf4..b98d87baef 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -192,7 +192,9 @@ def compute_hausdorff_distance( if not np.any(edges_pred): warnings.warn(f"the prediction of class {c} is all 0, this may result in nan/inf distance.") - distance_1 = compute_percent_hausdorff_distance(edges_pred, edges_gt, distance_metric, percentile, spacing_list[b]) + distance_1 = compute_percent_hausdorff_distance( + edges_pred, edges_gt, distance_metric, percentile, spacing_list[b] + ) if directed: hd[b, c] = distance_1 else: From f992e31b2f2b83525c91104fafc003d9612c3c4c Mon Sep 17 00:00:00 2001 From: gasperp Date: Wed, 12 Apr 2023 12:13:38 +0000 Subject: [PATCH 22/23] minor doc improvement Signed-off-by: gasperp --- monai/metrics/utils.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index be435cd89b..57ba04c8d6 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -195,10 +195,9 @@ def get_surface_distance( length equal to the image dimensions; if a single number, this is used for all axes. If ``None``, spacing of unity is used. Defaults to ``None``. spacing: spacing of pixel (or voxel). This parameter is relevant only if ``distance_metric`` is set to ``"euclidean"``. - Several input options are allowed: - - If a single number, isotropic spacing with that value is used. - - If a sequence of numbers, the length of the sequence must be equal to the image dimensions. - - If ``None``, spacing of unity is used. Defaults to ``None``. + Several input options are allowed: (1) If a single number, isotropic spacing with that value is used. + (2) If a sequence of numbers, the length of the sequence must be equal to the image dimensions. + (3) If ``None``, spacing of unity is used. Defaults to ``None``. Note: If seg_pred or seg_gt is all 0, may result in nan/inf distance. @@ -328,7 +327,7 @@ def prepare_spacing( assert ( len(spacing) == img_dim ), "if `spacing` is a sequence of numbers, it should have same length as image dim." - return cast(Sequence[Union[np.ndarray, Sequence[Union[int, float]]]], [spacing for _ in range(batch_size)]) + return [spacing for _ in range(batch_size)] # type: ignore else: raise AssertionError(f"`spacing` is a sequence of elements with unsupported type: {type(spacing[0])}") else: From de816d25147d718da90e014a7c3c8f0e16362326 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 12 Apr 2023 12:29:36 +0000 Subject: [PATCH 23/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- monai/metrics/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index 57ba04c8d6..f585cfd9aa 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -13,7 +13,7 @@ import warnings from collections.abc import Sequence -from typing import Any, Union, cast +from typing import Any import numpy as np import torch