batchgeneratorsv2 0.3.3__tar.gz → 0.3.5__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 (63) hide show
  1. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/PKG-INFO +1 -1
  2. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/intensity/brightness.py +1 -1
  3. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/intensity/contrast.py +1 -2
  4. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/intensity/gamma.py +1 -2
  5. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/intensity/gaussian_noise.py +4 -3
  6. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/intensity/inversion.py +5 -4
  7. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/intensity/random_clip.py +6 -6
  8. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/local/brightness_gradient.py +5 -1
  9. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/local/local_contrast.py +7 -5
  10. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/local/local_gamma.py +9 -9
  11. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/local/local_smoothing.py +4 -4
  12. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/local/local_transform.py +27 -1
  13. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/nnunet/seg_to_onehot.py +10 -3
  14. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/noise/gaussian_blur.py +13 -6
  15. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/noise/rician.py +9 -6
  16. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/noise/sharpen.py +4 -3
  17. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/spatial/channel_misalignment.py +63 -13
  18. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/spatial/spatial.py +35 -17
  19. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/deep_supervision_downsampling.py +5 -2
  20. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2.egg-info/PKG-INFO +1 -1
  21. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/pyproject.toml +1 -1
  22. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/LICENSE +0 -0
  23. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/benchmarks/__init__.py +0 -0
  24. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/benchmarks/bg_comparison/__init__.py +0 -0
  25. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/benchmarks/bg_comparison/nnUNet_pipeline_bg.py +0 -0
  26. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/benchmarks/bg_comparison/nnUNet_pipeline_here.py +0 -0
  27. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/benchmarks/unique_values.py +0 -0
  28. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/dataloading/__init__.py +0 -0
  29. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/helpers/__init__.py +0 -0
  30. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/helpers/fft_conv.py +0 -0
  31. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/helpers/scalar_type.py +0 -0
  32. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/__init__.py +0 -0
  33. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/base/__init__.py +0 -0
  34. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/base/basic_transform.py +0 -0
  35. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/intensity/__init__.py +0 -0
  36. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/local/__init__.py +0 -0
  37. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/nnunet/__init__.py +0 -0
  38. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/nnunet/random_binary_operator.py +0 -0
  39. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/nnunet/remove_connected_components.py +0 -0
  40. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/noise/__init__.py +0 -0
  41. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/noise/blank_rectangle.py +0 -0
  42. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/noise/median_filter.py +0 -0
  43. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/spatial/__init__.py +0 -0
  44. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/spatial/low_resolution.py +0 -0
  45. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/spatial/mirroring.py +0 -0
  46. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/spatial/rot90.py +0 -0
  47. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/spatial/transpose.py +0 -0
  48. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/__init__.py +0 -0
  49. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/compose.py +0 -0
  50. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/cropping.py +0 -0
  51. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/move_channels.py +0 -0
  52. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/nnunet_masking.py +0 -0
  53. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/pseudo2d.py +0 -0
  54. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/random.py +0 -0
  55. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/remove_label.py +0 -0
  56. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2/transforms/utils/seg_to_regions.py +0 -0
  57. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2.egg-info/SOURCES.txt +0 -0
  58. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2.egg-info/dependency_links.txt +0 -0
  59. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2.egg-info/requires.txt +0 -0
  60. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/batchgeneratorsv2.egg-info/top_level.txt +0 -0
  61. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/readme.md +0 -0
  62. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/setup.cfg +0 -0
  63. {batchgeneratorsv2-0.3.3 → batchgeneratorsv2-0.3.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batchgeneratorsv2
3
- Version: 0.3.3
3
+ Version: 0.3.5
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
 
@@ -140,6 +140,10 @@ class BrightnessGradientAdditiveTransform(ImageOnlyTransform, LocalTransform):
140
140
  return {'kernels': kernels}
141
141
 
142
142
  def _apply_to_image(self, img: torch.Tensor, **params) -> torch.Tensor:
143
+ if self.clip_intensities:
144
+ original_min = img.min()
145
+ original_max = img.max()
146
+
143
147
  for c, kernel in enumerate(params['kernels']):
144
148
  if kernel is None:
145
149
  continue
@@ -147,7 +151,7 @@ class BrightnessGradientAdditiveTransform(ImageOnlyTransform, LocalTransform):
147
151
  img[c].add_(kernel_tensor)
148
152
 
149
153
  if self.clip_intensities:
150
- img.clamp_(min=img.min(), max=img.max())
154
+ img.clamp_(min=original_min, max=original_max)
151
155
 
152
156
  return img
153
157
 
@@ -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)
@@ -201,24 +201,74 @@ class ChannelMisalignmentTransform(ImageOnlyTransform):
201
201
  if params['elastic_offsets'] is not None:
