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.
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/PKG-INFO +3 -2
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/base/basic_transform.py +1 -1
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/brightness.py +44 -1
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/contrast.py +1 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/intensity/random_clip.py +102 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/brightness_gradient.py +177 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/local_contrast.py +90 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/local_gamma.py +104 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/local_smoothing.py +98 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local/local_transform.py +86 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise/blank_rectangle.py +150 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise/median_filter.py +52 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise/rician.py +61 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise/sharpen.py +128 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/spatial/rot90.py +78 -0
- batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/utils/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/random.py +23 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/PKG-INFO +3 -2
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/SOURCES.txt +12 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/dependency_links.txt +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/requires.txt +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2.egg-info/top_level.txt +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/pyproject.toml +1 -1
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/LICENSE +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/bg_comparison/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/bg_comparison/nnUNet_pipeline_bg.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/bg_comparison/nnUNet_pipeline_here.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/benchmarks/unique_values.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/dataloading/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/helpers/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/helpers/scalar_type.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/base/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/gamma.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/gaussian_noise.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/intensity/inversion.py +0 -0
- {batchgeneratorsv2-0.2.3/batchgeneratorsv2/transforms/nnunet → batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/local}/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3/batchgeneratorsv2/transforms/noise → batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/nnunet}/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/nnunet/random_binary_operator.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/nnunet/remove_connected_components.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/nnunet/seg_to_onehot.py +0 -0
- {batchgeneratorsv2-0.2.3/batchgeneratorsv2/transforms/spatial → batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/noise}/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/noise/gaussian_blur.py +0 -0
- {batchgeneratorsv2-0.2.3/batchgeneratorsv2/transforms/utils → batchgeneratorsv2-0.3.0/batchgeneratorsv2/transforms/spatial}/__init__.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/spatial/low_resolution.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/spatial/mirroring.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/spatial/spatial.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/spatial/transpose.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/compose.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/cropping.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/deep_supervision_downsampling.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/nnunet_masking.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/pseudo2d.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/remove_label.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/batchgeneratorsv2/transforms/utils/seg_to_regions.py +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/readme.md +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/setup.cfg +0 -0
- {batchgeneratorsv2-0.2.3 → batchgeneratorsv2-0.3.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: batchgeneratorsv2
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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
|
+
|