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,59 @@
|
|
|
1
|
+
|
|
2
|
+
# Imports
|
|
3
|
+
from .common import Any, NDArray, check_image, np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Functions
|
|
7
|
+
def noise_image(image: NDArray[Any], amount: float, ignore_dtype: bool = False) -> NDArray[Any]:
|
|
8
|
+
""" Add Gaussian noise to an image.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
image (NDArray[Any]): Image to add noise to
|
|
12
|
+
amount (float): Amount of noise to add (between 0 and 1)
|
|
13
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
14
|
+
Returns:
|
|
15
|
+
NDArray[Any]: Noisy image
|
|
16
|
+
|
|
17
|
+
>>> ## Basic tests
|
|
18
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
19
|
+
>>> noisy = noise_image(image.astype(np.uint8), 0.5)
|
|
20
|
+
>>> noisy.shape == image.shape
|
|
21
|
+
True
|
|
22
|
+
>>> bool(np.all(noisy >= 0) and np.all(noisy <= 255))
|
|
23
|
+
True
|
|
24
|
+
|
|
25
|
+
>>> np.random.seed(0)
|
|
26
|
+
>>> image = np.array([[128] * 3] * 3)
|
|
27
|
+
>>> noise_image(image.astype(np.uint8), 0.1).tolist()
|
|
28
|
+
[[172, 138, 152], [185, 175, 104], [152, 125, 126]]
|
|
29
|
+
|
|
30
|
+
>>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
|
|
31
|
+
>>> noisy_rgb = noise_image(rgb, 0.1)
|
|
32
|
+
>>> noisy_rgb.shape == (3,3,3)
|
|
33
|
+
True
|
|
34
|
+
|
|
35
|
+
>>> ## Test invalid inputs
|
|
36
|
+
>>> noise_image("not an image", 0.5)
|
|
37
|
+
Traceback (most recent call last):
|
|
38
|
+
...
|
|
39
|
+
AssertionError: Image must be a numpy array
|
|
40
|
+
|
|
41
|
+
>>> noise_image(image.astype(np.uint8), "0.5")
|
|
42
|
+
Traceback (most recent call last):
|
|
43
|
+
...
|
|
44
|
+
AssertionError: amount must be a number, got <class 'str'>
|
|
45
|
+
|
|
46
|
+
>>> noise_image(image.astype(np.uint8), 1.5)
|
|
47
|
+
Traceback (most recent call last):
|
|
48
|
+
...
|
|
49
|
+
AssertionError: amount must be between 0 and 1, got 1.5
|
|
50
|
+
"""
|
|
51
|
+
# Check input data
|
|
52
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
53
|
+
assert isinstance(amount, float | int), f"amount must be a number, got {type(amount)}"
|
|
54
|
+
assert 0 <= amount <= 1, f"amount must be between 0 and 1, got {amount}"
|
|
55
|
+
|
|
56
|
+
# Generate noise
|
|
57
|
+
noise: NDArray[Any] = np.random.normal(0, amount * 255, image.shape).astype(np.int16)
|
|
58
|
+
return np.clip(image.astype(np.int16) + noise, 0, 255).astype(image.dtype) # pyright: ignore [reportUnknownMemberType]
|
|
59
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
|
|
2
|
+
# pyright: reportUnknownVariableType=false
|
|
3
|
+
# pyright: reportUnusedImport=false
|
|
4
|
+
# pyright: reportArgumentType=false
|
|
5
|
+
# pyright: reportCallIssue=false
|
|
6
|
+
|
|
7
|
+
# Imports
|
|
8
|
+
from .common import Any, NDArray, check_image, cv2, np # noqa: F401
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Functions
|
|
12
|
+
def normalize_image(
|
|
13
|
+
image: NDArray[Any],
|
|
14
|
+
a: float | int = 0,
|
|
15
|
+
b: float | int = 255,
|
|
16
|
+
method: int = cv2.NORM_MINMAX,
|
|
17
|
+
ignore_dtype: bool = False,
|
|
18
|
+
) -> NDArray[Any]:
|
|
19
|
+
""" Normalize an image to the range 0-255.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
image (NDArray[Any]): Image to normalize
|
|
23
|
+
a (float | int): Minimum value (default: 0)
|
|
24
|
+
b (float | int): Maximum value (default: 255)
|
|
25
|
+
method (int): Normalization method (default: cv2.NORM_MINMAX)
|
|
26
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
27
|
+
Returns:
|
|
28
|
+
NDArray[Any]: Normalized image
|
|
29
|
+
|
|
30
|
+
>>> ## Basic tests
|
|
31
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.uint8)
|
|
32
|
+
>>> normalized = normalize_image(image)
|
|
33
|
+
>>> normalized.tolist()
|
|
34
|
+
[[0, 32, 64], [96, 128, 159], [191, 223, 255]]
|
|
35
|
+
>>> normalized.shape == image.shape
|
|
36
|
+
True
|
|
37
|
+
>>> normalized.dtype == image.dtype
|
|
38
|
+
True
|
|
39
|
+
|
|
40
|
+
>>> ## Test invalid inputs
|
|
41
|
+
>>> normalize_image("not an image")
|
|
42
|
+
Traceback (most recent call last):
|
|
43
|
+
...
|
|
44
|
+
AssertionError: Image must be a numpy array
|
|
45
|
+
|
|
46
|
+
>>> normalize_image(image, a="not an integer")
|
|
47
|
+
Traceback (most recent call last):
|
|
48
|
+
...
|
|
49
|
+
AssertionError: a must be a float or an integer
|
|
50
|
+
|
|
51
|
+
>>> normalize_image(image, b="not an integer")
|
|
52
|
+
Traceback (most recent call last):
|
|
53
|
+
...
|
|
54
|
+
AssertionError: b must be a float or an integer
|
|
55
|
+
"""
|
|
56
|
+
# Check input data
|
|
57
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
58
|
+
assert isinstance(a, float | int), "a must be a float or an integer"
|
|
59
|
+
assert isinstance(b, float | int), "b must be a float or an integer"
|
|
60
|
+
assert isinstance(method, int), "method must be an integer"
|
|
61
|
+
assert a < b, "a must be less than b"
|
|
62
|
+
|
|
63
|
+
# Normalize image
|
|
64
|
+
return cv2.normalize(image, None, a, b, method)
|
|
65
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
# pyright: reportUnknownMemberType=false
|
|
3
|
+
|
|
4
|
+
# Imports
|
|
5
|
+
from .common import Any, NDArray, check_image, np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Functions
|
|
9
|
+
def random_erase_image(image: NDArray[Any], erase_factor: float, ignore_dtype: bool = False) -> NDArray[Any]:
|
|
10
|
+
""" Randomly erase a rectangle in the image.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
image (NDArray[Any]): Image to apply random erase
|
|
14
|
+
erase_factor (float): Factor determining the size of the rectangle to erase
|
|
15
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
16
|
+
Returns:
|
|
17
|
+
NDArray[Any]: Image with random erasing applied
|
|
18
|
+
|
|
19
|
+
>>> ## Basic tests
|
|
20
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
21
|
+
>>> erased = random_erase_image(image.astype(np.uint8), 0.5)
|
|
22
|
+
>>> erased.shape == image.shape
|
|
23
|
+
True
|
|
24
|
+
|
|
25
|
+
>>> np.random.seed(42)
|
|
26
|
+
>>> img = np.ones((5,5), dtype=np.uint8) * 255
|
|
27
|
+
>>> erased = random_erase_image(img, 0.4)
|
|
28
|
+
>>> bool(np.any(erased == 0)) # Should have some erased pixels
|
|
29
|
+
True
|
|
30
|
+
|
|
31
|
+
>>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
|
|
32
|
+
>>> erased_rgb = random_erase_image(rgb, 0.3)
|
|
33
|
+
>>> erased_rgb.shape == (3,3,3)
|
|
34
|
+
True
|
|
35
|
+
|
|
36
|
+
>>> ## Test invalid inputs
|
|
37
|
+
>>> random_erase_image("not an image", 0.5)
|
|
38
|
+
Traceback (most recent call last):
|
|
39
|
+
...
|
|
40
|
+
AssertionError: Image must be a numpy array
|
|
41
|
+
|
|
42
|
+
>>> random_erase_image(image.astype(np.uint8), "0.5")
|
|
43
|
+
Traceback (most recent call last):
|
|
44
|
+
...
|
|
45
|
+
AssertionError: erase_factor must be a number, got <class 'str'>
|
|
46
|
+
"""
|
|
47
|
+
# Check input data
|
|
48
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
49
|
+
assert isinstance(erase_factor, float | int), f"erase_factor must be a number, got {type(erase_factor)}"
|
|
50
|
+
|
|
51
|
+
# Get image dimensions
|
|
52
|
+
height, width = image.shape[:2]
|
|
53
|
+
|
|
54
|
+
# Determine size of the rectangle to erase
|
|
55
|
+
erase_height: int = int(height * erase_factor)
|
|
56
|
+
erase_width: int = int(width * erase_factor)
|
|
57
|
+
|
|
58
|
+
# Randomly choose the top-left corner of the rectangle
|
|
59
|
+
top_left_x: int = np.random.randint(0, width - erase_width)
|
|
60
|
+
top_left_y: int = np.random.randint(0, height - erase_height)
|
|
61
|
+
|
|
62
|
+
# Apply random erasing
|
|
63
|
+
erased_image: NDArray[Any] = image.copy()
|
|
64
|
+
erased_image[top_left_y:top_left_y + erase_height, top_left_x:top_left_x + erase_width] = 0
|
|
65
|
+
return erased_image
|
|
66
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
# Imports
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
from .common import Any, NDArray, check_image, np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Functions
|
|
9
|
+
def resize_image(
|
|
10
|
+
image: NDArray[Any],
|
|
11
|
+
width: int,
|
|
12
|
+
height: int,
|
|
13
|
+
resample: Image.Resampling | int = Image.Resampling.LANCZOS,
|
|
14
|
+
ignore_dtype: bool = False
|
|
15
|
+
) -> NDArray[Any]:
|
|
16
|
+
""" Resize an image to a new width and height.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
image (NDArray[Any]): Image to resize
|
|
20
|
+
width (int): New width
|
|
21
|
+
height (int): New height
|
|
22
|
+
resample (Image.Resampling | int): Resampling method
|
|
23
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
24
|
+
Returns:
|
|
25
|
+
NDArray[Any]: Image with resized dimensions
|
|
26
|
+
|
|
27
|
+
>>> ## Basic tests
|
|
28
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
29
|
+
>>> resized = resize_image(image.astype(np.uint8), 6, 6)
|
|
30
|
+
>>> resized.shape
|
|
31
|
+
(6, 6)
|
|
32
|
+
|
|
33
|
+
>>> img = np.ones((5, 5), dtype=np.uint8) * 255
|
|
34
|
+
>>> resized = resize_image(img, 10, 10)
|
|
35
|
+
>>> resized.shape
|
|
36
|
+
(10, 10)
|
|
37
|
+
|
|
38
|
+
>>> rgb = np.full((3, 3, 3), 128, dtype=np.uint8)
|
|
39
|
+
>>> resized_rgb = resize_image(rgb, 6, 6)
|
|
40
|
+
>>> resized_rgb.shape
|
|
41
|
+
(6, 6, 3)
|
|
42
|
+
|
|
43
|
+
>>> ## Test invalid inputs
|
|
44
|
+
>>> resize_image("not an image", 10, 10)
|
|
45
|
+
Traceback (most recent call last):
|
|
46
|
+
...
|
|
47
|
+
AssertionError: Image must be a numpy array
|
|
48
|
+
|
|
49
|
+
>>> resize_image(image.astype(np.uint8), "10", 10)
|
|
50
|
+
Traceback (most recent call last):
|
|
51
|
+
...
|
|
52
|
+
AssertionError: width must be integer, got <class 'str'>
|
|
53
|
+
|
|
54
|
+
>>> resize_image(image.astype(np.uint8), 10, "10")
|
|
55
|
+
Traceback (most recent call last):
|
|
56
|
+
...
|
|
57
|
+
AssertionError: height must be integer, got <class 'str'>
|
|
58
|
+
"""
|
|
59
|
+
# Check input data
|
|
60
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
61
|
+
assert isinstance(width, int), f"width must be integer, got {type(width)}"
|
|
62
|
+
assert isinstance(height, int), f"height must be integer, got {type(height)}"
|
|
63
|
+
assert isinstance(resample, Image.Resampling | int), f"resample must be Image.Resampling, got {type(resample)}"
|
|
64
|
+
|
|
65
|
+
# Resize image
|
|
66
|
+
new_image: Image.Image = Image.fromarray(image)
|
|
67
|
+
new_image = new_image.resize((width, height), resample=resample)
|
|
68
|
+
return np.array(new_image)
|
|
69
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
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 rotate_image(image: NDArray[Any], angle: float | int, ignore_dtype: bool = False) -> NDArray[Any]:
|
|
11
|
+
""" Rotate an image by a given angle.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
image (NDArray[Any]): Image to rotate
|
|
15
|
+
angle (float|int): Angle in degrees to rotate the image (between -360 and 360)
|
|
16
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
17
|
+
Returns:
|
|
18
|
+
NDArray[Any]: Rotated image
|
|
19
|
+
|
|
20
|
+
>>> ## Basic tests
|
|
21
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
22
|
+
>>> rotate_image(image.astype(np.int16), 90).tolist()
|
|
23
|
+
[[3, 6, 9], [2, 5, 8], [1, 4, 7]]
|
|
24
|
+
|
|
25
|
+
>>> rotate_image(image.astype(np.float32), 90).tolist()
|
|
26
|
+
[[3.0, 6.0, 9.0], [2.0, 5.0, 8.0], [1.0, 4.0, 7.0]]
|
|
27
|
+
|
|
28
|
+
>>> rotate_image(image.astype(np.uint8), 45).tolist()
|
|
29
|
+
[[1, 4, 4], [2, 5, 8], [2, 6, 5]]
|
|
30
|
+
|
|
31
|
+
>>> rotate_image(image.astype(np.float32), 45).tolist()
|
|
32
|
+
[[1.1875, 3.5625, 3.5625], [2.125, 5.0, 7.875], [2.375, 6.4375, 4.75]]
|
|
33
|
+
|
|
34
|
+
>>> ## Test invalid inputs
|
|
35
|
+
>>> rotate_image([1,2,3], 90)
|
|
36
|
+
Traceback (most recent call last):
|
|
37
|
+
...
|
|
38
|
+
AssertionError: Image must be a numpy array
|
|
39
|
+
|
|
40
|
+
>>> rotate_image(np.array([1,2,3]), 90)
|
|
41
|
+
Traceback (most recent call last):
|
|
42
|
+
...
|
|
43
|
+
AssertionError: Image must have at least 2 dimensions
|
|
44
|
+
|
|
45
|
+
>>> rotate_image(image.astype(np.uint8), "90")
|
|
46
|
+
Traceback (most recent call last):
|
|
47
|
+
...
|
|
48
|
+
AssertionError: Angle must be a number, got <class 'str'>
|
|
49
|
+
|
|
50
|
+
>>> rotate_image(image.astype(np.uint8), 400)
|
|
51
|
+
Traceback (most recent call last):
|
|
52
|
+
...
|
|
53
|
+
AssertionError: Angle must be between -360 and 360 degrees, got 400
|
|
54
|
+
|
|
55
|
+
>>> rotate_image(image.astype(np.int32), 90)
|
|
56
|
+
Traceback (most recent call last):
|
|
57
|
+
...
|
|
58
|
+
AssertionError: Image must be of type [<class 'numpy.uint8'>, <class 'numpy.int16'>, <class 'numpy.float32'>, <class 'numpy.float64'>]
|
|
59
|
+
""" # noqa: E501
|
|
60
|
+
# Check input data
|
|
61
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
62
|
+
assert isinstance(angle, float | int), f"Angle must be a number, got {type(angle)}"
|
|
63
|
+
assert -360 <= angle <= 360, f"Angle must be between -360 and 360 degrees, got {angle}"
|
|
64
|
+
|
|
65
|
+
# Get image dimensions
|
|
66
|
+
height: int
|
|
67
|
+
width: int
|
|
68
|
+
height, width = image.shape[:2]
|
|
69
|
+
image_center: tuple[int, int] = (width // 2, height // 2)
|
|
70
|
+
|
|
71
|
+
# Get rotation matrix and rotate image
|
|
72
|
+
rotation_matrix: NDArray[Any] = cv2.getRotationMatrix2D(image_center, angle, 1.0)
|
|
73
|
+
rotated_image: NDArray[Any] = cv2.warpAffine(
|
|
74
|
+
image,
|
|
75
|
+
rotation_matrix,
|
|
76
|
+
(width, height)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return rotated_image
|
|
80
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
|
|
2
|
+
# Imports
|
|
3
|
+
from .common import Any, NDArray, check_image, np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Functions
|
|
7
|
+
def salt_pepper_image(image: NDArray[Any], density: float, ignore_dtype: bool = False) -> NDArray[Any]:
|
|
8
|
+
""" Add salt and pepper noise to an image.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
image (NDArray[Any]): Image to add noise to
|
|
12
|
+
density (float): Density of the noise (between 0 and 1)
|
|
13
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
14
|
+
Returns:
|
|
15
|
+
NDArray[Any]: Image with salt and pepper noise
|
|
16
|
+
|
|
17
|
+
>>> ## Basic tests
|
|
18
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
19
|
+
>>> noisy = salt_pepper_image(image.astype(np.uint8), 0.1)
|
|
20
|
+
>>> noisy.shape == image.shape
|
|
21
|
+
True
|
|
22
|
+
|
|
23
|
+
>>> np.random.seed(42)
|
|
24
|
+
>>> img = np.full((4,4), 128, dtype=np.uint8)
|
|
25
|
+
>>> noisy = salt_pepper_image(img, 1.0)
|
|
26
|
+
>>> sorted(np.unique(noisy).tolist()) # Should only contain 0 and 255
|
|
27
|
+
[0, 255]
|
|
28
|
+
|
|
29
|
+
>>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
|
|
30
|
+
>>> noisy_rgb = salt_pepper_image(rgb, 0.1)
|
|
31
|
+
>>> noisy_rgb.shape == (3,3,3)
|
|
32
|
+
True
|
|
33
|
+
|
|
34
|
+
>>> ## Test invalid inputs
|
|
35
|
+
>>> salt_pepper_image("not an image", 0.1)
|
|
36
|
+
Traceback (most recent call last):
|
|
37
|
+
...
|
|
38
|
+
AssertionError: Image must be a numpy array
|
|
39
|
+
|
|
40
|
+
>>> salt_pepper_image(image.astype(np.uint8), "0.1")
|
|
41
|
+
Traceback (most recent call last):
|
|
42
|
+
...
|
|
43
|
+
AssertionError: density must be a number, got <class 'str'>
|
|
44
|
+
|
|
45
|
+
>>> salt_pepper_image(image.astype(np.uint8), 1.5)
|
|
46
|
+
Traceback (most recent call last):
|
|
47
|
+
...
|
|
48
|
+
AssertionError: density must be between 0 and 1, got 1.5
|
|
49
|
+
"""
|
|
50
|
+
# Check input data
|
|
51
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
52
|
+
assert isinstance(density, float | int), f"density must be a number, got {type(density)}"
|
|
53
|
+
assert 0 <= density <= 1, f"density must be between 0 and 1, got {density}"
|
|
54
|
+
|
|
55
|
+
# Create a mask of the same shape as the input image
|
|
56
|
+
mask: NDArray[Any] = np.random.choice( # pyright: ignore [reportUnknownMemberType]
|
|
57
|
+
[0, 1, 2],
|
|
58
|
+
size=image.shape,
|
|
59
|
+
p=[1-density, density/2, density/2],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Apply the mask to the input image
|
|
63
|
+
noisy_image: NDArray[Any] = image.copy()
|
|
64
|
+
noisy_image[mask == 1] = 0 # Pepper noise
|
|
65
|
+
noisy_image[mask == 2] = 255 # Salt noise
|
|
66
|
+
|
|
67
|
+
return noisy_image
|
|
68
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
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 sharpen_image(image: NDArray[Any], alpha: float, ignore_dtype: bool = False) -> NDArray[Any]:
|
|
11
|
+
""" Sharpen an image.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
image (NDArray[Any]): Image to sharpen
|
|
15
|
+
alpha (float): Sharpening factor
|
|
16
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
17
|
+
Returns:
|
|
18
|
+
NDArray[Any]: Sharpened image
|
|
19
|
+
|
|
20
|
+
>>> ## Basic tests
|
|
21
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
22
|
+
>>> sharpened = sharpen_image(image.astype(np.uint8), 1.5)
|
|
23
|
+
>>> sharpened.shape == image.shape
|
|
24
|
+
True
|
|
25
|
+
|
|
26
|
+
>>> img = np.full((5,5), 128, dtype=np.uint8)
|
|
27
|
+
>>> img[2,2] = 255 # Center bright pixel
|
|
28
|
+
>>> sharp = sharpen_image(img, 1.0)
|
|
29
|
+
>>> bool(sharp[2,2] > img[2,2] * 0.9) # Center should stay bright
|
|
30
|
+
True
|
|
31
|
+
|
|
32
|
+
>>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
|
|
33
|
+
>>> sharp_rgb = sharpen_image(rgb, 1.0)
|
|
34
|
+
>>> sharp_rgb.shape == (3,3,3)
|
|
35
|
+
True
|
|
36
|
+
|
|
37
|
+
>>> ## Test invalid inputs
|
|
38
|
+
>>> sharpen_image("not an image", 1.5)
|
|
39
|
+
Traceback (most recent call last):
|
|
40
|
+
...
|
|
41
|
+
AssertionError: Image must be a numpy array
|
|
42
|
+
|
|
43
|
+
>>> sharpen_image(image.astype(np.uint8), "1.5")
|
|
44
|
+
Traceback (most recent call last):
|
|
45
|
+
...
|
|
46
|
+
AssertionError: alpha must be a number, got <class 'str'>
|
|
47
|
+
"""
|
|
48
|
+
# Check input data
|
|
49
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
50
|
+
assert isinstance(alpha, float | int), f"alpha must be a number, got {type(alpha)}"
|
|
51
|
+
|
|
52
|
+
# Apply sharpening
|
|
53
|
+
blurred: NDArray[Any] = cv2.GaussianBlur(image, (0, 0), 3)
|
|
54
|
+
return cv2.addWeighted(image, 1 + alpha, blurred, -alpha, 0)
|
|
55
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
|
|
2
|
+
# Imports
|
|
3
|
+
from .common import Any, NDArray, check_image, cv2, np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Functions
|
|
7
|
+
def shear_image(image: NDArray[Any], x: float, y: float, ignore_dtype: bool = False) -> NDArray[Any]:
|
|
8
|
+
""" Shear an image
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
image (NDArray[Any]): Image to shear
|
|
12
|
+
x (float): Shearing along the x axis (between -180 and 180)
|
|
13
|
+
y (float): Shearing along the y axis (between -180 and 180)
|
|
14
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
15
|
+
Returns:
|
|
16
|
+
NDArray[Any]: Sheared image
|
|
17
|
+
|
|
18
|
+
>>> ## Basic tests
|
|
19
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
20
|
+
>>> shear_image(image.astype(np.uint8), 15, 0).tolist()
|
|
21
|
+
[[1, 2, 3], [3, 5, 6], [3, 7, 8]]
|
|
22
|
+
|
|
23
|
+
>>> shear_image(image.astype(np.float32), 0, 15).tolist()
|
|
24
|
+
[[1.0, 1.4375, 1.40625], [4.0, 4.15625, 4.40625], [7.0, 7.15625, 7.40625]]
|
|
25
|
+
|
|
26
|
+
>>> ## Test invalid inputs
|
|
27
|
+
>>> shear_image(image.astype(np.uint8), 200, 0)
|
|
28
|
+
Traceback (most recent call last):
|
|
29
|
+
...
|
|
30
|
+
AssertionError: x must be between -180 and 180, got 200
|
|
31
|
+
|
|
32
|
+
>>> shear_image(image.astype(np.uint8), 0, -200)
|
|
33
|
+
Traceback (most recent call last):
|
|
34
|
+
...
|
|
35
|
+
AssertionError: y must be between -180 and 180, got -200
|
|
36
|
+
|
|
37
|
+
>>> shear_image("not an image", 0, 0)
|
|
38
|
+
Traceback (most recent call last):
|
|
39
|
+
...
|
|
40
|
+
AssertionError: Image must be a numpy array
|
|
41
|
+
"""
|
|
42
|
+
# Check input data
|
|
43
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
44
|
+
assert isinstance(x, float | int), f"x must be a number, got {type(x)}"
|
|
45
|
+
assert isinstance(y, float | int), f"y must be a number, got {type(y)}"
|
|
46
|
+
assert -180 <= x <= 180, f"x must be between -180 and 180, got {x}"
|
|
47
|
+
assert -180 <= y <= 180, f"y must be between -180 and 180, got {y}"
|
|
48
|
+
|
|
49
|
+
# Get image dimensions
|
|
50
|
+
height, width = image.shape[:2]
|
|
51
|
+
|
|
52
|
+
# Convert relative shear angles to absolute values
|
|
53
|
+
x_shear: float = np.tan(np.radians(x))
|
|
54
|
+
y_shear: float = np.tan(np.radians(y))
|
|
55
|
+
|
|
56
|
+
# Create shear matrix
|
|
57
|
+
shear_matrix: NDArray[Any] = np.array([
|
|
58
|
+
[1, x_shear, 0],
|
|
59
|
+
[y_shear, 1, 0]
|
|
60
|
+
], dtype=np.float32)
|
|
61
|
+
|
|
62
|
+
# Apply affine transformation
|
|
63
|
+
return cv2.warpAffine(image, shear_matrix, (width, height))
|
|
64
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
|
|
2
|
+
# Imports
|
|
3
|
+
from .common import Any, NDArray, check_image, cv2, np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Functions
|
|
7
|
+
def threshold_image(image: NDArray[Any], thresholds: list[float], ignore_dtype: bool = False) -> NDArray[Any]:
|
|
8
|
+
""" Apply multi-level threshold to an image.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
image (NDArray[Any]): Image to threshold
|
|
12
|
+
threshold (list[float]): List of threshold values (between 0 and 1)
|
|
13
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
14
|
+
Returns:
|
|
15
|
+
NDArray[Any]: Multi-level thresholded image
|
|
16
|
+
|
|
17
|
+
>>> ## Basic tests
|
|
18
|
+
>>> image = np.array([[100, 150, 200], [50, 125, 175], [25, 75, 225]])
|
|
19
|
+
>>> threshold_image(image.astype(np.uint8), [0.3, 0.6]).tolist()
|
|
20
|
+
[[85, 85, 170], [0, 85, 170], [0, 0, 170]]
|
|
21
|
+
|
|
22
|
+
>>> rgb = np.random.randint(0, 256, (3,3,3), dtype=np.uint8)
|
|
23
|
+
>>> thresh_rgb = threshold_image(rgb, [0.3, 0.6])
|
|
24
|
+
>>> thresh_rgb.shape == rgb.shape
|
|
25
|
+
True
|
|
26
|
+
|
|
27
|
+
>>> ## Test invalid inputs
|
|
28
|
+
>>> threshold_image("not an image", [0.5])
|
|
29
|
+
Traceback (most recent call last):
|
|
30
|
+
...
|
|
31
|
+
AssertionError: Image must be a numpy array
|
|
32
|
+
|
|
33
|
+
>>> threshold_image(image.astype(np.uint8), [1.5])
|
|
34
|
+
Traceback (most recent call last):
|
|
35
|
+
...
|
|
36
|
+
AssertionError: threshold values must be between 0 and 1, got [1.5]
|
|
37
|
+
"""
|
|
38
|
+
# Check input data
|
|
39
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
40
|
+
assert all(0 <= t <= 1 for t in thresholds), f"threshold values must be between 0 and 1, got {thresholds}"
|
|
41
|
+
|
|
42
|
+
# Convert thresholds from 0-1 range to 0-255 range
|
|
43
|
+
threshold_values: list[int] = [int(t * 255) for t in sorted(thresholds)]
|
|
44
|
+
|
|
45
|
+
# Apply threshold
|
|
46
|
+
if len(image.shape) == 2:
|
|
47
|
+
# Grayscale image
|
|
48
|
+
result: NDArray[Any] = np.zeros_like(image)
|
|
49
|
+
for i, thresh in enumerate(threshold_values):
|
|
50
|
+
value: int = int(255 * (i + 1) / (len(threshold_values) + 1))
|
|
51
|
+
mask: NDArray[Any] = cv2.threshold(image, thresh, value, cv2.THRESH_BINARY)[1]
|
|
52
|
+
result = cv2.max(result, mask)
|
|
53
|
+
else:
|
|
54
|
+
# Color image - convert to grayscale first, then back to color
|
|
55
|
+
gray: NDArray[Any] = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
56
|
+
result: NDArray[Any] = np.zeros_like(gray)
|
|
57
|
+
for i, thresh in enumerate(threshold_values):
|
|
58
|
+
value: int = int(255 * (i + 1) / (len(threshold_values) + 1))
|
|
59
|
+
mask: NDArray[Any] = cv2.threshold(gray, thresh, value, cv2.THRESH_BINARY)[1]
|
|
60
|
+
result = cv2.max(result, mask)
|
|
61
|
+
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
# Imports
|
|
3
|
+
from .common import Any, NDArray, check_image, cv2, np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Functions
|
|
7
|
+
def translate_image(image: NDArray[Any], x: float, y: float, padding: int = 0, ignore_dtype: bool = False) -> NDArray[Any]:
|
|
8
|
+
""" Translate an image
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
image (NDArray[Any]): Image to translate
|
|
12
|
+
x (float): Translation along the x axis (between -1 and 1)
|
|
13
|
+
y (float): Translation along the y axis (between -1 and 1)
|
|
14
|
+
padding (int): Padding that has been added to the image before calling this function
|
|
15
|
+
ignore_dtype (bool): Ignore the dtype check
|
|
16
|
+
Returns:
|
|
17
|
+
NDArray[Any]: Translated image
|
|
18
|
+
|
|
19
|
+
>>> ## Basic tests
|
|
20
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]).astype(np.uint8)
|
|
21
|
+
>>> translate_image(image, 0.5, 0.5).tolist()
|
|
22
|
+
[[0, 0, 0], [0, 1, 2], [0, 4, 5]]
|
|
23
|
+
|
|
24
|
+
>>> translate_image(image, 0, -2/3).tolist()
|
|
25
|
+
[[7, 8, 9], [0, 0, 0], [0, 0, 0]]
|
|
26
|
+
|
|
27
|
+
>>> ## Test invalid inputs
|
|
28
|
+
>>> translate_image(image, 2, 0)
|
|
29
|
+
Traceback (most recent call last):
|
|
30
|
+
...
|
|
31
|
+
AssertionError: x must be between -1 and 1, got 2
|
|
32
|
+
|
|
33
|
+
>>> translate_image(image, 0, 2)
|
|
34
|
+
Traceback (most recent call last):
|
|
35
|
+
...
|
|
36
|
+
AssertionError: y must be between -1 and 1, got 2
|
|
37
|
+
|
|
38
|
+
>>> translate_image("not an image", 0, 0)
|
|
39
|
+
Traceback (most recent call last):
|
|
40
|
+
...
|
|
41
|
+
AssertionError: Image must be a numpy array
|
|
42
|
+
|
|
43
|
+
>>> translate_image(image, 0, 0, padding=-1)
|
|
44
|
+
Traceback (most recent call last):
|
|
45
|
+
...
|
|
46
|
+
AssertionError: padding must be positive, got -1
|
|
47
|
+
"""
|
|
48
|
+
# Check input data
|
|
49
|
+
check_image(image, ignore_dtype=ignore_dtype)
|
|
50
|
+
assert isinstance(x, float | int), f"x must be a number, got {type(x)}"
|
|
51
|
+
assert isinstance(y, float | int), f"y must be a number, got {type(y)}"
|
|
52
|
+
assert -1 <= x <= 1, f"x must be between -1 and 1, got {x}"
|
|
53
|
+
assert -1 <= y <= 1, f"y must be between -1 and 1, got {y}"
|
|
54
|
+
assert isinstance(padding, int), f"padding must be an integer, got {type(padding)}"
|
|
55
|
+
assert padding >= 0, f"padding must be positive, got {padding}"
|
|
56
|
+
|
|
57
|
+
# Get image dimensions
|
|
58
|
+
height, width = image.shape[:2]
|
|
59
|
+
original_width: int = width - 2 * padding
|
|
60
|
+
original_height: int = height - 2 * padding
|
|
61
|
+
|
|
62
|
+
# Convert relative translations to absolute pixels
|
|
63
|
+
x_pixels: int = int(x * original_width)
|
|
64
|
+
y_pixels: int = int(y * original_height)
|
|
65
|
+
|
|
66
|
+
# Create translation matrix
|
|
67
|
+
translation_matrix: NDArray[Any] = np.array([[1, 0, x_pixels], [0, 1, y_pixels]], dtype=np.float32)
|
|
68
|
+
|
|
69
|
+
# Apply affine transformation
|
|
70
|
+
return cv2.warpAffine(image, translation_matrix, (width, height))
|
|
71
|
+
|