hafnia 0.1.26__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- cli/__main__.py +2 -2
- cli/dataset_cmds.py +60 -0
- cli/runc_cmds.py +1 -1
- hafnia/data/__init__.py +2 -2
- hafnia/data/factory.py +9 -56
- hafnia/dataset/dataset_helpers.py +91 -0
- hafnia/dataset/dataset_names.py +71 -0
- hafnia/dataset/dataset_transformation.py +187 -0
- hafnia/dataset/dataset_upload_helper.py +468 -0
- hafnia/dataset/hafnia_dataset.py +453 -0
- hafnia/dataset/primitives/__init__.py +16 -0
- hafnia/dataset/primitives/bbox.py +137 -0
- hafnia/dataset/primitives/bitmask.py +182 -0
- hafnia/dataset/primitives/classification.py +56 -0
- hafnia/dataset/primitives/point.py +25 -0
- hafnia/dataset/primitives/polygon.py +100 -0
- hafnia/dataset/primitives/primitive.py +44 -0
- hafnia/dataset/primitives/segmentation.py +51 -0
- hafnia/dataset/primitives/utils.py +51 -0
- hafnia/dataset/table_transformations.py +183 -0
- hafnia/experiment/hafnia_logger.py +2 -2
- hafnia/helper_testing.py +63 -0
- hafnia/http.py +5 -3
- hafnia/platform/__init__.py +2 -2
- hafnia/platform/builder.py +25 -19
- hafnia/platform/datasets.py +184 -0
- hafnia/platform/download.py +85 -23
- hafnia/torch_helpers.py +180 -95
- hafnia/utils.py +1 -1
- hafnia/visualizations/colors.py +267 -0
- hafnia/visualizations/image_visualizations.py +202 -0
- {hafnia-0.1.26.dist-info → hafnia-0.2.0.dist-info}/METADATA +212 -99
- hafnia-0.2.0.dist-info/RECORD +46 -0
- cli/data_cmds.py +0 -53
- hafnia-0.1.26.dist-info/RECORD +0 -27
- {hafnia-0.1.26.dist-info → hafnia-0.2.0.dist-info}/WHEEL +0 -0
- {hafnia-0.1.26.dist-info → hafnia-0.2.0.dist-info}/entry_points.txt +0 -0
- {hafnia-0.1.26.dist-info → hafnia-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pycocotools.mask as coco_mask
|
|
8
|
+
|
|
9
|
+
from hafnia.dataset.primitives.primitive import Primitive
|
|
10
|
+
from hafnia.dataset.primitives.utils import (
|
|
11
|
+
anonymize_by_resizing,
|
|
12
|
+
class_color_by_name,
|
|
13
|
+
get_class_name,
|
|
14
|
+
text_org_from_left_bottom_to_centered,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Bitmask(Primitive):
|
|
19
|
+
# Names should match names in FieldName
|
|
20
|
+
top: int # Bitmask top coordinate in pixels
|
|
21
|
+
left: int # Bitmask left coordinate in pixels
|
|
22
|
+
height: int # Bitmask height of the bounding box in pixels
|
|
23
|
+
width: int # Bitmask width of the bounding box in pixels
|
|
24
|
+
rleString: str # Run-length encoding (RLE) string for the bitmask region of size (height, width) at (top, left).
|
|
25
|
+
area: Optional[float] = None # Area of the bitmask in pixels is calculated from the RLE string
|
|
26
|
+
class_name: Optional[str] = None # This should match the string in 'FieldName.CLASS_NAME'
|
|
27
|
+
class_idx: Optional[int] = None # This should match the string in 'FieldName.CLASS_IDX'
|
|
28
|
+
object_id: Optional[str] = None # This should match the string in 'FieldName.OBJECT_ID'
|
|
29
|
+
confidence: Optional[float] = None # Confidence score (0-1.0) for the primitive, e.g. 0.95 for Bbox
|
|
30
|
+
ground_truth: bool = True # Whether this is ground truth or a prediction
|
|
31
|
+
|
|
32
|
+
task_name: str = "" # Task name to support multiple Bitmask tasks in the same dataset. "" defaults to "bitmask"
|
|
33
|
+
meta: Optional[Dict[str, Any]] = None # This can be used to store additional information about the bitmask
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def default_task_name() -> str:
|
|
37
|
+
return "bitmask"
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def column_name() -> str:
|
|
41
|
+
return "bitmasks"
|
|
42
|
+
|
|
43
|
+
def calculate_area(self) -> float:
|
|
44
|
+
raise NotImplementedError()
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def from_mask(
|
|
48
|
+
mask: np.ndarray,
|
|
49
|
+
top: int, # Bounding box top coordinate in pixels
|
|
50
|
+
left: int, # Bounding box left coordinate in pixels
|
|
51
|
+
class_name: Optional[str] = None, # This should match the string in 'FieldName.CLASS_NAME'
|
|
52
|
+
class_idx: Optional[int] = None, # This should match the string in 'FieldName.CLASS_IDX'
|
|
53
|
+
object_id: Optional[str] = None, # This should match the string in 'FieldName.OBJECT_ID') -> "Bitmask":
|
|
54
|
+
):
|
|
55
|
+
if len(mask.shape) != 2:
|
|
56
|
+
raise ValueError("Bitmask should be a 2-dimensional array.")
|
|
57
|
+
|
|
58
|
+
if mask.dtype != "|b1":
|
|
59
|
+
raise TypeError("Bitmask should be an array of boolean values. For numpy array call .astype(bool).")
|
|
60
|
+
|
|
61
|
+
h, w = mask.shape[:2]
|
|
62
|
+
area_pixels = np.sum(mask != 0)
|
|
63
|
+
area = area_pixels / (h * w)
|
|
64
|
+
|
|
65
|
+
mask_fortran = np.asfortranarray(mask, np.prod(h * w)) # Convert to Fortran order for COCO encoding
|
|
66
|
+
rle_coding = coco_mask.encode(mask_fortran.astype(bool)) # Encode the mask using COCO RLE
|
|
67
|
+
rle_string = rle_coding["counts"].decode("utf-8") # Convert the counts to string
|
|
68
|
+
|
|
69
|
+
return Bitmask(
|
|
70
|
+
top=top,
|
|
71
|
+
left=left,
|
|
72
|
+
height=h,
|
|
73
|
+
width=w,
|
|
74
|
+
area=area,
|
|
75
|
+
rleString=rle_string,
|
|
76
|
+
class_name=class_name,
|
|
77
|
+
class_idx=class_idx,
|
|
78
|
+
object_id=object_id,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def squeeze_mask(self):
|
|
82
|
+
"""
|
|
83
|
+
A mask may have large redundant areas of zeros. This function squeezes the mask to remove those areas.
|
|
84
|
+
"""
|
|
85
|
+
region_mask = self.to_region_mask()
|
|
86
|
+
shift_left, last_left = np.flatnonzero(region_mask.sum(axis=0))[[0, -1]]
|
|
87
|
+
shift_top, last_top = np.flatnonzero(region_mask.sum(axis=1))[[0, -1]]
|
|
88
|
+
new_top = self.top + shift_top
|
|
89
|
+
new_left = self.left + shift_left
|
|
90
|
+
new_region_mask = region_mask[shift_top : last_top + 1, shift_left : last_left + 1]
|
|
91
|
+
|
|
92
|
+
bitmask_squeezed = Bitmask.from_mask(
|
|
93
|
+
mask=new_region_mask,
|
|
94
|
+
top=new_top,
|
|
95
|
+
left=new_left,
|
|
96
|
+
class_name=self.class_name,
|
|
97
|
+
class_idx=self.class_idx,
|
|
98
|
+
object_id=self.object_id,
|
|
99
|
+
)
|
|
100
|
+
return bitmask_squeezed
|
|
101
|
+
|
|
102
|
+
def anonymize_by_blurring(self, image: np.ndarray, inplace: bool = False, max_resolution: int = 20) -> np.ndarray:
|
|
103
|
+
mask_tight = self.squeeze_mask()
|
|
104
|
+
|
|
105
|
+
mask_region = mask_tight.to_region_mask()
|
|
106
|
+
region_image = image[
|
|
107
|
+
mask_tight.top : mask_tight.top + mask_tight.height, mask_tight.left : mask_tight.left + mask_tight.width
|
|
108
|
+
]
|
|
109
|
+
region_image_blurred = anonymize_by_resizing(blur_region=region_image, max_resolution=max_resolution)
|
|
110
|
+
image_mixed = np.where(mask_region[:, :, None], region_image_blurred, region_image)
|
|
111
|
+
image[
|
|
112
|
+
mask_tight.top : mask_tight.top + mask_tight.height, mask_tight.left : mask_tight.left + mask_tight.width
|
|
113
|
+
] = image_mixed
|
|
114
|
+
return image
|
|
115
|
+
|
|
116
|
+
def to_region_mask(self) -> np.ndarray:
|
|
117
|
+
"""Returns a binary mask from the RLE string. The masks is only the region of the object and not the full image."""
|
|
118
|
+
rle = {"counts": self.rleString.encode(), "size": [self.height, self.width]}
|
|
119
|
+
mask = coco_mask.decode(rle) > 0
|
|
120
|
+
return mask
|
|
121
|
+
|
|
122
|
+
def to_mask(self, img_height: int, img_width: int) -> np.ndarray:
|
|
123
|
+
"""Creates a full image mask from the RLE string."""
|
|
124
|
+
|
|
125
|
+
region_mask = self.to_region_mask()
|
|
126
|
+
bitmask_np = np.zeros((img_height, img_width), dtype=bool)
|
|
127
|
+
bitmask_np[self.top : self.top + self.height, self.left : self.left + self.width] = region_mask
|
|
128
|
+
return bitmask_np
|
|
129
|
+
|
|
130
|
+
def draw(self, image: np.ndarray, inplace: bool = False, draw_label: bool = True) -> np.ndarray:
|
|
131
|
+
if not inplace:
|
|
132
|
+
image = image.copy()
|
|
133
|
+
if image.ndim == 2: # for grayscale/monochromatic images
|
|
134
|
+
image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
|
|
135
|
+
img_height, img_width = image.shape[:2]
|
|
136
|
+
bitmask_np = self.to_mask(img_height=img_height, img_width=img_width)
|
|
137
|
+
|
|
138
|
+
class_name = self.get_class_name()
|
|
139
|
+
color = class_color_by_name(class_name)
|
|
140
|
+
|
|
141
|
+
# Creates transparent masking with the specified color
|
|
142
|
+
image_masked = image.copy()
|
|
143
|
+
image_masked[bitmask_np] = color
|
|
144
|
+
cv2.addWeighted(src1=image, alpha=0.3, src2=image_masked, beta=0.7, gamma=0, dst=image)
|
|
145
|
+
|
|
146
|
+
if draw_label:
|
|
147
|
+
# Determines the center of mask
|
|
148
|
+
xy = np.stack(np.nonzero(bitmask_np))
|
|
149
|
+
xy_org = tuple(np.median(xy, axis=1).astype(int))[::-1]
|
|
150
|
+
|
|
151
|
+
xy_org = np.median(xy, axis=1).astype(int)[::-1]
|
|
152
|
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
153
|
+
font_scale = 0.75
|
|
154
|
+
thickness = 2
|
|
155
|
+
xy_centered = text_org_from_left_bottom_to_centered(xy_org, class_name, font, font_scale, thickness)
|
|
156
|
+
|
|
157
|
+
cv2.putText(
|
|
158
|
+
img=image,
|
|
159
|
+
text=class_name,
|
|
160
|
+
org=xy_centered,
|
|
161
|
+
fontFace=font,
|
|
162
|
+
fontScale=font_scale,
|
|
163
|
+
color=(255, 255, 255),
|
|
164
|
+
thickness=thickness,
|
|
165
|
+
)
|
|
166
|
+
return image
|
|
167
|
+
|
|
168
|
+
def mask(
|
|
169
|
+
self, image: np.ndarray, inplace: bool = False, color: Optional[Tuple[np.uint8, np.uint8, np.uint8]] = None
|
|
170
|
+
) -> np.ndarray:
|
|
171
|
+
if not inplace:
|
|
172
|
+
image = image.copy()
|
|
173
|
+
|
|
174
|
+
bitmask_np = self.to_mask(img_height=image.shape[0], img_width=image.shape[1])
|
|
175
|
+
|
|
176
|
+
if color is None:
|
|
177
|
+
color = tuple(int(value) for value in np.mean(image[bitmask_np], axis=0)) # type: ignore[assignment]
|
|
178
|
+
image[bitmask_np] = color
|
|
179
|
+
return image
|
|
180
|
+
|
|
181
|
+
def get_class_name(self) -> str:
|
|
182
|
+
return get_class_name(self.class_name, self.class_idx)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from hafnia.dataset.primitives.primitive import Primitive
|
|
6
|
+
from hafnia.dataset.primitives.utils import anonymize_by_resizing, get_class_name
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Classification(Primitive):
|
|
10
|
+
# Names should match names in FieldName
|
|
11
|
+
class_name: Optional[str] = None # Class name, e.g. "car"
|
|
12
|
+
class_idx: Optional[int] = None # Class index, e.g. 0 for "car" if it is the first class
|
|
13
|
+
object_id: Optional[str] = None # Unique identifier for the object, e.g. "12345123"
|
|
14
|
+
confidence: Optional[float] = None # Confidence score (0-1.0) for the primitive, e.g. 0.95 for Classification
|
|
15
|
+
ground_truth: bool = True # Whether this is ground truth or a prediction
|
|
16
|
+
|
|
17
|
+
task_name: str = "" # To support multiple Classification tasks in the same dataset. "" defaults to "classification"
|
|
18
|
+
meta: Optional[Dict[str, Any]] = None # This can be used to store additional information about the bitmask
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def default_task_name() -> str:
|
|
22
|
+
return "classification"
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def column_name() -> str:
|
|
26
|
+
return "classifications"
|
|
27
|
+
|
|
28
|
+
def calculate_area(self) -> float:
|
|
29
|
+
return 1.0
|
|
30
|
+
|
|
31
|
+
def draw(self, image: np.ndarray, inplace: bool = False, draw_label: bool = True) -> np.ndarray:
|
|
32
|
+
if draw_label is False:
|
|
33
|
+
return image
|
|
34
|
+
from hafnia.visualizations import image_visualizations
|
|
35
|
+
|
|
36
|
+
class_name = self.get_class_name()
|
|
37
|
+
if self.task_name == self.default_task_name():
|
|
38
|
+
text = class_name
|
|
39
|
+
else:
|
|
40
|
+
text = f"{self.task_name}: {class_name}"
|
|
41
|
+
image = image_visualizations.append_text_below_frame(image, text=text)
|
|
42
|
+
|
|
43
|
+
return image
|
|
44
|
+
|
|
45
|
+
def mask(
|
|
46
|
+
self, image: np.ndarray, inplace: bool = False, color: Optional[Tuple[np.uint8, np.uint8, np.uint8]] = None
|
|
47
|
+
) -> np.ndarray:
|
|
48
|
+
# Classification does not have a mask effect, so we return the image as is
|
|
49
|
+
return image
|
|
50
|
+
|
|
51
|
+
def anonymize_by_blurring(self, image: np.ndarray, inplace: bool = False, max_resolution: int = 20) -> np.ndarray:
|
|
52
|
+
# Classification does not have a blur effect, so we return the image as is
|
|
53
|
+
return anonymize_by_resizing(image, max_resolution=max_resolution)
|
|
54
|
+
|
|
55
|
+
def get_class_name(self) -> str:
|
|
56
|
+
return get_class_name(self.class_name, self.class_idx)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any, Tuple
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from hafnia.dataset.primitives.utils import clip
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Point(BaseModel):
|
|
9
|
+
x: float
|
|
10
|
+
y: float
|
|
11
|
+
|
|
12
|
+
def to_pixel_coordinates(
|
|
13
|
+
self, image_shape: Tuple[int, int], as_int: bool = True, clip_values: bool = True
|
|
14
|
+
) -> Tuple[Any, Any]:
|
|
15
|
+
x = self.x * image_shape[1]
|
|
16
|
+
y = self.y * image_shape[0]
|
|
17
|
+
|
|
18
|
+
if as_int:
|
|
19
|
+
x, y = int(round(x)), int(round(y)) # noqa: RUF046
|
|
20
|
+
|
|
21
|
+
if clip_values:
|
|
22
|
+
x = clip(value=x, v_min=0, v_max=image_shape[1])
|
|
23
|
+
y = clip(value=y, v_min=0, v_max=image_shape[0])
|
|
24
|
+
|
|
25
|
+
return x, y
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from hafnia.dataset.primitives.bitmask import Bitmask
|
|
7
|
+
from hafnia.dataset.primitives.point import Point
|
|
8
|
+
from hafnia.dataset.primitives.primitive import Primitive
|
|
9
|
+
from hafnia.dataset.primitives.utils import class_color_by_name, get_class_name
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Polygon(Primitive):
|
|
13
|
+
# Names should match names in FieldName
|
|
14
|
+
points: List[Point]
|
|
15
|
+
class_name: Optional[str] = None # This should match the string in 'FieldName.CLASS_NAME'
|
|
16
|
+
class_idx: Optional[int] = None # This should match the string in 'FieldName.CLASS_IDX'
|
|
17
|
+
object_id: Optional[str] = None # This should match the string in 'FieldName.OBJECT_ID'
|
|
18
|
+
confidence: Optional[float] = None # Confidence score (0-1.0) for the primitive, e.g. 0.95 for Bbox
|
|
19
|
+
ground_truth: bool = True # Whether this is ground truth or a prediction
|
|
20
|
+
|
|
21
|
+
task_name: str = "" # Task name to support multiple Polygon tasks in the same dataset. "" defaults to "polygon"
|
|
22
|
+
meta: Optional[Dict[str, Any]] = None # This can be used to store additional information about the bitmask
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_list_of_points(
|
|
26
|
+
points: Sequence[Sequence[float]],
|
|
27
|
+
class_name: Optional[str] = None,
|
|
28
|
+
class_idx: Optional[int] = None,
|
|
29
|
+
object_id: Optional[str] = None,
|
|
30
|
+
) -> "Polygon":
|
|
31
|
+
list_points = [Point(x=point[0], y=point[1]) for point in points]
|
|
32
|
+
return Polygon(points=list_points, class_name=class_name, class_idx=class_idx, object_id=object_id)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def default_task_name() -> str:
|
|
36
|
+
return "polygon"
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def column_name() -> str:
|
|
40
|
+
return "polygons"
|
|
41
|
+
|
|
42
|
+
def calculate_area(self) -> float:
|
|
43
|
+
raise NotImplementedError()
|
|
44
|
+
|
|
45
|
+
def to_pixel_coordinates(
|
|
46
|
+
self, image_shape: Tuple[int, int], as_int: bool = True, clip_values: bool = True
|
|
47
|
+
) -> List[Tuple]:
|
|
48
|
+
points = [
|
|
49
|
+
point.to_pixel_coordinates(image_shape=image_shape, as_int=as_int, clip_values=clip_values)
|
|
50
|
+
for point in self.points
|
|
51
|
+
]
|
|
52
|
+
return points
|
|
53
|
+
|
|
54
|
+
def draw(self, image: np.ndarray, inplace: bool = False) -> np.ndarray:
|
|
55
|
+
if not inplace:
|
|
56
|
+
image = image.copy()
|
|
57
|
+
points = np.array(self.to_pixel_coordinates(image_shape=image.shape[:2]))
|
|
58
|
+
|
|
59
|
+
bottom_left_idx = np.lexsort((-points[:, 1], points[:, 0]))[0]
|
|
60
|
+
bottom_left_np = points[bottom_left_idx, :]
|
|
61
|
+
margin = 5
|
|
62
|
+
bottom_left = (bottom_left_np[0] + margin, bottom_left_np[1] - margin)
|
|
63
|
+
|
|
64
|
+
class_name = self.get_class_name()
|
|
65
|
+
color = class_color_by_name(class_name)
|
|
66
|
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
67
|
+
cv2.polylines(image, [points], isClosed=True, color=(0, 255, 0), thickness=2)
|
|
68
|
+
cv2.putText(
|
|
69
|
+
img=image, text=class_name, org=bottom_left, fontFace=font, fontScale=0.75, color=color, thickness=2
|
|
70
|
+
)
|
|
71
|
+
return image
|
|
72
|
+
|
|
73
|
+
def anonymize_by_blurring(self, image: np.ndarray, inplace: bool = False, max_resolution: int = 20) -> np.ndarray:
|
|
74
|
+
if not inplace:
|
|
75
|
+
image = image.copy()
|
|
76
|
+
points = np.array(self.to_pixel_coordinates(image_shape=image.shape[:2]))
|
|
77
|
+
mask = np.zeros(image.shape[:2], dtype=np.uint8)
|
|
78
|
+
mask = cv2.fillPoly(mask, [points], color=255).astype(bool)
|
|
79
|
+
bitmask = Bitmask.from_mask(mask=mask, top=0, left=0).squeeze_mask()
|
|
80
|
+
image = bitmask.anonymize_by_blurring(image=image, inplace=inplace, max_resolution=max_resolution)
|
|
81
|
+
|
|
82
|
+
return image
|
|
83
|
+
|
|
84
|
+
def mask(
|
|
85
|
+
self, image: np.ndarray, inplace: bool = False, color: Optional[Tuple[np.uint8, np.uint8, np.uint8]] = None
|
|
86
|
+
) -> np.ndarray:
|
|
87
|
+
if not inplace:
|
|
88
|
+
image = image.copy()
|
|
89
|
+
points = self.to_pixel_coordinates(image_shape=image.shape[:2])
|
|
90
|
+
|
|
91
|
+
if color is None:
|
|
92
|
+
mask = np.zeros_like(image[:, :, 0])
|
|
93
|
+
bitmask = cv2.fillPoly(mask, pts=[np.array(points)], color=255).astype(bool) # type: ignore[assignment]
|
|
94
|
+
color = tuple(int(value) for value in np.mean(image[bitmask], axis=0)) # type: ignore[assignment]
|
|
95
|
+
|
|
96
|
+
cv2.fillPoly(image, [np.array(points)], color=color)
|
|
97
|
+
return image
|
|
98
|
+
|
|
99
|
+
def get_class_name(self) -> str:
|
|
100
|
+
return get_class_name(self.class_name, self.class_idx)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABCMeta, abstractmethod
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Primitive(BaseModel, metaclass=ABCMeta):
|
|
10
|
+
def model_post_init(self, context) -> None:
|
|
11
|
+
if self.task_name == "": # type: ignore[has-type] # Hack because 'task_name' doesn't exist in base-class yet.
|
|
12
|
+
self.task_name = self.default_task_name()
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def default_task_name() -> str:
|
|
17
|
+
# E.g. "return bboxes" for Bbox
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def column_name() -> str:
|
|
23
|
+
"""
|
|
24
|
+
Name of field used in hugging face datasets for storing annotations
|
|
25
|
+
E.g. "objects" for Bbox.
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def calculate_area(self) -> float:
|
|
31
|
+
# Calculate the area of the primitive
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def draw(self, image: np.ndarray, inplace: bool = False) -> np.ndarray:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def mask(self, image: np.ndarray, inplace: bool = False) -> np.ndarray:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def anonymize_by_blurring(self, image: np.ndarray, inplace: bool = False, max_resolution: int = 20) -> np.ndarray:
|
|
44
|
+
pass
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from hafnia.dataset.primitives.primitive import Primitive
|
|
7
|
+
from hafnia.dataset.primitives.utils import get_class_name
|
|
8
|
+
from hafnia.visualizations.colors import get_n_colors
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Segmentation(Primitive):
|
|
12
|
+
# mask: np.ndarray
|
|
13
|
+
class_names: Optional[List[str]] = None # This should match the string in 'FieldName.CLASS_NAME'
|
|
14
|
+
ground_truth: bool = True # Whether this is ground truth or a prediction
|
|
15
|
+
|
|
16
|
+
# confidence: Optional[float] = None # Confidence score (0-1.0) for the primitive, e.g. 0.95 for Classification
|
|
17
|
+
task_name: str = (
|
|
18
|
+
"" # Task name to support multiple Segmentation tasks in the same dataset. "" defaults to "segmentation"
|
|
19
|
+
)
|
|
20
|
+
meta: Optional[Dict[str, Any]] = None # This can be used to store additional information about the bitmask
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def default_task_name() -> str:
|
|
24
|
+
return "segmentation"
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def column_name() -> str:
|
|
28
|
+
return "segmentation"
|
|
29
|
+
|
|
30
|
+
def calculate_area(self) -> float:
|
|
31
|
+
raise NotImplementedError()
|
|
32
|
+
|
|
33
|
+
def draw(self, image: np.ndarray, inplace: bool = False) -> np.ndarray:
|
|
34
|
+
if not inplace:
|
|
35
|
+
image = image.copy()
|
|
36
|
+
|
|
37
|
+
color_mapping = np.asarray(get_n_colors(len(self.class_names)), dtype=np.uint8) # type: ignore[arg-type]
|
|
38
|
+
label_image = color_mapping[self.mask]
|
|
39
|
+
blended = cv2.addWeighted(image, 0.5, label_image, 0.5, 0)
|
|
40
|
+
return blended
|
|
41
|
+
|
|
42
|
+
def mask(
|
|
43
|
+
self, image: np.ndarray, inplace: bool = False, color: Optional[Tuple[np.uint8, np.uint8, np.uint8]] = None
|
|
44
|
+
) -> np.ndarray:
|
|
45
|
+
return image
|
|
46
|
+
|
|
47
|
+
def anonymize_by_blurring(self, image: np.ndarray, inplace: bool = False, max_resolution: int = 20) -> np.ndarray:
|
|
48
|
+
return image
|
|
49
|
+
|
|
50
|
+
def get_class_name(self) -> str:
|
|
51
|
+
return get_class_name(self.class_name, self.class_idx)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from typing import Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def text_org_from_left_bottom_to_centered(xy_org: tuple, text: str, font, font_scale: float, thickness: int) -> tuple:
|
|
9
|
+
xy_text_size = cv2.getTextSize(text, fontFace=font, fontScale=font_scale, thickness=thickness)[0]
|
|
10
|
+
xy_text_size_half = np.array(xy_text_size) / 2
|
|
11
|
+
xy_centered_np = xy_org + xy_text_size_half * np.array([-1, 1])
|
|
12
|
+
xy_centered = tuple(int(value) for value in xy_centered_np)
|
|
13
|
+
return xy_centered
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def round_int_clip_value(value: Union[int, float], max_value: int) -> int:
|
|
17
|
+
return clip(value=int(round(value)), v_min=0, v_max=max_value) # noqa: RUF046
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def class_color_by_name(name: str) -> Tuple[int, int, int]:
|
|
21
|
+
# Create a hash of the class name
|
|
22
|
+
hash_object = hashlib.md5(name.encode())
|
|
23
|
+
# Use the hash to generate a color
|
|
24
|
+
hash_digest = hash_object.hexdigest()
|
|
25
|
+
color = (int(hash_digest[0:2], 16), int(hash_digest[2:4], 16), int(hash_digest[4:6], 16))
|
|
26
|
+
return color
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Define an abstract base class
|
|
30
|
+
def clip(value, v_min, v_max):
|
|
31
|
+
return min(max(v_min, value), v_max)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_class_name(class_name: Optional[str], class_idx: Optional[int]) -> str:
|
|
35
|
+
if class_name is not None:
|
|
36
|
+
return class_name
|
|
37
|
+
if class_idx is not None:
|
|
38
|
+
return f"IDX:{class_idx}"
|
|
39
|
+
return "NoName"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def anonymize_by_resizing(blur_region: np.ndarray, max_resolution: int = 20) -> np.ndarray:
|
|
43
|
+
"""
|
|
44
|
+
Removes high-frequency details from a region of an image by resizing it down and then back up.
|
|
45
|
+
"""
|
|
46
|
+
original_shape = blur_region.shape[:2]
|
|
47
|
+
resize_factor = max(original_shape) / max_resolution
|
|
48
|
+
new_size = (int(original_shape[0] / resize_factor), int(original_shape[1] / resize_factor))
|
|
49
|
+
blur_region_downsized = cv2.resize(blur_region, new_size[::-1], interpolation=cv2.INTER_LINEAR)
|
|
50
|
+
blur_region_upsized = cv2.resize(blur_region_downsized, original_shape[::-1], interpolation=cv2.INTER_LINEAR)
|
|
51
|
+
return blur_region_upsized
|