202
202
  grid += params['elastic_offsets']
203
203
  if params['affine'] is not None:
204
- grid = torch.matmul(grid, torch.from_numpy(params['affine']).float())
205
-
206
- # we center the grid around the center_location_in_pixels. We should center the mean of the grid, not the center position
207
- # only do this if we elastic deform
204
+ # grid stores spatial vectors as row vectors (shape [..., dim]), whereas affine is built to act on
205
+ # column vectors (x' = affine @ x). We therefore multiply by affine.T so that each grid point is
206
+ # transformed by affine rather than its transpose (see issue #24).
207
+ grid = torch.matmul(grid, torch.from_numpy(params['affine'].T).float())
208
+
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.
208
213
  if params['elastic_offsets'] is not None:
209
214
  mn = grid.mean(dim=list(range(img.ndim - 1)))
210
- else:
211
- 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)
212
218
 
213
- # new_center = torch.Tensor([c - s / 2 for c, s in zip(params['center_location_in_pixels'], img.shape[1:])])
214
- new_center = torch.Tensor([0, 0, 0])
215
- 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]
216
221
 
217
222
  for ch in self.im_channels_2_misalign:
218
- img[ch, ...] = grid_sample(img[ch, ...].unsqueeze(0).unsqueeze(0),
219
- _convert_my_grid_to_grid_sample_grid(grid, img.shape[1:])[None],
220
- mode='bilinear', padding_mode="zeros", align_corners=False)[0]
221
- 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,
222
228
  [math.floor(i) for i in params['center_location_in_pixels']], im_shape,
223
229
  pad_mode='constant', pad_kwargs={'value': 0})
224
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')
@@ -219,7 +219,10 @@ class SpatialTransform(BasicTransform):
219
219
  if offsets is not None:
220
220
  grid += offsets
221
221
  if affine is not None:
222
- grid = torch.matmul(grid, torch.from_numpy(affine).float())
222
+ # grid stores spatial vectors as row vectors (shape [..., dim]), whereas affine is built to act on
223
+ # column vectors (x' = affine @ x). We therefore multiply by affine.T so that each grid point is
224
+ # transformed by affine rather than its transpose (see issue #24).
225
+ grid = torch.matmul(grid, torch.from_numpy(affine.T).float())
223
226
 
224
227
  # we center the grid around the center_location_in_pixels. We should center the mean of the grid, not the center position
225
228
  # only do this if we elastic deform
@@ -300,6 +303,11 @@ class SpatialTransform(BasicTransform):
300
303
  if self.bg_style_seg_sampling:
301
304
  for c in range(segmentation.shape[0]):
302
305
  labels = torch.from_numpy(np.sort(pd.unique(segmentation[c].numpy().ravel())))
306
+ # result_seg is zero-initialized, so when the lowest label is the 0 background we never have to
307
+ # write it: any voxel it would claim is already 0, and being the lowest label it can only be
308
+ # overwritten by (never overwrite) the foreground labels processed after it. Skipping it saves a
309
+ # full grid_sample per channel. Output is bit-identical.
310
+ bg_is_zero = bool(labels[0] == 0)
303
311
  # if we only have 2 labels then we can save compute time
304
312
  if len(labels) == 2:
305
313
  out = grid_sample(
@@ -310,9 +318,10 @@ class SpatialTransform(BasicTransform):
310
318
  align_corners=self.align_corners
311
319
  )[0][0] >= 0.5
312
320
  result_seg[c][out] = labels[1]
313
- result_seg[c][~out] = labels[0]
321
+ if not bg_is_zero:
322
+ result_seg[c][~out] = labels[0]
314
323
  else:
315
- for i, u in enumerate(labels):
324
+ for u in (labels[1:] if bg_is_zero else labels):
316
325
  result_seg[c][
317
326
  grid_sample(
318
327
  ((segmentation[c] == u).float())[None, None],
@@ -322,26 +331,35 @@ class SpatialTransform(BasicTransform):
322
331
  align_corners=self.align_corners
323
332
  )[0][0] >= 0.5] = u
