stouputils 1.14.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.
Files changed (140) hide show
  1. stouputils/__init__.py +40 -0
  2. stouputils/__main__.py +86 -0
  3. stouputils/_deprecated.py +37 -0
  4. stouputils/all_doctests.py +160 -0
  5. stouputils/applications/__init__.py +22 -0
  6. stouputils/applications/automatic_docs.py +634 -0
  7. stouputils/applications/upscaler/__init__.py +39 -0
  8. stouputils/applications/upscaler/config.py +128 -0
  9. stouputils/applications/upscaler/image.py +247 -0
  10. stouputils/applications/upscaler/video.py +287 -0
  11. stouputils/archive.py +344 -0
  12. stouputils/backup.py +488 -0
  13. stouputils/collections.py +244 -0
  14. stouputils/continuous_delivery/__init__.py +27 -0
  15. stouputils/continuous_delivery/cd_utils.py +243 -0
  16. stouputils/continuous_delivery/github.py +522 -0
  17. stouputils/continuous_delivery/pypi.py +130 -0
  18. stouputils/continuous_delivery/pyproject.py +147 -0
  19. stouputils/continuous_delivery/stubs.py +86 -0
  20. stouputils/ctx.py +408 -0
  21. stouputils/data_science/config/get.py +51 -0
  22. stouputils/data_science/config/set.py +125 -0
  23. stouputils/data_science/data_processing/image/__init__.py +66 -0
  24. stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
  25. stouputils/data_science/data_processing/image/axis_flip.py +58 -0
  26. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
  27. stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
  28. stouputils/data_science/data_processing/image/blur.py +59 -0
  29. stouputils/data_science/data_processing/image/brightness.py +54 -0
  30. stouputils/data_science/data_processing/image/canny.py +110 -0
  31. stouputils/data_science/data_processing/image/clahe.py +92 -0
  32. stouputils/data_science/data_processing/image/common.py +30 -0
  33. stouputils/data_science/data_processing/image/contrast.py +53 -0
  34. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
  35. stouputils/data_science/data_processing/image/denoise.py +378 -0
  36. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
  37. stouputils/data_science/data_processing/image/invert.py +64 -0
  38. stouputils/data_science/data_processing/image/laplacian.py +60 -0
  39. stouputils/data_science/data_processing/image/median_blur.py +52 -0
  40. stouputils/data_science/data_processing/image/noise.py +59 -0
  41. stouputils/data_science/data_processing/image/normalize.py +65 -0
  42. stouputils/data_science/data_processing/image/random_erase.py +66 -0
  43. stouputils/data_science/data_processing/image/resize.py +69 -0
  44. stouputils/data_science/data_processing/image/rotation.py +80 -0
  45. stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
  46. stouputils/data_science/data_processing/image/sharpening.py +55 -0
  47. stouputils/data_science/data_processing/image/shearing.py +64 -0
  48. stouputils/data_science/data_processing/image/threshold.py +64 -0
  49. stouputils/data_science/data_processing/image/translation.py +71 -0
  50. stouputils/data_science/data_processing/image/zoom.py +83 -0
  51. stouputils/data_science/data_processing/image_augmentation.py +118 -0
  52. stouputils/data_science/data_processing/image_preprocess.py +183 -0
  53. stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
  54. stouputils/data_science/data_processing/technique.py +481 -0
  55. stouputils/data_science/dataset/__init__.py +45 -0
  56. stouputils/data_science/dataset/dataset.py +292 -0
  57. stouputils/data_science/dataset/dataset_loader.py +135 -0
  58. stouputils/data_science/dataset/grouping_strategy.py +296 -0
  59. stouputils/data_science/dataset/image_loader.py +100 -0
  60. stouputils/data_science/dataset/xy_tuple.py +696 -0
  61. stouputils/data_science/metric_dictionnary.py +106 -0
  62. stouputils/data_science/metric_utils.py +847 -0
  63. stouputils/data_science/mlflow_utils.py +206 -0
  64. stouputils/data_science/models/abstract_model.py +149 -0
  65. stouputils/data_science/models/all.py +85 -0
  66. stouputils/data_science/models/base_keras.py +765 -0
  67. stouputils/data_science/models/keras/all.py +38 -0
  68. stouputils/data_science/models/keras/convnext.py +62 -0
  69. stouputils/data_science/models/keras/densenet.py +50 -0
  70. stouputils/data_science/models/keras/efficientnet.py +60 -0
  71. stouputils/data_science/models/keras/mobilenet.py +56 -0
  72. stouputils/data_science/models/keras/resnet.py +52 -0
  73. stouputils/data_science/models/keras/squeezenet.py +233 -0
  74. stouputils/data_science/models/keras/vgg.py +42 -0
  75. stouputils/data_science/models/keras/xception.py +38 -0
  76. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
  77. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
  78. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
  79. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
  80. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
  81. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
  82. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
  83. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
  84. stouputils/data_science/models/keras_utils/visualizations.py +416 -0
  85. stouputils/data_science/models/model_interface.py +939 -0
  86. stouputils/data_science/models/sandbox.py +116 -0
  87. stouputils/data_science/range_tuple.py +234 -0
  88. stouputils/data_science/scripts/augment_dataset.py +77 -0
  89. stouputils/data_science/scripts/exhaustive_process.py +133 -0
  90. stouputils/data_science/scripts/preprocess_dataset.py +70 -0
  91. stouputils/data_science/scripts/routine.py +168 -0
  92. stouputils/data_science/utils.py +285 -0
  93. stouputils/decorators.py +605 -0
  94. stouputils/image.py +441 -0
  95. stouputils/installer/__init__.py +18 -0
  96. stouputils/installer/common.py +67 -0
  97. stouputils/installer/downloader.py +101 -0
  98. stouputils/installer/linux.py +144 -0
  99. stouputils/installer/main.py +223 -0
  100. stouputils/installer/windows.py +136 -0
  101. stouputils/io.py +486 -0
  102. stouputils/parallel.py +483 -0
  103. stouputils/print.py +482 -0
  104. stouputils/py.typed +1 -0
  105. stouputils/stouputils/__init__.pyi +15 -0
  106. stouputils/stouputils/_deprecated.pyi +12 -0
  107. stouputils/stouputils/all_doctests.pyi +46 -0
  108. stouputils/stouputils/applications/__init__.pyi +2 -0
  109. stouputils/stouputils/applications/automatic_docs.pyi +106 -0
  110. stouputils/stouputils/applications/upscaler/__init__.pyi +3 -0
  111. stouputils/stouputils/applications/upscaler/config.pyi +18 -0
  112. stouputils/stouputils/applications/upscaler/image.pyi +109 -0
  113. stouputils/stouputils/applications/upscaler/video.pyi +60 -0
  114. stouputils/stouputils/archive.pyi +67 -0
  115. stouputils/stouputils/backup.pyi +109 -0
  116. stouputils/stouputils/collections.pyi +86 -0
  117. stouputils/stouputils/continuous_delivery/__init__.pyi +5 -0
  118. stouputils/stouputils/continuous_delivery/cd_utils.pyi +129 -0
  119. stouputils/stouputils/continuous_delivery/github.pyi +162 -0
  120. stouputils/stouputils/continuous_delivery/pypi.pyi +53 -0
  121. stouputils/stouputils/continuous_delivery/pyproject.pyi +67 -0
  122. stouputils/stouputils/continuous_delivery/stubs.pyi +39 -0
  123. stouputils/stouputils/ctx.pyi +211 -0
  124. stouputils/stouputils/decorators.pyi +252 -0
  125. stouputils/stouputils/image.pyi +172 -0
  126. stouputils/stouputils/installer/__init__.pyi +5 -0
  127. stouputils/stouputils/installer/common.pyi +39 -0
  128. stouputils/stouputils/installer/downloader.pyi +24 -0
  129. stouputils/stouputils/installer/linux.pyi +39 -0
  130. stouputils/stouputils/installer/main.pyi +57 -0
  131. stouputils/stouputils/installer/windows.pyi +31 -0
  132. stouputils/stouputils/io.pyi +213 -0
  133. stouputils/stouputils/parallel.pyi +216 -0
  134. stouputils/stouputils/print.pyi +136 -0
  135. stouputils/stouputils/version_pkg.pyi +15 -0
  136. stouputils/version_pkg.py +189 -0
  137. stouputils-1.14.0.dist-info/METADATA +178 -0
  138. stouputils-1.14.0.dist-info/RECORD +140 -0
  139. stouputils-1.14.0.dist-info/WHEEL +4 -0
  140. stouputils-1.14.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +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)
