From be31e64dfe50e6277df98ab24730fd708d177ec1 Mon Sep 17 00:00:00 2001 From: AymanL <40838419+AymanL@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:10:18 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(metrics):=20add=20Fr=C3=A9chet=20Radio?= =?UTF-8?q?mics=20Distance=20(FRD)=20(#8643)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- monai/metrics/__init__.py | 1 + monai/metrics/frd.py | 65 ++++++++++++++++++++++++ tests/metrics/test_compute_frd_metric.py | 47 +++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 monai/metrics/frd.py create mode 100644 tests/metrics/test_compute_frd_metric.py diff --git a/monai/metrics/__init__.py b/monai/metrics/__init__.py index 702e3c48e2..c84604e0c9 100644 --- a/monai/metrics/__init__.py +++ b/monai/metrics/__init__.py @@ -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 diff --git a/monai/metrics/frd.py b/monai/metrics/frd.py new file mode 100644 index 0000000000..15063a238e --- /dev/null +++ b/monai/metrics/frd.py @@ -0,0 +1,65 @@ +# 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. + """ + return get_fid_score(y_pred, y) diff --git a/tests/metrics/test_compute_frd_metric.py b/tests/metrics/test_compute_frd_metric.py new file mode 100644 index 0000000000..90307ace6d --- /dev/null +++ b/tests/metrics/test_compute_frd_metric.py @@ -0,0 +1,47 @@ +# 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_input_dimensions(self): + with self.assertRaises(ValueError): + FrechetRadiomicsDistance()(torch.ones([3, 3, 144, 144]), torch.ones([3, 3, 145, 145])) + + +if __name__ == "__main__": + unittest.main() From e56a9197f1d0e74ee3f8feeda6b18fa3e45a6dd9 Mon Sep 17 00:00:00 2001 From: AymanL <40838419+AymanL@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:56:55 +0000 Subject: [PATCH 2/3] a new commit Signed-off-by: AymanL <40838419+AymanL@users.noreply.github.com> --- monai/metrics/frd.py | 4 ++++ tests/metrics/test_compute_frd_metric.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/monai/metrics/frd.py b/monai/metrics/frd.py index 15063a238e..0fee4d1db4 100644 --- a/monai/metrics/frd.py +++ b/monai/metrics/frd.py @@ -61,5 +61,9 @@ def get_frd_score(y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor: 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). """ return get_fid_score(y_pred, y) diff --git a/tests/metrics/test_compute_frd_metric.py b/tests/metrics/test_compute_frd_metric.py index 90307ace6d..581cde7301 100644 --- a/tests/metrics/test_compute_frd_metric.py +++ b/tests/metrics/test_compute_frd_metric.py @@ -38,9 +38,11 @@ def test_frd_matches_fid_for_same_features(self): fid_score = FIDMetric()(y_pred, y) np.testing.assert_allclose(frd_score.cpu().numpy(), fid_score.cpu().numpy(), atol=1e-6) - def test_input_dimensions(self): + 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()(torch.ones([3, 3, 144, 144]), torch.ones([3, 3, 145, 145])) + FrechetRadiomicsDistance()(high_dim, high_dim) if __name__ == "__main__": From 12ee17709fc0cb82569e08cbd531eabf3f07b7e9 Mon Sep 17 00:00:00 2001 From: AymanL <40838419+AymanL@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:06:10 +0000 Subject: [PATCH 3/3] DCO Remediation Commit for AymanL <40838419+AymanL@users.noreply.github.com> I, AymanL <40838419+AymanL@users.noreply.github.com>, hereby add my Signed-off-by to this commit: be31e64dfe50e6277df98ab24730fd708d177ec1 Signed-off-by: AymanL <40838419+AymanL@users.noreply.github.com> --- monai/metrics/frd.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monai/metrics/frd.py b/monai/metrics/frd.py index 0fee4d1db4..520a9e3da1 100644 --- a/monai/metrics/frd.py +++ b/monai/metrics/frd.py @@ -66,4 +66,6 @@ def get_frd_score(y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor: 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)