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.
- stouputils/__init__.py +40 -0
- stouputils/__main__.py +86 -0
- stouputils/_deprecated.py +37 -0
- stouputils/all_doctests.py +160 -0
- stouputils/applications/__init__.py +22 -0
- stouputils/applications/automatic_docs.py +634 -0
- stouputils/applications/upscaler/__init__.py +39 -0
- stouputils/applications/upscaler/config.py +128 -0
- stouputils/applications/upscaler/image.py +247 -0
- stouputils/applications/upscaler/video.py +287 -0
- stouputils/archive.py +344 -0
- stouputils/backup.py +488 -0
- stouputils/collections.py +244 -0
- stouputils/continuous_delivery/__init__.py +27 -0
- stouputils/continuous_delivery/cd_utils.py +243 -0
- stouputils/continuous_delivery/github.py +522 -0
- stouputils/continuous_delivery/pypi.py +130 -0
- stouputils/continuous_delivery/pyproject.py +147 -0
- stouputils/continuous_delivery/stubs.py +86 -0
- stouputils/ctx.py +408 -0
- stouputils/data_science/config/get.py +51 -0
- stouputils/data_science/config/set.py +125 -0
- stouputils/data_science/data_processing/image/__init__.py +66 -0
- stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
- stouputils/data_science/data_processing/image/axis_flip.py +58 -0
- stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
- stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
- stouputils/data_science/data_processing/image/blur.py +59 -0
- stouputils/data_science/data_processing/image/brightness.py +54 -0
- stouputils/data_science/data_processing/image/canny.py +110 -0
- stouputils/data_science/data_processing/image/clahe.py +92 -0
- stouputils/data_science/data_processing/image/common.py +30 -0
- stouputils/data_science/data_processing/image/contrast.py +53 -0
- stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
- stouputils/data_science/data_processing/image/denoise.py +378 -0
- stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
- stouputils/data_science/data_processing/image/invert.py +64 -0
- stouputils/data_science/data_processing/image/laplacian.py +60 -0
- stouputils/data_science/data_processing/image/median_blur.py +52 -0
- stouputils/data_science/data_processing/image/noise.py +59 -0
- stouputils/data_science/data_processing/image/normalize.py +65 -0
- stouputils/data_science/data_processing/image/random_erase.py +66 -0
- stouputils/data_science/data_processing/image/resize.py +69 -0
- stouputils/data_science/data_processing/image/rotation.py +80 -0
- stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
- stouputils/data_science/data_processing/image/sharpening.py +55 -0
- stouputils/data_science/data_processing/image/shearing.py +64 -0
- stouputils/data_science/data_processing/image/threshold.py +64 -0
- stouputils/data_science/data_processing/image/translation.py +71 -0
- stouputils/data_science/data_processing/image/zoom.py +83 -0
- stouputils/data_science/data_processing/image_augmentation.py +118 -0
- stouputils/data_science/data_processing/image_preprocess.py +183 -0
- stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
- stouputils/data_science/data_processing/technique.py +481 -0
- stouputils/data_science/dataset/__init__.py +45 -0
- stouputils/data_science/dataset/dataset.py +292 -0
- stouputils/data_science/dataset/dataset_loader.py +135 -0
- stouputils/data_science/dataset/grouping_strategy.py +296 -0
- stouputils/data_science/dataset/image_loader.py +100 -0
- stouputils/data_science/dataset/xy_tuple.py +696 -0
- stouputils/data_science/metric_dictionnary.py +106 -0
- stouputils/data_science/metric_utils.py +847 -0
- stouputils/data_science/mlflow_utils.py +206 -0
- stouputils/data_science/models/abstract_model.py +149 -0
- stouputils/data_science/models/all.py +85 -0
- stouputils/data_science/models/base_keras.py +765 -0
- stouputils/data_science/models/keras/all.py +38 -0
- stouputils/data_science/models/keras/convnext.py +62 -0
- stouputils/data_science/models/keras/densenet.py +50 -0
- stouputils/data_science/models/keras/efficientnet.py +60 -0
- stouputils/data_science/models/keras/mobilenet.py +56 -0
- stouputils/data_science/models/keras/resnet.py +52 -0
- stouputils/data_science/models/keras/squeezenet.py +233 -0
- stouputils/data_science/models/keras/vgg.py +42 -0
- stouputils/data_science/models/keras/xception.py +38 -0
- stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
- stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
- stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
- stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
- stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
- stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
- stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
- stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
- stouputils/data_science/models/keras_utils/visualizations.py +416 -0
- stouputils/data_science/models/model_interface.py +939 -0
- stouputils/data_science/models/sandbox.py +116 -0
- stouputils/data_science/range_tuple.py +234 -0
- stouputils/data_science/scripts/augment_dataset.py +77 -0
- stouputils/data_science/scripts/exhaustive_process.py +133 -0
- stouputils/data_science/scripts/preprocess_dataset.py +70 -0
- stouputils/data_science/scripts/routine.py +168 -0
- stouputils/data_science/utils.py +285 -0
- stouputils/decorators.py +605 -0
- stouputils/image.py +441 -0
- stouputils/installer/__init__.py +18 -0
- stouputils/installer/common.py +67 -0
- stouputils/installer/downloader.py +101 -0
- stouputils/installer/linux.py +144 -0
- stouputils/installer/main.py +223 -0
- stouputils/installer/windows.py +136 -0
- stouputils/io.py +486 -0
- stouputils/parallel.py +483 -0
- stouputils/print.py +482 -0
- stouputils/py.typed +1 -0
- stouputils/stouputils/__init__.pyi +15 -0
- stouputils/stouputils/_deprecated.pyi +12 -0
- stouputils/stouputils/all_doctests.pyi +46 -0
- stouputils/stouputils/applications/__init__.pyi +2 -0
- stouputils/stouputils/applications/automatic_docs.pyi +106 -0
- stouputils/stouputils/applications/upscaler/__init__.pyi +3 -0
- stouputils/stouputils/applications/upscaler/config.pyi +18 -0
- stouputils/stouputils/applications/upscaler/image.pyi +109 -0
- stouputils/stouputils/applications/upscaler/video.pyi +60 -0
- stouputils/stouputils/archive.pyi +67 -0
- stouputils/stouputils/backup.pyi +109 -0
- stouputils/stouputils/collections.pyi +86 -0
- stouputils/stouputils/continuous_delivery/__init__.pyi +5 -0
- stouputils/stouputils/continuous_delivery/cd_utils.pyi +129 -0
- stouputils/stouputils/continuous_delivery/github.pyi +162 -0
- stouputils/stouputils/continuous_delivery/pypi.pyi +53 -0
- stouputils/stouputils/continuous_delivery/pyproject.pyi +67 -0
- stouputils/stouputils/continuous_delivery/stubs.pyi +39 -0
- stouputils/stouputils/ctx.pyi +211 -0
- stouputils/stouputils/decorators.pyi +252 -0
- stouputils/stouputils/image.pyi +172 -0
- stouputils/stouputils/installer/__init__.pyi +5 -0
- stouputils/stouputils/installer/common.pyi +39 -0
- stouputils/stouputils/installer/downloader.pyi +24 -0
- stouputils/stouputils/installer/linux.pyi +39 -0
- stouputils/stouputils/installer/main.pyi +57 -0
- stouputils/stouputils/installer/windows.pyi +31 -0
- stouputils/stouputils/io.pyi +213 -0
- stouputils/stouputils/parallel.pyi +216 -0
- stouputils/stouputils/print.pyi +136 -0
- stouputils/stouputils/version_pkg.pyi +15 -0
- stouputils/version_pkg.py +189 -0
- stouputils-1.14.0.dist-info/METADATA +178 -0
- stouputils-1.14.0.dist-info/RECORD +140 -0
- stouputils-1.14.0.dist-info/WHEEL +4 -0
- 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
|
+
|