def store_binary_mask_as_nifti(image: np.ndarray, header: ImageHeader, file_name: PathOrString) -> Path: """ Saves a binary mask to nifti format, and performs the following operations: 1) Check that the image really only contains binary values (0 and 1) 2) transpose the image back into X,Y,Z from Z,Y,X 3) cast the image values to ubyte before saving :param image: binary 3D image in shape: Z x Y x X. :param header: The image header :param file_name: The name of the file for this image. :return: the path to the saved image :raises: when image is not binary """ if not is_binary_array(image): raise Exception("Array values must be binary.") return store_as_nifti(image=image, header=header, file_name=file_name, image_type=np.ubyte)
def calculate_metrics_per_class(segmentation: np.ndarray, ground_truth: np.ndarray, ground_truth_ids: List[str], voxel_spacing: TupleFloat3, patient_id: Optional[int] = None) -> MetricsDict: """ Calculate the dice for all foreground structures (the background class is completely ignored). Returns a MetricsDict with metrics for each of the foreground structures. Metrics are NaN if both ground truth and prediction are all zero for a class. :param ground_truth_ids: The names of all foreground classes. :param segmentation: predictions multi-value array with dimensions: [Z x Y x X] :param ground_truth: ground truth binary array with dimensions: [C x Z x Y x X] :param voxel_spacing: voxel_spacing in 3D Z x Y x X :param patient_id: for logging """ number_of_classes = ground_truth.shape[0] if len(ground_truth_ids) != (number_of_classes - 1): raise ValueError(f"Received {len(ground_truth_ids)} foreground class names, but " f"the label tensor indicates that there are {number_of_classes - 1} classes.") binaries = binaries_from_multi_label_array(segmentation, number_of_classes) all_classes_are_binary = [is_binary_array(ground_truth[label_id]) for label_id in range(ground_truth.shape[0])] if not np.all(all_classes_are_binary): raise ValueError("Ground truth values should be 0 or 1") overlap_measures_filter = sitk.LabelOverlapMeasuresImageFilter() hausdorff_distance_filter = sitk.HausdorffDistanceImageFilter() metrics = MetricsDict(hues=ground_truth_ids) for i, prediction in enumerate(binaries): if i == 0: continue check_size_matches(prediction, ground_truth[i], arg1_name="prediction", arg2_name="ground_truth") if not is_binary_array(prediction): raise ValueError("Predictions values should be 0 or 1") # simpleitk returns a Dice score of 0 if both ground truth and prediction are all zeros. # We want to be able to fish out those cases, and treat them specially later. prediction_zero = np.all(prediction == 0) gt_zero = np.all(ground_truth[i] == 0) dice = mean_surface_distance = hausdorff_distance = math.nan if not (prediction_zero and gt_zero): prediction_image = sitk.GetImageFromArray(prediction.astype(np.uint8)) prediction_image.SetSpacing(sitk.VectorDouble(reverse_tuple_float3(voxel_spacing))) ground_truth_image = sitk.GetImageFromArray(ground_truth[i].astype(np.uint8)) ground_truth_image.SetSpacing(sitk.VectorDouble(reverse_tuple_float3(voxel_spacing))) overlap_measures_filter.Execute(prediction_image, ground_truth_image) dice = overlap_measures_filter.GetDiceCoefficient() if prediction_zero or gt_zero: hausdorff_distance = mean_surface_distance = math.inf else: try: hausdorff_distance_filter.Execute(prediction_image, ground_truth_image) hausdorff_distance = hausdorff_distance_filter.GetHausdorffDistance() except Exception as e: logging.warning("Cannot calculate Hausdorff distance for " f"structure {i} of patient {patient_id}: {e}") try: mean_surface_distance = surface_distance(prediction_image, ground_truth_image) except Exception as e: logging.warning(f"Cannot calculate mean distance for structure {i} of patient {patient_id}: {e}") logging.debug(f"Patient {patient_id}, class {i} has Dice score {dice}") def add_metric(metric_type: MetricType, value: float) -> None: metrics.add_metric(metric_type, value, skip_nan_when_averaging=True, hue=ground_truth_ids[i - 1]) add_metric(MetricType.DICE, dice) add_metric(MetricType.HAUSDORFF_mm, hausdorff_distance) add_metric(MetricType.MEAN_SURFACE_DIST_mm, mean_surface_distance) return metrics
def test_is_binary_array(input_array: np.ndarray, expected: bool) -> None: """ Test is_binary_array function """ assert image_util.is_binary_array(input_array) == expected