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 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
File without changes
@@ -0,0 +1,11 @@
1
+ [project]
2
+ name = "rvimage"
3
+ version = "0.0.1"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "opencv-python-headless>=4.11.0.86",
9
+ "pydantic>=2.11.7",
10
+ "scipy>=1.15.3",
11
+ ]
@@ -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,3 @@
1
+ opencv-python-headless>=4.11.0.86
2
+ pydantic>=2.11.7
3
+ scipy>=1.15.3
@@ -0,0 +1 @@
1
+ rvimage
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()