batchgeneratorsv2 0.3.4__tar.gz → 0.3.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/PKG-INFO +1 -1
  2. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/intensity/brightness.py +1 -1
  3. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/intensity/contrast.py +1 -2
  4. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/intensity/gamma.py +1 -2
  5. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/intensity/gaussian_noise.py +4 -3
  6. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/intensity/inversion.py +5 -4
  7. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/intensity/random_clip.py +6 -6
  8. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/local/local_contrast.py +7 -5
  9. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/local/local_gamma.py +9 -9
  10. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/local/local_smoothing.py +4 -4
  11. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/local/local_transform.py +27 -1
  12. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/nnunet/seg_to_onehot.py +10 -3
  13. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/noise/gaussian_blur.py +13 -6
  14. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/noise/rician.py +9 -6
  15. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/noise/sharpen.py +4 -3
  16. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/spatial/channel_misalignment.py +58 -11
  17. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/spatial/spatial.py +203 -67
  18. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/deep_supervision_downsampling.py +5 -2
  19. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2.egg-info/PKG-INFO +1 -1
  20. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2.egg-info/SOURCES.txt +2 -1
  21. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/pyproject.toml +1 -1
  22. batchgeneratorsv2-0.3.6/tests/test_seg_sampling.py +383 -0
  23. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/LICENSE +0 -0
  24. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/benchmarks/__init__.py +0 -0
  25. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/benchmarks/bg_comparison/__init__.py +0 -0
  26. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/benchmarks/bg_comparison/nnUNet_pipeline_bg.py +0 -0
  27. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/benchmarks/bg_comparison/nnUNet_pipeline_here.py +0 -0
  28. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/benchmarks/unique_values.py +0 -0
  29. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/dataloading/__init__.py +0 -0
  30. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/helpers/__init__.py +0 -0
  31. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/helpers/fft_conv.py +0 -0
  32. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/helpers/scalar_type.py +0 -0
  33. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/__init__.py +0 -0
  34. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/base/__init__.py +0 -0
  35. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/base/basic_transform.py +0 -0
  36. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/intensity/__init__.py +0 -0
  37. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/local/__init__.py +0 -0
  38. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/local/brightness_gradient.py +0 -0
  39. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/nnunet/__init__.py +0 -0
  40. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/nnunet/random_binary_operator.py +0 -0
  41. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/nnunet/remove_connected_components.py +0 -0
  42. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/noise/__init__.py +0 -0
  43. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/noise/blank_rectangle.py +0 -0
  44. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/noise/median_filter.py +0 -0
  45. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/spatial/__init__.py +0 -0
  46. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/spatial/low_resolution.py +0 -0
  47. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/spatial/mirroring.py +0 -0
  48. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/spatial/rot90.py +0 -0
  49. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/spatial/transpose.py +0 -0
  50. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/__init__.py +0 -0
  51. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/compose.py +0 -0
  52. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/cropping.py +0 -0
  53. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/move_channels.py +0 -0
  54. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/nnunet_masking.py +0 -0
  55. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/pseudo2d.py +0 -0
  56. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/random.py +0 -0
  57. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/remove_label.py +0 -0
  58. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2/transforms/utils/seg_to_regions.py +0 -0
  59. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2.egg-info/dependency_links.txt +0 -0
  60. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2.egg-info/requires.txt +0 -0
  61. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/batchgeneratorsv2.egg-info/top_level.txt +0 -0
  62. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/readme.md +0 -0
  63. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/setup.cfg +0 -0
  64. {batchgeneratorsv2-0.3.4 → batchgeneratorsv2-0.3.6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batchgeneratorsv2
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Batchgenerators but better
5
5
  Author: Helmholtz Imaging Applied Computer Vision Lab
6
6
  Author-email: Fabian Isensee <f.isensee@dkfz-heidelberg.de>
@@ -30,7 +30,7 @@ class MultiplicativeBrightnessTransform(ImageOnlyTransform):
30
30
  # even though this is array notation it's a lot slower. Shame shame
31
31
  # img[params['apply_to_channel']] *= params['multipliers'].view(-1, *[1]*(img.ndim - 1))
32
32
  for c, m in zip(params['apply_to_channel'], params['multipliers']):
33
- img[c] *= m
33
+ img[c].mul_(m) # in-place on the channel view; avoids the __setitem__ write-back copy of `img[c] *= m`
34
34
  return img
35
35
 
36
36
 
@@ -73,8 +73,7 @@ class ContrastTransform(ImageOnlyTransform):
73
73
 
74
74
  x = img[c]
75
75
  mean = x.mean()
76
- minm = x.min()
77
- maxm = x.max()
76
+ minm, maxm = torch.aminmax(x) # single reduction pass instead of separate min()/max()
78
77
 
79
78
  x.sub_(mean)
80
79
  x.mul_(m)
@@ -79,8 +79,7 @@ class GammaTransform(ImageOnlyTransform):
79
79
  mean = x.mean()
80
80
  std = x.std()
81
81
 
82
- minm = x.min()
83
- maxm = x.max()
82
+ minm, maxm = torch.aminmax(x) # single reduction pass instead of separate min()/max()
84
83
  rnge = maxm - minm
85
84
  denom = torch.clamp(rnge, min=eps)
86
85
 
@@ -66,9 +66,10 @@ class GaussianNoiseTransform(ImageOnlyTransform):
66
66
  sigma = sigmas
67
67
  noise = torch.empty((n, *spatial), device=device, dtype=dtype).normal_(mean=0.0, std=float(sigma))
68
68
 
69
- # Advanced indexing (img[idx]) returns a copy, so use indexed assignment
70
- # to make sure modifications are written back to img.
71
- img[idx] += noise
69
+ # index_add_ adds noise[i] into channel img[idx[i]] fully in place. idx is unique (from nonzero),
70
+ # so there is no accumulation ambiguity. This avoids the full (n, *spatial) gather copy + scatter that
71
+ # advanced-indexed `img[idx] += noise` would allocate.
72
+ img.index_add_(0, idx, noise)
72
73
  return img
73
74
 
74
75
 
@@ -31,10 +31,11 @@ class InvertImageTransform(ImageOnlyTransform):
31
31
  return img
32
32
  else:
33
33
  for ch in params['apply_to_channel']:
34
- mn = img[ch].mean()
35
- img[ch] -= mn
36
- img[ch] *= -1
37
- img[ch] += mn
34
+ x = img[ch] # channel view; operate in place to drop the per-op __setitem__ write-backs
35
+ mn = x.mean()
36
+ x.sub_(mn)
37
+ x.mul_(-1)
38
+ x.add_(mn)
38
39
  return img
39
40
 
40
41
 
@@ -67,18 +67,18 @@ class CutOffOutliersTransform(ImageOnlyTransform):
67
67
  if retain_std[c]:
68
68
  orig_std = img_c.std()
69
69
 
70
- # Percentiles in torch to avoid numpy roundtrip
70
+ # Percentiles in torch to avoid numpy roundtrip; must run on the pre-clamp values
71
71
  q = torch.tensor([perc[0] / 100.0, perc[1] / 100.0], device=img_c.device, dtype=torch.float32)
72
72
  lower_val, upper_val = torch.quantile(img_c.float(), q)
73
73
 
74
- img_c_clipped = img_c.clamp(min=lower_val.item(), max=upper_val.item())
74
+ # clamp in place on the channel view
75
+ img_c.clamp_(min=lower_val.item(), max=upper_val.item())
75
76
 
76
77
  if retain_std[c]:
77
- clipped_std = img_c_clipped.std()
78
+ clipped_std = img_c.std()
78
79
  if clipped_std > 1e-8:
79
- img_c_clipped = (img_c_clipped - img_c_clipped.mean()) / clipped_std * orig_std + img_c_clipped.mean()
80
-
81
- img[c] = img_c_clipped
80
+ m = img_c.mean() # reused for both centering steps
81
+ img[c] = (img_c - m) / clipped_std * orig_std + m
82
82
 
83
83
  return img
84
84
 
@@ -40,7 +40,7 @@ class LocalContrastTransform(ImageOnlyTransform, LocalTransform):
40
40
  return {'kernels': [None] * C, 'contrasts': [None] * C}
41
41
 
42
42
  if self.same_for_all_channels:
43
- kernel = self._generate_kernel(spatial).astype(np.float32)
43
+ kernel = self._generate_kernel(spatial)
44
44
  contrast = sample_scalar(self.new_contrast)
45
45
 
46
46
  kernels = [kernel if apply else None for apply in apply_channel]
@@ -52,7 +52,7 @@ class LocalContrastTransform(ImageOnlyTransform, LocalTransform):
52
52
  kernels.append(None)
53
53
  contrasts.append(None)
54
54
  continue
55
- kernel = self._generate_kernel(spatial).astype(np.float32)
55
+ kernel = self._generate_kernel(spatial)
56
56
  contrast = sample_scalar(self.new_contrast)
57
57
  kernels.append(kernel)
58
58
  contrasts.append(contrast)
@@ -67,9 +67,11 @@ class LocalContrastTransform(ImageOnlyTransform, LocalTransform):
67
67
  continue
68
68
 
69
69
  channel = img_np[c]
70
- mean = (channel * kernel).sum() / (kernel.sum() + 1e-8)
71
- modified = (channel - mean) * contrast + mean
72
- img_np[c] = self.run_interpolation(channel, modified, kernel)
70
+ mean = (channel * kernel).sum() / (kernel.sum() + 1e-8) # keep exact summation order
71
+ modified = channel - mean # fresh owned array
72
+ modified *= contrast
73
+ modified += mean # (channel - mean) * contrast + mean
74
+ img_np[c] = self.run_interpolation_inplace(channel, modified, kernel)
73
75
 
74
76
  return torch.from_numpy(img_np).to(img.device, dtype=img.dtype)
75
77
 
@@ -42,7 +42,7 @@ class LocalGammaTransform(ImageOnlyTransform, LocalTransform):
42
42
  return {'kernels': [None] * C, 'gammas': [None] * C}
43
43
 
44
44
  if self.same_for_all_channels:
45
- kernel = self._generate_kernel(spatial).astype(np.float32)
45
+ kernel = self._generate_kernel(spatial)
46
46
  gamma = sample_scalar(self.gamma)
47
47
 
48
48
  kernels = [kernel if apply else None for apply in apply_channel]
@@ -54,7 +54,7 @@ class LocalGammaTransform(ImageOnlyTransform, LocalTransform):
54
54
  kernels.append(None)
55
55
  gammas.append(None)
56
56
  continue
57
- kernel = self._generate_kernel(spatial).astype(np.float32)
57
+ kernel = self._generate_kernel(spatial)
58
58
  gamma = sample_scalar(self.gamma)
59
59
  kernels.append(kernel)
60
60
  gammas.append(gamma)
@@ -72,15 +72,15 @@ class LocalGammaTransform(ImageOnlyTransform, LocalTransform):
72
72
  min_val, max_val = channel.min(), channel.max()
73
73
  denom = max(max_val - min_val, 1e-8)
74
74
 
75
- # Normalize to [0, 1]
76
- norm = (channel - min_val) / denom
75
+ # Normalize to [0, 1] (norm is a fresh owned array; channel/kernel are never mutated)
76
+ norm = channel - min_val
77
+ norm /= denom
77
78
  gamma_corrected = np.power(norm, gamma)
78
79
 
79
- # Blend using kernel
80
- blended = self.run_interpolation(norm, gamma_corrected, kernel)
81
-
82
- # Rescale to original range
83
- img_np[c] = blended * denom + min_val
80
+ blended = self.run_interpolation_inplace(norm, gamma_corrected, kernel)
81
+ blended *= denom # rescale to original range
82
+ blended += min_val
83
+ img_np[c] = blended
84
84
 
85
85
  return torch.from_numpy(img_np).to(img.device, dtype=img.dtype)
86
86
 
@@ -47,7 +47,7 @@ class LocalSmoothingTransform(ImageOnlyTransform, LocalTransform):
47
47
  sigma = sample_scalar(self.kernel_size)
48
48
 
49
49
  if self.same_for_all_channels:
50
- kernel = self._generate_kernel(spatial).astype(np.float32)
50
+ kernel = self._generate_kernel(spatial)
51
51
  strength = sample_scalar(self.smoothing_strength)
52
52
 
53
53
  kernels = [kernel if apply else None for apply in apply_channel]
@@ -59,7 +59,7 @@ class LocalSmoothingTransform(ImageOnlyTransform, LocalTransform):
59
59
  kernels.append(None)
60
60
  strengths.append(None)
61
61
  continue
62
- kernel = self._generate_kernel(spatial).astype(np.float32)
62
+ kernel = self._generate_kernel(spatial)
63
63
  strength = sample_scalar(self.smoothing_strength)
64
64
  kernels.append(kernel)
65
65
  strengths.append(strength)
@@ -74,9 +74,9 @@ class LocalSmoothingTransform(ImageOnlyTransform, LocalTransform):
74
74
  if kernel is None:
75
75
  continue
76
76
 
77
- kernel = kernel * strength # scale kernel by smoothing strength
77
+ kernel = kernel * strength # fresh owned array (shared param kernel is never mutated)
78
78
  smoothed = gaussian_filter(img_np[c], sigma=sigma)
79
- img_np[c] = self.run_interpolation(img_np[c], smoothed, kernel)
79
+ img_np[c] = self.run_interpolation_inplace(img_np[c], smoothed, kernel)
80
80
 
81
81
  return torch.from_numpy(img_np).to(img.device, dtype=img.dtype)
82
82
 
@@ -11,6 +11,9 @@ class LocalTransform(ABC):
11
11
  self.scale = scale
12
12
 
13
13
  def _generate_kernel(self, img_shp: Tuple[int, ...]) -> np.ndarray:
14
+ """
15
+ Always returns a float32 array in [0, 1]; callers rely on this and do not re-cast.
16
+ """
14
17
  ndim = len(img_shp)
15
18
  x_grids = [np.arange(-0.5, s + 0.5, dtype=np.float32) for s in img_shp]
16
19
  kernels = []
@@ -75,6 +78,8 @@ class LocalTransform(ABC):
75
78
  """
76
79
  Blends original and modified images using the given kernel as a per-pixel weight map.
77
80
 
81
+ Reference implementation; prefer run_interpolation_inplace in per-sample code paths.
82
+
78
83
  Parameters:
79
84
  original_image (np.ndarray): Unmodified input image
80
85
  modified_image (np.ndarray): Modified version (e.g., gamma-corrected)
@@ -83,4 +88,25 @@ class LocalTransform(ABC):
83
88
  Returns:
84
89
  np.ndarray: Blended result
85
90
  """
86
- return original_image * (1.0 - kernel_image) + modified_image * kernel_image
91
+ return original_image * (1.0 - kernel_image) + modified_image * kernel_image
92
+
93
+ @staticmethod
94
+ def run_interpolation_inplace(original_image: np.ndarray,
95
+ modified_image: np.ndarray,
96
+ kernel_image: np.ndarray) -> np.ndarray:
97
+ """
98
+ Same blend as run_interpolation, computed into modified_image's buffer with a single extra
99
+ temporary. Uses the identical individual * and + ops with only IEEE-commutative operand swaps
100
+ (no reassociation), so the result is bit-identical to run_interpolation.
101
+
102
+ Contract: modified_image must be a fresh buffer owned by the caller (it is overwritten);
103
+ original_image and kernel_image are only read. original_image may alias the eventual output
104
+ slot as long as that slot is written only after this call returns.
105
+
106
+ Returns modified_image (now holding the blended result).
107
+ """
108
+ tmp = 1.0 - kernel_image
109
+ tmp *= original_image # original * (1 - kernel)
110
+ modified_image *= kernel_image
111
+ modified_image += tmp
112
+ return modified_image
@@ -22,10 +22,17 @@ class MoveSegAsOneHotToDataTransform(BasicTransform):
22
22
 
23
23
  def apply(self, data_dict, **params):
24
24
  seg = data_dict['segmentation'][self.source_channel_idx]
25
- seg_onehot = torch.zeros((len(self.all_labels), *seg.shape), dtype=data_dict['image'].dtype)
25
+ img = data_dict['image']
26
+ C = img.shape[0]
27
+ # Allocate the concatenated result once and write the one-hot channels straight into their final slots,
28
+ # instead of building a separate seg_onehot tensor and then torch.cat-copying everything again. Only the
29
+ # one-hot tail needs zeroing; the image channels are fully overwritten anyway.
30
+ out = torch.empty((C + len(self.all_labels), *seg.shape), dtype=img.dtype)
31
+ out[:C] = img
32
+ out[C:].zero_()
26
33
  for i, l in enumerate(self.all_labels):
27
- seg_onehot[i][seg == l] = 1
28
- data_dict['image'] = torch.cat((data_dict['image'], seg_onehot))
34
+ out[C + i][seg == l] = 1
35
+ data_dict['image'] = out
29
36
  if self.remove_channel_from_source:
30
37
  remaining_channels = [i for i in range(data_dict['segmentation'].shape[0]) if i != self.source_channel_idx]
31
38
  data_dict['segmentation'] = data_dict['segmentation'][remaining_channels]
@@ -20,7 +20,8 @@ def blur_dimension(img: torch.Tensor, sigma: float, dim_to_blur: int, force_use_
20
20
  where C is the channel dimension and X, Y, Z are spatial dimensions.
21
21
  :param sigma: The standard deviation of the Gaussian kernel.
22
22
  :param dim_to_blur: The dimension along which to apply the Gaussian blur (0 for X, 1 for Y, 2 for Z).
23
- :return: The blurred image tensor.
23
+ :return: The blurred image tensor. `img` is never modified; a new tensor is returned (callers such as
24
+ GaussianBlurTransform chain these calls and rely on this non-mutation contract).
24
25
  """
25
26
  assert img.ndim - 1 > dim_to_blur, "dim_to_blur must be a valid spatial dimension of the input image."
26
27
  # Adjustments for kernel based on image dimensions
@@ -122,22 +123,28 @@ class GaussianBlurTransform(ImageOnlyTransform):
122
123
  # print(params['sigmas'])
123
124
  if self.synchronize_channels:
124
125
  # we can compute that in one go as the conv implementation supports arbitrary input channels (with expanded kernel)
126
+ # Gather the selected channels once (advanced indexing copies), chain the per-dim blurs, scatter back once.
127
+ # blur_dimension never mutates its input, so this is the same composition as writing back each dim.
128
+ mask = params['apply_to_channel']
129
+ sub = img[mask]
125
130
  for d in range(dim):
126
- # print(d, params['sigmas'][d])
127
131
  if not self.benchmark:
128
- img[params['apply_to_channel']] = blur_dimension(img[params['apply_to_channel']], params['sigmas'][d], d)
132
+ sub = blur_dimension(sub, params['sigmas'][d], d)
129
133
  else:
130
- img[params['apply_to_channel']] = self._benchmark_wrapper(img[params['apply_to_channel']], params['sigmas'][d], d)
134
+ sub = self._benchmark_wrapper(sub, params['sigmas'][d], d)
135
+ img[mask] = sub
131
136
  else:
132
137
  # we have to go through all the channels, build the kernel for each channel etc
133
138
  idx = np.where(params['apply_to_channel'])[0]
134
139
  for j, i in enumerate(idx):
140
+ sub = img[i:i+1] # view for the first read; blur_dimension returns fresh tensors thereafter
135
141
  for d in range(dim):
136
142
  # print(i, d, params['sigmas'][i][d])
137
143
  if not self.benchmark:
138
- img[i:i+1] = blur_dimension(img[i:i+1], params['sigmas'][j][d], d)
144
+ sub = blur_dimension(sub, params['sigmas'][j][d], d)
139
145
  else:
140
- img[i:i+1] = self._benchmark_wrapper(img[i:i+1], params['sigmas'][j][d], d)
146
+ sub = self._benchmark_wrapper(sub, params['sigmas'][j][d], d)
147
+ img[i:i+1] = sub
141
148
  return img
142
149
 
143
150
  def _benchmark_wrapper(self, img: torch.Tensor, sigma: float, dim_to_blur: int):
@@ -1,3 +1,4 @@
1
+ import numpy as np
1
2
  import torch
2
3
  from typing import Tuple
3
4
  from batchgeneratorsv2.transforms.base.basic_transform import ImageOnlyTransform
@@ -25,19 +26,21 @@ class RicianNoiseTransform(ImageOnlyTransform):
25
26
  noise_imag = torch.empty_like(img).normal_(mean=0.0, std=var)
26
27
 
27
28
  min_val = img.min()
28
- shifted = img - min_val
29
+ shifted = img - min_val # fresh owned buffer (img itself is never mutated below)
29
30
 
30
- rician = torch.sqrt((shifted + noise_real).pow_(2).add_(noise_imag.pow_(2)))
31
- rician = rician + min_val
31
+ # Run the whole Rician pipeline in `shifted`'s buffer. Same elementwise ops in the same order as
32
+ # sqrt((shifted+noise_real)^2 + noise_imag^2) + min_val, just in place -> bit-identical.
33
+ rician = shifted.add_(noise_real).pow_(2).add_(noise_imag.pow_(2)).sqrt_()
34
+ rician.add_(min_val)
32
35
 
33
- # Normalize to match original mean and std
36
+ # Normalize to match original mean and std (scalars captured before the in-place normalization)
34
37
  input_mean, input_std = img.mean(), img.std()
35
38
  rician_mean, rician_std = rician.mean(), rician.std()
36
39
 
37
40
  if rician_std > 0:
38
- rician = (rician - rician_mean) / rician_std * input_std + input_mean
41
+ rician.sub_(rician_mean).div_(rician_std).mul_(input_std).add_(input_mean)
39
42
  else:
40
- rician = rician * 0 + input_mean # fallback if std is zero (flat image)
43
+ rician.mul_(0).add_(input_mean) # keep mul_(0) (not fill_) so inf/nan propagate as before
41
44
 
42
45
  return rician
43
46
 
@@ -81,7 +81,7 @@ class SharpeningTransform(ImageOnlyTransform):
81
81
  continue
82
82
 
83
83
  if clamp:
84
- mn, mx = torch.min(img[c]), torch.max(img[c])
84
+ mn, mx = torch.aminmax(img[c]) # single reduction pass instead of separate min()/max()
85
85
 
86
86
  x = img[c].unsqueeze(0).unsqueeze(0) # (1, 1, H, W) or (1, 1, D, H, W)
87
87
  padded = F.pad(x, pad, mode='replicate')
@@ -91,8 +91,9 @@ class SharpeningTransform(ImageOnlyTransform):
91
91
  else:
92
92
  laplace = F.conv3d(padded, kernel)
93
93
 
94
- sharpened = x + strength * laplace
95
- out[c] = sharpened.squeeze()
94
+ # laplace is a fresh conv output (owned, not reused). Combine in place: strength*laplace + x == x +
95
+ # strength*laplace (IEEE add commutes). x is read here before out[c] (== img[c]) is written -> no hazard.
96
+ out[c] = laplace.mul_(strength).add_(x).squeeze()
96
97
 
97
98
  if clamp:
98
99
  out[c].clamp_(mn, mx)
@@ -206,22 +206,69 @@ class ChannelMisalignmentTransform(ImageOnlyTransform):
206
206
  # transformed by affine rather than its transpose (see issue #24).
207
207
  grid = torch.matmul(grid, torch.from_numpy(params['affine'].T).float())
208
208
 
209
- # we center the grid around the center_location_in_pixels. We should center the mean of the grid, not the center position
210
- # only do this if we elastic deform
209
+ # re-center the mean of the grid (not the center position). new_center is all-zeros here (translation is
210
+ # handled via the shifted crop center below, not via the grid), so in the non-deform case mn is also 0 and
211
+ # `grid += (0 - 0)` would be a wasted full-grid pass -> skipped. This skip is only valid while new_center
212
+ # stays all-zeros; if a nonzero new_center is ever reintroduced, the shift must apply in both paths again.
211
213
  if params['elastic_offsets'] is not None:
212
214
  mn = grid.mean(dim=list(range(img.ndim - 1)))
213
- else:
214
- mn = 0
215
+ # new_center = torch.Tensor([c - s / 2 for c, s in zip(params['center_location_in_pixels'], img.shape[1:])])
216
+ new_center = torch.zeros(len(im_shape))
217
+ grid += (new_center - mn)
215
218
 
216
- # new_center = torch.Tensor([c - s / 2 for c, s in zip(params['center_location_in_pixels'], img.shape[1:])])
217
- new_center = torch.Tensor([0, 0, 0])
218
- grid += (new_center - mn)
219
+ # compute the grid_sample grid once instead of once per channel
220
+ grid_sample_grid = _convert_my_grid_to_grid_sample_grid(grid, img.shape[1:])[None]
219
221
 
220
222
  for ch in self.im_channels_2_misalign:
221
- img[ch, ...] = grid_sample(img[ch, ...].unsqueeze(0).unsqueeze(0),
222
- _convert_my_grid_to_grid_sample_grid(grid, img.shape[1:])[None],
223
- mode='bilinear', padding_mode="zeros", align_corners=False)[0]
224
- img[ch, ...] = crop_tensor(img[ch, ...].unsqueeze(0),
223
+ # crop the grid_sample result directly instead of writing it into img[ch] and reading it back
224
+ resampled = grid_sample(img[ch, ...].unsqueeze(0).unsqueeze(0),
225
+ grid_sample_grid,
226
+ mode='bilinear', padding_mode="zeros", align_corners=False)[0]
227
+ img[ch, ...] = crop_tensor(resampled,
225
228
  [math.floor(i) for i in params['center_location_in_pixels']], im_shape,
226
229
  pad_mode='constant', pad_kwargs={'value': 0})
227
230
  return img
231
+
232
+
233
+ # from copy import deepcopy
234
+ # import SimpleITK as sitk
235
+ # from pathlib import Path
236
+
237
+ # if __name__ == '__main__':
238
+
239
+ # source_folder = Path('./test_images/')
240
+
241
+ # img0 = sitk.GetArrayFromImage(sitk.ReadImage(source_folder / 'case001_0000.nii.gz'))
242
+ # img1 = sitk.GetArrayFromImage(sitk.ReadImage(source_folder / 'case001_0001.nii.gz'))
243
+ # img2 = sitk.GetArrayFromImage(sitk.ReadImage(source_folder / 'case001_0002.nii.gz'))
244
+ # seg = sitk.GetArrayFromImage(sitk.ReadImage(source_folder / 'case001.nii.gz'))
245
+
246
+ # sp = ChannelMisalignmentTransform(
247
+ # im_channels_2_misalign=(0,),
248
+
249
+ # squeezing_zyx=(0.0, 0.1, 0.0),
250
+ # p_squeeze=1.0,
251
+
252
+ # rotation_ax_cor_sag=(30, 0, 0),
253
+ # rad_or_deg="deg",
254
+ # p_rotation=1.0,
255
+
256
+ # shift_zyx=(5, 100, 20),
257
+ # p_shift = 1.0,
258
+
259
+ # )
260
+
261
+ # data_dict = {'image': torch.cat([torch.from_numpy(deepcopy(img0[None])).float(),
262
+ # torch.from_numpy(deepcopy(img1[None])).float(),
263
+ # torch.from_numpy(deepcopy(img2[None])).float()], dim=0)}
264
+ # if seg is not None:
265
+ # data_dict['segmentation'] = torch.from_numpy(deepcopy(seg[None]))
266
+
267
+ # out = sp(**data_dict)
268
+
269
+ # sitk.WriteImage(sitk.GetImageFromArray(img0), source_folder / 'original_ch0.nii.gz')
270
+ # sitk.WriteImage(sitk.GetImageFromArray(img1), source_folder / 'original_ch1.nii.gz')
271
+ # sitk.WriteImage(sitk.GetImageFromArray(img2), source_folder / 'original_ch2.nii.gz')
272
+ # sitk.WriteImage(sitk.GetImageFromArray(out['image'][0].numpy()), source_folder / 'transformed_ch0.nii.gz')
273
+ # sitk.WriteImage(sitk.GetImageFromArray(out['image'][1].numpy()), source_folder / 'transformed_ch1.nii.gz')
274
+ # sitk.WriteImage(sitk.GetImageFromArray(out['image'][2].numpy()), source_folder / 'transformed_ch2.nii.gz')