rvimage 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.
- rvimage-0.0.1/PKG-INFO +9 -0
- rvimage-0.0.1/README.md +0 -0
- rvimage-0.0.1/pyproject.toml +11 -0
- rvimage-0.0.1/rvimage/collection_types.py +134 -0
- rvimage-0.0.1/rvimage/converters.py +99 -0
- rvimage-0.0.1/rvimage/domain.py +120 -0
- rvimage-0.0.1/rvimage.egg-info/PKG-INFO +9 -0
- rvimage-0.0.1/rvimage.egg-info/SOURCES.txt +11 -0
- rvimage-0.0.1/rvimage.egg-info/dependency_links.txt +1 -0
- rvimage-0.0.1/rvimage.egg-info/requires.txt +3 -0
- rvimage-0.0.1/rvimage.egg-info/top_level.txt +1 -0
- rvimage-0.0.1/setup.cfg +4 -0
- rvimage-0.0.1/test/test.py +44 -0
rvimage-0.0.1/PKG-INFO
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: rvimage
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Add your description here
|
5
|
+
Requires-Python: >=3.12
|
6
|
+
Description-Content-Type: text/markdown
|
7
|
+
Requires-Dist: opencv-python-headless>=4.11.0.86
|
8
|
+
Requires-Dist: pydantic>=2.11.7
|
9
|
+
Requires-Dist: scipy>=1.15.3
|
rvimage-0.0.1/README.md
ADDED
File without changes
|
@@ -0,0 +1,134 @@
|
|
1
|
+
from typing import Self
|
2
|
+
from pydantic import BaseModel
|
3
|
+
import numpy as np
|
4
|
+
from rvimage.converters import (
|
5
|
+
extract_polys_from_mask,
|
6
|
+
fill_bbs_on_mask,
|
7
|
+
fill_polys_on_mask,
|
8
|
+
mask_to_rle,
|
9
|
+
rle_to_mask,
|
10
|
+
)
|
11
|
+
from rvimage.domain import BbF, BbI, Poly, enclosing_bb, find_ccs
|
12
|
+
|
13
|
+
|
14
|
+
class GeoFig(BaseModel):
|
15
|
+
bbox: BbF | None = None
|
16
|
+
poly: Poly | None = None
|
17
|
+
|
18
|
+
|
19
|
+
class Labelinfo(BaseModel):
|
20
|
+
new_label: str
|
21
|
+
labels: list[str]
|
22
|
+
colors: list[list[int]]
|
23
|
+
cat_ids: list[int]
|
24
|
+
cat_idx_current: int
|
25
|
+
show_only_current: bool
|
26
|
+
|
27
|
+
|
28
|
+
class BboxAnnos(BaseModel):
|
29
|
+
elts: list[GeoFig]
|
30
|
+
cat_idxs: list[int]
|
31
|
+
selected_mask: list[bool]
|
32
|
+
|
33
|
+
@classmethod
|
34
|
+
def from_mask(cls, mask: np.ndarray, cat_idx: int) -> "BboxAnnos":
|
35
|
+
"""
|
36
|
+
Create BboxAnnos from a binary mask.
|
37
|
+
"""
|
38
|
+
polys = extract_polys_from_mask(mask, abs_coords_output=True)
|
39
|
+
cat_idxs = [cat_idx] * len(polys)
|
40
|
+
|
41
|
+
return cls(
|
42
|
+
elts=[
|
43
|
+
GeoFig(poly=Poly(points=points, enclosing_bb=enclosing_bb(points)))
|
44
|
+
for points in polys
|
45
|
+
],
|
46
|
+
cat_idxs=cat_idxs,
|
47
|
+
selected_mask=[False] * len(polys),
|
48
|
+
)
|
49
|
+
|
50
|
+
def extend(self, other: Self) -> "BboxAnnos":
|
51
|
+
"""
|
52
|
+
Extend the current BboxAnnos with another BboxAnnos.
|
53
|
+
"""
|
54
|
+
return BboxAnnos(
|
55
|
+
elts=self.elts + other.elts,
|
56
|
+
cat_idxs=self.cat_idxs + other.cat_idxs,
|
57
|
+
selected_mask=self.selected_mask + other.selected_mask,
|
58
|
+
)
|
59
|
+
|
60
|
+
def fill_mask(self, im_mask: np.ndarray, cat_idx: int):
|
61
|
+
fill_polys_on_mask(
|
62
|
+
polygons=(
|
63
|
+
elt.poly.points
|
64
|
+
for elt, cat_idx_ in zip(self.elts, self.cat_idxs)
|
65
|
+
if cat_idx == cat_idx_ and elt.poly is not None
|
66
|
+
),
|
67
|
+
value=1,
|
68
|
+
im_mask=im_mask,
|
69
|
+
abs_coords_input=True,
|
70
|
+
)
|
71
|
+
fill_bbs_on_mask(
|
72
|
+
bbs=(
|
73
|
+
elt.bbox
|
74
|
+
for elt, cat_idx_ in zip(self.elts, self.cat_idxs)
|
75
|
+
if cat_idx == cat_idx_ and elt.bbox is not None
|
76
|
+
),
|
77
|
+
value=1,
|
78
|
+
im_mask=im_mask,
|
79
|
+
abs_coords_input=True,
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
class BboxData(BaseModel):
|
84
|
+
annos: BboxAnnos
|
85
|
+
labelinfo: Labelinfo
|
86
|
+
|
87
|
+
|
88
|
+
class Canvas(BaseModel):
|
89
|
+
rle: list[int]
|
90
|
+
bb: BbI
|
91
|
+
intensity: float
|
92
|
+
|
93
|
+
|
94
|
+
class BrushAnnos(BaseModel):
|
95
|
+
elts: list[Canvas]
|
96
|
+
cat_idxs: list[int]
|
97
|
+
selected_mask: list[bool]
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def from_mask(cls, im_mask: np.ndarray, cat_idx: int) -> "BrushAnnos":
|
101
|
+
"""
|
102
|
+
Create BrushAnnos from a binary mask.
|
103
|
+
"""
|
104
|
+
ccs, _ = find_ccs(im_mask)
|
105
|
+
rles = [mask_to_rle(cc.im) for cc in ccs]
|
106
|
+
|
107
|
+
return BrushAnnos(
|
108
|
+
elts=[
|
109
|
+
Canvas(rle=rle, bb=cc.bb, intensity=1.0) for rle, cc in zip(rles, ccs)
|
110
|
+
],
|
111
|
+
cat_idxs=[cat_idx] * len(ccs),
|
112
|
+
selected_mask=[False] * len(ccs),
|
113
|
+
)
|
114
|
+
|
115
|
+
def fill_mask(self, im_mask: np.ndarray, cat_idx: int):
|
116
|
+
for elt, cat_idx_ in zip(self.elts, self.cat_idxs):
|
117
|
+
if cat_idx == cat_idx_:
|
118
|
+
im_bb_mask = rle_to_mask(elt.rle, value=1, mask=elt.bb)
|
119
|
+
im_mask[elt.bb.slices] = im_bb_mask
|
120
|
+
|
121
|
+
|
122
|
+
class BrushData(BaseModel):
|
123
|
+
annos: BrushAnnos
|
124
|
+
labelinfo: Labelinfo
|
125
|
+
|
126
|
+
|
127
|
+
class InputAnnotationData(BaseModel):
|
128
|
+
bbox: BboxData
|
129
|
+
brush: BrushData
|
130
|
+
|
131
|
+
|
132
|
+
class OutputAnnotationData(BaseModel):
|
133
|
+
bbox: BboxAnnos
|
134
|
+
brush: BrushAnnos
|
@@ -0,0 +1,99 @@
|
|
1
|
+
from collections.abc import Sequence, Iterable
|
2
|
+
import numpy as np
|
3
|
+
import cv2
|
4
|
+
|
5
|
+
from rvimage.domain import BbF, BbI, Point
|
6
|
+
|
7
|
+
|
8
|
+
def rle_to_mask(rle: list[int], value: float, mask: np.ndarray | BbI) -> np.ndarray:
|
9
|
+
if isinstance(mask, BbI):
|
10
|
+
mask = np.zeros((mask.h, mask.w), dtype=np.uint8)
|
11
|
+
else:
|
12
|
+
mask = mask
|
13
|
+
shape = mask.shape[:2]
|
14
|
+
flat_mask = mask.ravel()
|
15
|
+
pos = 0
|
16
|
+
for i, n_elts in enumerate(rle):
|
17
|
+
if i % 2 == 1:
|
18
|
+
flat_mask[pos : pos + n_elts] = value
|
19
|
+
pos = pos + n_elts
|
20
|
+
return flat_mask.reshape(shape)
|
21
|
+
|
22
|
+
|
23
|
+
def mask_to_rle(mask: np.ndarray) -> list[int]:
|
24
|
+
mask_w, mask_h = mask.shape
|
25
|
+
flat_mask = mask.flatten()
|
26
|
+
rle = []
|
27
|
+
current_run = 0
|
28
|
+
current_value = 0
|
29
|
+
for y in range(mask_h):
|
30
|
+
for x in range(mask_w):
|
31
|
+
value = flat_mask[int(y * mask_w + x)]
|
32
|
+
if value == current_value:
|
33
|
+
current_run += 1
|
34
|
+
else:
|
35
|
+
rle.append(current_run)
|
36
|
+
current_run = 1
|
37
|
+
current_value = value
|
38
|
+
rle.append(current_run)
|
39
|
+
return rle
|
40
|
+
|
41
|
+
|
42
|
+
def fill_bbs_on_mask(
|
43
|
+
bbs: Iterable[BbI | BbF],
|
44
|
+
value: int,
|
45
|
+
im_mask: np.ndarray,
|
46
|
+
abs_coords_input: bool = True,
|
47
|
+
):
|
48
|
+
if abs_coords_input:
|
49
|
+
h, w = 1, 1
|
50
|
+
else:
|
51
|
+
h, w = im_mask.shape
|
52
|
+
|
53
|
+
im_mask = im_mask.copy()
|
54
|
+
for bb in bbs:
|
55
|
+
if isinstance(bb, BbF):
|
56
|
+
bb = bb.scale(w, h).to_bbi()
|
57
|
+
im_mask[bb.slices] = value
|
58
|
+
|
59
|
+
|
60
|
+
def fill_polys_on_mask(
|
61
|
+
polygons: Iterable[Sequence[Point]],
|
62
|
+
value: int,
|
63
|
+
im_mask: np.ndarray,
|
64
|
+
abs_coords_input: bool = True,
|
65
|
+
):
|
66
|
+
if abs_coords_input:
|
67
|
+
h, w = 1, 1
|
68
|
+
else:
|
69
|
+
h, w = im_mask.shape
|
70
|
+
|
71
|
+
im_mask = im_mask.copy()
|
72
|
+
for poly in polygons:
|
73
|
+
polygon_ = np.round(np.array([[[p.x * w, p.y * h] for p in poly]])).astype(
|
74
|
+
np.int32
|
75
|
+
)
|
76
|
+
im_mask = cv2.fillPoly(img=im_mask, pts=polygon_, color=value) # type: ignore
|
77
|
+
return im_mask
|
78
|
+
|
79
|
+
|
80
|
+
def extract_polys_from_mask(
|
81
|
+
im_mask: np.ndarray, abs_coords_output: bool
|
82
|
+
) -> list[list[Point]]:
|
83
|
+
contours, _ = cv2.findContours(im_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
|
84
|
+
polygons = []
|
85
|
+
h, w = im_mask.shape
|
86
|
+
|
87
|
+
for obj in contours:
|
88
|
+
polygon = []
|
89
|
+
|
90
|
+
for point in obj:
|
91
|
+
assert isinstance(point, np.ndarray)
|
92
|
+
if abs_coords_output:
|
93
|
+
x, y = point[0][0], point[0][1]
|
94
|
+
else:
|
95
|
+
x, y = point[0][0] / w, point[0][1] / h
|
96
|
+
polygon.append(Point(x=x, y=y))
|
97
|
+
|
98
|
+
polygons.append(polygon)
|
99
|
+
return polygons
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import math
|
2
|
+
import numpy as np
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from scipy.ndimage import find_objects
|
5
|
+
from scipy.ndimage import label as scpiy_label
|
6
|
+
|
7
|
+
|
8
|
+
class BbI(BaseModel):
|
9
|
+
x: int
|
10
|
+
y: int
|
11
|
+
w: int
|
12
|
+
h: int
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def from_slices(cls, slices: tuple[slice, slice]):
|
16
|
+
x = slices[1].start
|
17
|
+
y = slices[0].start
|
18
|
+
w = slices[1].stop - x
|
19
|
+
h = slices[0].stop - y
|
20
|
+
return cls(x=x, y=y, w=w, h=h)
|
21
|
+
|
22
|
+
@property
|
23
|
+
def slices(self) -> tuple[slice, slice]:
|
24
|
+
"""
|
25
|
+
Returns the slices for indexing a numpy array.
|
26
|
+
"""
|
27
|
+
return slice(self.y, self.y + self.h), slice(self.x, self.x + self.w)
|
28
|
+
|
29
|
+
|
30
|
+
class BbF(BaseModel):
|
31
|
+
x: float
|
32
|
+
y: float
|
33
|
+
w: float
|
34
|
+
h: float
|
35
|
+
|
36
|
+
def to_bbi(self) -> "BbI":
|
37
|
+
"""
|
38
|
+
Convert to integer bounding box.
|
39
|
+
"""
|
40
|
+
return BbI(
|
41
|
+
x=int(np.round(self.x)),
|
42
|
+
y=int(np.round(self.y)),
|
43
|
+
w=int(np.round(self.w)),
|
44
|
+
h=int(np.round(self.h)),
|
45
|
+
)
|
46
|
+
|
47
|
+
def scale(self, scale_x: float, scale_y) -> "BbF":
|
48
|
+
"""
|
49
|
+
Scale the bounding box by a factor.
|
50
|
+
"""
|
51
|
+
return BbF(
|
52
|
+
x=self.x * scale_x,
|
53
|
+
y=self.y * scale_y,
|
54
|
+
w=self.w * scale_x,
|
55
|
+
h=self.h * scale_y,
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
class Point(BaseModel):
|
60
|
+
x: float
|
61
|
+
y: float
|
62
|
+
|
63
|
+
|
64
|
+
class Poly(BaseModel):
|
65
|
+
points: list[Point]
|
66
|
+
enclosing_bb: BbF
|
67
|
+
|
68
|
+
|
69
|
+
class CC:
|
70
|
+
def __init__(
|
71
|
+
self,
|
72
|
+
slices: tuple[slice, slice],
|
73
|
+
label: int,
|
74
|
+
im: np.ndarray,
|
75
|
+
im_labeled: np.ndarray,
|
76
|
+
):
|
77
|
+
self.im = im.copy()
|
78
|
+
self.im[im_labeled != label] = 0
|
79
|
+
self.slices = slices
|
80
|
+
self.bb = BbI.from_slices(slices)
|
81
|
+
self.label = label
|
82
|
+
|
83
|
+
def __str__(self):
|
84
|
+
return "CC with " + str(self.bb)
|
85
|
+
|
86
|
+
|
87
|
+
def _find_cc_slices(im: np.ndarray):
|
88
|
+
im_labeled, n_ccs = scpiy_label(im) # type: ignore
|
89
|
+
return find_objects(im_labeled), im_labeled, n_ccs
|
90
|
+
|
91
|
+
|
92
|
+
def find_ccs(im: np.ndarray) -> tuple[list[CC], np.ndarray]:
|
93
|
+
"""Find connected components in a binary image.
|
94
|
+
Args:
|
95
|
+
im: A binary image (2D numpy array) where connected components are to be found.
|
96
|
+
Returns:
|
97
|
+
A tuple containing:
|
98
|
+
- A list of CC objects representing the connected components.
|
99
|
+
- A labeled image where each connected component is assigned a unique label.
|
100
|
+
"""
|
101
|
+
cc_slices, im_labeled, _ = _find_cc_slices(im)
|
102
|
+
ccs = [CC(slc, i + 1, im, im_labeled) for i, slc in enumerate(cc_slices)]
|
103
|
+
return ccs, im_labeled
|
104
|
+
|
105
|
+
|
106
|
+
def enclosing_bb(points: list[Point]) -> BbF:
|
107
|
+
min_x = math.inf
|
108
|
+
min_y = math.inf
|
109
|
+
max_x = -math.inf
|
110
|
+
max_y = -math.inf
|
111
|
+
for point in points:
|
112
|
+
if point.x < min_x:
|
113
|
+
min_x = point.x
|
114
|
+
if point.y < min_y:
|
115
|
+
min_y = point.y
|
116
|
+
if point.x > max_x:
|
117
|
+
max_x = point.x
|
118
|
+
if point.y > max_y:
|
119
|
+
max_y = point.y
|
120
|
+
return BbF(x=min_x, y=min_y, w=max_x - min_x, h=max_y - min_y)
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: rvimage
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Add your description here
|
5
|
+
Requires-Python: >=3.12
|
6
|
+
Description-Content-Type: text/markdown
|
7
|
+
Requires-Dist: opencv-python-headless>=4.11.0.86
|
8
|
+
Requires-Dist: pydantic>=2.11.7
|
9
|
+
Requires-Dist: scipy>=1.15.3
|
@@ -0,0 +1,11 @@
|
|
1
|
+
README.md
|
2
|
+
pyproject.toml
|
3
|
+
rvimage/collection_types.py
|
4
|
+
rvimage/converters.py
|
5
|
+
rvimage/domain.py
|
6
|
+
rvimage.egg-info/PKG-INFO
|
7
|
+
rvimage.egg-info/SOURCES.txt
|
8
|
+
rvimage.egg-info/dependency_links.txt
|
9
|
+
rvimage.egg-info/requires.txt
|
10
|
+
rvimage.egg-info/top_level.txt
|
11
|
+
test/test.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
rvimage
|
rvimage-0.0.1/setup.cfg
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from rvimage.converters import extract_polys_from_mask, fill_polys_on_mask
|
3
|
+
from rvimage.converters import rle_to_mask, mask_to_rle
|
4
|
+
from rvimage.domain import Point
|
5
|
+
|
6
|
+
|
7
|
+
def test_rle():
|
8
|
+
im_mask = np.zeros((10, 10), dtype=np.uint8)
|
9
|
+
im_mask[0:2, 0:5] = 1
|
10
|
+
|
11
|
+
rle = mask_to_rle(im_mask)
|
12
|
+
assert rle == [0, 5, 5, 5, 85]
|
13
|
+
im_mask_converted = rle_to_mask(rle, 1, im_mask)
|
14
|
+
assert np.array_equal(im_mask, im_mask_converted)
|
15
|
+
|
16
|
+
|
17
|
+
def test_polygon():
|
18
|
+
im_mask = np.zeros((10, 10), dtype=np.uint8)
|
19
|
+
polygons = [
|
20
|
+
[
|
21
|
+
Point(x=0, y=0),
|
22
|
+
Point(x=5, y=0),
|
23
|
+
Point(x=5, y=5),
|
24
|
+
Point(x=2, y=7),
|
25
|
+
Point(x=0, y=5),
|
26
|
+
]
|
27
|
+
]
|
28
|
+
value = 1
|
29
|
+
|
30
|
+
mask = fill_polys_on_mask(polygons, value, im_mask, abs_coords_input=True)
|
31
|
+
print(mask)
|
32
|
+
assert np.sum(mask) > 0
|
33
|
+
|
34
|
+
polygons_converted = extract_polys_from_mask(mask, abs_coords_output=True)
|
35
|
+
assert len(polygons_converted) > 0
|
36
|
+
im_mask = np.zeros((10, 10), dtype=np.uint8)
|
37
|
+
mask_converted = fill_polys_on_mask(polygons, value, im_mask, abs_coords_input=True)
|
38
|
+
|
39
|
+
assert np.allclose(mask, mask_converted), "Masks are not equal after conversion"
|
40
|
+
|
41
|
+
|
42
|
+
if __name__ == "__main__":
|
43
|
+
test_rle()
|
44
|
+
test_polygon()
|