cv4 0.0.1__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.
- cv4-0.0.1/LICENSE +21 -0
- cv4-0.0.1/PKG-INFO +90 -0
- cv4-0.0.1/README.md +69 -0
- cv4-0.0.1/pyproject.toml +37 -0
- cv4-0.0.1/pyproject.toml.orig +35 -0
- cv4-0.0.1/src/cv4/__init__.py +14 -0
- cv4-0.0.1/src/cv4/main.py +242 -0
- cv4-0.0.1/src/cv4/py.typed +0 -0
cv4-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Sebastián Ramírez
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
cv4-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cv4
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A type-safe, builder-pattern, pythonic wrapper for OpenCV
|
|
5
|
+
Author: kytpbs
|
|
6
|
+
Author-email: kytpbs <email@example.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=2.0.0
|
|
10
|
+
Requires-Dist: opencv-contrib-python>=5.0.0.93 ; extra == 'contrib'
|
|
11
|
+
Requires-Dist: opencv-contrib-python-headless>=5.0.0.93 ; extra == 'contrib-headless'
|
|
12
|
+
Requires-Dist: opencv-python>=5.0.0.93 ; extra == 'opencv'
|
|
13
|
+
Requires-Dist: opencv-python-headless>=5.0.0.93 ; extra == 'opencv-headless'
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Project-URL: Homepage, https://github.com/kytpbs/cv4
|
|
16
|
+
Provides-Extra: contrib
|
|
17
|
+
Provides-Extra: contrib-headless
|
|
18
|
+
Provides-Extra: opencv
|
|
19
|
+
Provides-Extra: opencv-headless
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Type Safe OpenCV
|
|
23
|
+
|
|
24
|
+
A simple wrapper around opencv to make it type-safe and also follow pythonic syntax
|
|
25
|
+
using builder pattern and similar systems.
|
|
26
|
+
|
|
27
|
+
## Example
|
|
28
|
+
|
|
29
|
+
<table>
|
|
30
|
+
<tr>
|
|
31
|
+
<th>The old way: raw <code>cv2</code></th>
|
|
32
|
+
<th>The builder pattern: <code>cv4</code></th>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr>
|
|
35
|
+
<td>
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
|
|
39
|
+
mask = cv2.inRange(hsv, lower, upper)
|
|
40
|
+
|
|
41
|
+
kernel = np.ones((5, 5), np.uint8)
|
|
42
|
+
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
|
|
43
|
+
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
|
44
|
+
|
|
45
|
+
# nothing stops you passing the HSV image here
|
|
46
|
+
# by mistake it runs, but the result is very wrong
|
|
47
|
+
contours, _ = cv2.findContours(
|
|
48
|
+
hsv, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
49
|
+
|
|
50
|
+
for c in contours:
|
|
51
|
+
if cv2.contourArea(c) < MIN_AREA:
|
|
52
|
+
continue
|
|
53
|
+
m = cv2.moments(c)
|
|
54
|
+
cx = m["m10"] / m["m00"] # can divide by zero
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
</td>
|
|
58
|
+
<td>
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
contours = (
|
|
62
|
+
hsv.mask(BLUE)
|
|
63
|
+
.clean(5)
|
|
64
|
+
.find_contours()
|
|
65
|
+
.filter(lambda c: c.area > MIN_AREA)
|
|
66
|
+
.filter(lambda c: c.base_y >= MIN_Y)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# find_contours() lives ONLY on HSVMask.
|
|
70
|
+
# hsv.find_contours() does not exist: the
|
|
71
|
+
# editor won't autocomplete it, and the type
|
|
72
|
+
# checker rejects it before you ever run.
|
|
73
|
+
|
|
74
|
+
for c in contours:
|
|
75
|
+
cx = c.cx # centroid, typed
|
|
76
|
+
base = c.base_y # ground contact
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
</table>
|
|
82
|
+
|
|
83
|
+
Where `hsv` and `BLUE` come from:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from cv4 import BGRImage, HSV, HSVRange
|
|
87
|
+
|
|
88
|
+
BLUE = HSVRange(lower=HSV(100, 150, 50), upper=HSV(130, 255, 255))
|
|
89
|
+
hsv = BGRImage(frame).as_hsv() # frame: a BGR image from cv2
|
|
90
|
+
```
|
cv4-0.0.1/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Type Safe OpenCV
|
|
2
|
+
|
|
3
|
+
A simple wrapper around opencv to make it type-safe and also follow pythonic syntax
|
|
4
|
+
using builder pattern and similar systems.
|
|
5
|
+
|
|
6
|
+
## Example
|
|
7
|
+
|
|
8
|
+
<table>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>The old way: raw <code>cv2</code></th>
|
|
11
|
+
<th>The builder pattern: <code>cv4</code></th>
|
|
12
|
+
</tr>
|
|
13
|
+
<tr>
|
|
14
|
+
<td>
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
|
|
18
|
+
mask = cv2.inRange(hsv, lower, upper)
|
|
19
|
+
|
|
20
|
+
kernel = np.ones((5, 5), np.uint8)
|
|
21
|
+
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
|
|
22
|
+
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
|
23
|
+
|
|
24
|
+
# nothing stops you passing the HSV image here
|
|
25
|
+
# by mistake it runs, but the result is very wrong
|
|
26
|
+
contours, _ = cv2.findContours(
|
|
27
|
+
hsv, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
28
|
+
|
|
29
|
+
for c in contours:
|
|
30
|
+
if cv2.contourArea(c) < MIN_AREA:
|
|
31
|
+
continue
|
|
32
|
+
m = cv2.moments(c)
|
|
33
|
+
cx = m["m10"] / m["m00"] # can divide by zero
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
</td>
|
|
37
|
+
<td>
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
contours = (
|
|
41
|
+
hsv.mask(BLUE)
|
|
42
|
+
.clean(5)
|
|
43
|
+
.find_contours()
|
|
44
|
+
.filter(lambda c: c.area > MIN_AREA)
|
|
45
|
+
.filter(lambda c: c.base_y >= MIN_Y)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# find_contours() lives ONLY on HSVMask.
|
|
49
|
+
# hsv.find_contours() does not exist: the
|
|
50
|
+
# editor won't autocomplete it, and the type
|
|
51
|
+
# checker rejects it before you ever run.
|
|
52
|
+
|
|
53
|
+
for c in contours:
|
|
54
|
+
cx = c.cx # centroid, typed
|
|
55
|
+
base = c.base_y # ground contact
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
</table>
|
|
61
|
+
|
|
62
|
+
Where `hsv` and `BLUE` come from:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from cv4 import BGRImage, HSV, HSVRange
|
|
66
|
+
|
|
67
|
+
BLUE = HSVRange(lower=HSV(100, 150, 50), upper=HSV(130, 255, 255))
|
|
68
|
+
hsv = BGRImage(frame).as_hsv() # frame: a BGR image from cv2
|
|
69
|
+
```
|
cv4-0.0.1/pyproject.toml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cv4"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A type-safe, builder-pattern, pythonic wrapper for OpenCV"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = ["numpy>=2.0.0"]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICEN[CS]E*"]
|
|
10
|
+
|
|
11
|
+
[[project.authors]]
|
|
12
|
+
name = "kytpbs"
|
|
13
|
+
email = "email@example.com"
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
opencv = ["opencv-python>=5.0.0.93"]
|
|
17
|
+
opencv-headless = ["opencv-python-headless>=5.0.0.93"]
|
|
18
|
+
contrib = ["opencv-contrib-python>=5.0.0.93"]
|
|
19
|
+
contrib-headless = ["opencv-contrib-python-headless>=5.0.0.93"]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/kytpbs/cv4"
|
|
23
|
+
|
|
24
|
+
[tool.uv]
|
|
25
|
+
conflicts = [[
|
|
26
|
+
{ extra = "opencv" },
|
|
27
|
+
{ extra = "opencv-headless" },
|
|
28
|
+
{ extra = "contrib" },
|
|
29
|
+
{ extra = "contrib-headless" },
|
|
30
|
+
]]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.13.4,<0.14.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = ["opencv-python>=5.0.0.93"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cv4"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A type-safe, builder-pattern, pythonic wrapper for OpenCV"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "kytpbs", email = "email@example.com" }]
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = ["numpy>=2.0.0"]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICEN[CS]E*"]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
opencv = ["opencv-python>=5.0.0.93"]
|
|
14
|
+
opencv-headless = ["opencv-python-headless>=5.0.0.93"]
|
|
15
|
+
contrib = ["opencv-contrib-python>=5.0.0.93"]
|
|
16
|
+
contrib-headless = ["opencv-contrib-python-headless>=5.0.0.93"]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/kytpbs/cv4"
|
|
20
|
+
|
|
21
|
+
# tell uv these extras can't be installed together
|
|
22
|
+
[tool.uv]
|
|
23
|
+
conflicts = [[
|
|
24
|
+
{ extra = "opencv" }, { extra = "opencv-headless" },
|
|
25
|
+
{ extra = "contrib" }, { extra = "contrib-headless" },
|
|
26
|
+
]]
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["uv_build>=0.13.4,<0.14.0"]
|
|
30
|
+
build-backend = "uv_build"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"opencv-python>=5.0.0.93",
|
|
35
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""cv4 is a Python library for computer vision.""" # TODO: Update description
|
|
2
|
+
|
|
3
|
+
__version__ = "0.0.1"
|
|
4
|
+
try:
|
|
5
|
+
import cv2 # pyright: ignore[reportUnusedImport] # noqa: F401
|
|
6
|
+
except ImportError as e:
|
|
7
|
+
raise ImportError(
|
|
8
|
+
"cv4 requires OpenCV. Install it with `pip install cv4[opencv]`, or provide your own cv2 build."
|
|
9
|
+
) from e
|
|
10
|
+
|
|
11
|
+
from .main import * # noqa: F401,F403
|
|
12
|
+
|
|
13
|
+
def hello() -> str:
|
|
14
|
+
return "Hello from cv4!"
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class HSV:
|
|
11
|
+
hue: float
|
|
12
|
+
saturation: float
|
|
13
|
+
value: float
|
|
14
|
+
|
|
15
|
+
def as_tuple(self) -> tuple[float, float, float]:
|
|
16
|
+
return (self.hue, self.saturation, self.value)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def np_array(self) -> np.ndarray[np.float64, np.dtype[np.float64]]:
|
|
20
|
+
return np.array(self.as_tuple())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class RGB:
|
|
25
|
+
red: float
|
|
26
|
+
green: float
|
|
27
|
+
blue: float
|
|
28
|
+
|
|
29
|
+
def as_tuple(self) -> tuple[float, float, float]:
|
|
30
|
+
return (self.red, self.green, self.blue)
|
|
31
|
+
|
|
32
|
+
def as_bgr(self) -> "BGR":
|
|
33
|
+
return BGR(self.blue, self.green, self.red)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Point:
|
|
38
|
+
x: float
|
|
39
|
+
y: float
|
|
40
|
+
|
|
41
|
+
def as_tuple(self) -> tuple[float, float]:
|
|
42
|
+
return (self.x, self.y)
|
|
43
|
+
|
|
44
|
+
def as_cv2_point(self) -> "cv2.typing.Point":
|
|
45
|
+
return (int(self.x), int(self.y))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class BGR:
|
|
50
|
+
blue: float
|
|
51
|
+
green: float
|
|
52
|
+
red: float
|
|
53
|
+
|
|
54
|
+
def as_tuple(self) -> tuple[float, float, float]:
|
|
55
|
+
return (self.blue, self.green, self.red)
|
|
56
|
+
|
|
57
|
+
def as_rgb(self) -> RGB:
|
|
58
|
+
return RGB(self.red, self.green, self.blue)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class HSVRange:
|
|
63
|
+
lower: HSV
|
|
64
|
+
upper: HSV
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Image:
|
|
68
|
+
def __init__(self, image: "cv2.typing.MatLike"):
|
|
69
|
+
self.cv2_image: cv2.typing.MatLike = image
|
|
70
|
+
|
|
71
|
+
def copy(self) -> "Image":
|
|
72
|
+
return self.__class__(self.cv2_image.copy())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DrawableImage(Image):
|
|
76
|
+
def draw_contours(self, contours: list["Contour"], color: RGB, thickness: int = 2):
|
|
77
|
+
_ = cv2.drawContours(
|
|
78
|
+
self.cv2_image,
|
|
79
|
+
[contour.cv2_image for contour in contours],
|
|
80
|
+
-1,
|
|
81
|
+
color.as_bgr().as_tuple(),
|
|
82
|
+
thickness,
|
|
83
|
+
)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def draw_circle(self, center: Point, radius: int, color: RGB, thickness: int = -1):
|
|
87
|
+
_ = cv2.circle(
|
|
88
|
+
self.cv2_image,
|
|
89
|
+
center.as_cv2_point(),
|
|
90
|
+
radius,
|
|
91
|
+
color.as_bgr().as_tuple(),
|
|
92
|
+
thickness,
|
|
93
|
+
)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def draw_marker( # noqa: PLR0913, PLR0917 # just replicating cv2 API
|
|
97
|
+
self,
|
|
98
|
+
position: Point,
|
|
99
|
+
color: RGB,
|
|
100
|
+
markerType: int = cv2.MARKER_CROSS,
|
|
101
|
+
markerSize: int = 10,
|
|
102
|
+
thickness: int = 2,
|
|
103
|
+
lineType: int = cv2.LINE_AA,
|
|
104
|
+
):
|
|
105
|
+
_ = cv2.drawMarker(
|
|
106
|
+
self.cv2_image,
|
|
107
|
+
position.as_cv2_point(),
|
|
108
|
+
color.as_bgr().as_tuple(),
|
|
109
|
+
markerType,
|
|
110
|
+
markerSize,
|
|
111
|
+
thickness,
|
|
112
|
+
lineType,
|
|
113
|
+
)
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class BGRImage(DrawableImage):
|
|
118
|
+
def __init__(self, image: "cv2.typing.MatLike"):
|
|
119
|
+
super().__init__(image)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def height(self) -> int:
|
|
123
|
+
return self.cv2_image.shape[0]
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def width(self) -> int:
|
|
127
|
+
return self.cv2_image.shape[1]
|
|
128
|
+
|
|
129
|
+
def as_grayscale(self) -> "GrayscaleImage":
|
|
130
|
+
return GrayscaleImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_BGR2GRAY))
|
|
131
|
+
|
|
132
|
+
def as_hsv(self) -> "HSVImage":
|
|
133
|
+
return HSVImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_BGR2HSV))
|
|
134
|
+
|
|
135
|
+
def as_rgb(self) -> "RGBImage":
|
|
136
|
+
return RGBImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_BGR2RGB))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class HSVImage(Image):
|
|
140
|
+
def __init__(self, image: "cv2.typing.MatLike"):
|
|
141
|
+
super().__init__(image)
|
|
142
|
+
|
|
143
|
+
def as_bgr(self) -> "BGRImage":
|
|
144
|
+
return BGRImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_HSV2BGR))
|
|
145
|
+
|
|
146
|
+
def as_rgb(self) -> "RGBImage":
|
|
147
|
+
return RGBImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_HSV2RGB))
|
|
148
|
+
|
|
149
|
+
def mask(self, range: HSVRange) -> "HSVMask":
|
|
150
|
+
return HSVMask(
|
|
151
|
+
cv2.inRange(self.cv2_image, range.lower.np_array, range.upper.np_array),
|
|
152
|
+
range,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class HSVMask(Image):
|
|
157
|
+
def __init__(self, image: "cv2.typing.MatLike", range: HSVRange | None = None):
|
|
158
|
+
super().__init__(image)
|
|
159
|
+
|
|
160
|
+
def bitwise_or(self, other: "HSVMask") -> "HSVMask":
|
|
161
|
+
self.cv2_image = cv2.bitwise_or(self.cv2_image, other.cv2_image)
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def clean(self, kernel_size: int) -> "HSVMask":
|
|
165
|
+
kernel = np.ones((kernel_size, kernel_size), np.uint8)
|
|
166
|
+
self.cv2_image = cv2.morphologyEx(self.cv2_image, cv2.MORPH_OPEN, kernel)
|
|
167
|
+
self.cv2_image = cv2.morphologyEx(self.cv2_image, cv2.MORPH_CLOSE, kernel)
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
def find_contours(self) -> "Contours":
|
|
171
|
+
contours, _ = cv2.findContours(
|
|
172
|
+
self.cv2_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
|
173
|
+
)
|
|
174
|
+
return Contours([Contour(contour) for contour in contours])
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Contours(list["Contour"]):
|
|
178
|
+
def __init__(self, contours: list["Contour"]):
|
|
179
|
+
super().__init__(contours)
|
|
180
|
+
|
|
181
|
+
def filter(self, function: Callable[["Contour"], bool]) -> "Contours":
|
|
182
|
+
return Contours([contour for contour in self if function(contour)])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Contour(Image):
|
|
186
|
+
def __init__(self, image: "cv2.typing.MatLike"):
|
|
187
|
+
super().__init__(image)
|
|
188
|
+
self._moments: Final = cv2.moments(self.cv2_image)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def area(self) -> float:
|
|
192
|
+
return self._moments["m00"]
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def moments(self) -> "cv2.typing.Moments":
|
|
196
|
+
return self._moments
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def cx(self) -> float:
|
|
200
|
+
return self.centroid.x
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def cy(self) -> float:
|
|
204
|
+
return self.centroid.y
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def centroid(self) -> Point:
|
|
208
|
+
m00 = self._moments["m00"]
|
|
209
|
+
if abs(m00) > 1e-9: # noqa: PLR2004
|
|
210
|
+
return Point(self._moments["m10"] / m00, self._moments["m01"] / m00)
|
|
211
|
+
# degenerate contour (point / line) the mean of its vertices should be close enough.
|
|
212
|
+
x, y = self.cv2_image.reshape(-1, 2).mean(axis=0)
|
|
213
|
+
return Point(float(x), float(y))
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def base_y(self) -> float:
|
|
217
|
+
return float(self.cv2_image[:, :, 1].max()) # pyright: ignore[reportAny]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class RGBImage(DrawableImage):
|
|
221
|
+
def __init__(self, image: "cv2.typing.MatLike"):
|
|
222
|
+
super().__init__(image)
|
|
223
|
+
|
|
224
|
+
def as_bgr(self) -> "BGRImage":
|
|
225
|
+
return BGRImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_RGB2BGR))
|
|
226
|
+
|
|
227
|
+
def as_hsv(self) -> "HSVImage":
|
|
228
|
+
return HSVImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_RGB2HSV))
|
|
229
|
+
|
|
230
|
+
def as_grayscale(self) -> "GrayscaleImage":
|
|
231
|
+
return GrayscaleImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_RGB2GRAY))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class GrayscaleImage(Image):
|
|
235
|
+
def __init__(self, image: "cv2.typing.MatLike"):
|
|
236
|
+
super().__init__(image)
|
|
237
|
+
|
|
238
|
+
def as_bgr(self) -> "BGRImage":
|
|
239
|
+
return BGRImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_GRAY2BGR))
|
|
240
|
+
|
|
241
|
+
def as_rgb(self) -> "RGBImage":
|
|
242
|
+
return RGBImage(cv2.cvtColor(self.cv2_image, cv2.COLOR_GRAY2RGB))
|
|
File without changes
|