Skip to content

Commit be31e64

Browse files
committed
feat(metrics): add Fréchet Radiomics Distance (FRD) (#8643)
1 parent daaedaa commit be31e64

3 files changed

Lines changed: 113 additions & 0 deletions

File tree

monai/metrics/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .cumulative_average import CumulativeAverage
1919
from .f_beta_score import FBetaScore
2020
from .fid import FIDMetric, compute_frechet_distance
21+
from .frd import FrechetRadiomicsDistance, get_frd_score
2122
from .froc import compute_fp_tp_probs, compute_fp_tp_probs_nd, compute_froc_curve_data, compute_froc_score
2223
from .generalized_dice import GeneralizedDiceScore, compute_generalized_dice
2324
from .hausdorff_distance import HausdorffDistanceMetric, compute_hausdorff_distance

monai/metrics/frd.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright (c) MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
from __future__ import annotations
13+
14+
import torch
15+
16+
from monai.metrics.fid import get_fid_score
17+
from monai.metrics.metric import Metric
18+
19+
__all__ = ["FrechetRadiomicsDistance", "get_frd_score"]
20+
21+
22+
class FrechetRadiomicsDistance(Metric):
23+
"""
24+
Fréchet Radiomics Distance (FRD). Computes the Fréchet distance between two
25+
distributions of radiomic feature vectors, in the same way as the Fréchet
26+
Inception Distance (FID) but for radiomics-based features.
27+
28+
Unlike FID, FRD uses interpretable, clinically relevant radiomic features
29+
(e.g. from PyRadiomics) and works for both 2D and 3D images, with optional
30+
conditioning by anatomical masks. See Konz et al. "Fréchet Radiomic Distance
31+
(FRD): A Versatile Metric for Comparing Medical Imaging Datasets."
32+
https://arxiv.org/abs/2412.01496
33+
34+
This metric accepts two groups of pre-extracted radiomic feature vectors with
35+
shape (number of samples, number of features). The same Fréchet distance
36+
formula as in FID is applied to the mean and covariance of these features.
37+
38+
Args:
39+
y_pred: Radiomic feature vectors for the first distribution (e.g. from
40+
generated or reconstructed images), shape (N, F).
41+
y: Radiomic feature vectors for the second distribution (e.g. from real
42+
images), shape (N, F).
43+
44+
Returns:
45+
Scalar tensor containing the FRD value.
46+
"""
47+
48+
def __call__(self, y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
49+
return get_frd_score(y_pred, y)
50+
51+
52+
def get_frd_score(y_pred: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
53+
"""Computes the FRD score from two batches of radiomic feature vectors.
54+
55+
The implementation reuses the same Fréchet distance as FID; only the
56+
semantics (radiomic features vs. deep features) differ.
57+
58+
Args:
59+
y_pred: Feature vectors for the first distribution, shape (N, F).
60+
y: Feature vectors for the second distribution, shape (N, F).
61+
62+
Returns:
63+
Scalar tensor containing the Fréchet Radiomics Distance.
64+
"""
65+
return get_fid_score(y_pred, y)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright (c) MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
from __future__ import annotations
13+
14+
import unittest
15+
16+
import numpy as np
17+
import torch
18+
19+
from monai.metrics import FIDMetric, FrechetRadiomicsDistance
20+
from monai.utils import optional_import
21+
22+
_, has_scipy = optional_import("scipy")
23+
24+
25+
@unittest.skipUnless(has_scipy, "Requires scipy")
26+
class TestFrechetRadiomicsDistance(unittest.TestCase):
27+
def test_results(self):
28+
x = torch.Tensor([[1, 2], [1, 2], [1, 2]])
29+
y = torch.Tensor([[2, 2], [1, 2], [1, 2]])
30+
results = FrechetRadiomicsDistance()(x, y)
31+
np.testing.assert_allclose(results.cpu().numpy(), 0.4444, atol=1e-4)
32+
33+
def test_frd_matches_fid_for_same_features(self):
34+
"""FRD uses the same Fréchet formula as FID; same inputs give same value."""
35+
y_pred = torch.Tensor([[1.0, 2.0], [1.0, 2.0], [1.0, 2.0]])
36+
y = torch.Tensor([[2.0, 2.0], [1.0, 2.0], [1.0, 2.0]])
37+
frd_score = FrechetRadiomicsDistance()(y_pred, y)
38+
fid_score = FIDMetric()(y_pred, y)
39+
np.testing.assert_allclose(frd_score.cpu().numpy(), fid_score.cpu().numpy(), atol=1e-6)
40+
41+
def test_input_dimensions(self):
42+
with self.assertRaises(ValueError):
43+
FrechetRadiomicsDistance()(torch.ones([3, 3, 144, 144]), torch.ones([3, 3, 145, 145]))
44+
45+
46+
if __name__ == "__main__":
47+
unittest.main()

0 commit comments

Comments
 (0)