Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions monai/metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .cumulative_average import CumulativeAverage
from .f_beta_score import FBetaScore
from .fid import FIDMetric, compute_frechet_distance
from .frd import FrechetRadiomicsDistance, get_frd_score
from .froc import compute_fp_tp_probs, compute_fp_tp_probs_nd, compute_froc_curve_data, compute_froc_score
from .generalized_dice import GeneralizedDiceScore, compute_generalized_dice
from .hausdorff_distance import HausdorffDistanceMetric, compute_hausdorff_distance
Expand Down
71 changes: 71 additions & 0 deletions monai/metrics/frd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import torch

from monai.metrics.fid import get_fid_score
from monai.metrics.metric import Metric

__all__ = ["FrechetRadiomicsDistance", "get_frd_score"]


class FrechetRadiomicsDistance(Metric):
"""
Fréchet Radiomics Distance (FRD). Computes the Fréchet distance between two
distributions of radiomic feature vectors, in the same way as the Fréchet
Inception Distance (FID) but for radiomics-based features.

Unlike FID, FRD uses interpretable, clinically relevant radiomic features
(e.g. from PyRadiomics) and works for both 2D and 3D images, with optional
conditioning by anatomical masks. See Konz et al. "Fréchet Radiomic Distance
(FRD): A Versatile Metric for Comparing Medical Imaging Datasets."
https://arxiv.org/abs/2412.01496

This metric accepts two groups of pre-extracted radiomic feature vectors with
shape (number of samples, number of features). The same Fréchet distance
formula as in FID is applied to the mean and covariance of these features.

Args:
y_pred: Radiomic feature vectors for the first distribution (e.g. from
generated or reconstructed images), shape (N, F).
y: Radiomic feature vectors for the second distribution (e.g. from real
images), shape (N, F).

Returns:
Scalar tensor containing the FRD value.
"""

def __call__(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
return get_frd_score(y_pred, y)


def get_frd_score(y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
"""Computes the FRD score from two batches of radiomic feature vectors.

The implementation reuses the same Fréchet distance as FID; only the
semantics (radiomic features vs. deep features) differ.

Args:
y_pred: Feature vectors for the first distribution, shape (N, F).
y: Feature vectors for the second distribution, shape (N, F).

Returns:
Scalar tensor containing the Fréchet Radiomics Distance.

Raises:
ValueError: When either tensor has more than 2 dimensions. Inputs must have
shape (number of samples, number of features).
"""
if y_pred.ndimension() > 2 or y.ndimension() > 2:
raise ValueError("Inputs should have (number images, number of features) shape.")
return get_fid_score(y_pred, y)
Comment on lines +52 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce the full (N, F) contract here.

ndimension() > 2 still lets 0D/1D tensors and N < 2 batches fall through into get_fid_score, where covariance estimation is not well-defined and failures get opaque. Please reject non-2D inputs and single-sample batches here, then add tests for both cases.

🩹 Proposed fix
 def get_frd_score(y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
@@
-    if y_pred.ndimension() > 2 or y.ndimension() > 2:
+    if y_pred.ndimension() != 2 or y.ndimension() != 2:
         raise ValueError("Inputs should have (number images, number of features) shape.")
+    if y_pred.shape[0] < 2 or y.shape[0] < 2:
+        raise ValueError("At least 2 samples are required to estimate covariance.")
     return get_fid_score(y_pred, y)
As per coding guidelines, "Examine code for logical error or inconsistencies, and suggest what may be changed to addressed these. Ensure new or modified definitions will be covered by existing or new unit tests."
🧰 Tools
🪛 Ruff (0.15.4)

[warning] 70-70: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@monai/metrics/frd.py` around lines 52 - 71, The get_frd_score function
currently only rejects tensors with ndimension() > 2 but should enforce a strict
(N, F) contract and reject non-2D inputs and single-sample batches before
delegating to get_fid_score; modify get_frd_score to validate that both
y_pred.ndimension() == 2 and y.ndimension() == 2 and that y_pred.size(0) >= 2
and y.size(0) >= 2, raising a clear ValueError if any check fails (mentioning
expected shape and minimum batch size), then keep calling get_fid_score when
checks pass, and add unit tests that assert ValueError is raised for 0D/1D
inputs and for N < 2 for both y_pred and y.

49 changes: 49 additions & 0 deletions tests/metrics/test_compute_frd_metric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright (c) MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import unittest

import numpy as np
import torch

from monai.metrics import FIDMetric, FrechetRadiomicsDistance
from monai.utils import optional_import

_, has_scipy = optional_import("scipy")


@unittest.skipUnless(has_scipy, "Requires scipy")
class TestFrechetRadiomicsDistance(unittest.TestCase):
def test_results(self):
x = torch.Tensor([[1, 2], [1, 2], [1, 2]])
y = torch.Tensor([[2, 2], [1, 2], [1, 2]])
results = FrechetRadiomicsDistance()(x, y)
np.testing.assert_allclose(results.cpu().numpy(), 0.4444, atol=1e-4)

def test_frd_matches_fid_for_same_features(self):
"""FRD uses the same Fréchet formula as FID; same inputs give same value."""
y_pred = torch.Tensor([[1.0, 2.0], [1.0, 2.0], [1.0, 2.0]])
y = torch.Tensor([[2.0, 2.0], [1.0, 2.0], [1.0, 2.0]])
frd_score = FrechetRadiomicsDistance()(y_pred, y)
fid_score = FIDMetric()(y_pred, y)
np.testing.assert_allclose(frd_score.cpu().numpy(), fid_score.cpu().numpy(), atol=1e-6)

def test_rejects_high_dimensional_input(self):
"""FrechetRadiomicsDistance raises ValueError when inputs have ndimension() > 2."""
high_dim = torch.ones([3, 3, 144, 144])
with self.assertRaises(ValueError):
FrechetRadiomicsDistance()(high_dim, high_dim)


if __name__ == "__main__":
unittest.main()
Loading