stouputils 1.14.0__py3-none-any.whl → 1.14.2__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.
Files changed (108) hide show
  1. stouputils/__init__.pyi +15 -0
  2. stouputils/_deprecated.pyi +12 -0
  3. stouputils/all_doctests.pyi +46 -0
  4. stouputils/applications/__init__.pyi +2 -0
  5. stouputils/applications/automatic_docs.py +3 -0
  6. stouputils/applications/automatic_docs.pyi +106 -0
  7. stouputils/applications/upscaler/__init__.pyi +3 -0
  8. stouputils/applications/upscaler/config.pyi +18 -0
  9. stouputils/applications/upscaler/image.pyi +109 -0
  10. stouputils/applications/upscaler/video.pyi +60 -0
  11. stouputils/archive.pyi +67 -0
  12. stouputils/backup.pyi +109 -0
  13. stouputils/collections.pyi +86 -0
  14. stouputils/continuous_delivery/__init__.pyi +5 -0
  15. stouputils/continuous_delivery/cd_utils.pyi +129 -0
  16. stouputils/continuous_delivery/github.pyi +162 -0
  17. stouputils/continuous_delivery/pypi.pyi +52 -0
  18. stouputils/continuous_delivery/pyproject.pyi +67 -0
  19. stouputils/continuous_delivery/stubs.pyi +39 -0
  20. stouputils/ctx.pyi +211 -0
  21. stouputils/data_science/config/get.py +51 -51
  22. stouputils/data_science/data_processing/image/__init__.py +66 -66
  23. stouputils/data_science/data_processing/image/auto_contrast.py +79 -79
  24. stouputils/data_science/data_processing/image/axis_flip.py +58 -58
  25. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -74
  26. stouputils/data_science/data_processing/image/binary_threshold.py +73 -73
  27. stouputils/data_science/data_processing/image/blur.py +59 -59
  28. stouputils/data_science/data_processing/image/brightness.py +54 -54
  29. stouputils/data_science/data_processing/image/canny.py +110 -110
  30. stouputils/data_science/data_processing/image/clahe.py +92 -92
  31. stouputils/data_science/data_processing/image/common.py +30 -30
  32. stouputils/data_science/data_processing/image/contrast.py +53 -53
  33. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -74
  34. stouputils/data_science/data_processing/image/denoise.py +378 -378
  35. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -123
  36. stouputils/data_science/data_processing/image/invert.py +64 -64
  37. stouputils/data_science/data_processing/image/laplacian.py +60 -60
  38. stouputils/data_science/data_processing/image/median_blur.py +52 -52
  39. stouputils/data_science/data_processing/image/noise.py +59 -59
  40. stouputils/data_science/data_processing/image/normalize.py +65 -65
  41. stouputils/data_science/data_processing/image/random_erase.py +66 -66
  42. stouputils/data_science/data_processing/image/resize.py +69 -69
  43. stouputils/data_science/data_processing/image/rotation.py +80 -80
  44. stouputils/data_science/data_processing/image/salt_pepper.py +68 -68
  45. stouputils/data_science/data_processing/image/sharpening.py +55 -55
  46. stouputils/data_science/data_processing/image/shearing.py +64 -64
  47. stouputils/data_science/data_processing/image/threshold.py +64 -64
  48. stouputils/data_science/data_processing/image/translation.py +71 -71
  49. stouputils/data_science/data_processing/image/zoom.py +83 -83
  50. stouputils/data_science/data_processing/image_augmentation.py +118 -118
  51. stouputils/data_science/data_processing/image_preprocess.py +183 -183
  52. stouputils/data_science/data_processing/prosthesis_detection.py +359 -359
  53. stouputils/data_science/data_processing/technique.py +481 -481
  54. stouputils/data_science/dataset/__init__.py +45 -45
  55. stouputils/data_science/dataset/dataset.py +292 -292
  56. stouputils/data_science/dataset/dataset_loader.py +135 -135
  57. stouputils/data_science/dataset/grouping_strategy.py +296 -296
  58. stouputils/data_science/dataset/image_loader.py +100 -100
  59. stouputils/data_science/dataset/xy_tuple.py +696 -696
  60. stouputils/data_science/metric_dictionnary.py +106 -106
  61. stouputils/data_science/mlflow_utils.py +206 -206
  62. stouputils/data_science/models/abstract_model.py +149 -149
  63. stouputils/data_science/models/all.py +85 -85
  64. stouputils/data_science/models/keras/all.py +38 -38
  65. stouputils/data_science/models/keras/convnext.py +62 -62
  66. stouputils/data_science/models/keras/densenet.py +50 -50
  67. stouputils/data_science/models/keras/efficientnet.py +60 -60
  68. stouputils/data_science/models/keras/mobilenet.py +56 -56
  69. stouputils/data_science/models/keras/resnet.py +52 -52
  70. stouputils/data_science/models/keras/squeezenet.py +233 -233
  71. stouputils/data_science/models/keras/vgg.py +42 -42
  72. stouputils/data_science/models/keras/xception.py +38 -38
  73. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -20
  74. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -219
  75. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -148
  76. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -31
  77. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -249
  78. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -66
  79. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -12
  80. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -56
  81. stouputils/data_science/models/keras_utils/visualizations.py +416 -416
  82. stouputils/data_science/models/sandbox.py +116 -116
  83. stouputils/data_science/range_tuple.py +234 -234
  84. stouputils/data_science/utils.py +285 -285
  85. stouputils/decorators.pyi +242 -0
  86. stouputils/image.pyi +172 -0
  87. stouputils/installer/__init__.py +18 -18
  88. stouputils/installer/__init__.pyi +5 -0
  89. stouputils/installer/common.pyi +39 -0
  90. stouputils/installer/downloader.pyi +24 -0
  91. stouputils/installer/linux.py +144 -144
  92. stouputils/installer/linux.pyi +39 -0
  93. stouputils/installer/main.py +223 -223
  94. stouputils/installer/main.pyi +57 -0
  95. stouputils/installer/windows.py +136 -136
  96. stouputils/installer/windows.pyi +31 -0
  97. stouputils/io.pyi +213 -0
  98. stouputils/parallel.py +12 -10
  99. stouputils/parallel.pyi +211 -0
  100. stouputils/print.pyi +136 -0
  101. stouputils/py.typed +1 -1
  102. stouputils/stouputils/parallel.pyi +4 -4
  103. stouputils/version_pkg.pyi +15 -0
  104. {stouputils-1.14.0.dist-info → stouputils-1.14.2.dist-info}/METADATA +1 -1
  105. stouputils-1.14.2.dist-info/RECORD +171 -0
  106. stouputils-1.14.0.dist-info/RECORD +0 -140
  107. {stouputils-1.14.0.dist-info → stouputils-1.14.2.dist-info}/WHEEL +0 -0
  108. {stouputils-1.14.0.dist-info → stouputils-1.14.2.dist-info}/entry_points.txt +0 -0
