stouputils 1.14.0__py3-none-any.whl → 1.14.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. stouputils/__init__.pyi +15 -0
  2. stouputils/_deprecated.pyi +12 -0
  3. stouputils/all_doctests.pyi +46 -0
  4. stouputils/applications/__init__.pyi +2 -0
  5. stouputils/applications/automatic_docs.py +3 -0
  6. stouputils/applications/automatic_docs.pyi +106 -0
  7. stouputils/applications/upscaler/__init__.pyi +3 -0
  8. stouputils/applications/upscaler/config.pyi +18 -0
  9. stouputils/applications/upscaler/image.pyi +109 -0
  10. stouputils/applications/upscaler/video.pyi +60 -0
  11. stouputils/archive.pyi +67 -0
  12. stouputils/backup.pyi +109 -0
  13. stouputils/collections.pyi +86 -0
  14. stouputils/continuous_delivery/__init__.pyi +5 -0
  15. stouputils/continuous_delivery/cd_utils.pyi +129 -0
  16. stouputils/continuous_delivery/github.pyi +162 -0
  17. stouputils/continuous_delivery/pypi.pyi +52 -0
  18. stouputils/continuous_delivery/pyproject.pyi +67 -0
  19. stouputils/continuous_delivery/stubs.pyi +39 -0
  20. stouputils/ctx.pyi +211 -0
  21. stouputils/data_science/config/get.py +51 -51
  22. stouputils/data_science/data_processing/image/__init__.py +66 -66
  23. stouputils/data_science/data_processing/image/auto_contrast.py +79 -79
  24. stouputils/data_science/data_processing/image/axis_flip.py +58 -58
  25. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -74
  26. stouputils/data_science/data_processing/image/binary_threshold.py +73 -73
  27. stouputils/data_science/data_processing/image/blur.py +59 -59
  28. stouputils/data_science/data_processing/image/brightness.py +54 -54
  29. stouputils/data_science/data_processing/image/canny.py +110 -110
  30. stouputils/data_science/data_processing/image/clahe.py +92 -92
  31. stouputils/data_science/data_processing/image/common.py +30 -30
  32. stouputils/data_science/data_processing/image/contrast.py +53 -53
  33. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -74
  34. stouputils/data_science/data_processing/image/denoise.py +378 -378
  35. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -123
  36. stouputils/data_science/data_processing/image/invert.py +64 -64
  37. stouputils/data_science/data_processing/image/laplacian.py +60 -60
  38. stouputils/data_science/data_processing/image/median_blur.py +52 -52
  39. stouputils/data_science/data_processing/image/noise.py +59 -59
  40. stouputils/data_science/data_processing/image/normalize.py +65 -65
  41. stouputils/data_science/data_processing/image/random_erase.py +66 -66
  42. stouputils/data_science/data_processing/image/resize.py +69 -69
  43. stouputils/data_science/data_processing/image/rotation.py +80 -80
  44. stouputils/data_science/data_processing/image/salt_pepper.py +68 -68
  45. stouputils/data_science/data_processing/image/sharpening.py +55 -55
  46. stouputils/data_science/data_processing/image/shearing.py +64 -64
  47. stouputils/data_science/data_processing/image/threshold.py +64 -64
  48. stouputils/data_science/data_processing/image/translation.py +71 -71
  49. stouputils/data_science/data_processing/image/zoom.py +83 -83
  50. stouputils/data_science/data_processing/image_augmentation.py +118 -118
  51. stouputils/data_science/data_processing/image_preprocess.py +183 -183
  52. stouputils/data_science/data_processing/prosthesis_detection.py +359 -359
  53. stouputils/data_science/data_processing/technique.py +481 -481
  54. stouputils/data_science/dataset/__init__.py +45 -45
  55. stouputils/data_science/dataset/dataset.py +292 -292
  56. stouputils/data_science/dataset/dataset_loader.py +135 -135
  57. stouputils/data_science/dataset/grouping_strategy.py +296 -296
  58. stouputils/data_science/dataset/image_loader.py +100 -100
  59. stouputils/data_science/dataset/xy_tuple.py +696 -696
  60. stouputils/data_science/metric_dictionnary.py +106 -106
  61. stouputils/data_science/mlflow_utils.py +206 -206
  62. stouputils/data_science/models/abstract_model.py +149 -149
  63. stouputils/data_science/models/all.py +85 -85
  64. stouputils/data_science/models/keras/all.py +38 -38
  65. stouputils/data_science/models/keras/convnext.py +62 -62
  66. stouputils/data_science/models/keras/densenet.py +50 -50
  67. stouputils/data_science/models/keras/efficientnet.py +60 -60
  68. stouputils/data_science/models/keras/mobilenet.py +56 -56
  69. stouputils/data_science/models/keras/resnet.py +52 -52
  70. stouputils/data_science/models/keras/squeezenet.py +233 -233
  71. stouputils/data_science/models/keras/vgg.py +42 -42
  72. stouputils/data_science/models/keras/xception.py +38 -38
  73. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -20
  74. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -219
  75. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -148
  76. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -31
  77. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -249
  78. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -66
  79. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -12
  80. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -56
  81. stouputils/data_science/models/keras_utils/visualizations.py +416 -416
  82. stouputils/data_science/models/sandbox.py +116 -116
  83. stouputils/data_science/range_tuple.py +234 -234
  84. stouputils/data_science/utils.py +285 -285
  85. stouputils/decorators.pyi +242 -0
  86. stouputils/image.pyi +172 -0
  87. stouputils/installer/__init__.py +18 -18
  88. stouputils/installer/__init__.pyi +5 -0
  89. stouputils/installer/common.pyi +39 -0
  90. stouputils/installer/downloader.pyi +24 -0
  91. stouputils/installer/linux.py +144 -144
  92. stouputils/installer/linux.pyi +39 -0
  93. stouputils/installer/main.py +223 -223
  94. stouputils/installer/main.pyi +57 -0
  95. stouputils/installer/windows.py +136 -136
  96. stouputils/installer/windows.pyi +31 -0
  97. stouputils/io.pyi +213 -0
  98. stouputils/parallel.py +12 -10
  99. stouputils/parallel.pyi +211 -0
  100. stouputils/print.pyi +136 -0
  101. stouputils/py.typed +1 -1
  102. stouputils/stouputils/parallel.pyi +4 -4
  103. stouputils/version_pkg.pyi +15 -0
  104. {stouputils-1.14.0.dist-info → stouputils-1.14.2.dist-info}/METADATA +1 -1
  105. stouputils-1.14.2.dist-info/RECORD +171 -0
  106. stouputils-1.14.0.dist-info/RECORD +0 -140
  107. {stouputils-1.14.0.dist-info → stouputils-1.14.2.dist-info}/WHEEL +0 -0
  108. {stouputils-1.14.0.dist-info → stouputils-1.14.2.dist-info}/entry_points.txt +0 -0
