def pimo_curves(
    anomaly_maps: torch.Tensor,
    masks: torch.Tensor,
    num_thresholds: int,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
    """Compute the Per-IMage Overlap (PIMO) curves.

    PIMO curves plot True Positive Rate (TPR) values for each image across
    multiple anomaly score thresholds. The thresholds are indexed by a shared
    False Positive Rate (FPR) measure computed on normal images.

    Args:
        anomaly_maps: Anomaly score maps of shape ``(N, H, W)`` where:
            - ``N``: number of images
            - ``H``: image height
            - ``W``: image width
        masks: Binary ground truth masks of shape ``(N, H, W)``
        num_thresholds: Number of thresholds ``K`` to compute

    Returns:
        tuple containing:
            - thresholds: Shape ``(K,)`` in ascending order
            - shared_fpr: Shape ``(K,)`` in descending order
            - per_image_tprs: Shape ``(N, K)`` in descending order
            - image_classes: Shape ``(N,)`` with values 0 (normal) or 1
              (anomalous)

    Raises:
        ValueError: If inputs are invalid or have inconsistent shapes
        RuntimeError: If per-image FPR curves from normal images are invalid

    Example:
        >>> anomaly_maps = torch.rand(10, 32, 32)  # 10 images of 32x32
        >>> masks = torch.randint(0, 2, (10, 32, 32))  # Binary masks
        >>> thresholds, shared_fpr, per_image_tprs, classes = pimo_curves(
        ...     anomaly_maps,
        ...     masks,
        ...     num_thresholds=100
        ... )
    """
    # validate the strings are valid
    _validate.is_num_thresholds_gte2(num_thresholds)
    _validate.is_anomaly_maps(anomaly_maps)
    _validate.is_masks(masks)
    _validate.is_same_shape(anomaly_maps, masks)
    _validate.has_at_least_one_anomalous_image(masks)
    _validate.has_at_least_one_normal_image(masks)

    image_classes = images_classes_from_masks(masks)

    # the thresholds are computed here so that they can be restrained to the
    # normal images therefore getting a better resolution in terms of FPR
    # quantization otherwise the function
    # `binclf_curve_numpy.per_image_binclf_curve` would have the range of
    # thresholds computed from all the images (normal + anomalous)
    thresholds = _get_linspaced_thresholds(
        anomaly_maps[image_classes == 0],
        num_thresholds,
    )

    # N: number of images, K: number of thresholds
    # shapes are (K,) and (N, K, 2, 2)
    thresholds, binclf_curves = threshold_and_binary_classification_curve(
        anomaly_maps=anomaly_maps,
        masks=masks,
        threshold_choice=ThresholdMethod.GIVEN.value,
        thresholds=thresholds,
        num_thresholds=None,
    )

    shared_fpr: torch.Tensor
    # mean-per-image-fpr on normal images
    # shape -> (N, K)
    per_image_fprs_normals = per_image_fpr(binclf_curves[image_classes == 0])
    try:
        _validate.is_per_image_rate_curves(per_image_fprs_normals, nan_allowed=False, decreasing=True)
    except ValueError as ex:
        msg = f"Cannot compute PIMO because the per-image FPR curves from normal images are invalid. Cause: {ex}"
        raise RuntimeError(msg) from ex

    # shape -> (K,)
    # this is the only shared FPR metric implemented so far,
    # see note about shared FPR in Details: `anomalib.metrics.per_image.pimo`.
    shared_fpr = per_image_fprs_normals.mean(axis=0)

    # shape -> (N, K)
    per_image_tprs = per_image_tpr(binclf_curves)

    return thresholds, shared_fpr, per_image_tprs, image_classes
Compute the Per-IMage Overlap (PIMO) curves.

    PIMO curves plot True Positive Rate (TPR) values for each image across
    multiple anomaly score thresholds. The thresholds are indexed by a shared
    False Positive Rate (FPR) measure computed on normal images.

    Args:
        anomaly_maps: Anomaly score maps of shape ``(N, H, W)`` where:
            - ``N``: number of images
            - ``H``: image height
            - ``W``: image width
        masks: Binary ground truth masks of shape ``(N, H, W)``
        num_thresholds: Number of thresholds ``K`` to compute

    Returns:
        tuple containing:
            - thresholds: Shape ``(K,)`` in ascending order
            - shared_fpr: Shape ``(K,)`` in descending order
            - per_image_tprs: Shape ``(N, K)`` in descending order
            - image_classes: Shape ``(N,)`` with values 0 (normal) or 1
              (anomalous)

    Raises:
        ValueError: If inputs are invalid or have inconsistent shapes
        RuntimeError: If per-image FPR curves from normal images are invalid

    Example:
        >>> anomaly_maps = torch.rand(10, 32, 32)  # 10 images of 32x32
        >>> masks = torch.randint(0, 2, (10, 32, 32))  # Binary masks
        >>> thresholds, shared_fpr, per_image_tprs, classes = pimo_curves(
        ...     anomaly_maps,
        ...     masks,
        ...     num_thresholds=100
        ... )
    
src/anomalib/metrics/pimo/functional.py