Skip to content

Commit dc3b7f0

Browse files
committed
fix: handle constant B-spline mutual information
Signed-off-by: kyinhub <kevinpyin@gmail.com>
1 parent 3ee058b commit dc3b7f0

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

monai/losses/image_dissimilarity.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def __init__(
214214
IEEE Transactions in Medical Imaging. Vol.22, No.1,
215215
January 2003. pp.120-128.
216216
217-
num_bins: number of bins for intensity
217+
num_bins: number of bins for intensity. The b-spline kernel requires more than 4 bins.
218218
sigma_ratio: a hyper param for gaussian function
219219
reduction: {``"none"``, ``"mean"``, ``"sum"``}
220220
Specifies the reduction to apply to the output. Defaults to ``"mean"``.
@@ -231,6 +231,8 @@ def __init__(
231231
bin_centers = torch.linspace(0.0, 1.0, num_bins) # (num_bins,)
232232
sigma = torch.mean(bin_centers[1:] - bin_centers[:-1]) * sigma_ratio
233233
self.kernel_type = look_up_option(kernel_type, ["gaussian", "b-spline"])
234+
if self.kernel_type == "b-spline" and num_bins <= 4:
235+
raise ValueError(f"num_bins must be greater than 4 for b-spline kernel, got {num_bins}")
234236
self.num_bins = num_bins
235237
# declared as buffers so they move with the module (e.g. ``.to(device)``); only populated for the
236238
# gaussian kernel, hence the ``Tensor`` annotation reflects the type at the use sites in that path.
@@ -283,7 +285,13 @@ def parzen_windowing_b_spline(self, img: torch.Tensor, order: int) -> tuple[torc
283285
# window.
284286
_max, _min = torch.max(img), torch.min(img)
285287
padding = 2
286-
bin_size = (_max - _min) / (self.num_bins - 2 * padding)
288+
value_range = _max - _min
289+
bin_size = value_range / (self.num_bins - 2 * padding)
290+
bin_size = torch.where(
291+
value_range > 0,
292+
bin_size,
293+
torch.ones_like(bin_size),
294+
)
287295
norm_min = torch.div(_min, bin_size) - padding
288296

289297
# assign bin/window index to each voxel
@@ -293,6 +301,8 @@ def parzen_windowing_b_spline(self, img: torch.Tensor, order: int) -> tuple[torc
293301
window_term = window_term.reshape(window_term.shape[0], -1, 1) # (batch, num_sample, 1)
294302
bins = torch.arange(self.num_bins, device=window_term.device).reshape(1, 1, -1) # (1, 1, num_bins)
295303
sample_bin_matrix = torch.abs(bins - window_term) # (batch, num_sample, num_bins)
304+
if sample_bin_matrix.dtype == torch.float16:
305+
sample_bin_matrix = sample_bin_matrix.float() # avoid overflow in the cubic polynomial
296306

297307
# b-spleen kernel
298308
# (4 - 6 * abs ** 2 + 3 * abs ** 3) / 6 when 0 <= abs < 1

tests/losses/image_dissimilarity/test_global_mutual_information_loss.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ def test_b_spline_bin_centers_exists_as_none(self):
135135

136136
self.assertIsNone(loss.bin_centers)
137137

138+
def test_b_spline_num_bins_must_allow_padding(self):
139+
with self.assertRaisesRegex(ValueError, "num_bins must be greater than 4"):
140+
GlobalMutualInformationLoss(kernel_type="b-spline", num_bins=4)
141+
138142
@parameterized.expand(
139143
[
140144
(torch.ones((1, 2), dtype=torch.float), torch.ones((1, 3), dtype=torch.float)), # mismatched_simple_dims
@@ -164,6 +168,34 @@ def test_ill_opts(self, num_bins, reduction, expected_exception, expected_messag
164168
GlobalMutualInformationLoss(num_bins=num_bins, reduction=reduction)(pred, target)
165169

166170

171+
class TestGlobalMutualInformationLossBSpline(unittest.TestCase):
172+
def test_b_spline_constant_images_are_finite(self):
173+
pred = torch.zeros((1, 1, 8, 8), requires_grad=True)
174+
target = torch.ones_like(pred)
175+
loss = GlobalMutualInformationLoss(kernel_type="b-spline")
176+
177+
result = loss(pred, target)
178+
179+
self.assertTrue(torch.isfinite(result))
180+
self.assertAlmostEqual(result.item(), 0.0, places=6)
181+
result.backward()
182+
self.assertIsNotNone(pred.grad)
183+
self.assertTrue(torch.isfinite(pred.grad).all())
184+
185+
def test_b_spline_constant_half_precision_images_are_finite(self):
186+
pred = torch.zeros((1, 1, 8, 8), dtype=torch.float16, requires_grad=True)
187+
target = torch.ones_like(pred)
188+
loss = GlobalMutualInformationLoss(kernel_type="b-spline", num_bins=32)
189+
190+
result = loss(pred, target)
191+
192+
self.assertTrue(torch.isfinite(result))
193+
self.assertAlmostEqual(result.item(), 0.0, places=6)
194+
result.backward()
195+
self.assertIsNotNone(pred.grad)
196+
self.assertTrue(torch.isfinite(pred.grad).all())
197+
198+
167199
class TestGlobalMutualInformationLossBuffers(unittest.TestCase):
168200
def test_gaussian_kernel_registers_buffers(self):
169201
"""Verify gaussian kernel registers preterm and bin_centers as non-trainable, non-persistent buffers."""

0 commit comments

Comments
 (0)