@@ -1,73 +1,73 @@
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 binary_threshold_image(image: NDArray[Any], threshold: float, ignore_dtype: bool = False) -> NDArray[Any]:
11
- """ Apply binary threshold to an image.
12
-
13
- Args:
14
- image (NDArray[Any]): Image to threshold
15
- threshold (float): Threshold value (between 0 and 1)
16
- ignore_dtype (bool): Ignore the dtype check
17
- Returns:
18
- NDArray[Any]: Thresholded binary image
19
-
20
- >>> ## Basic tests
21
- >>> image = np.array([[100, 150, 200], [50, 125, 175], [25, 75, 225]])
22
- >>> binary_threshold_image(image.astype(np.uint8), 0.5).tolist()
23
- [[0, 255, 255], [0, 0, 255], [0, 0, 255]]
24
-
25
- >>> np.random.seed(42)
26
- >>> img = np.random.randint(0, 256, (4,4), dtype=np.uint8)
27
- >>> thresholded = binary_threshold_image(img, 0.7)
28
- >>> set(np.unique(thresholded).tolist()) <= {0, 255} # Should only contain 0 and 255
29
- True
30
-
31
- >>> rgb = np.random.randint(0, 256, (3,3,3), dtype=np.uint8)
32
- >>> thresh_rgb = binary_threshold_image(rgb, 0.5)
33
- >>> thresh_rgb.shape == rgb.shape
34
- True
35
- >>> set(np.unique(thresh_rgb).tolist()) <= {0, 255}
36
- True
37
-
38
- >>> ## Test invalid inputs
39
- >>> binary_threshold_image("not an image", 0.5)
40
- Traceback (most recent call last):
41
- ...
42
- AssertionError: Image must be a numpy array
43
-
44
- >>> binary_threshold_image(image.astype(np.uint8), "0.5")
45
- Traceback (most recent call last):
46
- ...
47
- AssertionError: threshold must be a number, got <class 'str'>
48
-
49
- >>> binary_threshold_image(image.astype(np.uint8), 1.5)
50
- Traceback (most recent call last):
51
- ...
52
- AssertionError: threshold must be between 0 and 1, got 1.5
53
- """
54
- # Check input data
55
- check_image(image, ignore_dtype=ignore_dtype)
56
- assert isinstance(threshold, float | int), f"threshold must be a number, got {type(threshold)}"
57
- assert 0 <= threshold <= 1, f"threshold must be between 0 and 1, got {threshold}"
58
-
59
- # Convert threshold from 0-1 range to 0-255 range
60
- threshold_value: int = int(threshold * 255)
61
-
62
- # Apply threshold
63
- if len(image.shape) == 2:
64
- # Grayscale image
65
- binary: NDArray[Any] = cv2.threshold(image, threshold_value, 255, cv2.THRESH_BINARY)[1]
66
- else:
67
- # Color image - convert to grayscale first, then back to color
68
- gray: NDArray[Any] = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
69
- binary: NDArray[Any] = cv2.threshold(gray, threshold_value, 255, cv2.THRESH_BINARY)[1]
70
- binary = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)
71
-
72
- return binary
73
-
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 binary_threshold_image(image: NDArray[Any], threshold: float, ignore_dtype: bool = False) -> NDArray[Any]:
11
+ """ Apply binary threshold to an image.
12
+
13
+ Args:
14
+ image (NDArray[Any]): Image to threshold
15
+ threshold (float): Threshold value (between 0 and 1)
16
+ ignore_dtype (bool): Ignore the dtype check
17
+ Returns:
18
+ NDArray[Any]: Thresholded binary image
19
+
20
+ >>> ## Basic tests
21
+ >>> image = np.array([[100, 150, 200], [50, 125, 175], [25, 75, 225]])
22
+ >>> binary_threshold_image(image.astype(np.uint8), 0.5).tolist()
23
+ [[0, 255, 255], [0, 0, 255], [0, 0, 255]]
24
+
25
+ >>> np.random.seed(42)
26
+ >>> img = np.random.randint(0, 256, (4,4), dtype=np.uint8)
27
+ >>> thresholded = binary_threshold_image(img, 0.7)
28
+ >>> set(np.unique(thresholded).tolist()) <= {0, 255} # Should only contain 0 and 255
29
+ True
30
+
31
+ >>> rgb = np.random.randint(0, 256, (3,3,3), dtype=np.uint8)
32
+ >>> thresh_rgb = binary_threshold_image(rgb, 0.5)
33
+ >>> thresh_rgb.shape == rgb.shape
34
+ True
35
+ >>> set(np.unique(thresh_rgb).tolist()) <= {0, 255}
36
+ True
37
+
38
+ >>> ## Test invalid inputs
39
+ >>> binary_threshold_image("not an image", 0.5)
40
+ Traceback (most recent call last):
41
+ ...
42
+ AssertionError: Image must be a numpy array
43
+
44
+ >>> binary_threshold_image(image.astype(np.uint8), "0.5")
45
+ Traceback (most recent call last):
46
+ ...
47
+ AssertionError: threshold must be a number, got <class 'str'>
48
+
49
+ >>> binary_threshold_image(image.astype(np.uint8), 1.5)
50
+ Traceback (most recent call last):
51
+ ...
52
+ AssertionError: threshold must be between 0 and 1, got 1.5
53
+ """
54
+ # Check input data
55
+ check_image(image, ignore_dtype=ignore_dtype)
56
+ assert isinstance(threshold, float | int), f"threshold must be a number, got {type(threshold)}"
57
+ assert 0 <= threshold <= 1, f"threshold must be between 0 and 1, got {threshold}"
58
+
59
+ # Convert threshold from 0-1 range to 0-255 range
60
+ threshold_value: int = int(threshold * 255)
61
+
62
+ # Apply threshold
63
+ if len(image.shape) == 2:
64
+ # Grayscale image
65
+ binary: NDArray[Any] = cv2.threshold(image, threshold_value, 255, cv2.THRESH_BINARY)[1]
66
+ else:
67
+ # Color image - convert to grayscale first, then back to color
68
+ gray: NDArray[Any] = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
69
+ binary: NDArray[Any] = cv2.threshold(gray, threshold_value, 255, cv2.THRESH_BINARY)[1]
70
+ binary = cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)
71
+
72
+ return binary
73
+
@@ -1,59 +1,59 @@
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 blur_image(image: NDArray[Any], blur_strength: float, ignore_dtype: bool = False) -> NDArray[Any]:
11
- """ Apply Gaussian blur to an image.
12
-
13
- Args:
14
- image (NDArray[Any]): Image to blur
15
- blur_strength (float): Strength of the blur
16
- ignore_dtype (bool): Ignore the dtype check
17
- Returns:
18
- NDArray[Any]: Blurred image
19
-
20
- >>> ## Basic tests
21
- >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
22
- >>> blurred = blur_image(image.astype(np.uint8), 1.5)
23
- >>> blurred.shape == image.shape
24
- True
25
-
26
- >>> img = np.zeros((5,5), dtype=np.uint8)
27
- >>> img[2,2] = 255 # Single bright pixel
28
- >>> blurred = blur_image(img, 1.0)
29
- >>> bool(blurred[2,2] < 255) # Center should be blurred
30
- True
31
-
32
- >>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
33
- >>> blurred_rgb = blur_image(rgb, 1.0)
34
- >>> blurred_rgb.shape == (3,3,3)
35
- True
36
-
37
- >>> ## Test invalid inputs
38
- >>> blur_image("not an image", 1.5)
39
- Traceback (most recent call last):
40
- ...
41
- AssertionError: Image must be a numpy array
42
-
43
- >>> blur_image(image.astype(np.uint8), "1.5")
44
- Traceback (most recent call last):
45
- ...
46
- AssertionError: blur_strength must be a number, got <class 'str'>
47
- """
48
- # Check input data
49
- check_image(image, ignore_dtype=ignore_dtype)
50
- assert isinstance(blur_strength, float | int), f"blur_strength must be a number, got {type(blur_strength)}"
51
-
52
- # Apply Gaussian blur
53
- kernel_size: int = max(3, int(blur_strength * 2) + 1)
54
- if kernel_size % 2 == 0:
55
- kernel_size += 1
56
- blurred_image: NDArray[Any] = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
57
-
58
- return blurred_image
59
-
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 blur_image(image: NDArray[Any], blur_strength: float, ignore_dtype: bool = False) -> NDArray[Any]:
11
+ """ Apply Gaussian blur to an image.
12
+
13
+ Args:
14
+ image (NDArray[Any]): Image to blur
15
+ blur_strength (float): Strength of the blur
16
+ ignore_dtype (bool): Ignore the dtype check
17
+ Returns:
18
+ NDArray[Any]: Blurred image
19
+
20
+ >>> ## Basic tests
21
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
22
+ >>> blurred = blur_image(image.astype(np.uint8), 1.5)
23
+ >>> blurred.shape == image.shape
24
+ True
25
+
26
+ >>> img = np.zeros((5,5), dtype=np.uint8)
27
+ >>> img[2,2] = 255 # Single bright pixel
28
+ >>> blurred = blur_image(img, 1.0)
29
+ >>> bool(blurred[2,2] < 255) # Center should be blurred
30
+ True
31
+
32
+ >>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
33
+ >>> blurred_rgb = blur_image(rgb, 1.0)
34
+ >>> blurred_rgb.shape == (3,3,3)
35
+ True
36
+
37
+ >>> ## Test invalid inputs
38
+ >>> blur_image("not an image", 1.5)
39
+ Traceback (most recent call last):
40
+ ...
41
+ AssertionError: Image must be a numpy array
42
+
43
+ >>> blur_image(image.astype(np.uint8), "1.5")
44
+ Traceback (most recent call last):
45
+ ...
46
+ AssertionError: blur_strength must be a number, got <class 'str'>
47
+ """
48
+ # Check input data
49
+ check_image(image, ignore_dtype=ignore_dtype)
50
+ assert isinstance(blur_strength, float | int), f"blur_strength must be a number, got {type(blur_strength)}"
51
+
52
+ # Apply Gaussian blur
53
+ kernel_size: int = max(3, int(blur_strength * 2) + 1)
54
+ if kernel_size % 2 == 0:
55
+ kernel_size += 1
56
+ blurred_image: NDArray[Any] = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
57
+
58
+ return blurred_image
59
+
@@ -1,54 +1,54 @@
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 brightness_image(image: NDArray[Any], brightness_factor: float, ignore_dtype: bool = False) -> NDArray[Any]:
11
- """ Adjust the brightness of an image.
12
-
13
- Args:
14
- image (NDArray[Any]): Image to adjust brightness
15
- brightness_factor (float): Brightness adjustment factor
16
- ignore_dtype (bool): Ignore the dtype check
17
- Returns:
18
- NDArray[Any]: Image with adjusted brightness
19
-
20
- >>> ## Basic tests
21
- >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
22
- >>> brightened = brightness_image(image.astype(np.uint8), 1.5)
23
- >>> brightened.shape == image.shape
24
- True
25
-
26
- >>> img = np.full((3,3), 100, dtype=np.uint8)
27
- >>> bright = brightness_image(img, 2.0)
28
- >>> dark = brightness_image(img, 0.5)
29
- >>> bool(np.mean(bright) > np.mean(img) > np.mean(dark))
30
- True
31
-
32
- >>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
33
- >>> bright_rgb = brightness_image(rgb, 1.5)
34
- >>> bright_rgb.shape == (3,3,3)
35
- True
36
-
37
- >>> ## Test invalid inputs
38
- >>> brightness_image("not an image", 1.5)
39
- Traceback (most recent call last):
40
- ...
41
- AssertionError: Image must be a numpy array
42
-
43
- >>> brightness_image(image.astype(np.uint8), "1.5")
44
- Traceback (most recent call last):
45
- ...
46
- AssertionError: brightness_factor must be a number, got <class 'str'>
47
- """
48
- # Check input data
49
- check_image(image, ignore_dtype=ignore_dtype)
50
- assert isinstance(brightness_factor, float | int), f"brightness_factor must be a number, got {type(brightness_factor)}"
51
-
52
- # Apply brightness adjustment
53
- return cv2.convertScaleAbs(image, alpha=brightness_factor, beta=0)
54
-
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 brightness_image(image: NDArray[Any], brightness_factor: float, ignore_dtype: bool = False) -> NDArray[Any]:
11
+ """ Adjust the brightness of an image.
12
+
13
+ Args:
14
+ image (NDArray[Any]): Image to adjust brightness
15
+ brightness_factor (float): Brightness adjustment factor
16
+ ignore_dtype (bool): Ignore the dtype check
17
+ Returns:
18
+ NDArray[Any]: Image with adjusted brightness
19
+
20
+ >>> ## Basic tests
21
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
22
+ >>> brightened = brightness_image(image.astype(np.uint8), 1.5)
23
+ >>> brightened.shape == image.shape
24
+ True
25
+
26
+ >>> img = np.full((3,3), 100, dtype=np.uint8)
27
+ >>> bright = brightness_image(img, 2.0)
28
+ >>> dark = brightness_image(img, 0.5)
29
+ >>> bool(np.mean(bright) > np.mean(img) > np.mean(dark))
30
+ True
31
+
32
+ >>> rgb = np.full((3,3,3), 128, dtype=np.uint8)
33
+ >>> bright_rgb = brightness_image(rgb, 1.5)
34
+ >>> bright_rgb.shape == (3,3,3)
35
+ True
36
+
37
+ >>> ## Test invalid inputs
38
+ >>> brightness_image("not an image", 1.5)
39
+ Traceback (most recent call last):
40
+ ...
41
+ AssertionError: Image must be a numpy array
42
+
43
+ >>> brightness_image(image.astype(np.uint8), "1.5")
44
+ Traceback (most recent call last):
45
+ ...
46
+ AssertionError: brightness_factor must be a number, got <class 'str'>
47
+ """
48
+ # Check input data
49
+ check_image(image, ignore_dtype=ignore_dtype)
50
+ assert isinstance(brightness_factor, float | int), f"brightness_factor must be a number, got {type(brightness_factor)}"
51
+
52
+ # Apply brightness adjustment
53
+ return cv2.convertScaleAbs(image, alpha=brightness_factor, beta=0)
54
+
@@ -1,110 +1,110 @@
1
-
2
- # Imports
3
- from .common import Any, NDArray, check_image, cv2, np
4
-
5
-
6
- # Functions
7
- def canny_image(
8
- image: NDArray[Any],
9
- threshold1: float,
10
- threshold2: float,
11
- aperture_size: int = 3,
12
- sigma: float = 3,
13
- stop_at_nms: bool = False,
14
- ignore_dtype: bool = False,
15
- ) -> NDArray[Any]:
16
- """ Apply Canny edge detection to an image.
17
-
18
- Args:
19
- image (NDArray[Any]): Image to apply Canny edge detection
20
- threshold1 (float): First threshold for hysteresis (between 0 and 1)
21
- threshold2 (float): Second threshold for hysteresis (between 0 and 1)
22
- aperture_size (int): Aperture size for Sobel operator (3, 5, or 7)
23
- sigma (float): Standard deviation for Gaussian blur
24
- stop_at_nms (bool): Stop at non-maximum suppression step (don't apply thresholding)
25
- ignore_dtype (bool): Ignore the dtype check
26
- Returns:
27
- NDArray[Any]: Image with Canny edge detection applied
28
-
29
- >>> ## Basic tests
30
- >>> image = np.array([[100, 150, 200], [50, 125, 175], [25, 75, 225]])
31
- >>> edges = canny_image(image.astype(np.uint8), 0.1, 0.2)
32
- >>> edges.shape == image.shape[:2] # Canny returns single channel
33
- True
34
- >>> set(np.unique(edges).tolist()) <= {0, 255} # Only contains 0 and 255
35
- True
36
-
37
- >>> rgb = np.random.randint(0, 256, (3,3,3), dtype=np.uint8)
38
- >>> edges_rgb = canny_image(rgb, 0.1, 0.2)
39
- >>> edges_rgb.shape == rgb.shape[:2] # Canny returns single channel
40
- True
41
-
42
- >>> ## Test invalid inputs
43
- >>> canny_image("not an image", 0.1, 0.2)
44
- Traceback (most recent call last):
45
- ...
46
- AssertionError: Image must be a numpy array
47
-
48
- >>> canny_image(image.astype(np.uint8), 1.5, 0.2)
49
- Traceback (most recent call last):
50
- ...
51
- AssertionError: threshold1 must be between 0 and 1, got 1.5
52
- """
53
- # Check input data
54
- check_image(image, ignore_dtype=ignore_dtype)
55
- assert 0 <= threshold1 <= 1, f"threshold1 must be between 0 and 1, got {threshold1}"
56
- assert 0 <= threshold2 <= 1, f"threshold2 must be between 0 and 1, got {threshold2}"
57
- assert aperture_size in (3, 5, 7), f"aperture_size must be 3, 5 or 7, got {aperture_size}"
58
-
59
- # Convert to grayscale if needed
60
- if len(image.shape) > 2:
61
- image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
62
-
63
- # Convert thresholds to 0-255 range
64
- t1: int = int(threshold1 * 255)
65
- t2: int = int(threshold2 * 255)
66
-
67
- if not stop_at_nms:
68
- # Apply full Canny edge detection
69
- return cv2.Canny(image, t1, t2, apertureSize=aperture_size)
70
- else:
71
- # Manual implementation up to non-maximum suppression
72
- # 1. Apply Gaussian blur to reduce noise
73
- blurred: NDArray[Any] = cv2.GaussianBlur(image, (5, 5), sigma)
74
-
75
- # 2. Calculate gradients using Sobel
76
- # Use constant value 6 for 64-bit float depth (CV_64F)
77
- gx: NDArray[Any] = cv2.Sobel(blurred, 6, 1, 0, ksize=aperture_size)
78
- gy: NDArray[Any] = cv2.Sobel(blurred, 6, 0, 1, ksize=aperture_size)
79
-
80
- # Calculate gradient magnitude and direction
81
- magnitude: NDArray[Any] = np.sqrt(gx**2 + gy**2)
82
- direction: NDArray[Any] = np.arctan2(gy, gx) * 180 / np.pi
83
-
84
- # 3. Non-maximum suppression
85
- nms: NDArray[Any] = np.zeros_like(magnitude, dtype=np.uint8)
86
- height, width = magnitude.shape
87
-
88
- for i in range(1, height - 1):
89
- for j in range(1, width - 1):
90
- # Get gradient direction
91
- angle: float = direction[i, j]
92
- if angle < 0:
93
- angle += 180
94
-
95
- # Round to 0, 45, 90, or 135 degrees
96
- if (0 <= angle < 22.5) or (157.5 <= angle <= 180):
97
- neighbors = [magnitude[i, j-1], magnitude[i, j+1]]
98
- elif 22.5 <= angle < 67.5:
99
- neighbors = [magnitude[i-1, j-1], magnitude[i+1, j+1]]
100
- elif 67.5 <= angle < 112.5:
101
- neighbors = [magnitude[i-1, j], magnitude[i+1, j]]
102
- else: # 112.5 <= angle < 157.5
103
- neighbors = [magnitude[i-1, j+1], magnitude[i+1, j-1]]
104
-
105
- # Check if current pixel is local maximum
106
- if magnitude[i, j] >= max(neighbors):
107
- nms[i, j] = np.uint8(min(magnitude[i, j], 255))
108
-
109
- return nms
110
-
1
+
2
+ # Imports
3
+ from .common import Any, NDArray, check_image, cv2, np
4
+
5
+
6
+ # Functions
7
+ def canny_image(
8
+ image: NDArray[Any],
9
+ threshold1: float,
10
+ threshold2: float,
11
+ aperture_size: int = 3,
12
+ sigma: float = 3,
13
+ stop_at_nms: bool = False,
14
+ ignore_dtype: bool = False,
15
+ ) -> NDArray[Any]:
16
+ """ Apply Canny edge detection to an image.
17
+
18
+ Args:
19
+ image (NDArray[Any]): Image to apply Canny edge detection
20
+ threshold1 (float): First threshold for hysteresis (between 0 and 1)
21
+ threshold2 (float): Second threshold for hysteresis (between 0 and 1)
22
+ aperture_size (int): Aperture size for Sobel operator (3, 5, or 7)
23
+ sigma (float): Standard deviation for Gaussian blur
24
+ stop_at_nms (bool): Stop at non-maximum suppression step (don't apply thresholding)
25
+ ignore_dtype (bool): Ignore the dtype check
26
+ Returns:
27
+ NDArray[Any]: Image with Canny edge detection applied
28
+
29
+ >>> ## Basic tests
30
+ >>> image = np.array([[100, 150, 200], [50, 125, 175], [25, 75, 225]])
31
+ >>> edges = canny_image(image.astype(np.uint8), 0.1, 0.2)
32
+ >>> edges.shape == image.shape[:2] # Canny returns single channel
33
+ True
34
+ >>> set(np.unique(edges).tolist()) <= {0, 255} # Only contains 0 and 255
35
+ True
36
+
37
+ >>> rgb = np.random.randint(0, 256, (3,3,3), dtype=np.uint8)
38
+ >>> edges_rgb = canny_image(rgb, 0.1, 0.2)
39
+ >>> edges_rgb.shape == rgb.shape[:2] # Canny returns single channel
40
+ True
41
+
42
+ >>> ## Test invalid inputs
43
+ >>> canny_image("not an image", 0.1, 0.2)
44
+ Traceback (most recent call last):
45
+ ...
46
+ AssertionError: Image must be a numpy array
47
+
48
+ >>> canny_image(image.astype(np.uint8), 1.5, 0.2)
49
+ Traceback (most recent call last):
50
+ ...
51
+ AssertionError: threshold1 must be between 0 and 1, got 1.5
52
+ """
53
+ # Check input data
54
+ check_image(image, ignore_dtype=ignore_dtype)
55
+ assert 0 <= threshold1 <= 1, f"threshold1 must be between 0 and 1, got {threshold1}"
56
+ assert 0 <= threshold2 <= 1, f"threshold2 must be between 0 and 1, got {threshold2}"
57
+ assert aperture_size in (3, 5, 7), f"aperture_size must be 3, 5 or 7, got {aperture_size}"
58
+
59
+ # Convert to grayscale if needed
60
+ if len(image.shape) > 2:
61
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
62
+
63
+ # Convert thresholds to 0-255 range
64
+ t1: int = int(threshold1 * 255)
65
+ t2: int = int(threshold2 * 255)
66
+
67
+ if not stop_at_nms:
68
+ # Apply full Canny edge detection
69
+ return cv2.Canny(image, t1, t2, apertureSize=aperture_size)
70
+ else:
71
+ # Manual implementation up to non-maximum suppression
72
+ # 1. Apply Gaussian blur to reduce noise
73
+ blurred: NDArray[Any] = cv2.GaussianBlur(image, (5, 5), sigma)
74
+
75
+ # 2. Calculate gradients using Sobel
76
+ # Use constant value 6 for 64-bit float depth (CV_64F)
77
+ gx: NDArray[Any] = cv2.Sobel(blurred, 6, 1, 0, ksize=aperture_size)
78
+ gy: NDArray[Any] = cv2.Sobel(blurred, 6, 0, 1, ksize=aperture_size)
79
+
80
+ # Calculate gradient magnitude and direction
81
+ magnitude: NDArray[Any] = np.sqrt(gx**2 + gy**2)
82
+ direction: NDArray[Any] = np.arctan2(gy, gx) * 180 / np.pi
83
+
84
+ # 3. Non-maximum suppression
85
+ nms: NDArray[Any] = np.zeros_like(magnitude, dtype=np.uint8)
86
+ height, width = magnitude.shape
87
+
88
+ for i in range(1, height - 1):
89
+ for j in range(1, width - 1):
90
+ # Get gradient direction
91
+ angle: float = direction[i, j]
92
+ if angle < 0:
93
+ angle += 180
94
+
95
+ # Round to 0, 45, 90, or 135 degrees
96
+ if (0 <= angle < 22.5) or (157.5 <= angle <= 180):
97
+ neighbors = [magnitude[i, j-1], magnitude[i, j+1]]
98
+ elif 22.5 <= angle < 67.5:
99
+ neighbors = [magnitude[i-1, j-1], magnitude[i+1, j+1]]
100
+ elif 67.5 <= angle < 112.5:
101
+ neighbors = [magnitude[i-1, j], magnitude[i+1, j]]
102
+ else: # 112.5 <= angle < 157.5
103
+ neighbors = [magnitude[i-1, j+1], magnitude[i+1, j-1]]
104
+
105
+ # Check if current pixel is local maximum
106
+ if magnitude[i, j] >= max(neighbors):
107
+ nms[i, j] = np.uint8(min(magnitude[i, j], 255))
108
+
109
+ return nms
110
+