batchgeneratorsv2 0.2.3__tar.gz → 0.3.0__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 (61) hide show
  1. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/PKG-INFO +3 -2
  2. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/base/basic_transform.py +1 -1
  3. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/brightness.py +44 -1
  4. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/contrast.py +1 -0
  5. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/intensity/random_clip.py +102 -0
  6. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/brightness_gradient.py +177 -0
  7. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/local_contrast.py +90 -0
  8. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/local_gamma.py +104 -0
  9. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/local_smoothing.py +98 -0
  10. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/local_transform.py +86 -0
  11. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise/blank_rectangle.py +150 -0
  12. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise/median_filter.py +52 -0
  13. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise/rician.py +61 -0
  14. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise/sharpen.py +128 -0
  15. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/spatial/rot90.py +78 -0
  16. batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/utils/__init__.py +0 -0
  17. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/random.py +23 -0
  18. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/PKG-INFO +3 -2
  19. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/SOURCES.txt +12 -0
  20. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/dependency_links.txt +0 -0
  21. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/requires.txt +0 -0
  22. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/top_level.txt +0 -0
  23. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/pyproject.toml +1 -1
  24. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/LICENSE +0 -0
  25. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/__init__.py +0 -0
  26. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/__init__.py +0 -0
  27. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/bg_comparison/__init__.py +0 -0
  28. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/bg_comparison/nnUNet_pipeline_bg.py +0 -0
  29. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/bg_comparison/nnUNet_pipeline_here.py +0 -0
  30. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/unique_values.py +0 -0
  31. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/dataloading/__init__.py +0 -0
  32. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/helpers/__init__.py +0 -0
  33. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/helpers/scalar_type.py +0 -0
  34. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/__init__.py +0 -0
  35. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/base/__init__.py +0 -0
  36. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/__init__.py +0 -0
  37. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/gamma.py +0 -0
  38. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/gaussian_noise.py +0 -0
  39. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/inversion.py +0 -0
  40. {batchgeneratorsv2-0.2.3/batchgeneratorsv2/transforms/nnunet → batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local}/__init__.py +0 -0
  41. {batchgeneratorsv2-0.2.3/batchgeneratorsv2/transforms/noise → batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/nnunet}/__init__.py +0 -0
  42. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/nnunet/random_binary_operator.py +0 -0
  43. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/nnunet/remove_connected_components.py +0 -0
  44. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/nnunet/seg_to_onehot.py +0 -0
  45. {batchgeneratorsv2-0.2.3/batchgeneratorsv2/transforms/spatial → batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise}/__init__.py +0 -0
  46. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/noise/gaussian_blur.py +0 -0
  47. {batchgeneratorsv2-0.2.3/batchgeneratorsv2/transforms/utils → batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/spatial}/__init__.py +0 -0
  48. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/spatial/low_resolution.py +0 -0
  49. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/spatial/mirroring.py +0 -0
  50. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/spatial/spatial.py +0 -0
  51. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/spatial/transpose.py +0 -0
  52. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/compose.py +0 -0
  53. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/cropping.py +0 -0
  54. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/deep_supervision_downsampling.py +0 -0
  55. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/nnunet_masking.py +0 -0
  56. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/pseudo2d.py +0 -0
  57. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/remove_label.py +0 -0
  58. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/seg_to_regions.py +0 -0
  59. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/readme.md +0 -0
  60. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/setup.cfg +0 -0
  61. {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: batchgeneratorsv2
3
- Version: 0.2.3
3
+ Version: 0.3.0
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>
@@ -222,6 +222,7 @@ Requires-Dist: torch>=2.0.0
222
222
  Requires-Dist: numpy
223
223
  Requires-Dist: fft-conv-pytorch
224
224
  Requires-Dist: batchgenerators>=0.25
225
+ Dynamic: license-file
225
226
 
226
227
  # batchgeneratorsv2
227
228
  This repository is work in progress. If builds upon the [batchgenerators](https://github.com/MIC-DKFZ/batchgenerators)
@@ -22,7 +22,7 @@ class BasicTransform(abc.ABC):
22
22
  data_dict['image'] = self._apply_to_image(data_dict['image'], **params)
23
23
 
24
24
  if data_dict.get('regression_target') is not None:
25
- data_dict['regression_target'] = self._apply_to_segmentation(data_dict['regression_target'], **params)
25
+ data_dict['regression_target'] = self._apply_to_regr_target(data_dict['regression_target'], **params)
26
26
 
27
27
  if data_dict.get('segmentation') is not None:
28
28
  data_dict['segmentation'] = self._apply_to_segmentation(data_dict['segmentation'], **params)
@@ -1,3 +1,4 @@
1
+ import numpy as np
1
2
  import torch
2
3
 
3
4
  from batchgeneratorsv2.helpers.scalar_type import RandomScalar, sample_scalar
@@ -33,9 +34,51 @@ class MultiplicativeBrightnessTransform(ImageOnlyTransform):
33
34
  return img
34
35
 
35
36
 
37
+ class BrightnessAdditiveTransform(ImageOnlyTransform):
38
+ """
39
+ Adds random additive brightness noise sampled from a Gaussian distribution (mu, sigma).
40
+
41
+ Supports per-channel brightness sampling or shared brightness across all channels.
42
+
43
+ Args:
44
+ mu (float): Mean of the Gaussian used to sample brightness shifts.
45
+ sigma (float): Standard deviation of the Gaussian.
46
+ per_channel (bool): If True, brightness shifts are sampled separately per channel.
47
+ p_per_channel (float): Probability to apply the brightness shift to each channel.
48
+ """
49
+
50
+ def __init__(self,
51
+ mu: float,
52
+ sigma: float,
53
+ per_channel: bool = True,
54
+ p_per_channel: float = 1.0):
55
+ super().__init__()
56
+ self.mu = mu
57
+ self.sigma = sigma
58
+ self.per_channel = per_channel
59
+ self.p_per_channel = p_per_channel
60
+
61
+ def get_parameters(self, image: torch.Tensor, **kwargs) -> dict:
62
+ C = image.shape[0]
63
+ apply_channel = [np.random.rand() < self.p_per_channel for _ in range(C)]
64
+
65
+ if self.per_channel:
66
+ brightness = [np.random.normal(self.mu, self.sigma) if apply else None for apply in apply_channel]
67
+ else:
68
+ global_brightness = np.random.normal(self.mu, self.sigma)
69
+ brightness = [global_brightness if apply else None for apply in apply_channel]
70
+
71
+ return {'brightness': brightness}
72
+
73
+ def _apply_to_image(self, img: torch.Tensor, **params) -> torch.Tensor:
74
+ for c, b in enumerate(params['brightness']):
75
+ if b is not None:
76
+ img[c].add_(float(b))
77
+ return img
78
+
79
+
36
80
  if __name__ == '__main__':
37
81
  from time import time
38
- import numpy as np
39
82
  import os
40
83
 
41
84
  os.environ['OMP_NUM_THREADS'] = '1'
@@ -25,6 +25,7 @@ class BGContrast():
25
25
  def __repr__(self):
26
26
  return self.__class__.__name__ + f"(contrast_range={self.contrast_range})"
27
27
 
28
+
28
29
  class ContrastTransform(ImageOnlyTransform):
29
30
  def __init__(self, contrast_range: RandomScalar, preserve_range: bool, synchronize_channels: bool, p_per_channel: float = 1):
30
31
  super().__init__()
@@ -0,0 +1,102 @@
1
+ import torch
2
+ import numpy as np
3
+ from batchgeneratorsv2.transforms.base.basic_transform import ImageOnlyTransform
4
+ from batchgeneratorsv2.helpers.scalar_type import RandomScalar, sample_scalar
5
+
6
+
7
+ class CutOffOutliersTransform(ImageOnlyTransform):
8
+ """
9
+ Clamps intensities in the image to percentiles to remove outliers,
10
+ and optionally rescales the result to retain original standard deviation.
11
+
12
+ Args:
13
+ percentile_lower (RandomScalar): Lower cutoff percentile (0-100).
14
+ percentile_upper (RandomScalar): Upper cutoff percentile (0-100).
15
+ p_synchronize_channels (bool): If True, same percentiles are used for all channels.
16
+ p_per_channel (float): Probability to apply cutoff to each channel.
17
+ p_retain_std (float): Probability of retaining the original standard deviation after clipping.
18
+ """
19
+
20
+ def __init__(self,
21
+ percentile_lower: RandomScalar = 0.2,
22
+ percentile_upper: RandomScalar = 99.8,
23
+ p_synchronize_channels: bool = False,
24
+ p_per_channel: float = 1.0,
25
+ p_retain_std: float = 1.0):
26
+ super().__init__()
27
+ self.percentile_lower = percentile_lower
28
+ self.percentile_upper = percentile_upper
29
+ self.p_synchronize_channels = p_synchronize_channels
30
+ self.p_per_channel = p_per_channel
31
+ self.p_retain_std = p_retain_std
32
+
33
+ def get_parameters(self, image: torch.Tensor, **kwargs) -> dict:
34
+ C = image.shape[0]
35
+ apply_channel = [np.random.rand() < self.p_per_channel for _ in range(C)]
36
+
37
+ if self.p_synchronize_channels:
38
+ lower = float(sample_scalar(self.percentile_lower))
39
+ upper = float(sample_scalar(self.percentile_upper))
40
+ percentiles = [(lower, upper) if apply else None for apply in apply_channel]
41
+ else:
42
+ percentiles = []
43
+ for apply in apply_channel:
44
+ if not apply:
45
+ percentiles.append(None)
46
+ else:
47
+ lower = float(sample_scalar(self.percentile_lower))
48
+ upper = float(sample_scalar(self.percentile_upper))
49
+ percentiles.append((lower, upper))
50
+
51
+ retain_std_flags = [
52
+ np.random.rand() < self.p_retain_std if p is not None else False
53
+ for p in percentiles
54
+ ]
55
+
56
+ return {'percentiles': percentiles, 'retain_std': retain_std_flags}
57
+
58
+ def _apply_to_image(self, img: torch.Tensor, **params) -> torch.Tensor:
59
+ percentiles = params['percentiles']
60
+ retain_std = params['retain_std']
61
+
62
+ for c, perc in enumerate(percentiles):
63
+ if perc is None:
64
+ continue
65
+
66
+ img_c = img[c]
67
+ if retain_std[c]:
68
+ orig_std = img_c.std()
69
+
70
+ # Use numpy only to calculate percentiles
71
+ img_c_np = img_c.detach().cpu().numpy()
72
+ lower_val = np.percentile(img_c_np, perc[0])
73
+ upper_val = np.percentile(img_c_np, perc[1])
74
+
75
+ img_c_clipped = img_c.clamp(min=float(lower_val), max=float(upper_val))
76
+
77
+ if retain_std[c]:
78
+ clipped_std = img_c_clipped.std()
79
+ if clipped_std > 1e-8:
80
+ img_c_clipped = (img_c_clipped - img_c_clipped.mean()) / clipped_std * orig_std + img_c_clipped.mean()
81
+
82
+ img[c] = img_c_clipped
83
+
84
+ return img
85
+
86
+ if __name__ == '__main__':
87
+ from batchviewer import view_batch
88
+
89
+ image = torch.randn(1, 32, 64, 64) * 5
90
+
91
+ transform = CutOffOutliersTransform(
92
+ percentile_lower=(0.5, 5),
93
+ percentile_upper=(95, 99.5),
94
+ p_synchronize_channels=True,
95
+ p_per_channel=1.0,
96
+ p_retain_std=0.5
97
+ )
98
+
99
+ params = transform.get_parameters(image=image)
100
+ image_clipped = transform._apply_to_image(image.clone(), **params)
101
+
102
+ view_batch(image, image_clipped, image_clipped-image)
@@ -0,0 +1,177 @@
1
+ import torch
2
+ import numpy as np
3
+ from typing import List
4
+
5
+ from batchgeneratorsv2.transforms.base.basic_transform import ImageOnlyTransform
6
+ from batchgeneratorsv2.helpers.scalar_type import RandomScalar, sample_scalar
7
+ from batchgeneratorsv2.transforms.local.local_transform import LocalTransform
8
+
9
+
10
+ class BrightnessGradientAdditiveTransform(ImageOnlyTransform, LocalTransform):
11
+ """
12
+ Applies a localized brightness modulation to an image using a smooth Gaussian gradient.
13
+
14
+ This transform creates a spatial Gaussian kernel (in 2D or 3D), optionally zero-centers it,
15
+ scales its peak intensity, and adds it to the image. This can simulate intensity drift,
16
+ local contrast changes, or smooth lighting artifacts.
17
+
18
+ The effect is applied per channel, and each channel can have a different gradient or share the same one.
19
+
20
+ ---
21
+ Example use cases:
22
+ - Simulating local contrast shifts in MRI
23
+ - Adding spatial brightness gradients for robustness
24
+ - Mimicking smooth scanner inhomogeneity fields
25
+
26
+ Args:
27
+ scale (RandomScalar):
28
+ Controls the spatial spread of the Gaussian kernel (standard deviation).
29
+ Can be:
30
+ - float: fixed spread
31
+ - (min, max): uniformly sampled per-dimension
32
+ - callable(image_shape, dim): custom sampling per axis
33
+
34
+ loc (RandomScalar):
35
+ Controls the relative location of the Gaussian kernel (in percentage of image size).
36
+ Can be:
37
+ - (min, max): e.g. (-1, 2) allows centers to be far outside the image for smoother edges
38
+ - callable(image_shape, dim): custom sampling per axis
39
+
40
+ max_strength (RandomScalar):
41
+ Peak value of the additive brightness change (positive or negative depending on the Gaussian).
42
+ Can be:
43
+ - float: fixed strength
44
+ - (min, max): sampled strength
45
+ - callable(image, kernel): fully custom
46
+
47
+ same_for_all_channels (bool):
48
+ If True, one shared kernel is used across all channels.
49
+ If False, each channel gets its own random kernel and strength.
50
+
51
+ mean_centered (bool):
52
+ If True, the Gaussian kernel is mean-centered (i.e., ∑kernel = 0),
53
+ which ensures the overall mean intensity of the image stays constant.
54
+
55
+ clip_intensities (bool):
56
+ If True, clamps image values after modification to their original min/max.
57
+ Useful to prevent range overflow.
58
+
59
+ p_per_channel (float):
60
+ Probability to apply the transform to each channel independently.
61
+
62
+ Returns:
63
+ Modified image of the same shape with localized brightness modulation applied.
64
+
65
+ Example:
66
+ transform = BrightnessGradientAdditiveTransform(
67
+ scale=(5, 15),
68
+ max_strength=(0.1, 0.5),
69
+ same_for_all_channels=True,
70
+ mean_centered=True
71
+ )
72
+ """
73
+ def __init__(self,
74
+ scale: RandomScalar,
75
+ loc: RandomScalar = (-1, 2),
76
+ max_strength: RandomScalar = 1.0,
77
+ same_for_all_channels: bool = True,
78
+ mean_centered: bool = True,
79
+ clip_intensities: bool = False,
80
+ p_per_channel: float = 1.0):
81
+ ImageOnlyTransform.__init__(self)
82
+ LocalTransform.__init__(self, scale, loc)
83
+
84
+ self.max_strength = max_strength
85
+ self.same_for_all_channels = same_for_all_channels
86
+ self.mean_centered = mean_centered
87
+ self.clip_intensities = clip_intensities
88
+ self.p_per_channel = p_per_channel
89
+
90
+ def get_parameters(self, image: torch.Tensor, **kwargs) -> dict:
91
+ C, *spatial = image.shape
92
+ apply_channel = [np.random.rand() < self.p_per_channel for _ in range(C)]
93
+
94
+ # Early exit if nothing will be applied
95
+ if not any(apply_channel):
96
+ return {'kernels': [None] * C}
97
+
98
+ if self.same_for_all_channels:
99
+ kernel = self._generate_kernel(spatial)
100
+ if self.mean_centered:
101
+ kernel -= kernel.mean()
102
+
103
+ max_abs = np.abs(kernel).max()
104
+ if max_abs < 1e-8:
105
+ return {'kernels': [None] * C}
106
+
107
+ strength = sample_scalar(self.max_strength, image, kernel)
108
+ if strength == 0.0:
109
+ return {'kernels': [None] * C}
110
+
111
+ kernel /= max_abs
112
+ kernel *= strength
113
+
114
+ kernels = [kernel if apply else None for apply in apply_channel]
115
+
116
+ else:
117
+ kernels = []
118
+ for apply in apply_channel:
119
+ if not apply:
120
+ kernels.append(None)
121
+ continue
122
+
123
+ kernel = self._generate_kernel(spatial)
124
+ if self.mean_centered:
125
+ kernel -= kernel.mean()
126
+ max_abs = np.abs(kernel).max()
127
+ if max_abs < 1e-8:
128
+ kernels.append(None)
129
+ continue
130
+
131
+ strength = sample_scalar(self.max_strength, image, kernel)
132
+ if strength == 0.0:
133
+ kernels.append(None)
134
+ continue
135
+
136
+ kernel /= max_abs
137
+ kernel *= strength
138
+ kernels.append(kernel)
139
+
140
+ return {'kernels': kernels}
141
+
142
+ def _apply_to_image(self, img: torch.Tensor, **params) -> torch.Tensor:
143
+ for c, kernel in enumerate(params['kernels']):
144
+ if kernel is None:
145
+ continue
146
+ kernel_tensor = torch.from_numpy(kernel).to(img.device, dtype=img.dtype)
147
+ img[c].add_(kernel_tensor)
148
+
149
+ if self.clip_intensities:
150
+ img.clamp_(min=img.min(), max=img.max())
151
+
152
+ return img
153
+
154
+ if __name__ == '__main__':
155
+ import torch
156
+ from batchviewer import view_batch
157
+
158
+ # Create synthetic z-score normalized 3D image (C, D, H, W)
159
+ image = torch.randn(1, 32, 64, 64) # single-channel 3D volume
160
+
161
+ # Instantiate the transform
162
+ transform = BrightnessGradientAdditiveTransform(
163
+ scale=(25, 50), # controls width of Gaussian
164
+ loc=(-0.5, 1.5),
165
+ max_strength=(2, 5), # how strong the modulation is
166
+ same_for_all_channels=True,
167
+ mean_centered=True,
168
+ clip_intensities=False,
169
+ p_per_channel=1.0 # always apply
170
+ )
171
+
172
+ # Get transform parameters and apply
173
+ params = transform.get_parameters(image=image)
174
+ image_modulated = transform._apply_to_image(image.clone(), **params)
175
+
176
+ # Visualize with your preferred viewer
177
+ view_batch(image, image_modulated)
@@ -0,0 +1,90 @@
1
+ import torch
2
+ import numpy as np
3
+ from batchgeneratorsv2.transforms.base.basic_transform import ImageOnlyTransform
4
+ from batchgeneratorsv2.helpers.scalar_type import RandomScalar, sample_scalar
5
+ from batchgeneratorsv2.transforms.local.local_transform import LocalTransform
6
+
7
+
8
+ class LocalContrastTransform(ImageOnlyTransform, LocalTransform):
9
+ """
10
+ Applies localized contrast modification using a spatial Gaussian mask.
11
+
12
+ A contrast-modified version of the image is blended with the original using a kernel-based interpolation.
13
+
14
+ Args:
15
+ scale (RandomScalar): Gaussian spread for the spatial weighting mask.
16
+ loc (RandomScalar): Relative center position for the Gaussian (in % of image size).
17
+ new_contrast (RandomScalar): Multiplicative factor for local contrast. 1.0 = no change.
18
+ same_for_all_channels (bool): Whether to use one kernel/contrast value for all channels.
19
+ p_per_channel (float): Probability to apply to each channel.
20
+ """
21
+
22
+ def __init__(self,
23
+ scale: RandomScalar,
24
+ loc: RandomScalar = (-1, 2),
25
+ new_contrast: RandomScalar = (0.5, 1.5),
26
+ same_for_all_channels: bool = True,
27
+ p_per_channel: float = 1.0):
28
+ ImageOnlyTransform.__init__(self)
29
+ LocalTransform.__init__(self, scale, loc)
30
+
31
+ self.new_contrast = new_contrast
32
+ self.same_for_all_channels = same_for_all_channels
33
+ self.p_per_channel = p_per_channel
34
+
35
+ def get_parameters(self, image: torch.Tensor, **kwargs) -> dict:
36
+ C, *spatial = image.shape
37
+ apply_channel = [np.random.rand() < self.p_per_channel for _ in range(C)]
38
+
39
+ if not any(apply_channel):
40
+ return {'kernels': [None] * C, 'contrasts': [None] * C}
41
+
42
+ if self.same_for_all_channels:
43
+ kernel = self._generate_kernel(spatial).astype(np.float32)
44
+ contrast = sample_scalar(self.new_contrast)
45
+
46
+ kernels = [kernel if apply else None for apply in apply_channel]
47
+ contrasts = [contrast if apply else None for apply in apply_channel]
48
+ else:
49
+ kernels, contrasts = [], []
50
+ for apply in apply_channel:
51
+ if not apply:
52
+ kernels.append(None)
53
+ contrasts.append(None)
54
+ continue
55
+ kernel = self._generate_kernel(spatial).astype(np.float32)
56
+ contrast = sample_scalar(self.new_contrast)
57
+ kernels.append(kernel)
58
+ contrasts.append(contrast)
59
+
60
+ return {'kernels': kernels, 'contrasts': contrasts}
61
+
62
+ def _apply_to_image(self, img: torch.Tensor, **params) -> torch.Tensor:
63
+ img_np = img.cpu().numpy()
64
+
65
+ for c, (kernel, contrast) in enumerate(zip(params['kernels'], params['contrasts'])):
66
+ if kernel is None:
67
+ continue
68
+
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)
73
+
74
+ return torch.from_numpy(img_np).to(img.device, dtype=img.dtype)
75
+
76
+
77
+ if __name__ == '__main__':
78
+ from batchviewer import view_batch
79
+
80
+ # Single-channel synthetic volume
81
+ image = torch.rand(1, 32, 64, 64)
82
+
83
+ # Or contrast
84
+ contrast = LocalContrastTransform(scale=(10, 20), new_contrast=(0.3, 2.0), p_per_channel=1.0)
85
+
86
+ # Apply either one
87
+ params = contrast.get_parameters(image=image)
88
+ image_aug = contrast._apply_to_image(image.clone(), **params)
89
+
90
+ view_batch(image, image_aug)
@@ -0,0 +1,104 @@
1
+ import torch
2
+ import numpy as np
3
+ from batchgeneratorsv2.transforms.base.basic_transform import ImageOnlyTransform
4
+ from batchgeneratorsv2.helpers.scalar_type import RandomScalar, sample_scalar
5
+ from batchgeneratorsv2.transforms.local.local_transform import LocalTransform
6
+
7
+
8
+ class LocalGammaTransform(ImageOnlyTransform, LocalTransform):
9
+ """
10
+ Applies locally varying gamma correction to an image using a spatial Gaussian weighting mask.
11
+
12
+ A Gaussian kernel is randomly placed in the image and used to blend between the original image and
13
+ a gamma-corrected version. This simulates localized nonlinear intensity shifts, useful for data augmentation
14
+ in medical imaging or general contrast robustness.
15
+
16
+ Parameters:
17
+ scale (RandomScalar): Controls the width of the Gaussian (std dev). Recommend large values (e.g. 10–30).
18
+ loc (RandomScalar): Controls Gaussian center as a % of image size. E.g. (-1, 2) allows off-canvas kernels.
19
+ gamma (RandomScalar): The gamma exponent applied locally. Try wild distributions :)
20
+ same_for_all_channels (bool): If True, one kernel is reused across all channels. Otherwise sampled per-channel.
21
+ p_per_channel (float): Probability to apply gamma correction to each channel independently.
22
+ """
23
+
24
+ def __init__(self,
25
+ scale: RandomScalar,
26
+ loc: RandomScalar = (-1, 2),
27
+ gamma: RandomScalar = (0.5, 1),
28
+ same_for_all_channels: bool = True,
29
+ p_per_channel: float = 1.0):
30
+ ImageOnlyTransform.__init__(self)
31
+ LocalTransform.__init__(self, scale, loc)
32
+
33
+ self.gamma = gamma
34
+ self.same_for_all_channels = same_for_all_channels
35
+ self.p_per_channel = p_per_channel
36
+
37
+ def get_parameters(self, image: torch.Tensor, **kwargs) -> dict:
38
+ C, *spatial = image.shape
39
+ apply_channel = [np.random.rand() < self.p_per_channel for _ in range(C)]
40
+
41
+ if not any(apply_channel):
42
+ return {'kernels': [None] * C, 'gammas': [None] * C}
43
+
44
+ if self.same_for_all_channels:
45
+ kernel = self._generate_kernel(spatial).astype(np.float32)
46
+ gamma = sample_scalar(self.gamma)
47
+
48
+ kernels = [kernel if apply else None for apply in apply_channel]
49
+ gammas = [gamma if apply else None for apply in apply_channel]
50
+ else:
51
+ kernels, gammas = [], []
52
+ for apply in apply_channel:
53
+ if not apply:
54
+ kernels.append(None)
55
+ gammas.append(None)
56
+ continue
57
+ kernel = self._generate_kernel(spatial).astype(np.float32)
58
+ gamma = sample_scalar(self.gamma)
59
+ kernels.append(kernel)
60
+ gammas.append(gamma)
61
+
62
+ return {'kernels': kernels, 'gammas': gammas}
63
+
64
+ def _apply_to_image(self, img: torch.Tensor, **params) -> torch.Tensor:
65
+ img_np = img.cpu().numpy()
66
+
67
+ for c, (kernel, gamma) in enumerate(zip(params['kernels'], params['gammas'])):
68
+ if kernel is None:
69
+ continue
70
+
71
+ channel = img_np[c]
72
+ min_val, max_val = channel.min(), channel.max()
73
+ denom = max(max_val - min_val, 1e-8)
74
+
75
+ # Normalize to [0, 1]
76
+ norm = (channel - min_val) / denom
77
+ gamma_corrected = np.power(norm, gamma)
78
+
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
84
+
85
+ return torch.from_numpy(img_np).to(img.device, dtype=img.dtype)
86
+
87
+
88
+ if __name__ == '__main__':
89
+ import torch
90
+ from batchviewer import view_batch
91
+
92
+ image = torch.rand(1, 32, 64, 64) # (C, D, H, W)
93
+
94
+ transform = LocalGammaTransform(
95
+ scale=(10, 20),
96
+ gamma=lambda *_: np.random.uniform(0.01, 1) if np.random.rand() < 0.5 else np.random.uniform(1, 3),
97
+ same_for_all_channels=False,
98
+ p_per_channel=1.0
99
+ )
100
+
101
+ params = transform.get_parameters(image=image)
102
+ image_gamma = transform._apply_to_image(image.clone(), **params)
103
+
104
+ view_batch(image, image_gamma)
@@ -0,0 +1,98 @@
1
+ import torch
2
+ import numpy as np
3
+ from batchgeneratorsv2.transforms.local.local_transform import LocalTransform
4
+ from scipy.ndimage import gaussian_filter
5
+ from batchgeneratorsv2.transforms.base.basic_transform import ImageOnlyTransform
6
+ from batchgeneratorsv2.helpers.scalar_type import RandomScalar, sample_scalar
7
+
8
+
9
+ class LocalSmoothingTransform(ImageOnlyTransform, LocalTransform):
10
+ """
11
+ Applies localized Gaussian smoothing to parts of the image using a spatial Gaussian mask.
12
+
13
+ A blurred copy of the image is interpolated with the original, weighted by a Gaussian kernel.
14
+ The strength and extent of the blur are both controllable.
15
+
16
+ Args:
17
+ scale (RandomScalar): Gaussian spread for the spatial weighting mask.
18
+ loc (RandomScalar): Relative center position for the Gaussian (in % of image size).
19
+ smoothing_strength (RandomScalar): Max weight of the smoothed image in the interpolation [0, 1].
20
+ kernel_size (RandomScalar): Sigma for the actual Gaussian smoothing of the image.
21
+ same_for_all_channels (bool): Whether to apply the same kernel to all channels.
22
+ p_per_channel (float): Probability of applying transform per channel.
23
+ """
24
+
25
+ def __init__(self,
26
+ scale: RandomScalar,
27
+ loc: RandomScalar = (-1, 2),
28
+ smoothing_strength: RandomScalar = (0.5, 1.0),
29
+ kernel_size: RandomScalar = (0.5, 1.5),
30
+ same_for_all_channels: bool = True,
31
+ p_per_channel: float = 1.0):
32
+ ImageOnlyTransform.__init__(self)
33
+ LocalTransform.__init__(self, scale, loc)
34
+
35
+ self.smoothing_strength = smoothing_strength
36
+ self.kernel_size = kernel_size
37
+ self.same_for_all_channels = same_for_all_channels
38
+ self.p_per_channel = p_per_channel
39
+
40
+ def get_parameters(self, image: torch.Tensor, **kwargs) -> dict:
41
+ C, *spatial = image.shape
42
+ apply_channel = [np.random.rand() < self.p_per_channel for _ in range(C)]
43
+
44
+ if not any(apply_channel):
45
+ return {'kernels': [None] * C, 'sigma': None, 'strengths': [None] * C}
46
+
47
+ sigma = sample_scalar(self.kernel_size)
48
+
49
+ if self.same_for_all_channels:
50
+ kernel = self._generate_kernel(spatial).astype(np.float32)
51
+ strength = sample_scalar(self.smoothing_strength)
52
+
53
+ kernels = [kernel if apply else None for apply in apply_channel]
54
+ strengths = [strength if apply else None for apply in apply_channel]
55
+ else:
56
+ kernels, strengths = [], []
57
+ for apply in apply_channel:
58
+ if not apply:
59
+ kernels.append(None)
60
+ strengths.append(None)
61
+ continue
62
+ kernel = self._generate_kernel(spatial).astype(np.float32)
63
+ strength = sample_scalar(self.smoothing_strength)
64
+ kernels.append(kernel)
65
+ strengths.append(strength)
66
+
67
+ return {'kernels': kernels, 'sigma': sigma, 'strengths': strengths}
68
+
69
+ def _apply_to_image(self, img: torch.Tensor, **params) -> torch.Tensor:
70
+ img_np = img.cpu().numpy()
71
+ sigma = params['sigma']
72
+
73
+ for c, (kernel, strength) in enumerate(zip(params['kernels'], params['strengths'])):
74
+ if kernel is None:
75
+ continue
76
+
77
+ kernel = kernel * strength # scale kernel by smoothing strength
78
+ smoothed = gaussian_filter(img_np[c], sigma=sigma)
79
+ img_np[c] = self.run_interpolation(img_np[c], smoothed, kernel)
80
+
81
+ return torch.from_numpy(img_np).to(img.device, dtype=img.dtype)
82
+
83
+
84
+ if __name__ == '__main__':
85
+ from batchviewer import view_batch
86
+
87
+ # Single-channel synthetic volume
88
+ image = torch.rand(1, 32, 64, 64)
89
+
90
+ # Or contrast
91
+ smoother = LocalSmoothingTransform(loc=(0, 1), scale=(10, 20), kernel_size=(3, 10), p_per_channel=1.0)
92
+
93
+ # Apply either one
94
+ params = smoother.get_parameters(image=image)
95
+ image_aug = smoother._apply_to_image(image.clone(), **params)
96
+
97
+ view_batch(image, image_aug)
98
+