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.
Files changed (38) hide show
  1. cli/__main__.py +2 -2
  2. cli/dataset_cmds.py +60 -0
  3. cli/runc_cmds.py +1 -1
  4. hafnia/data/__init__.py +2 -2
  5. hafnia/data/factory.py +9 -56
  6. hafnia/dataset/dataset_helpers.py +91 -0
  7. hafnia/dataset/dataset_names.py +71 -0
  8. hafnia/dataset/dataset_transformation.py +187 -0
  9. hafnia/dataset/dataset_upload_helper.py +468 -0
  10. hafnia/dataset/hafnia_dataset.py +453 -0
  11. hafnia/dataset/primitives/__init__.py +16 -0
  12. hafnia/dataset/primitives/bbox.py +137 -0
  13. hafnia/dataset/primitives/bitmask.py +182 -0
  14. hafnia/dataset/primitives/classification.py +56 -0
  15. hafnia/dataset/primitives/point.py +25 -0
  16. hafnia/dataset/primitives/polygon.py +100 -0
  17. hafnia/dataset/primitives/primitive.py +44 -0
  18. hafnia/dataset/primitives/segmentation.py +51 -0
  19. hafnia/dataset/primitives/utils.py +51 -0
  20. hafnia/dataset/table_transformations.py +183 -0
  21. hafnia/experiment/hafnia_logger.py +2 -2
  22. hafnia/helper_testing.py +63 -0
  23. hafnia/http.py +5 -3
  24. hafnia/platform/__init__.py +2 -2
  25. hafnia/platform/builder.py +25 -19
  26. hafnia/platform/datasets.py +184 -0
  27. hafnia/platform/download.py +85 -23
  28. hafnia/torch_helpers.py +180 -95
  29. hafnia/utils.py +1 -1
  30. hafnia/visualizations/colors.py +267 -0
  31. hafnia/visualizations/image_visualizations.py +202 -0
  32. {hafnia-0.1.26.dist-info → hafnia-0.2.0.dist-info}/METADATA +212 -99
  33. hafnia-0.2.0.dist-info/RECORD +46 -0
  34. cli/data_cmds.py +0 -53
  35. hafnia-0.1.26.dist-info/RECORD +0 -27
  36. {hafnia-0.1.26.dist-info → hafnia-0.2.0.dist-info}/WHEEL +0 -0
  37. {hafnia-0.1.26.dist-info → hafnia-0.2.0.dist-info}/entry_points.txt +0 -0
  38. {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