@@ -1,378 +1,378 @@
1
-
2
- # pyright: reportUnknownMemberType=false
3
- # pyright: reportUnknownVariableType=false
4
- # pyright: reportUnknownArgumentType=false
5
-
6
- # Imports
7
- from typing import Literal
8
-
9
- from .common import Any, NDArray, check_image, cv2, np
10
- from ....ctx import Muffle
11
-
12
-
13
- # Functions
14
- def nlm_denoise_image(
15
- image: NDArray[Any],
16
- h: float = 10,
17
- template_window_size: int = 7,
18
- search_window_size: int = 21,
19
- ignore_dtype: bool = False
20
- ) -> NDArray[Any]:
21
- """ Apply Non-Local Means denoising to an image.
22
-
23
- This algorithm replaces each pixel with an average of similar pixels
24
- found anywhere in the image. It is highly effective for removing Gaussian noise
25
- while preserving edges and details.
26
-
27
- Args:
28
- image (NDArray[Any]): Image to denoise
29
- h (float): Filter strength (higher values remove more noise but may blur details)
30
- template_window_size (int): Size of the template window for patch comparison (should be odd)
31
- search_window_size (int): Size of the search window (should be odd)
32
- ignore_dtype (bool): Ignore the dtype check
33
- Returns:
34
- NDArray[Any]: Denoised image
35
-
36
- >>> ## Basic tests
37
- >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
38
- >>> denoised = nlm_denoise_image(image.astype(np.uint8), 10, 3, 5)
39
- >>> denoised.shape == image.shape
40
- True
41
-
42
- >>> ## Test with colored image
43
- >>> rgb = np.random.randint(0, 256, (10, 10, 3), dtype=np.uint8)
44
- >>> denoised_rgb = nlm_denoise_image(rgb, 10, 5, 11)
45
- >>> denoised_rgb.shape == rgb.shape
46
- True
47
-
48
- >>> ## Test invalid inputs
49
- >>> nlm_denoise_image("not an image", 10)
50
- Traceback (most recent call last):
51
- ...
52
- AssertionError: Image must be a numpy array
53
-
54
- >>> nlm_denoise_image(image.astype(np.uint8), "10")
55
- Traceback (most recent call last):
56
- ...
57
- AssertionError: h must be a number, got <class 'str'>
58
-
59
- >>> nlm_denoise_image(image.astype(np.uint8), 10, 4)
60
- Traceback (most recent call last):
61
- ...
62
- AssertionError: template_window_size must be odd, got 4
63
- """
64
- # Check input data
65
- check_image(image, ignore_dtype=ignore_dtype)
66
- assert isinstance(h, float | int), f"h must be a number, got {type(h)}"
67
- assert template_window_size % 2 == 1, f"template_window_size must be odd, got {template_window_size}"
68
- assert search_window_size % 2 == 1, f"search_window_size must be odd, got {search_window_size}"
69
-
70
- # Apply Non-Local Means denoising based on image type
71
- if len(image.shape) == 2 or image.shape[2] == 1: # Grayscale
72
- return cv2.fastNlMeansDenoising(
73
- image, None, float(h), template_window_size, search_window_size
74
- )
75
- else: # Color
76
- return cv2.fastNlMeansDenoisingColored(
77
- image, None, float(h), float(h), template_window_size, search_window_size
78
- )
79
-
80
-
81
- def bilateral_denoise_image(
82
- image: NDArray[Any],
83
- d: int = 9,
84
- sigma_color: float = 75,
85
- sigma_space: float = 75,
86
- ignore_dtype: bool = False
87
- ) -> NDArray[Any]:
88
- """ Apply Bilateral Filter denoising to an image.
89
-
90
- Bilateral filtering smooths images while preserving edges by considering
91
- both spatial proximity and color similarity between pixels.
92
-
93
- Args:
94
- image (NDArray[Any]): Image to denoise
95
- d (int): Diameter of each pixel neighborhood
96
- sigma_color (float): Filter sigma in the color space
97
- sigma_space (float): Filter sigma in the coordinate space
98
- ignore_dtype (bool): Ignore the dtype check
99
- Returns:
100
- NDArray[Any]: Denoised image
101
-
102
- >>> ## Basic tests
103
- >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
104
- >>> denoised = bilateral_denoise_image(image.astype(np.uint8))
105
- >>> denoised.shape == image.shape
106
- True
107
-
108
- >>> ## Test with colored image
109
- >>> rgb = np.random.randint(0, 256, (10, 10, 3), dtype=np.uint8)
110
- >>> denoised_rgb = bilateral_denoise_image(rgb)
111
- >>> denoised_rgb.shape == rgb.shape
112
- True
113
-
114
- >>> ## Test invalid inputs
115
- >>> bilateral_denoise_image("not an image")
116
- Traceback (most recent call last):
117
- ...
118
- AssertionError: Image must be a numpy array
119
-
120
- >>> bilateral_denoise_image(image.astype(np.uint8), "9")
121
- Traceback (most recent call last):
122
- ...
123
- AssertionError: d must be a number, got <class 'str'>
124
- """
125
- # Check input data
126
- check_image(image, ignore_dtype=ignore_dtype)
127
- assert isinstance(d, int), f"d must be a number, got {type(d)}"
128
- assert isinstance(sigma_color, float | int), f"sigma_color must be a number, got {type(sigma_color)}"
129
- assert isinstance(sigma_space, float | int), f"sigma_space must be a number, got {type(sigma_space)}"
130
-
131
- # Apply bilateral filter
132
- return cv2.bilateralFilter(image, d, sigma_color, sigma_space)
133
-
134
-
135
- def tv_denoise_image(
136
- image: NDArray[Any],
137
- weight: float = 0.1,
138
- iterations: int = 30,
139
- method: Literal["chambolle", "bregman"] = "chambolle",
140
- ignore_dtype: bool = False
141
- ) -> NDArray[Any]:
142
- """ Apply Total Variation denoising to an image.
143
-
144
- Total Variation denoising removes noise while preserving sharp edges by
145
- minimizing the total variation of the image.
146
-
147
- Args:
148
- image (NDArray[Any]): Image to denoise
149
- weight (float): Denoising weight (higher values remove more noise)
150
- iterations (int): Number of iterations
151
- method (str): Method to use ("chambolle" or "bregman")
152
- ignore_dtype (bool): Ignore the dtype check
153
- Returns:
154
- NDArray[Any]: Denoised image
155
-
156
- >>> ## Basic tests
157
- >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
158
- >>> denoised = tv_denoise_image(image.astype(np.uint8), 0.1, 30)
159
- >>> denoised.shape == image.shape
160
- True
161
-
162
- >>> ## Test invalid inputs
163
- >>> tv_denoise_image("not an image")
164
- Traceback (most recent call last):
165
- ...
166
- AssertionError: Image must be a numpy array
167
-
168
- >>> tv_denoise_image(image.astype(np.uint8), "0.1")
169
- Traceback (most recent call last):
170
- ...
171
- AssertionError: weight must be a number, got <class 'str'>
172
- """
173
- # Check input data
174
- check_image(image, ignore_dtype=ignore_dtype)
175
- assert isinstance(weight, float | int), f"weight must be a number, got {type(weight)}"
176
- assert isinstance(iterations, int), f"iterations must be an integer, got {type(iterations)}"
177
- assert method in ["chambolle", "bregman"], f"method must be 'chambolle' or 'bregman', got {method}"
178
-
179
- # Import skimage for TV denoising
180
- try:
181
- from skimage.restoration import denoise_tv_bregman, denoise_tv_chambolle
182
- except ImportError as e:
183
- raise ImportError("scikit-image is required for TV denoising. Install with 'pip install scikit-image'") from e
184
-
185
- # Normalize image to [0, 1] for skimage functions
186
- is_int_type = np.issubdtype(image.dtype, np.integer)
187
- if is_int_type:
188
- img_norm = image.astype(np.float32) / 255.0
189
- else:
190
- img_norm = image.astype(np.float32)
191
-
192
- # Apply TV denoising based on method
193
- if method == "chambolle":
194
- denoised = denoise_tv_chambolle(
195
- img_norm,
196
- weight=weight,
197
- max_num_iter=iterations,
198
- channel_axis=-1 if len(image.shape) > 2 else None
199
- )
200
- else:
201
- denoised = denoise_tv_bregman(
202
- img_norm,
203
- weight=weight,
204
- max_num_iter=iterations,
205
- channel_axis=-1 if len(image.shape) > 2 else None
206
- )
207
-
208
- # Convert back to original data type
209
- if is_int_type:
210
- denoised = np.clip(denoised * 255, 0, 255).astype(image.dtype)
211
- else:
212
- denoised = denoised.astype(image.dtype)
213
-
214
- return denoised
215
-
216
-
217
- def wavelet_denoise_image(
218
- image: NDArray[Any],
219
- sigma: float | None = None,
220
- wavelet: str = 'db1',
221
- mode: str = 'soft',
222
- wavelet_levels: int = 3,
223
- ignore_dtype: bool = False
224
- ) -> NDArray[Any]:
225
- """ Apply Wavelet denoising to an image.
226
-
227
- Wavelet denoising decomposes the image into wavelet coefficients,
228
- applies thresholding, and reconstructs the image with reduced noise.
229
-
230
- Args:
231
- image (NDArray[Any]): Image to denoise
232
- sigma (float): Noise standard deviation. If None, it's estimated from the image.
233
- wavelet (str): Wavelet to use
234
- mode (str): Thresholding mode ('soft' or 'hard')
235
- wavelet_levels (int): Number of wavelet decomposition levels
236
- ignore_dtype (bool): Ignore the dtype check
237
- Returns:
238
- NDArray[Any]: Denoised image
239
-
240
- >>> ## Basic tests
241
- >>> import importlib.util
242
- >>> has_pywt = importlib.util.find_spec('pywt') is not None
243
- >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
244
- >>> if has_pywt:
245
- ... denoised = wavelet_denoise_image(image.astype(np.uint8))
246
- ... denoised.shape == image.shape
247
- ... else:
248
- ... True
249
- True
250
-
251
- >>> ## Test invalid inputs
252
- >>> wavelet_denoise_image("not an image")
253
- Traceback (most recent call last):
254
- ...
255
- AssertionError: Image must be a numpy array
256
-
257
- >>> wavelet_denoise_image(image.astype(np.uint8), wavelet=123)
258
- Traceback (most recent call last):
259
- ...
260
- AssertionError: wavelet must be a string, got <class 'int'>
261
- """
262
- # Check input data
263
- check_image(image, ignore_dtype=ignore_dtype)
264
- if sigma is not None:
265
- assert isinstance(sigma, float | int), f"sigma must be a number or None, got {type(sigma)}"
266
- assert isinstance(wavelet, str), f"wavelet must be a string, got {type(wavelet)}"
267
- assert mode in ["soft", "hard"], f"mode must be 'soft' or 'hard', got {mode}"
268
- assert isinstance(wavelet_levels, int), f"wavelet_levels must be an integer, got {type(wavelet_levels)}"
269
-
270
- # Import skimage for wavelet denoising
271
- try:
272
- from skimage.restoration import denoise_wavelet
273
-
274
- # Check for PyWavelets dependency specifically
275
- try:
276
- import pywt # type: ignore
277
- except ImportError as e:
278
- raise ImportError(
279
- "PyWavelets (pywt) is required for wavelet denoising. Install with 'pip install PyWavelets'", name="pywt"
280
- ) from e
281
- except ImportError as e:
282
- if e.name != "pywt":
283
- raise ImportError("skimage is required for wavelet denoising. Install with 'pip install scikit-image'") from e
284
- else:
285
- raise e
286
-
287
- # Normalize image to [0, 1] for skimage functions
288
- is_int_type = np.issubdtype(image.dtype, np.integer)
289
- if is_int_type:
290
- img_norm = image.astype(np.float32) / 255.0
291
- else:
292
- img_norm = image.astype(np.float32)
293
-
294
- # Apply wavelet denoising
295
- with Muffle(mute_stderr=True):
296
- denoised = denoise_wavelet(
297
- img_norm, sigma=sigma, wavelet=wavelet, mode=mode,
298
- wavelet_levels=wavelet_levels, channel_axis=-1 if len(image.shape) > 2 else None
299
- )
300
-
301
- # Convert back to original data type
302
- if is_int_type:
303
- denoised = np.clip(denoised * 255, 0, 255).astype(image.dtype)
304
- else:
305
- denoised = denoised.astype(image.dtype)
306
-
307
- return denoised
308
-
309
-
310
- def adaptive_denoise_image(
311
- image: NDArray[Any],
312
- method: Literal["nlm", "bilateral", "tv", "wavelet"] | str = "nlm",
313
- strength: float = 0.5,
314
- ignore_dtype: bool = False
315
- ) -> NDArray[Any]:
316
- """ Apply adaptive denoising to an image using the specified method.
317
-
318
- This is a convenience function that selects the appropriate denoising method
319
- and parameters based on the image content and noise level.
320
-
321
- Args:
322
- image (NDArray[Any]): Image to denoise
323
- method (str): Denoising method to use ("nlm", "bilateral", "tv", or "wavelet")
324
- strength (float): Denoising strength (0.0 to 1.0)
325
- ignore_dtype (bool): Ignore the dtype check
326
- Returns:
327
- NDArray[Any]: Denoised image
328
-
329
- >>> ## Basic tests
330
- >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
331
- >>> denoised = adaptive_denoise_image(image.astype(np.uint8), "nlm", 0.5)
332
- >>> denoised.shape == image.shape
333
- True
334
-
335
- >>> ## Test invalid inputs
336
- >>> adaptive_denoise_image("not an image")
337
- Traceback (most recent call last):
338
- ...
339
- AssertionError: Image must be a numpy array
340
-
341
- >>> adaptive_denoise_image(image.astype(np.uint8), "invalid_method")
342
- Traceback (most recent call last):
343
- ...
344
- AssertionError: method must be one of: nlm, bilateral, tv, wavelet
345
- """
346
- # Check input data
347
- check_image(image, ignore_dtype=ignore_dtype)
348
- valid_methods = ["nlm", "bilateral", "tv", "wavelet"]
349
- assert method in valid_methods, f"method must be one of: {', '.join(valid_methods)}"
350
- assert isinstance(strength, float | int), f"strength must be a number, got {type(strength)}"
351
- assert 0 <= strength <= 1, f"strength must be between 0 and 1, got {strength}"
352
-
353
- # Scale parameters based on strength
354
- if method == "bilateral":
355
- # sigma parameters scale from 30 (minimal) to 150 (strong)
356
- sigma = 30 + strength * 120
357
- return bilateral_denoise_image(
358
- image, d=9, sigma_color=sigma, sigma_space=sigma, ignore_dtype=ignore_dtype
359
- )
360
-
361
- elif method == "tv":
362
- # weight scales from 0.05 (minimal) to 0.5 (strong)
363
- weight = 0.05 + strength * 0.45
364
- return tv_denoise_image(
365
- image, weight=weight, iterations=30, ignore_dtype=ignore_dtype
366
- )
367
-
368
- elif method == "wavelet":
369
- # We'll estimate sigma from the image, but scale wavelet levels
370
- wavelet_levels = max(2, min(5, int(2 + strength * 3)))
371
- return wavelet_denoise_image(
372
- image, wavelet_levels=wavelet_levels, ignore_dtype=ignore_dtype
373
- )
374
-
375
- else:
376
- # h parameter scales from 5 (minimal) to 20 (strong)
377
- h = 5 + strength * 15
378
- return nlm_denoise_image(image, h=h, ignore_dtype=ignore_dtype)
1
+
2
+ # pyright: reportUnknownMemberType=false
3
+ # pyright: reportUnknownVariableType=false
4
+ # pyright: reportUnknownArgumentType=false
5
+
6
+ # Imports
7
+ from typing import Literal
8
+
9
+ from .common import Any, NDArray, check_image, cv2, np
10
+ from ....ctx import Muffle
11
+
12
+
13
+ # Functions
14
+ def nlm_denoise_image(
15
+ image: NDArray[Any],
16
+ h: float = 10,
17
+ template_window_size: int = 7,
18
+ search_window_size: int = 21,
19
+ ignore_dtype: bool = False
20
+ ) -> NDArray[Any]:
21
+ """ Apply Non-Local Means denoising to an image.
22
+
23
+ This algorithm replaces each pixel with an average of similar pixels
24
+ found anywhere in the image. It is highly effective for removing Gaussian noise
25
+ while preserving edges and details.
26
+
27
+ Args:
28
+ image (NDArray[Any]): Image to denoise
29
+ h (float): Filter strength (higher values remove more noise but may blur details)
30
+ template_window_size (int): Size of the template window for patch comparison (should be odd)
31
+ search_window_size (int): Size of the search window (should be odd)
32
+ ignore_dtype (bool): Ignore the dtype check
33
+ Returns:
34
+ NDArray[Any]: Denoised image
35
+
36
+ >>> ## Basic tests
37
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
38
+ >>> denoised = nlm_denoise_image(image.astype(np.uint8), 10, 3, 5)
39
+ >>> denoised.shape == image.shape
40
+ True
41
+
42
+ >>> ## Test with colored image
43
+ >>> rgb = np.random.randint(0, 256, (10, 10, 3), dtype=np.uint8)
44
+ >>> denoised_rgb = nlm_denoise_image(rgb, 10, 5, 11)
45
+ >>> denoised_rgb.shape == rgb.shape
46
+ True
47
+
48
+ >>> ## Test invalid inputs
49
+ >>> nlm_denoise_image("not an image", 10)
50
+ Traceback (most recent call last):
51
+ ...
52
+ AssertionError: Image must be a numpy array
53
+
54
+ >>> nlm_denoise_image(image.astype(np.uint8), "10")
55
+ Traceback (most recent call last):
56
+ ...
57
+ AssertionError: h must be a number, got <class 'str'>
58
+
59
+ >>> nlm_denoise_image(image.astype(np.uint8), 10, 4)
60
+ Traceback (most recent call last):
61
+ ...
62
+ AssertionError: template_window_size must be odd, got 4
63
+ """
64
+ # Check input data
65
+ check_image(image, ignore_dtype=ignore_dtype)
66
+ assert isinstance(h, float | int), f"h must be a number, got {type(h)}"
67
+ assert template_window_size % 2 == 1, f"template_window_size must be odd, got {template_window_size}"
68
+ assert search_window_size % 2 == 1, f"search_window_size must be odd, got {search_window_size}"
69
+
70
+ # Apply Non-Local Means denoising based on image type
71
+ if len(image.shape) == 2 or image.shape[2] == 1: # Grayscale
72
+ return cv2.fastNlMeansDenoising(
73
+ image, None, float(h), template_window_size, search_window_size
74
+ )
75
+ else: # Color
76
+ return cv2.fastNlMeansDenoisingColored(
77
+ image, None, float(h), float(h), template_window_size, search_window_size
78
+ )
79
+
80
+
81
+ def bilateral_denoise_image(
82
+ image: NDArray[Any],
83
+ d: int = 9,
84
+ sigma_color: float = 75,
85
+ sigma_space: float = 75,
86
+ ignore_dtype: bool = False
87
+ ) -> NDArray[Any]:
88
+ """ Apply Bilateral Filter denoising to an image.
89
+
90
+ Bilateral filtering smooths images while preserving edges by considering
91
+ both spatial proximity and color similarity between pixels.
92
+
93
+ Args:
94
+ image (NDArray[Any]): Image to denoise
95
+ d (int): Diameter of each pixel neighborhood
96
+ sigma_color (float): Filter sigma in the color space
97
+ sigma_space (float): Filter sigma in the coordinate space
98
+ ignore_dtype (bool): Ignore the dtype check
99
+ Returns:
100
+ NDArray[Any]: Denoised image
101
+
102
+ >>> ## Basic tests
103
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
104
+ >>> denoised = bilateral_denoise_image(image.astype(np.uint8))
105
+ >>> denoised.shape == image.shape
106
+ True
107
+
108
+ >>> ## Test with colored image
109
+ >>> rgb = np.random.randint(0, 256, (10, 10, 3), dtype=np.uint8)
110
+ >>> denoised_rgb = bilateral_denoise_image(rgb)
111
+ >>> denoised_rgb.shape == rgb.shape
112
+ True
113
+
114
+ >>> ## Test invalid inputs
115
+ >>> bilateral_denoise_image("not an image")
116
+ Traceback (most recent call last):
117
+ ...
118
+ AssertionError: Image must be a numpy array
119
+
120
+ >>> bilateral_denoise_image(image.astype(np.uint8), "9")
121
+ Traceback (most recent call last):
122
+ ...
123
+ AssertionError: d must be a number, got <class 'str'>
124
+ """
125
+ # Check input data
126
+ check_image(image, ignore_dtype=ignore_dtype)
127
+ assert isinstance(d, int), f"d must be a number, got {type(d)}"
128
+ assert isinstance(sigma_color, float | int), f"sigma_color must be a number, got {type(sigma_color)}"
129
+ assert isinstance(sigma_space, float | int), f"sigma_space must be a number, got {type(sigma_space)}"
130
+
131
+ # Apply bilateral filter
132
+ return cv2.bilateralFilter(image, d, sigma_color, sigma_space)
133
+
134
+
135
+ def tv_denoise_image(
136
+ image: NDArray[Any],
137
+ weight: float = 0.1,
138
+ iterations: int = 30,
139
+ method: Literal["chambolle", "bregman"] = "chambolle",
140
+ ignore_dtype: bool = False
141
+ ) -> NDArray[Any]:
142
+ """ Apply Total Variation denoising to an image.
143
+
144
+ Total Variation denoising removes noise while preserving sharp edges by
145
+ minimizing the total variation of the image.
146
+
147
+ Args:
148
+ image (NDArray[Any]): Image to denoise
149
+ weight (float): Denoising weight (higher values remove more noise)
150
+ iterations (int): Number of iterations
151
+ method (str): Method to use ("chambolle" or "bregman")
152
+ ignore_dtype (bool): Ignore the dtype check
153
+ Returns:
154
+ NDArray[Any]: Denoised image
155
+
156
+ >>> ## Basic tests
157
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
158
+ >>> denoised = tv_denoise_image(image.astype(np.uint8), 0.1, 30)
159
+ >>> denoised.shape == image.shape
160
+ True
161
+
162
+ >>> ## Test invalid inputs
163
+ >>> tv_denoise_image("not an image")
164
+ Traceback (most recent call last):
165
+ ...
166
+ AssertionError: Image must be a numpy array
167
+
168
+ >>> tv_denoise_image(image.astype(np.uint8), "0.1")
169
+ Traceback (most recent call last):
170
+ ...
171
+ AssertionError: weight must be a number, got <class 'str'>
172
+ """
173
+ # Check input data
174
+ check_image(image, ignore_dtype=ignore_dtype)
175
+ assert isinstance(weight, float | int), f"weight must be a number, got {type(weight)}"
176
+ assert isinstance(iterations, int), f"iterations must be an integer, got {type(iterations)}"
177
+ assert method in ["chambolle", "bregman"], f"method must be 'chambolle' or 'bregman', got {method}"
178
+
179
+ # Import skimage for TV denoising
180
+ try:
181
+ from skimage.restoration import denoise_tv_bregman, denoise_tv_chambolle
182
+ except ImportError as e:
183
+ raise ImportError("scikit-image is required for TV denoising. Install with 'pip install scikit-image'") from e
184
+
185
+ # Normalize image to [0, 1] for skimage functions
186
+ is_int_type = np.issubdtype(image.dtype, np.integer)
187
+ if is_int_type:
188
+ img_norm = image.astype(np.float32) / 255.0
189
+ else:
190
+ img_norm = image.astype(np.float32)
191
+
192
+ # Apply TV denoising based on method
193
+ if method == "chambolle":
194
+ denoised = denoise_tv_chambolle(
195
+ img_norm,
196
+ weight=weight,
197
+ max_num_iter=iterations,
198
+ channel_axis=-1 if len(image.shape) > 2 else None
199
+ )
200
+ else:
201
+ denoised = denoise_tv_bregman(
202
+ img_norm,
203
+ weight=weight,
204
+ max_num_iter=iterations,
205
+ channel_axis=-1 if len(image.shape) > 2 else None
206
+ )
207
+
208
+ # Convert back to original data type
209
+ if is_int_type:
210
+ denoised = np.clip(denoised * 255, 0, 255).astype(image.dtype)
211
+ else:
212
+ denoised = denoised.astype(image.dtype)
213
+
214
+ return denoised
215
+
216
+
217
+ def wavelet_denoise_image(
218
+ image: NDArray[Any],
219
+ sigma: float | None = None,
220
+ wavelet: str = 'db1',
221
+ mode: str = 'soft',
222
+ wavelet_levels: int = 3,
223
+ ignore_dtype: bool = False
224
+ ) -> NDArray[Any]:
225
+ """ Apply Wavelet denoising to an image.
226
+
227
+ Wavelet denoising decomposes the image into wavelet coefficients,
228
+ applies thresholding, and reconstructs the image with reduced noise.
229
+
230
+ Args:
231
+ image (NDArray[Any]): Image to denoise
232
+ sigma (float): Noise standard deviation. If None, it's estimated from the image.
233
+ wavelet (str): Wavelet to use
234
+ mode (str): Thresholding mode ('soft' or 'hard')
235
+ wavelet_levels (int): Number of wavelet decomposition levels
236
+ ignore_dtype (bool): Ignore the dtype check
237
+ Returns:
238
+ NDArray[Any]: Denoised image
239
+
240
+ >>> ## Basic tests
241
+ >>> import importlib.util
242
+ >>> has_pywt = importlib.util.find_spec('pywt') is not None
243
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
244
+ >>> if has_pywt:
245
+ ... denoised = wavelet_denoise_image(image.astype(np.uint8))
246
+ ... denoised.shape == image.shape
247
+ ... else:
248
+ ... True
249
+ True
250
+
251
+ >>> ## Test invalid inputs
252
+ >>> wavelet_denoise_image("not an image")
253
+ Traceback (most recent call last):
254
+ ...
255
+ AssertionError: Image must be a numpy array
256
+
257
+ >>> wavelet_denoise_image(image.astype(np.uint8), wavelet=123)
258
+ Traceback (most recent call last):
259
+ ...
260
+ AssertionError: wavelet must be a string, got <class 'int'>
261
+ """
262
+ # Check input data
263
+ check_image(image, ignore_dtype=ignore_dtype)
264
+ if sigma is not None:
265
+ assert isinstance(sigma, float | int), f"sigma must be a number or None, got {type(sigma)}"
266
+ assert isinstance(wavelet, str), f"wavelet must be a string, got {type(wavelet)}"
267
+ assert mode in ["soft", "hard"], f"mode must be 'soft' or 'hard', got {mode}"
268
+ assert isinstance(wavelet_levels, int), f"wavelet_levels must be an integer, got {type(wavelet_levels)}"
269
+
270
+ # Import skimage for wavelet denoising
271
+ try:
272
+ from skimage.restoration import denoise_wavelet
273
+
274
+ # Check for PyWavelets dependency specifically
275
+ try:
276
+ import pywt # type: ignore
277
+ except ImportError as e:
278
+ raise ImportError(
279
+ "PyWavelets (pywt) is required for wavelet denoising. Install with 'pip install PyWavelets'", name="pywt"
280
+ ) from e
281
+ except ImportError as e:
282
+ if e.name != "pywt":
283
+ raise ImportError("skimage is required for wavelet denoising. Install with 'pip install scikit-image'") from e
284
+ else:
285
+ raise e
286
+
287
+ # Normalize image to [0, 1] for skimage functions
288
+ is_int_type = np.issubdtype(image.dtype, np.integer)
289
+ if is_int_type:
290
+ img_norm = image.astype(np.float32) / 255.0
291
+ else:
292
+ img_norm = image.astype(np.float32)
293
+
294
+ # Apply wavelet denoising
295
+ with Muffle(mute_stderr=True):
296
+ denoised = denoise_wavelet(
297
+ img_norm, sigma=sigma, wavelet=wavelet, mode=mode,
298
+ wavelet_levels=wavelet_levels, channel_axis=-1 if len(image.shape) > 2 else None
299
+ )
300
+
301
+ # Convert back to original data type
302
+ if is_int_type:
303
+ denoised = np.clip(denoised * 255, 0, 255).astype(image.dtype)
304
+ else:
305
+ denoised = denoised.astype(image.dtype)
306
+
307
+ return denoised
308
+
309
+
310
+ def adaptive_denoise_image(
311
+ image: NDArray[Any],
312
+ method: Literal["nlm", "bilateral", "tv", "wavelet"] | str = "nlm",
313
+ strength: float = 0.5,
314
+ ignore_dtype: bool = False
315
+ ) -> NDArray[Any]:
316
+ """ Apply adaptive denoising to an image using the specified method.
317
+
318
+ This is a convenience function that selects the appropriate denoising method
319
+ and parameters based on the image content and noise level.
320
+
321
+ Args:
322
+ image (NDArray[Any]): Image to denoise
323
+ method (str): Denoising method to use ("nlm", "bilateral", "tv", or "wavelet")
324
+ strength (float): Denoising strength (0.0 to 1.0)
325
+ ignore_dtype (bool): Ignore the dtype check
326
+ Returns:
327
+ NDArray[Any]: Denoised image
328
+
329
+ >>> ## Basic tests
330
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
331
+ >>> denoised = adaptive_denoise_image(image.astype(np.uint8), "nlm", 0.5)
332
+ >>> denoised.shape == image.shape
333
+ True
334
+
335
+ >>> ## Test invalid inputs
336
+ >>> adaptive_denoise_image("not an image")
337
+ Traceback (most recent call last):
338
+ ...
339
+ AssertionError: Image must be a numpy array
340
+
341
+ >>> adaptive_denoise_image(image.astype(np.uint8), "invalid_method")
342
+ Traceback (most recent call last):
343
+ ...
344
+ AssertionError: method must be one of: nlm, bilateral, tv, wavelet
345
+ """
346
+ # Check input data
347
+ check_image(image, ignore_dtype=ignore_dtype)
348
+ valid_methods = ["nlm", "bilateral", "tv", "wavelet"]
349
+ assert method in valid_methods, f"method must be one of: {', '.join(valid_methods)}"
350
+ assert isinstance(strength, float | int), f"strength must be a number, got {type(strength)}"
351
+ assert 0 <= strength <= 1, f"strength must be between 0 and 1, got {strength}"
352
+
353
+ # Scale parameters based on strength
354
+ if method == "bilateral":
355
+ # sigma parameters scale from 30 (minimal) to 150 (strong)
356
+ sigma = 30 + strength * 120
357
+ return bilateral_denoise_image(
358
+ image, d=9, sigma_color=sigma, sigma_space=sigma, ignore_dtype=ignore_dtype
359
+ )
360
+
361
+ elif method == "tv":
362
+ # weight scales from 0.05 (minimal) to 0.5 (strong)
363
+ weight = 0.05 + strength * 0.45
364
+ return tv_denoise_image(
365
+ image, weight=weight, iterations=30, ignore_dtype=ignore_dtype
366
+ )
367
+
368
+ elif method == "wavelet":
369
+ # We'll estimate sigma from the image, but scale wavelet levels
370
+ wavelet_levels = max(2, min(5, int(2 + strength * 3)))
371
+ return wavelet_denoise_image(
372
+ image, wavelet_levels=wavelet_levels, ignore_dtype=ignore_dtype
373
+ )
374
+
375
+ else:
376
+ # h parameter scales from 5 (minimal) to 20 (strong)
377
+ h = 5 + strength * 15
378
+ return nlm_denoise_image(image, h=h, ignore_dtype=ignore_dtype)