324
333
  else:
334
+ # Per-voxel argmax over the interpolated one-hot label channels, computed incrementally so the
335
+ # (num_labels, *patch_size) stack is never materialized: only a running best value and the winning
336
+ # label (written straight into result_seg) are kept. This equals threshold-then-argmax sampling
337
+ # because the interpolated channels form a convex combination (they sum to <= scale_factor per
338
+ # voxel, so at most one label can exceed 0.7 * scale_factor). Load-bearing for bit-identical
339
+ # results: the running comparison is done in float16, and the strict > keeps the lowest label
340
+ # index on ties, exactly like torch.argmax(0) over a float16 stack.
341
+ scale_factor = 1000
325
342
  for c in range(segmentation.shape[0]):
326
343
  labels = torch.from_numpy(np.sort(pd.unique(segmentation[c].numpy().ravel())))
327
- # torch.where(torch.bincount(segmentation.ravel()) > 0)[0].to(segmentation.dtype)
328
- tmp = torch.zeros((len(labels), *self.patch_size), dtype=torch.float16)
329
- scale_factor = 1000
330
- done_mask = torch.zeros(*self.patch_size, dtype=torch.bool)
331
- for i, u in enumerate(labels):
332
- tmp[i] = grid_sample(
333
- ((segmentation[c] == u).float() * scale_factor)[None, None],
344
+ best_val = None
345
+ for u in labels:
346
+ onehot = (segmentation[c] == u).float()
347
+ onehot *= scale_factor
348
+ cur = grid_sample(
349
+ onehot[None, None],
334
350
  grid[None],
335
351
  mode=self.mode_seg,
336
352
  padding_mode=grid_sample_padding_mode,
337
353
  align_corners=self.align_corners
338
- )[0][0]
339
- mask = tmp[i] > (0.7 * scale_factor)
340
- result_seg[c][mask] = u
341
- done_mask = done_mask | mask
342
- if not torch.all(done_mask):
343
- result_seg[c][~done_mask] = labels[tmp[:, ~done_mask].argmax(0)]
344
- del tmp
354
+ )[0][0].to(torch.float16)
355
+ if best_val is None:
356
+ best_val = cur
357
+ if u != 0: # result_seg is zero-initialized, so filling with 0 would be a no-op
358
+ result_seg[c] = u
359
+ else:
360
+ better = cur > best_val
361
+ torch.maximum(best_val, cur, out=best_val)
362
+ result_seg[c][better] = u
345
363
 
346
364
  if self._requires_constant_padding_fixup(self.border_mode_seg, self.padding_value_seg):
347
365
  out_of_bounds_mask = self._compute_out_of_bounds_mask(grid, segmentation.shape[1:])
@@ -12,6 +12,8 @@ class DownsampleSegForDSTransform(SegOnlyTransform):
12
12
 
13
13
  def _apply_to_segmentation(self, segmentation: torch.Tensor, **params) -> List[torch.Tensor]:
14
14
  results = []
15
+ dtype = segmentation.dtype
16
+ seg_float = None # full-res float copy, built once and shared across scales (interpolate only reads it)
15
17
  for s in self.ds_scales:
16
18
  if not isinstance(s, (tuple, list)):
17
19
  s = [s] * (segmentation.ndim - 1)
@@ -22,9 +24,10 @@ class DownsampleSegForDSTransform(SegOnlyTransform):
22
24
  results.append(segmentation)
23
25
  else:
24
26
  new_shape = [round(i * j) for i, j in zip(segmentation.shape[1:], s)]
25
- dtype = segmentation.dtype
26
27
  # interpolate is not defined for short etc
27
- results.append(interpolate(segmentation[None].float(), new_shape, mode='nearest-exact')[0].to(dtype))
28
+ if seg_float is None:
29
+ seg_float = segmentation[None].float()
30
+ results.append(interpolate(seg_float, new_shape, mode='nearest-exact')[0].to(dtype))
28
31
  return results
29
32
 
30
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batchgeneratorsv2
3
- Version: 0.3.3
3
+ Version: 0.3.5
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "batchgeneratorsv2"
3
- version = "0.3.3"
3
+ version = "0.3.5"
4
4
  requires-python = ">=3.9"
5
5
  description = "Batchgenerators but better"
6
6
  readme = "readme.md"