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..520a9e3da1 --- /dev/null +++ b/monai/metrics/frd.py @@ -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) diff --git a/tests/metrics/test_compute_frd_metric.py b/tests/metrics/test_compute_frd_metric.py new file mode 100644 index 0000000000..581cde7301 --- /dev/null +++ b/tests/metrics/test_compute_frd_metric.py @@ -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()