@@ -0,0 +1,123 @@
1
+
2
+ # pyright: reportUnusedImport=false
3
+ # ruff: noqa: F401
4
+
5
+ # Imports
6
+ from typing import Literal
7
+
8
+ from .common import Any, NDArray, check_image, cv2, np
9
+
10
+ # Constants
11
+ VALID_SPACES: list[str] = ["lab", "ycbcr", "hsv"]
12
+
13
+ # Color space conversion constants
14
+ COLOR_SPACE_CONSTANTS: dict[str, tuple[int, int, int]] = {
15
+ "lab": (cv2.COLOR_BGR2LAB, cv2.COLOR_LAB2BGR, 0), # L channel index is 0
16
+ "ycbcr": (cv2.COLOR_BGR2YCrCb, cv2.COLOR_YCrCb2BGR, 0), # Y channel index is 0
17
+ "hsv": (cv2.COLOR_BGR2HSV, cv2.COLOR_HSV2BGR, 2), # V channel index is 2
18
+ }
19
+
20
+
21
+ # Functions
22
+ def histogram_equalization_image(
23
+ image: NDArray[Any],
24
+ color_space: Literal["lab", "ycbcr", "hsv"] = "lab",
25
+ ignore_dtype: bool = False,
26
+ ) -> NDArray[Any]:
27
+ """ Apply standard histogram equalization to an image.
28
+
29
+ Histogram equalization improves the contrast in images by stretching
30
+ the intensity range to utilize the full range of intensity values.
31
+
32
+ Args:
33
+ image (NDArray[Any]): Image to apply histogram equalization to
34
+ color_space (str): Color space to use for equalization ("lab", "ycbcr", or "hsv")
35
+ "lab": CIELab color space (perceptually uniform, best visual fidelity)
36
+ "ycbcr": YCbCr color space (fast, good balance)
37
+ "hsv": HSV color space (intuitive, may cause color shifts)
38
+ ignore_dtype (bool): Ignore the dtype check
39
+ Returns:
40
+ NDArray[Any]: Image with histogram equalization applied
41
+
42
+ >>> ## Basic tests
43
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
44
+ >>> histogram_equalization_image(image.astype(np.uint8)).tolist()
45
+ [[0, 32, 64], [96, 128, 159], [191, 223, 255]]
46
+
47
+ >>> img = np.full((5,5), 128, dtype=np.uint8)
48
+ >>> img[1:3, 1:3] = 200 # Create a bright region
49
+ >>> histogram_equalization_image(img).tolist()
50
+ [[0, 0, 0, 0, 0], [0, 255, 255, 0, 0], [0, 255, 255, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
51
+
52
+ >>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
53
+ >>> rgb[1, 1, :] = 50 # Create a dark region
54
+ >>> equalized_rgb = histogram_equalization_image(rgb)
55
+ >>> bool(np.std(equalized_rgb) > np.std(rgb)) # Should enhance contrast
56
+ True
57
+ >>> equalized_rgb.tolist()
58
+ [[[255, 255, 255], [255, 255, 255], [255, 255, 255]], [[255, 255, 255], [0, 0, 0], [255, 255, 255]], [[255, 255, 255], [255, 255, 255], [255, 255, 255]]]
59
+
60
+ >>> ## Test each color space
61
+ >>> test_img = np.zeros((20, 20, 3), dtype=np.uint8)
62
+ >>> test_img[5:15, 5:15] = 200 # Add contrast region
63
+
64
+ >>> # Test LAB color space
65
+ >>> lab_result = histogram_equalization_image(test_img, color_space="lab")
66
+ >>> isinstance(lab_result, np.ndarray) and lab_result.shape == test_img.shape
67
+ True
68
+ >>> bool(np.std(lab_result) > np.std(test_img)) # Verify contrast enhancement
69
+ True
70
+
71
+ >>> # Test YCbCr color space
72
+ >>> ycbcr_result = histogram_equalization_image(test_img, color_space="ycbcr")
73
+ >>> isinstance(ycbcr_result, np.ndarray) and ycbcr_result.shape == test_img.shape
74
+ True
75
+ >>> bool(np.std(ycbcr_result) > np.std(test_img)) # Verify contrast enhancement
76
+ True
77
+
78
+ >>> # Test HSV color space
79
+ >>> hsv_result = histogram_equalization_image(test_img, color_space="hsv")
80
+ >>> isinstance(hsv_result, np.ndarray) and hsv_result.shape == test_img.shape
81
+ True
82
+ >>> bool(np.std(hsv_result) > np.std(test_img)) # Verify contrast enhancement
83
+ True
84
+
85
+ >>> ## Test invalid inputs
86
+ >>> histogram_equalization_image("not an image")
87
+ Traceback (most recent call last):
88
+ ...
89
+ AssertionError: Image must be a numpy array
90
+
91
+ >>> histogram_equalization_image(rgb, "invalid_space")
92
+ Traceback (most recent call last):
93
+ ...
94
+ AssertionError: color_space must be one of: lab, ycbcr, hsv
95
+ """ # noqa: E501
96
+ # Check input data
97
+ check_image(image, ignore_dtype=ignore_dtype)
98
+ lowered_color_space = color_space.lower()
99
+ assert lowered_color_space in VALID_SPACES, f"color_space must be one of: {', '.join(VALID_SPACES)}"
100
+
101
+ # Handle different image types
102
+ if len(image.shape) == 2:
103
+ # Grayscale image - just apply histogram equalization directly
104
+ return cv2.equalizeHist(image)
105
+ else:
106
+ # Color image - apply equalization based on selected color space
107
+ convert_to, convert_from, channel_idx = COLOR_SPACE_CONSTANTS[lowered_color_space]
108
+
109
+ # Convert to target color space
110
+ converted: NDArray[Any] = cv2.cvtColor(image, convert_to)
111
+
112
+ # Split channels
113
+ channels: list[NDArray[Any]] = list(cv2.split(converted))
114
+
115
+ # Apply histogram equalization to the appropriate channel
116
+ channels[channel_idx] = cv2.equalizeHist(channels[channel_idx])
117
+
118
+ # Merge channels
119
+ result: NDArray[Any] = cv2.merge(channels)
120
+
121
+ # Convert back to BGR
122
+ return cv2.cvtColor(result, convert_from)
123
+
@@ -0,0 +1,64 @@
1
+
2
+ # Imports
3
+ from .common import Any, NDArray, check_image, np
4
+
5
+
6
+ # Function
7
+ def invert_image(image: NDArray[Any], ignore_dtype: bool = False) -> NDArray[Any]:
8
+ """ Invert the colors of an image.
9
+
10
+ This function inverts the colors of the input image by subtracting each pixel value
11
+ from the maximum possible value based on the image type.
12
+
13
+ Args:
14
+ image (NDArray[Any]): Input image as a NumPy array.
15
+ ignore_dtype (bool): Ignore the dtype check.
16
+ Returns:
17
+ NDArray[Any]: Image with inverted colors.
18
+
19
+ >>> ## Basic tests
20
+ >>> image = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]], dtype=np.uint8)
21
+ >>> inverted = invert_image(image)
22
+ >>> inverted.tolist()
23
+ [[245, 235, 225], [215, 205, 195], [185, 175, 165]]
24
+
25
+ >>> # Test with floating point image
26
+ >>> float_img = np.array([[0.1, 0.2], [0.3, 0.4]], dtype=np.float32)
27
+ >>> [round(float(x), 1) for x in invert_image(float_img).flatten()]
28
+ [0.9, 0.8, 0.7, 0.6]
29
+
30
+ >>> # Test with RGB image
31
+ >>> rgb = np.zeros((2, 2, 3), dtype=np.uint8)
32
+ >>> rgb[0, 0] = [255, 0, 0] # Red pixel
33
+ >>> inverted_rgb = invert_image(rgb)
34
+ >>> inverted_rgb[0, 0].tolist()
35
+ [0, 255, 255]
36
+
37
+ >>> ## Test invalid inputs
38
+ >>> invert_image("not an image")
39
+ Traceback (most recent call last):
40
+ ...
41
+ AssertionError: Image must be a numpy array
42
+ """
43
+ # Check input data
44
+ check_image(image, ignore_dtype=ignore_dtype)
45
+
46
+ # Get the maximum value based on the image's data type
47
+ if image.dtype == np.uint8:
48
+ max_value = 255
49
+ elif image.dtype == np.uint16:
50
+ max_value = 65535
51
+ elif image.dtype == np.float32 or image.dtype == np.float64:
52
+ # For float images, we assume range [0, 1]
53
+ max_value = 1.0
54
+ else:
55
+ # Default case, assuming 8-bit
56
+ max_value = 255
57
+ image = image.astype(np.uint8)
58
+
59
+ # Invert the image
60
+ inverted = max_value - image
61
+
62
+ # Ensure we return the same dtype as the input
63
+ return inverted.astype(image.dtype)
64
+
@@ -0,0 +1,60 @@
1
+
2
+ # pyright: reportUnknownMemberType=false
3
+ # pyright: reportUnknownVariableType=false
4
+ # pyright: reportUnknownArgumentType=false
5
+ # pyright: reportAttributeAccessIssue=false
6
+ # pyright: reportArgumentType=false
7
+ # pyright: reportCallIssue=false
8
+
9
+ # Imports
10
+ from .common import Any, NDArray, check_image, cv2, np
11
+
12
+
13
+ # Functions
14
+ def laplacian_image(image: NDArray[Any], kernel_size: int = 3, ignore_dtype: bool = False) -> NDArray[Any]:
15
+ """ Apply Laplacian edge detection to an image.
16
+
17
+ Args:
18
+ image (NDArray[Any]): Image to apply Laplacian edge detection
19
+ kernel_size (int): Size of the kernel (must be odd)
20
+ ignore_dtype (bool): Ignore the dtype check
21
+ Returns:
22
+ NDArray[Any]: Image with Laplacian edge detection applied
23
+
24
+ >>> ## Basic tests
25
+ >>> image = np.array([[100, 150, 200], [50, 125, 175], [25, 75, 225]])
26
+ >>> edges = laplacian_image(image.astype(np.uint8))
27
+ >>> edges.shape == image.shape[:2] # Laplacian returns single channel
28
+ True
29
+
30
+ >>> rgb = np.random.randint(0, 256, (3,3,3), dtype=np.uint8)
31
+ >>> edges_rgb = laplacian_image(rgb)
32
+ >>> edges_rgb.shape == rgb.shape[:2] # Laplacian returns single channel
33
+ True
34
+
35
+ >>> ## Test invalid inputs
36
+ >>> laplacian_image("not an image")
37
+ Traceback (most recent call last):
38
+ ...
39
+ AssertionError: Image must be a numpy array
40
+
41
+ >>> laplacian_image(image.astype(np.uint8), kernel_size=2)
42
+ Traceback (most recent call last):
43
+ ...
44
+ AssertionError: kernel_size must be odd, got 2
45
+ """
46
+ # Check input data
47
+ check_image(image, ignore_dtype=ignore_dtype)
48
+ assert kernel_size % 2 == 1, f"kernel_size must be odd, got {kernel_size}"
49
+
50
+ # Convert to grayscale if needed
51
+ if len(image.shape) > 2:
52
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
53
+
54
+ # Apply Laplacian edge detection
55
+ laplacian: NDArray[Any] = cv2.Laplacian(image, cv2.CV_64F, ksize=kernel_size)
56
+
57
+ # Convert back to uint8 and normalize to 0-255 range
58
+ normalized: NDArray[Any] = cv2.normalize(laplacian, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX)
59
+ return normalized.astype(np.uint8)
60
+
@@ -0,0 +1,52 @@
1
+
2
+ # pyright: reportUnusedImport=false
3
+ # ruff: noqa: F401
4
+
5
+ # Imports
6
+ from .common import Any, NDArray, check_image, cv2, np
7
+
8
+
9
+ # Functions
10
+ def median_blur_image(image: NDArray[Any], kernel_size: int = 7, iterations: int = 1) -> NDArray[Any]:
11
+ """ Apply median blur to an image.
12
+
13
+ Args:
14
+ image (NDArray[Any]): Image to apply median blur
15
+ kernel_size (int): Kernel size for the median blur
16
+ iterations (int): Number of iterations for the median blur
17
+ Returns:
18
+ NDArray[Any]: Image with median blur applied
19
+
20
+ >>> ## Basic tests
21
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)
22
+ >>> adjusted = median_blur_image(image, kernel_size=7, iterations=1)
23
+ >>> adjusted.tolist()
24
+ [[3, 3, 3], [4, 5, 6], [7, 7, 7]]
25
+ >>> adjusted.shape == image.shape
26
+ True
27
+ >>> adjusted.dtype == image.dtype
28
+ True
29
+
30
+ >>> median_blur_image(image, kernel_size=3, iterations=1).tolist()
31
+ [[2, 3, 3], [4, 5, 6], [7, 7, 8]]
32
+ >>> median_blur_image(image, kernel_size=3, iterations=2).tolist()
33
+ [[3, 3, 3], [4, 5, 6], [7, 7, 7]]
34
+ >>> median_blur_image(image, kernel_size=3, iterations=5).tolist()
35
+ [[3, 3, 3], [4, 5, 6], [7, 7, 7]]
36
+
37
+ >>> ## Test invalid inputs
38
+ >>> median_blur_image("not an image")
39
+ Traceback (most recent call last):
40
+ ...
41
+ AssertionError: Image must be a numpy array
42
+ """
43
+ # Check input data
44
+ check_image(image, ignore_dtype=True)
45
+
46
+ # Apply median blur
47
+ for _ in range(iterations):
48
+ image = cv2.medianBlur(image, kernel_size)
49
+
50
+ # Return the image
51
+ return image
52
+