nrtk-albumentations 2.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of nrtk-albumentations might be problematic. Click here for more details.
- albumentations/__init__.py +21 -0
- albumentations/augmentations/__init__.py +23 -0
- albumentations/augmentations/blur/__init__.py +0 -0
- albumentations/augmentations/blur/functional.py +438 -0
- albumentations/augmentations/blur/transforms.py +1633 -0
- albumentations/augmentations/crops/__init__.py +0 -0
- albumentations/augmentations/crops/functional.py +494 -0
- albumentations/augmentations/crops/transforms.py +3647 -0
- albumentations/augmentations/dropout/__init__.py +0 -0
- albumentations/augmentations/dropout/channel_dropout.py +134 -0
- albumentations/augmentations/dropout/coarse_dropout.py +567 -0
- albumentations/augmentations/dropout/functional.py +1017 -0
- albumentations/augmentations/dropout/grid_dropout.py +166 -0
- albumentations/augmentations/dropout/mask_dropout.py +274 -0
- albumentations/augmentations/dropout/transforms.py +461 -0
- albumentations/augmentations/dropout/xy_masking.py +186 -0
- albumentations/augmentations/geometric/__init__.py +0 -0
- albumentations/augmentations/geometric/distortion.py +1238 -0
- albumentations/augmentations/geometric/flip.py +752 -0
- albumentations/augmentations/geometric/functional.py +4151 -0
- albumentations/augmentations/geometric/pad.py +676 -0
- albumentations/augmentations/geometric/resize.py +956 -0
- albumentations/augmentations/geometric/rotate.py +864 -0
- albumentations/augmentations/geometric/transforms.py +1962 -0
- albumentations/augmentations/mixing/__init__.py +0 -0
- albumentations/augmentations/mixing/domain_adaptation.py +787 -0
- albumentations/augmentations/mixing/domain_adaptation_functional.py +453 -0
- albumentations/augmentations/mixing/functional.py +878 -0
- albumentations/augmentations/mixing/transforms.py +832 -0
- albumentations/augmentations/other/__init__.py +0 -0
- albumentations/augmentations/other/lambda_transform.py +180 -0
- albumentations/augmentations/other/type_transform.py +261 -0
- albumentations/augmentations/pixel/__init__.py +0 -0
- albumentations/augmentations/pixel/functional.py +4226 -0
- albumentations/augmentations/pixel/transforms.py +7556 -0
- albumentations/augmentations/spectrogram/__init__.py +0 -0
- albumentations/augmentations/spectrogram/transform.py +220 -0
- albumentations/augmentations/text/__init__.py +0 -0
- albumentations/augmentations/text/functional.py +272 -0
- albumentations/augmentations/text/transforms.py +299 -0
- albumentations/augmentations/transforms3d/__init__.py +0 -0
- albumentations/augmentations/transforms3d/functional.py +393 -0
- albumentations/augmentations/transforms3d/transforms.py +1422 -0
- albumentations/augmentations/utils.py +249 -0
- albumentations/core/__init__.py +0 -0
- albumentations/core/bbox_utils.py +920 -0
- albumentations/core/composition.py +1885 -0
- albumentations/core/hub_mixin.py +299 -0
- albumentations/core/keypoints_utils.py +521 -0
- albumentations/core/label_manager.py +339 -0
- albumentations/core/pydantic.py +239 -0
- albumentations/core/serialization.py +352 -0
- albumentations/core/transforms_interface.py +976 -0
- albumentations/core/type_definitions.py +127 -0
- albumentations/core/utils.py +605 -0
- albumentations/core/validation.py +129 -0
- albumentations/pytorch/__init__.py +1 -0
- albumentations/pytorch/transforms.py +189 -0
- nrtk_albumentations-2.1.0.dist-info/METADATA +196 -0
- nrtk_albumentations-2.1.0.dist-info/RECORD +62 -0
- nrtk_albumentations-2.1.0.dist-info/WHEEL +4 -0
- nrtk_albumentations-2.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from importlib.metadata import metadata
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
_metadata = metadata("albumentations")
|
|
5
|
+
__version__ = _metadata["Version"]
|
|
6
|
+
__author__ = _metadata["Author"]
|
|
7
|
+
__maintainer__ = _metadata["Maintainer"]
|
|
8
|
+
except Exception: # noqa: BLE001
|
|
9
|
+
__version__ = "unknown"
|
|
10
|
+
__author__ = "Vladimir Iglovikov"
|
|
11
|
+
__maintainer__ = "Kitware, Inc."
|
|
12
|
+
|
|
13
|
+
from contextlib import suppress
|
|
14
|
+
|
|
15
|
+
from .augmentations import *
|
|
16
|
+
from .core.composition import *
|
|
17
|
+
from .core.serialization import *
|
|
18
|
+
from .core.transforms_interface import *
|
|
19
|
+
|
|
20
|
+
with suppress(ImportError):
|
|
21
|
+
from .pytorch import *
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .blur.transforms import *
|
|
2
|
+
from .crops.transforms import *
|
|
3
|
+
from .dropout.channel_dropout import *
|
|
4
|
+
from .dropout.coarse_dropout import *
|
|
5
|
+
from .dropout.grid_dropout import *
|
|
6
|
+
from .dropout.mask_dropout import *
|
|
7
|
+
from .dropout.transforms import *
|
|
8
|
+
from .dropout.xy_masking import *
|
|
9
|
+
from .geometric.distortion import *
|
|
10
|
+
from .geometric.flip import *
|
|
11
|
+
from .geometric.pad import *
|
|
12
|
+
from .geometric.resize import *
|
|
13
|
+
from .geometric.rotate import *
|
|
14
|
+
from .geometric.transforms import *
|
|
15
|
+
from .mixing.domain_adaptation import *
|
|
16
|
+
from .mixing.transforms import *
|
|
17
|
+
from .other.lambda_transform import *
|
|
18
|
+
from .other.type_transform import *
|
|
19
|
+
from .pixel.transforms import *
|
|
20
|
+
from .spectrogram.transform import *
|
|
21
|
+
from .text.transforms import *
|
|
22
|
+
from .transforms3d.transforms import *
|
|
23
|
+
from .utils import *
|
|
File without changes
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""Functional implementations of various blur operations for image processing.
|
|
2
|
+
|
|
3
|
+
This module provides a collection of low-level functions for applying different blur effects
|
|
4
|
+
to images, including standard blur, median blur, glass blur, defocus, and zoom effects.
|
|
5
|
+
These functions form the foundation for the corresponding transform classes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from itertools import product
|
|
13
|
+
from math import ceil
|
|
14
|
+
from typing import Literal
|
|
15
|
+
from warnings import warn
|
|
16
|
+
|
|
17
|
+
import cv2
|
|
18
|
+
import numpy as np
|
|
19
|
+
from albucore import clipped, float32_io, maybe_process_in_chunks, preserve_channel_dim, uint8_io
|
|
20
|
+
from pydantic import ValidationInfo
|
|
21
|
+
|
|
22
|
+
from albumentations.augmentations.geometric.functional import scale
|
|
23
|
+
from albumentations.augmentations.pixel.functional import convolve
|
|
24
|
+
from albumentations.core.type_definitions import EIGHT
|
|
25
|
+
|
|
26
|
+
__all__ = ["box_blur", "central_zoom", "defocus", "glass_blur", "median_blur", "zoom_blur"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@preserve_channel_dim
|
|
30
|
+
def box_blur(img: np.ndarray, ksize: int) -> np.ndarray:
|
|
31
|
+
"""Blur an image.
|
|
32
|
+
|
|
33
|
+
This function applies a blur to an image.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
img (np.ndarray): Input image.
|
|
37
|
+
ksize (int): Kernel size.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
np.ndarray: Blurred image.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
blur_fn = maybe_process_in_chunks(cv2.blur, ksize=(ksize, ksize))
|
|
44
|
+
return blur_fn(img)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@preserve_channel_dim
|
|
48
|
+
@uint8_io
|
|
49
|
+
def median_blur(img: np.ndarray, ksize: int) -> np.ndarray:
|
|
50
|
+
"""Median blur an image.
|
|
51
|
+
|
|
52
|
+
This function applies a median blur to an image.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
img (np.ndarray): Input image.
|
|
56
|
+
ksize (int): Kernel size.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
np.ndarray: Median blurred image.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
blur_fn = maybe_process_in_chunks(cv2.medianBlur, ksize=ksize)
|
|
63
|
+
return blur_fn(img)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@preserve_channel_dim
|
|
67
|
+
def glass_blur(
|
|
68
|
+
img: np.ndarray,
|
|
69
|
+
sigma: float,
|
|
70
|
+
max_delta: int,
|
|
71
|
+
iterations: int,
|
|
72
|
+
dxy: np.ndarray,
|
|
73
|
+
mode: Literal["fast", "exact"],
|
|
74
|
+
) -> np.ndarray:
|
|
75
|
+
"""Glass blur an image.
|
|
76
|
+
|
|
77
|
+
This function applies a glass blur to an image.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
img (np.ndarray): Input image.
|
|
81
|
+
sigma (float): Sigma.
|
|
82
|
+
max_delta (int): Maximum delta.
|
|
83
|
+
iterations (int): Number of iterations.
|
|
84
|
+
dxy (np.ndarray): Dxy.
|
|
85
|
+
mode (Literal["fast", "exact"]): Mode.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
np.ndarray: Glass blurred image.
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
x = cv2.GaussianBlur(np.array(img), sigmaX=sigma, ksize=(0, 0))
|
|
92
|
+
|
|
93
|
+
if mode == "fast":
|
|
94
|
+
hs = np.arange(img.shape[0] - max_delta, max_delta, -1)
|
|
95
|
+
ws = np.arange(img.shape[1] - max_delta, max_delta, -1)
|
|
96
|
+
h: int | np.ndarray = np.tile(hs, ws.shape[0])
|
|
97
|
+
w: int | np.ndarray = np.repeat(ws, hs.shape[0])
|
|
98
|
+
|
|
99
|
+
for i in range(iterations):
|
|
100
|
+
dy = dxy[:, i, 0]
|
|
101
|
+
dx = dxy[:, i, 1]
|
|
102
|
+
x[h, w], x[h + dy, w + dx] = x[h + dy, w + dx], x[h, w]
|
|
103
|
+
|
|
104
|
+
elif mode == "exact":
|
|
105
|
+
for ind, (i, h, w) in enumerate(
|
|
106
|
+
product(
|
|
107
|
+
range(iterations),
|
|
108
|
+
range(img.shape[0] - max_delta, max_delta, -1),
|
|
109
|
+
range(img.shape[1] - max_delta, max_delta, -1),
|
|
110
|
+
),
|
|
111
|
+
):
|
|
112
|
+
idx = ind if ind < len(dxy) else ind % len(dxy)
|
|
113
|
+
dy = dxy[idx, i, 0]
|
|
114
|
+
dx = dxy[idx, i, 1]
|
|
115
|
+
x[h, w], x[h + dy, w + dx] = x[h + dy, w + dx], x[h, w]
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError(f"Unsupported mode `{mode}`. Supports only `fast` and `exact`.")
|
|
118
|
+
|
|
119
|
+
return cv2.GaussianBlur(x, sigmaX=sigma, ksize=(0, 0))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def defocus(img: np.ndarray, radius: int, alias_blur: float) -> np.ndarray:
|
|
123
|
+
"""Defocus an image.
|
|
124
|
+
|
|
125
|
+
This function defocuses an image.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
img (np.ndarray): Input image.
|
|
129
|
+
radius (int): Radius.
|
|
130
|
+
alias_blur (float): Alias blur.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
np.ndarray: Defocused image.
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
length = np.arange(-max(8, radius), max(8, radius) + 1)
|
|
137
|
+
ksize = 3 if radius <= EIGHT else 5
|
|
138
|
+
|
|
139
|
+
x, y = np.meshgrid(length, length)
|
|
140
|
+
aliased_disk = np.array((x**2 + y**2) <= radius**2, dtype=np.float32)
|
|
141
|
+
aliased_disk /= np.sum(aliased_disk)
|
|
142
|
+
|
|
143
|
+
kernel = cv2.GaussianBlur(aliased_disk, (ksize, ksize), sigmaX=alias_blur)
|
|
144
|
+
|
|
145
|
+
return convolve(img, kernel=kernel)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def central_zoom(img: np.ndarray, zoom_factor: int) -> np.ndarray:
|
|
149
|
+
"""Central zoom an image.
|
|
150
|
+
|
|
151
|
+
This function zooms an image.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
img (np.ndarray): Input image.
|
|
155
|
+
zoom_factor (int): Zoom factor.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
np.ndarray: Zoomed image.
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
height, width = img.shape[:2]
|
|
162
|
+
h_ch, w_ch = ceil(height / zoom_factor), ceil(width / zoom_factor)
|
|
163
|
+
h_top, w_top = (height - h_ch) // 2, (width - w_ch) // 2
|
|
164
|
+
|
|
165
|
+
img = scale(img[h_top : h_top + h_ch, w_top : w_top + w_ch], zoom_factor, cv2.INTER_LINEAR)
|
|
166
|
+
h_trim_top, w_trim_top = (img.shape[0] - height) // 2, (img.shape[1] - width) // 2
|
|
167
|
+
return img[h_trim_top : h_trim_top + height, w_trim_top : w_trim_top + width]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@float32_io
|
|
171
|
+
@clipped
|
|
172
|
+
def zoom_blur(img: np.ndarray, zoom_factors: np.ndarray | Sequence[int]) -> np.ndarray:
|
|
173
|
+
"""Zoom blur an image.
|
|
174
|
+
|
|
175
|
+
This function zooms and blurs an image.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
img (np.ndarray): Input image.
|
|
179
|
+
zoom_factors (np.ndarray | Sequence[int]): Zoom factors.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
np.ndarray: Zoomed and blurred image.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
out = np.zeros_like(img, dtype=np.float32)
|
|
186
|
+
|
|
187
|
+
for zoom_factor in zoom_factors:
|
|
188
|
+
out += central_zoom(img, zoom_factor)
|
|
189
|
+
|
|
190
|
+
return (img + out) / (len(zoom_factors) + 1)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _ensure_min_value(result: tuple[int, int], min_value: int, field_name: str | None) -> tuple[int, int]:
|
|
194
|
+
if result[0] < min_value or result[1] < min_value:
|
|
195
|
+
new_result = (max(min_value, result[0]), max(min_value, result[1]))
|
|
196
|
+
warn(
|
|
197
|
+
f"{field_name}: Invalid kernel size range {result}. "
|
|
198
|
+
f"Values less than {min_value} are not allowed. "
|
|
199
|
+
f"Range automatically adjusted to {new_result}.",
|
|
200
|
+
UserWarning,
|
|
201
|
+
stacklevel=2,
|
|
202
|
+
)
|
|
203
|
+
return new_result
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _ensure_odd_values(result: tuple[int, int], field_name: str | None = None) -> tuple[int, int]:
|
|
208
|
+
new_result = (
|
|
209
|
+
result[0] if result[0] == 0 or result[0] % 2 == 1 else result[0] + 1,
|
|
210
|
+
result[1] if result[1] == 0 or result[1] % 2 == 1 else result[1] + 1,
|
|
211
|
+
)
|
|
212
|
+
if new_result != result:
|
|
213
|
+
warn(
|
|
214
|
+
f"{field_name}: Non-zero kernel sizes must be odd. Range {result} automatically adjusted to {new_result}.",
|
|
215
|
+
UserWarning,
|
|
216
|
+
stacklevel=2,
|
|
217
|
+
)
|
|
218
|
+
return new_result
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def process_blur_limit(value: int | tuple[int, int], info: ValidationInfo, min_value: int = 0) -> tuple[int, int]:
|
|
222
|
+
"""Process blur limit to ensure valid kernel sizes."""
|
|
223
|
+
# Convert value to tuple[int, int]
|
|
224
|
+
if isinstance(value, Sequence):
|
|
225
|
+
if len(value) != 2:
|
|
226
|
+
raise ValueError("Sequence must contain exactly 2 elements")
|
|
227
|
+
result = (int(value[0]), int(value[1]))
|
|
228
|
+
else:
|
|
229
|
+
result = (min_value, int(value))
|
|
230
|
+
|
|
231
|
+
result = _ensure_min_value(result, min_value, info.field_name)
|
|
232
|
+
result = _ensure_odd_values(result, info.field_name)
|
|
233
|
+
|
|
234
|
+
if result[0] > result[1]:
|
|
235
|
+
final_result = (result[1], result[1])
|
|
236
|
+
warn(
|
|
237
|
+
f"{info.field_name}: Invalid range {result} (min > max). Range automatically adjusted to {final_result}.",
|
|
238
|
+
UserWarning,
|
|
239
|
+
stacklevel=2,
|
|
240
|
+
)
|
|
241
|
+
return final_result
|
|
242
|
+
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def create_motion_kernel(
|
|
247
|
+
kernel_size: int,
|
|
248
|
+
angle: float,
|
|
249
|
+
direction: float,
|
|
250
|
+
allow_shifted: bool,
|
|
251
|
+
random_state: random.Random,
|
|
252
|
+
) -> np.ndarray:
|
|
253
|
+
"""Create a motion blur kernel.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
kernel_size (int): Size of the kernel (must be odd)
|
|
257
|
+
angle (float): Angle in degrees (counter-clockwise)
|
|
258
|
+
direction (float): Blur direction (-1.0 to 1.0)
|
|
259
|
+
allow_shifted (bool): Allow kernel to be randomly shifted from center
|
|
260
|
+
random_state (random.Random): Python's random.Random instance
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
np.ndarray: Motion blur kernel
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
# Validate direction range to prevent unexpected interpolation results
|
|
267
|
+
direction = np.clip(direction, -1.0, 1.0)
|
|
268
|
+
|
|
269
|
+
kernel = np.zeros((kernel_size, kernel_size), dtype=np.float32)
|
|
270
|
+
center = kernel_size // 2
|
|
271
|
+
|
|
272
|
+
# Convert angle to radians
|
|
273
|
+
angle_rad = np.deg2rad(angle)
|
|
274
|
+
|
|
275
|
+
# Calculate direction vector
|
|
276
|
+
dx = np.cos(angle_rad)
|
|
277
|
+
dy = np.sin(angle_rad)
|
|
278
|
+
|
|
279
|
+
# Create line points with direction bias
|
|
280
|
+
line_length = kernel_size // 2
|
|
281
|
+
|
|
282
|
+
# Apply direction bias to control the distribution of blur
|
|
283
|
+
if direction < 0:
|
|
284
|
+
# Backward bias: interpolate between symmetric and backward-only
|
|
285
|
+
# direction = -1: only backward, direction = 0: symmetric
|
|
286
|
+
bias_factor = abs(direction)
|
|
287
|
+
t_start = float(-line_length)
|
|
288
|
+
t_end = line_length * (1 - bias_factor)
|
|
289
|
+
elif direction > 0:
|
|
290
|
+
# Forward bias: interpolate between symmetric and forward-only
|
|
291
|
+
# direction = 1: only forward, direction = 0: symmetric
|
|
292
|
+
bias_factor = direction
|
|
293
|
+
t_start = -line_length * (1 - bias_factor)
|
|
294
|
+
t_end = float(line_length)
|
|
295
|
+
else:
|
|
296
|
+
# Symmetric case (direction = 0)
|
|
297
|
+
t_start = float(-line_length)
|
|
298
|
+
t_end = float(line_length)
|
|
299
|
+
|
|
300
|
+
# Generate points along the biased line
|
|
301
|
+
t = np.linspace(t_start, t_end, kernel_size)
|
|
302
|
+
|
|
303
|
+
# Generate line coordinates
|
|
304
|
+
x = center + dx * t
|
|
305
|
+
y = center + dy * t
|
|
306
|
+
|
|
307
|
+
# Apply random shift if allowed
|
|
308
|
+
if allow_shifted:
|
|
309
|
+
shift_x = random_state.uniform(-1, 1) * line_length / 2
|
|
310
|
+
shift_y = random_state.uniform(-1, 1) * line_length / 2
|
|
311
|
+
x += shift_x
|
|
312
|
+
y += shift_y
|
|
313
|
+
|
|
314
|
+
# Round coordinates and clip to kernel bounds
|
|
315
|
+
x = np.clip(np.round(x), 0, kernel_size - 1).astype(int)
|
|
316
|
+
y = np.clip(np.round(y), 0, kernel_size - 1).astype(int)
|
|
317
|
+
|
|
318
|
+
# Keep only unique points to avoid multiple assignments
|
|
319
|
+
points = np.unique(np.column_stack([y, x]), axis=0)
|
|
320
|
+
kernel[points[:, 0], points[:, 1]] = 1
|
|
321
|
+
|
|
322
|
+
# Ensure at least one point is set
|
|
323
|
+
if not kernel.any():
|
|
324
|
+
kernel[center, center] = 1
|
|
325
|
+
|
|
326
|
+
return kernel
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def sample_odd_from_range(random_state: random.Random, low: int, high: int) -> int:
|
|
330
|
+
"""Sample an odd number from the range [low, high] (inclusive).
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
random_state (random.Random): instance of random.Random
|
|
334
|
+
low (int): lower bound (will be converted to nearest valid odd number)
|
|
335
|
+
high (int): upper bound (will be converted to nearest valid odd number)
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
int: Randomly sampled odd number from the range
|
|
339
|
+
|
|
340
|
+
Note:
|
|
341
|
+
- Input values will be converted to nearest valid odd numbers:
|
|
342
|
+
* Values less than 3 will become 3
|
|
343
|
+
* Even values will be rounded up to next odd number
|
|
344
|
+
- After normalization, high must be >= low
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
# Normalize low value
|
|
348
|
+
low = max(3, low + (low % 2 == 0))
|
|
349
|
+
# Normalize high value
|
|
350
|
+
high = max(3, high + (high % 2 == 0))
|
|
351
|
+
|
|
352
|
+
# Ensure high >= low after normalization
|
|
353
|
+
high = max(high, low)
|
|
354
|
+
|
|
355
|
+
if low == high:
|
|
356
|
+
return low
|
|
357
|
+
|
|
358
|
+
# Calculate number of possible odd values
|
|
359
|
+
num_odd_values = (high - low) // 2 + 1
|
|
360
|
+
# Generate random index and convert to corresponding odd number
|
|
361
|
+
rand_idx = random_state.randint(0, num_odd_values - 1)
|
|
362
|
+
return low + (2 * rand_idx)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def create_gaussian_kernel(sigma: float, ksize: int = 0) -> np.ndarray:
|
|
366
|
+
"""Create a Gaussian kernel following PIL's approach.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
sigma (float): Standard deviation for Gaussian kernel.
|
|
370
|
+
ksize (int): Kernel size. If 0, size is computed as int(sigma * 3.5) * 2 + 1
|
|
371
|
+
to match PIL's implementation. Otherwise, must be positive and odd.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
np.ndarray: 2D normalized Gaussian kernel.
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
# PIL's kernel creation approach
|
|
378
|
+
size = int(sigma * 3.5) * 2 + 1 if ksize == 0 else ksize
|
|
379
|
+
|
|
380
|
+
# Ensure odd size
|
|
381
|
+
size = size + 1 if size % 2 == 0 else size
|
|
382
|
+
|
|
383
|
+
# Create x coordinates
|
|
384
|
+
x = np.linspace(-(size // 2), size // 2, size)
|
|
385
|
+
|
|
386
|
+
# Compute 1D kernel using vectorized operations
|
|
387
|
+
kernel_1d = np.exp(-0.5 * (x / sigma) ** 2)
|
|
388
|
+
kernel_1d = kernel_1d / kernel_1d.sum()
|
|
389
|
+
|
|
390
|
+
# Create 2D kernel
|
|
391
|
+
return kernel_1d[:, np.newaxis] @ kernel_1d[np.newaxis, :]
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def create_gaussian_kernel_1d(sigma: float, ksize: int = 0) -> np.ndarray:
|
|
395
|
+
"""Create a 1D Gaussian kernel following PIL's approach.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
sigma (float): Standard deviation for Gaussian kernel.
|
|
399
|
+
ksize (int): Kernel size. If 0, size is computed as int(sigma * 3.5) * 2 + 1
|
|
400
|
+
to match PIL's implementation. Otherwise, must be positive and odd.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
np.ndarray: 1D normalized Gaussian kernel.
|
|
404
|
+
|
|
405
|
+
"""
|
|
406
|
+
# PIL's kernel creation approach
|
|
407
|
+
size = int(sigma * 3.5) * 2 + 1 if ksize == 0 else ksize
|
|
408
|
+
|
|
409
|
+
# Ensure odd size
|
|
410
|
+
size = size + 1 if size % 2 == 0 else size
|
|
411
|
+
|
|
412
|
+
# Create x coordinates
|
|
413
|
+
x = create_gaussian_kernel_input_array(size=size)
|
|
414
|
+
|
|
415
|
+
# Compute 1D kernel using vectorized operations
|
|
416
|
+
kernel_1d = np.exp(-0.5 * (x / sigma) ** 2)
|
|
417
|
+
return kernel_1d / kernel_1d.sum()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def create_gaussian_kernel_input_array(size: int) -> np.ndarray:
|
|
421
|
+
"""Creates a 1-D array which will create an array of x-coordinates which will be input for the
|
|
422
|
+
gaussian function (values from -size/2 to size/2 with step size of 1)
|
|
423
|
+
|
|
424
|
+
Piecewise function is needed as equivalent python list comprehension is faster than np.linspace
|
|
425
|
+
for values of size < 100
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
size (int): kernel size
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
np.ndarray: x-coordinate array which will be input for gaussian function that will be used for
|
|
432
|
+
separable gaussian blur
|
|
433
|
+
|
|
434
|
+
"""
|
|
435
|
+
if size < 100:
|
|
436
|
+
return np.array(list(range(-(size // 2), (size // 2) + 1, 1)))
|
|
437
|
+
|
|
438
|
+
return np.linspace(-(size // 2), size // 2, size)
|