sdimg 0.2.0__tar.gz

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 (41) hide show
  1. sdimg-0.2.0/LICENSE +21 -0
  2. sdimg-0.2.0/PKG-INFO +90 -0
  3. sdimg-0.2.0/README.md +69 -0
  4. sdimg-0.2.0/pyproject.toml +37 -0
  5. sdimg-0.2.0/setup.cfg +4 -0
  6. sdimg-0.2.0/src/sdimg/__init__.py +0 -0
  7. sdimg-0.2.0/src/sdimg/_core/__init__.py +9 -0
  8. sdimg-0.2.0/src/sdimg/_core/errors.py +6 -0
  9. sdimg-0.2.0/src/sdimg/_core/types.py +3 -0
  10. sdimg-0.2.0/src/sdimg/_core/validate.py +46 -0
  11. sdimg-0.2.0/src/sdimg/fusion/__init__.py +2 -0
  12. sdimg-0.2.0/src/sdimg/fusion/grabcut.py +158 -0
  13. sdimg-0.2.0/src/sdimg/fusion/otsu.py +34 -0
  14. sdimg-0.2.0/src/sdimg/image/__init__.py +6 -0
  15. sdimg-0.2.0/src/sdimg/image/bc.py +28 -0
  16. sdimg-0.2.0/src/sdimg/image/blur.py +31 -0
  17. sdimg-0.2.0/src/sdimg/image/denoise.py +34 -0
  18. sdimg-0.2.0/src/sdimg/image/helper.py +58 -0
  19. sdimg-0.2.0/src/sdimg/image/norm.py +77 -0
  20. sdimg-0.2.0/src/sdimg/image/sharpen.py +27 -0
  21. sdimg-0.2.0/src/sdimg/mask/__init__.py +17 -0
  22. sdimg-0.2.0/src/sdimg/mask/component.py +26 -0
  23. sdimg-0.2.0/src/sdimg/mask/distance.py +38 -0
  24. sdimg-0.2.0/src/sdimg/mask/edge.py +21 -0
  25. sdimg-0.2.0/src/sdimg/mask/helper.py +120 -0
  26. sdimg-0.2.0/src/sdimg/mask/hole.py +21 -0
  27. sdimg-0.2.0/src/sdimg/mask/hull.py +51 -0
  28. sdimg-0.2.0/src/sdimg/mask/morphology.py +47 -0
  29. sdimg-0.2.0/src/sdimg/mask/pad.py +14 -0
  30. sdimg-0.2.0/src/sdimg/py.typed +0 -0
  31. sdimg-0.2.0/src/sdimg/spatial/__init__.py +5 -0
  32. sdimg-0.2.0/src/sdimg/spatial/crop.py +12 -0
  33. sdimg-0.2.0/src/sdimg/spatial/pad.py +26 -0
  34. sdimg-0.2.0/src/sdimg/spatial/patch.py +167 -0
  35. sdimg-0.2.0/src/sdimg/spatial/resize.py +89 -0
  36. sdimg-0.2.0/src/sdimg/spatial/transform.py +33 -0
  37. sdimg-0.2.0/src/sdimg.egg-info/PKG-INFO +90 -0
  38. sdimg-0.2.0/src/sdimg.egg-info/SOURCES.txt +39 -0
  39. sdimg-0.2.0/src/sdimg.egg-info/dependency_links.txt +1 -0
  40. sdimg-0.2.0/src/sdimg.egg-info/requires.txt +3 -0
  41. sdimg-0.2.0/src/sdimg.egg-info/top_level.txt +1 -0
