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.
- sdimg-0.2.0/LICENSE +21 -0
- sdimg-0.2.0/PKG-INFO +90 -0
- sdimg-0.2.0/README.md +69 -0
- sdimg-0.2.0/pyproject.toml +37 -0
- sdimg-0.2.0/setup.cfg +4 -0
- sdimg-0.2.0/src/sdimg/__init__.py +0 -0
- sdimg-0.2.0/src/sdimg/_core/__init__.py +9 -0
- sdimg-0.2.0/src/sdimg/_core/errors.py +6 -0
- sdimg-0.2.0/src/sdimg/_core/types.py +3 -0
- sdimg-0.2.0/src/sdimg/_core/validate.py +46 -0
- sdimg-0.2.0/src/sdimg/fusion/__init__.py +2 -0
- sdimg-0.2.0/src/sdimg/fusion/grabcut.py +158 -0
- sdimg-0.2.0/src/sdimg/fusion/otsu.py +34 -0
- sdimg-0.2.0/src/sdimg/image/__init__.py +6 -0
- sdimg-0.2.0/src/sdimg/image/bc.py +28 -0
- sdimg-0.2.0/src/sdimg/image/blur.py +31 -0
- sdimg-0.2.0/src/sdimg/image/denoise.py +34 -0
- sdimg-0.2.0/src/sdimg/image/helper.py +58 -0
- sdimg-0.2.0/src/sdimg/image/norm.py +77 -0
- sdimg-0.2.0/src/sdimg/image/sharpen.py +27 -0
- sdimg-0.2.0/src/sdimg/mask/__init__.py +17 -0
- sdimg-0.2.0/src/sdimg/mask/component.py +26 -0
- sdimg-0.2.0/src/sdimg/mask/distance.py +38 -0
- sdimg-0.2.0/src/sdimg/mask/edge.py +21 -0
- sdimg-0.2.0/src/sdimg/mask/helper.py +120 -0
- sdimg-0.2.0/src/sdimg/mask/hole.py +21 -0
- sdimg-0.2.0/src/sdimg/mask/hull.py +51 -0
- sdimg-0.2.0/src/sdimg/mask/morphology.py +47 -0
- sdimg-0.2.0/src/sdimg/mask/pad.py +14 -0
- sdimg-0.2.0/src/sdimg/py.typed +0 -0
- sdimg-0.2.0/src/sdimg/spatial/__init__.py +5 -0
- sdimg-0.2.0/src/sdimg/spatial/crop.py +12 -0
- sdimg-0.2.0/src/sdimg/spatial/pad.py +26 -0
- sdimg-0.2.0/src/sdimg/spatial/patch.py +167 -0
- sdimg-0.2.0/src/sdimg/spatial/resize.py +89 -0
- sdimg-0.2.0/src/sdimg/spatial/transform.py +33 -0
- sdimg-0.2.0/src/sdimg.egg-info/PKG-INFO +90 -0
- sdimg-0.2.0/src/sdimg.egg-info/SOURCES.txt +39 -0
- sdimg-0.2.0/src/sdimg.egg-info/dependency_links.txt +1 -0
- sdimg-0.2.0/src/sdimg.egg-info/requires.txt +3 -0
- 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
|
File without changes
|
|
@@ -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,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)
|