sdimg-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sdimg-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: sdimg
3
+ Version: 0.2.0
4
+ Summary: Small, function-based image and mask processing library built on numpy
5
+ Author: kn
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kn/sdimg
8
+ Project-URL: Repository, https://github.com/kn/sdimg
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy
18
+ Requires-Dist: opencv-python-headless
19
+ Requires-Dist: concave-hull
20
+ Dynamic: license-file
21
+
22
+ # sdimg
23
+
24
+ Small, function-based image and mask processing library built on `numpy.ndarray`.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install sdimg
30
+ ```
31
+
32
+ ## Modules
33
+
34
+ - `sdimg.image`: normalize, blur, denoise, sharpen, color helpers
35
+ - `sdimg.mask`: binary mask cleanup, hull/edge/distance, bbox/ROI helpers
36
+ - `sdimg.spatial`: resize, crop, rotate/flip, pad, split/merge patches
37
+ - `sdimg.fusion`: `otsu_threshold` (OpenCV Otsu), `grabcut`
38
+
39
+ ## Core Contracts
40
+
41
+ - Input arrays must be `numpy.ndarray`
42
+ - Images: shape `(H, W)` or `(H, W, C)` with `C in 1..4`
43
+ - Masks: shape `(H, W)`, binary values (`bool`, `{0,1}`, `{0,255}`)
44
+ - Output images are `np.uint8`
45
+ - Output masks are binary `np.uint8` in `{0, 1}`
46
+ - BBox format: `(wmin, hmin, wmax, hmax)`
47
+ - Empty-mask returns `None` for:
48
+ - `to_roi_box`
49
+ - `get_box_from_mask`
50
+ - `get_box_from_coords`
51
+ - `get_centroid`
52
+
53
+ ## Error Policy
54
+
55
+ - `TypeError`: wrong input type (non-`numpy.ndarray`)
56
+ - `ValueError`: invalid shape, invalid params, invalid mask values, invalid bbox
57
+ - `RuntimeError`: wrapped lower-level failures (`cv2`, internal processing)
58
+
59
+ ## Internal Structure
60
+
61
+ - `sdimg/_core/validate.py`: shared validators (`ensure_src`, `ensure_image`, `ensure_mask`, `ensure_bbox`)
62
+ - `sdimg/_core/types.py`: shared type aliases
63
+ - `sdimg/_core/errors.py`: shared error helpers
64
+
65
+ ## Quick Example
66
+
67
+ ```python
68
+ import numpy as np
69
+ from sdimg.image import hist_norm, gaussian_blur
70
+ from sdimg.mask import morphology, to_roi_box
71
+ from sdimg.fusion import grabcut
72
+
73
+ image = np.random.randint(0, 256, (128, 128, 3), dtype=np.uint8)
74
+ mask = np.zeros((128, 128), dtype=np.uint8)
75
+ mask[32:96, 40:88] = 1
76
+
77
+ image = hist_norm(image)
78
+ image = gaussian_blur(image, (5, 5), 1.2)
79
+ mask = morphology(mask, "open", (3, 3), 1)
80
+
81
+ roi_box = to_roi_box(mask)
82
+ if roi_box is not None:
83
+ refined = grabcut(image=image, roi=roi_box["roi"], box=roi_box["box"])
84
+ ```
85
+
86
+ ## Local Test
87
+
88
+ ```bash
89
+ PYTHONPATH=. pytest -q
90
+ ```
sdimg-0.2.0/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # sdimg
2
+
3
+ Small, function-based image and mask processing library built on `numpy.ndarray`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install sdimg
9
+ ```
10
+
11
+ ## Modules
12
+
13
+ - `sdimg.image`: normalize, blur, denoise, sharpen, color helpers
14
+ - `sdimg.mask`: binary mask cleanup, hull/edge/distance, bbox/ROI helpers
15
+ - `sdimg.spatial`: resize, crop, rotate/flip, pad, split/merge patches
16
+ - `sdimg.fusion`: `otsu_threshold` (OpenCV Otsu), `grabcut`
17
+
18
+ ## Core Contracts
19
+
20
+ - Input arrays must be `numpy.ndarray`
21
+ - Images: shape `(H, W)` or `(H, W, C)` with `C in 1..4`
22
+ - Masks: shape `(H, W)`, binary values (`bool`, `{0,1}`, `{0,255}`)
23
+ - Output images are `np.uint8`
24
+ - Output masks are binary `np.uint8` in `{0, 1}`
25
+ - BBox format: `(wmin, hmin, wmax, hmax)`
26
+ - Empty-mask returns `None` for:
27
+ - `to_roi_box`
28
+ - `get_box_from_mask`
29
+ - `get_box_from_coords`
30
+ - `get_centroid`
31
+
32
+ ## Error Policy
33
+
34
+ - `TypeError`: wrong input type (non-`numpy.ndarray`)
35
+ - `ValueError`: invalid shape, invalid params, invalid mask values, invalid bbox
36
+ - `RuntimeError`: wrapped lower-level failures (`cv2`, internal processing)
37
+
38
+ ## Internal Structure
39
+
40
+ - `sdimg/_core/validate.py`: shared validators (`ensure_src`, `ensure_image`, `ensure_mask`, `ensure_bbox`)
41
+ - `sdimg/_core/types.py`: shared type aliases
42
+ - `sdimg/_core/errors.py`: shared error helpers
43
+
44
+ ## Quick Example
45
+
46
+ ```python
47
+ import numpy as np
48
+ from sdimg.image import hist_norm, gaussian_blur
49
+ from sdimg.mask import morphology, to_roi_box
50
+ from sdimg.fusion import grabcut
51
+
52
+ image = np.random.randint(0, 256, (128, 128, 3), dtype=np.uint8)
53
+ mask = np.zeros((128, 128), dtype=np.uint8)
54
+ mask[32:96, 40:88] = 1
55
+
56
+ image = hist_norm(image)
57
+ image = gaussian_blur(image, (5, 5), 1.2)
58
+ mask = morphology(mask, "open", (3, 3), 1)
59
+
60
+ roi_box = to_roi_box(mask)
61
+ if roi_box is not None:
62
+ refined = grabcut(image=image, roi=roi_box["roi"], box=roi_box["box"])
63
+ ```
64
+
65
+ ## Local Test
66
+
67
+ ```bash
68
+ PYTHONPATH=. pytest -q
69
+ ```
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sdimg"
7
+ version = "0.2.0"
8
+ description = "Small, function-based image and mask processing library built on numpy"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.12"
12
+ authors = [
13
+ { name = "kn" },
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Scientific/Engineering :: Image Processing",
21
+ ]
22
+ dependencies = [
23
+ "numpy",
24
+ "opencv-python-headless",
25
+ "concave-hull",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/kn/sdimg"
30
+ Repository = "https://github.com/kn/sdimg"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+ include = ["sdimg*"]
35
+
36
+ [tool.pytest.ini_options]
37
+ markers = []
sdimg-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,9 @@
1
+ from .errors import type_error, value_error
2
+ from .types import BBox
3
+ from .validate import (
4
+ ensure_bbox,
5
+ ensure_image,
6
+ ensure_mask,
7
+ ensure_ndarray,
8
+ ensure_src,
9
+ )
@@ -0,0 +1,6 @@
1
+ def type_error(name: str, expected: str) -> TypeError:
2
+ return TypeError(f"{name} must be {expected}.")
3
+
4
+
5
+ def value_error(message: str) -> ValueError:
6
+ return ValueError(message)
@@ -0,0 +1,3 @@
1
+ from typing import TypeAlias
2
+
3
+ BBox: TypeAlias = tuple[int, int, int, int]
@@ -0,0 +1,46 @@
1
+ import numpy as np
2
+
3
+ from .errors import type_error, value_error
4
+ from .types import BBox
5
+
6
+
7
+ def ensure_ndarray(value: object, name: str = "value") -> np.ndarray:
8
+ if not isinstance(value, np.ndarray):
9
+ raise type_error(name=name, expected="a numpy.ndarray")
10
+ return value
11
+
12
+
13
+ def ensure_src(src: object, name: str = "src") -> np.ndarray:
14
+ arr = ensure_ndarray(src, name=name)
15
+ if arr.ndim not in {2, 3}:
16
+ raise value_error(f"{name} must have shape (H, W) or (H, W, C).")
17
+ return arr
18
+
19
+
20
+ def ensure_image(image: object, name: str = "image") -> np.ndarray:
21
+ arr = ensure_src(image, name=name)
22
+ if arr.ndim == 2:
23
+ return arr
24
+ channels = arr.shape[2]
25
+ if channels not in {1, 2, 3, 4}:
26
+ raise value_error(f"{name} must have shape (H, W) or (H, W, C) with C in 1..4.")
27
+ return arr
28
+
29
+
30
+ def ensure_mask(mask: object, name: str = "mask") -> np.ndarray:
31
+ arr = ensure_ndarray(mask, name=name)
32
+ if arr.ndim != 2:
33
+ raise value_error(f"{name} must have shape (H, W).")
34
+ return arr
35
+
36
+
37
+ def ensure_bbox(
38
+ bbox: BBox,
39
+ shape: tuple[int, int],
40
+ name: str = "bbox",
41
+ ) -> BBox:
42
+ wmin, hmin, wmax, hmax = bbox
43
+ h, w = shape
44
+ if wmin < 0 or hmin < 0 or wmax > w or hmax > h or wmin >= wmax or hmin >= hmax:
45
+ raise value_error(f"{name} is out of bounds or invalid.")
46
+ return bbox
@@ -0,0 +1,2 @@
1
+ from .grabcut import grabcut
2
+ from .otsu import otsu_threshold
@@ -0,0 +1,158 @@
1
+ import cv2
2
+ import numpy as np
3
+
4
+ from .._core.validate import ensure_image
5
+ from ..image.helper import to_gray
6
+ from ..spatial.crop import crop
7
+ from ..mask.helper import get_roi_size, to_mask
8
+
9
+
10
+ def _get_k(shape: tuple[int, int]) -> int:
11
+ h, w = shape
12
+ k = int(round(min(h, w) / 5.0))
13
+ k = max(3, k)
14
+ return k if k % 2 == 1 else k + 1
15
+
16
+
17
+ def _blur_mask(mask: np.ndarray) -> np.ndarray:
18
+ k = _get_k(shape=mask.shape[:2])
19
+ blur = cv2.GaussianBlur(
20
+ src=mask.astype(np.float32),
21
+ ksize=(k, k),
22
+ sigmaX=0.0,
23
+ sigmaY=0.0,
24
+ )
25
+ return np.rint(np.clip(blur * 255.0, 0.0, 255.0)).astype(np.uint8)
26
+
27
+
28
+ def _edge(gray: np.ndarray) -> np.ndarray:
29
+ median = cv2.medianBlur(src=gray, ksize=3)
30
+ grad = cv2.morphologyEx(
31
+ src=median,
32
+ op=cv2.MORPH_GRADIENT,
33
+ kernel=np.ones((3, 3), dtype=np.uint8),
34
+ )
35
+ return cv2.normalize(
36
+ src=grad,
37
+ dst=None,
38
+ alpha=0,
39
+ beta=255,
40
+ norm_type=cv2.NORM_MINMAX,
41
+ dtype=cv2.CV_8U,
42
+ )
43
+
44
+
45
+ def _build_img(image: np.ndarray, roi: np.ndarray) -> np.ndarray:
46
+ gray = to_gray(image)
47
+ edge_map = _edge(gray=gray)
48
+ blur = _blur_mask(mask=roi)
49
+ return np.stack([gray, edge_map, blur], axis=2)
50
+
51
+
52
+ def _build_mask(roi: np.ndarray) -> np.ndarray:
53
+ din = cv2.distanceTransform(roi, cv2.DIST_L2, 3, dstType=cv2.CV_32F)
54
+ dout = cv2.distanceTransform(1 - roi, cv2.DIST_L2, 3, dstType=cv2.CV_32F)
55
+
56
+ max_in = float(din.max())
57
+ max_out = float(dout.max())
58
+
59
+ th = min(max_in, max_out) / 5.0
60
+
61
+ if th <= 0:
62
+ mask = np.full(roi.shape, cv2.GC_PR_BGD, dtype=np.uint8)
63
+ mask[roi == 1] = cv2.GC_PR_FGD
64
+ return mask
65
+
66
+ mask = np.full(roi.shape, cv2.GC_BGD, dtype=np.uint8)
67
+ mask[(roi == 0) & (dout < th)] = cv2.GC_PR_BGD
68
+ mask[(roi == 1) & (din < th)] = cv2.GC_PR_FGD
69
+ mask[din >= th] = cv2.GC_FGD
70
+
71
+ return mask
72
+
73
+
74
+ def grabcut(
75
+ image: np.ndarray,
76
+ roi: np.ndarray,
77
+ box: tuple[int, int, int, int],
78
+ iter_count: int = 5,
79
+ margin: int = 20,
80
+ tol: float = 0.5,
81
+ ) -> np.ndarray:
82
+ """Refine a binary ROI mask using GrabCut.
83
+
84
+ Args:
85
+ image: Source image (H, W, C).
86
+ roi: Binary mask cropped to the bounding box region.
87
+ box: Bounding box as (wmin, hmin, wmax, hmax).
88
+ iter_count: Number of GrabCut iterations.
89
+ margin: Pixel margin added around the ROI for context.
90
+ tol: Maximum allowed relative change in mask area.
91
+ If abs(new - old) / old > tol, the original roi is returned.
92
+ """
93
+ if iter_count <= 0:
94
+ raise ValueError("iter_count must be greater than 0.")
95
+ if margin <= 0:
96
+ raise ValueError("margin must be greater than 0.")
97
+ if tol < 0:
98
+ raise ValueError("tol must be greater than or equal to 0.")
99
+
100
+ image = ensure_image(image, name="image")
101
+ roi = to_mask(roi)
102
+
103
+ orig_area = float(get_roi_size(roi))
104
+ if orig_area == 0:
105
+ return roi
106
+
107
+ image = crop(src=image, bbox=box)
108
+
109
+ if image.shape[:2] != roi.shape[:2]:
110
+ raise ValueError(
111
+ f"Cropped image shape {image.shape[:2]} does not match "
112
+ f"roi shape {roi.shape[:2]}."
113
+ )
114
+
115
+ try:
116
+ image = cv2.copyMakeBorder(
117
+ src=image,
118
+ top=margin,
119
+ bottom=margin,
120
+ left=margin,
121
+ right=margin,
122
+ borderType=cv2.BORDER_REFLECT,
123
+ )
124
+ roi = cv2.copyMakeBorder(
125
+ src=roi,
126
+ top=margin,
127
+ bottom=margin,
128
+ left=margin,
129
+ right=margin,
130
+ borderType=cv2.BORDER_CONSTANT,
131
+ value=0,
132
+ )
133
+
134
+ feat = _build_img(image=image, roi=roi)
135
+ gc_mask = _build_mask(roi=roi)
136
+
137
+ bgd = np.zeros((1, 65), dtype=np.float64)
138
+ fgd = np.zeros((1, 65), dtype=np.float64)
139
+ cv2.grabCut(
140
+ img=feat,
141
+ mask=gc_mask,
142
+ rect=None,
143
+ bgdModel=bgd,
144
+ fgdModel=fgd,
145
+ iterCount=iter_count,
146
+ mode=cv2.GC_INIT_WITH_MASK,
147
+ )
148
+ except Exception as exc:
149
+ raise RuntimeError(f"grabcut failed: {exc}") from exc
150
+
151
+ out = np.isin(gc_mask, (cv2.GC_FGD, cv2.GC_PR_FGD)).astype(np.uint8)
152
+ out_final = out[margin:-margin, margin:-margin]
153
+
154
+ new_area = float(get_roi_size(out_final))
155
+ if abs(new_area - orig_area) / orig_area > tol:
156
+ return roi[margin:-margin, margin:-margin]
157
+
158
+ return out_final
@@ -0,0 +1,34 @@
1
+ import cv2
2
+ import numpy as np
3
+
4
+ from .._core.validate import ensure_image
5
+ from ..image.helper import to_gray
6
+
7
+
8
+ def otsu_threshold(
9
+ image: np.ndarray,
10
+ scale: float = 1.0,
11
+ ) -> np.ndarray:
12
+ """Binarize an image using Otsu's threshold.
13
+
14
+ Args:
15
+ scale: Multiplier for the computed threshold. scale=0 makes all
16
+ nonzero pixels foreground; scale=1 uses the raw Otsu threshold.
17
+ """
18
+ if scale < 0:
19
+ raise ValueError("scale must be greater than or equal to 0.")
20
+
21
+ image = ensure_image(image, name="image")
22
+ gray = to_gray(image)
23
+ try:
24
+ threshold, _ = cv2.threshold(
25
+ gray,
26
+ 0,
27
+ 255,
28
+ cv2.THRESH_BINARY + cv2.THRESH_OTSU,
29
+ )
30
+ except Exception as exc:
31
+ raise RuntimeError(f"otsu_threshold failed: {exc}") from exc
32
+
33
+ adjusted_threshold = float(threshold) * scale
34
+ return (gray > adjusted_threshold).astype(np.uint8)
@@ -0,0 +1,6 @@
1
+ from .bc import adjust_brightness_contrast
2
+ from .blur import gaussian_blur, median_blur
3
+ from .denoise import denoise
4
+ from .helper import is_image, to_gray, to_rgb, to_uint8
5
+ from .norm import clahe_norm, hist_norm, minmax_norm, zscore_norm
6
+ from .sharpen import sharpen
@@ -0,0 +1,28 @@
1
+ import numpy as np
2
+
3
+ from .._core.validate import ensure_image
4
+ from .helper import to_uint8
5
+
6
+
7
+ def adjust_brightness_contrast(
8
+ image: np.ndarray,
9
+ brightness: float = 0.0,
10
+ contrast: float = 0.0,
11
+ ) -> np.ndarray:
12
+ image = ensure_image(image, name="image")
13
+
14
+ brightness_val = np.clip(brightness, -1.0, 1.0)
15
+ contrast_val = np.clip(contrast, -1.0, 1.0)
16
+
17
+ adjusted = image.astype(np.float32)
18
+
19
+ if brightness_val != 0.0:
20
+ adjusted += brightness_val * 255.0
21
+
22
+ factor = 1.0 + contrast_val
23
+ if factor != 1.0:
24
+ adjusted -= 128.0
25
+ adjusted *= factor
26
+ adjusted += 128.0
27
+
28
+ return to_uint8(adjusted)
@@ -0,0 +1,31 @@
1
+ import cv2
2
+ import numpy as np
3
+
4
+ from .._core.validate import ensure_image
5
+
6
+
7
+ def gaussian_blur(
8
+ image: np.ndarray,
9
+ ksize: tuple[int, int],
10
+ sigmaX: float,
11
+ sigmaY: float = 0.0,
12
+ borderType: int = cv2.BORDER_DEFAULT,
13
+ ) -> np.ndarray:
14
+ image = ensure_image(image, name="image")
15
+
16
+ return cv2.GaussianBlur(
17
+ image,
18
+ ksize,
19
+ sigmaX,
20
+ sigmaY=sigmaY,
21
+ borderType=borderType,
22
+ )
23
+
24
+
25
+ def median_blur(
26
+ image: np.ndarray,
27
+ ksize: int,
28
+ ) -> np.ndarray:
29
+ image = ensure_image(image, name="image")
30
+
31
+ return cv2.medianBlur(image, ksize)
@@ -0,0 +1,34 @@
1
+ import cv2
2
+ import numpy as np
3
+
4
+ from .._core.validate import ensure_image
5
+ from .helper import to_gray
6
+
7
+
8
+ def denoise(
9
+ image: np.ndarray,
10
+ h: float = 3.0,
11
+ hColor: float = 3.0,
12
+ templateWindowSize: int = 7,
13
+ searchWindowSize: int = 21,
14
+ ) -> np.ndarray:
15
+ image = ensure_image(image, name="image")
16
+
17
+ if image.ndim == 3 and image.shape[2] == 3:
18
+ return cv2.fastNlMeansDenoisingColored(
19
+ image,
20
+ h=h,
21
+ hColor=hColor,
22
+ templateWindowSize=templateWindowSize,
23
+ searchWindowSize=searchWindowSize,
24
+ )
25
+
26
+ if image.ndim == 3:
27
+ image = to_gray(image)
28
+
29
+ return cv2.fastNlMeansDenoising(
30
+ image,
31
+ h=h,
32
+ templateWindowSize=templateWindowSize,
33
+ searchWindowSize=searchWindowSize,
34
+ )
@@ -0,0 +1,58 @@
1
+ import numpy as np
2
+
3
+ from .._core.validate import ensure_image, ensure_ndarray
4
+
5
+
6
+ def is_image(image: object) -> bool:
7
+ try:
8
+ ensure_image(image, name="image")
9
+ except (TypeError, ValueError):
10
+ return False
11
+ return True
12
+
13
+
14
+ def to_rgb(image: np.ndarray) -> np.ndarray:
15
+ image = ensure_image(image, name="image")
16
+
17
+ if image.ndim == 2:
18
+ rgb = np.repeat(image[..., None], 3, axis=2)
19
+ return to_uint8(rgb)
20
+
21
+ channels = image.shape[2]
22
+ if channels <= 2:
23
+ rgb = np.repeat(image[..., 0:1], 3, axis=2)
24
+ elif channels == 3:
25
+ rgb = image
26
+ else:
27
+ rgb = image[..., :3]
28
+ return to_uint8(rgb)
29
+
30
+
31
+ def to_gray(image: np.ndarray) -> np.ndarray:
32
+ image = ensure_image(image, name="image")
33
+
34
+ if image.ndim == 2:
35
+ return to_uint8(image)
36
+
37
+ channels = image.shape[2]
38
+ if channels <= 2:
39
+ gray = image[..., 0]
40
+ else:
41
+ gray = image[..., 0] * np.float32(0.299)
42
+ gray += image[..., 1] * np.float32(0.587)
43
+ gray += image[..., 2] * np.float32(0.114)
44
+
45
+ return to_uint8(gray)
46
+
47
+
48
+ def to_uint8(image: np.ndarray) -> np.ndarray:
49
+ image = ensure_ndarray(image, name="image")
50
+
51
+ if image.dtype == np.uint8:
52
+ return image
53
+
54
+ if np.issubdtype(image.dtype, np.floating):
55
+ clipped = np.clip(image, 0.0, 255.0)
56
+ return np.rint(clipped).astype(np.uint8)
57
+
58
+ return np.clip(image, 0, 255).astype(np.uint8)