vkit.element.box
1# Copyright 2022 vkit-x Administrator. All Rights Reserved. 2# 3# This project (vkit-x/vkit) is dual-licensed under commercial and SSPL licenses. 4# 5# The commercial license gives you the full rights to create and distribute software 6# on your own terms without any SSPL license obligations. For more information, 7# please see the "LICENSE_COMMERCIAL.txt" file. 8# 9# This project is also available under Server Side Public License (SSPL). 10# The SSPL licensing is ideal for use cases such as open source projects with 11# SSPL distribution, student/academic purposes, hobby projects, internal research 12# projects without external distribution, or other projects where all SSPL 13# obligations can be met. For more information, please see the "LICENSE_SSPL.txt" file. 14from typing import Optional, Tuple, Union, Iterable 15import math 16 17import attrs 18import numpy as np 19from shapely.geometry import box as build_shapely_polygon_as_box 20from shapely.strtree import STRtree 21 22from .type import Shapable, ElementSetOperationMode 23from .opt import ( 24 clip_val, 25 resize_val, 26 extract_shape_from_shapable_or_shape, 27 generate_shape_and_resized_shape, 28 fill_np_array, 29) 30 31 32@attrs.define(frozen=True) 33class Box(Shapable): 34 # By design, smooth positioning is not supported in Box. 35 up: int 36 down: int 37 left: int 38 right: int 39 40 ############### 41 # Constructor # 42 ############### 43 @classmethod 44 def from_shape(cls, shape: Tuple[int, int]): 45 height, width = shape 46 return cls( 47 up=0, 48 down=height - 1, 49 left=0, 50 right=width - 1, 51 ) 52 53 @classmethod 54 def from_shapable(cls, shapable: Shapable): 55 return cls.from_shape(shapable.shape) 56 57 @classmethod 58 def from_boxes(cls, boxes: Iterable['Box']): 59 # Build a bounding box. 60 boxes_iter = iter(boxes) 61 62 first_box = next(boxes_iter) 63 up = first_box.up 64 down = first_box.down 65 left = first_box.left 66 right = first_box.right 67 68 for box in boxes_iter: 69 up = min(up, box.up) 70 down = max(down, box.down) 71 left = min(left, box.left) 72 right = max(right, box.right) 73 74 return cls(up=up, down=down, left=left, right=right) 75 76 ############ 77 # Property # 78 ############ 79 @property 80 def height(self): 81 return self.down + 1 - self.up 82 83 @property 84 def width(self): 85 return self.right + 1 - self.left 86 87 @property 88 def valid(self): 89 return (0 <= self.up <= self.down) and (0 <= self.left <= self.right) 90 91 ############## 92 # Conversion # 93 ############## 94 def to_polygon(self, step: Optional[int] = None): 95 if self.up == self.down or self.left == self.right: 96 raise RuntimeError(f'Cannot convert box={self} to polygon.') 97 98 # NOTE: Up left -> up right -> down right -> down left 99 # Char-level labelings are generated based on this ordering. 100 if step is None: 101 points = PointTuple.from_xy_pairs(( 102 (self.left, self.up), 103 (self.right, self.up), 104 (self.right, self.down), 105 (self.left, self.down), 106 )) 107 108 else: 109 assert step > 0 110 111 xs = list(range(self.left, self.right + 1, step)) 112 if xs[-1] < self.right: 113 xs.append(self.right) 114 115 ys = list(range(self.up, self.down + 1, step)) 116 if ys[-1] == self.down: 117 # NOTE: check first to avoid oob error. 118 ys.pop() 119 ys.pop(0) 120 121 points = PointList() 122 # Up. 123 for x in xs: 124 points.append(Point.create(y=self.up, x=x)) 125 # Right. 126 for y in ys: 127 points.append(Point.create(y=y, x=self.right)) 128 # Down. 129 for x in reversed(xs): 130 points.append(Point.create(y=self.down, x=x)) 131 # Left. 132 for y in reversed(ys): 133 points.append(Point.create(y=y, x=self.left)) 134 135 return Polygon.create(points=points) 136 137 def to_shapely_polygon(self): 138 return build_shapely_polygon_as_box( 139 miny=self.up, 140 maxy=self.down, 141 minx=self.left, 142 maxx=self.right, 143 ) 144 145 ############ 146 # Operator # 147 ############ 148 def get_center_point(self): 149 return Point.create(y=(self.up + self.down) / 2, x=(self.left + self.right) / 2) 150 151 def to_clipped_box(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]): 152 height, width = extract_shape_from_shapable_or_shape(shapable_or_shape) 153 return Box( 154 up=clip_val(self.up, height), 155 down=clip_val(self.down, height), 156 left=clip_val(self.left, width), 157 right=clip_val(self.right, width), 158 ) 159 160 def to_conducted_resized_box( 161 self, 162 shapable_or_shape: Union[Shapable, Tuple[int, int]], 163 resized_height: Optional[int] = None, 164 resized_width: Optional[int] = None, 165 ): 166 ( 167 height, 168 width, 169 resized_height, 170 resized_width, 171 ) = generate_shape_and_resized_shape( 172 shapable_or_shape=shapable_or_shape, 173 resized_height=resized_height, 174 resized_width=resized_width 175 ) 176 return Box( 177 up=round(resize_val(self.up, height, resized_height)), 178 down=round(resize_val(self.down, height, resized_height)), 179 left=round(resize_val(self.left, width, resized_width)), 180 right=round(resize_val(self.right, width, resized_width)), 181 ) 182 183 def to_resized_box( 184 self, 185 resized_height: Optional[int] = None, 186 resized_width: Optional[int] = None, 187 ): 188 return self.to_conducted_resized_box( 189 shapable_or_shape=self, 190 resized_height=resized_height, 191 resized_width=resized_width, 192 ) 193 194 def to_shifted_box(self, offset_y: int = 0, offset_x: int = 0): 195 return Box( 196 up=self.up + offset_y, 197 down=self.down + offset_y, 198 left=self.left + offset_x, 199 right=self.right + offset_x, 200 ) 201 202 def to_relative_box(self, origin_y: int, origin_x: int): 203 return self.to_shifted_box(offset_y=-origin_y, offset_x=-origin_x) 204 205 def to_dilated_box(self, ratio: float, clip_long_side: bool = False): 206 expand_vert = math.ceil(self.height * ratio / 2) 207 expand_hori = math.ceil(self.width * ratio / 2) 208 209 if clip_long_side: 210 expand_min = min(expand_vert, expand_hori) 211 expand_vert = expand_min 212 expand_hori = expand_min 213 214 return Box( 215 up=self.up - expand_vert, 216 down=self.down + expand_vert, 217 left=self.left - expand_hori, 218 right=self.right + expand_hori, 219 ) 220 221 def get_boxes_for_box_attached_opt(self, element_box: Optional['Box']): 222 if element_box is None: 223 relative_box = self 224 new_element_box = None 225 226 else: 227 assert element_box.up <= self.up <= self.down <= element_box.down 228 assert element_box.left <= self.left <= self.right <= element_box.right 229 230 # NOTE: Some shape, implicitly. 231 relative_box = self.to_relative_box( 232 origin_y=element_box.up, 233 origin_x=element_box.left, 234 ) 235 new_element_box = self 236 237 return relative_box, new_element_box 238 239 def extract_np_array(self, mat: np.ndarray) -> np.ndarray: 240 assert 0 <= self.up <= self.down <= mat.shape[0] 241 assert 0 <= self.left <= self.right <= mat.shape[1] 242 return mat[self.up:self.down + 1, self.left:self.right + 1] 243 244 def extract_mask(self, mask: 'Mask'): 245 relative_box, new_mask_box = self.get_boxes_for_box_attached_opt(mask.box) 246 247 if relative_box.shape == mask.shape: 248 return mask 249 250 return attrs.evolve( 251 mask, 252 mat=relative_box.extract_np_array(mask.mat), 253 box=new_mask_box, 254 ) 255 256 def extract_score_map(self, score_map: 'ScoreMap'): 257 relative_box, new_score_map_box = self.get_boxes_for_box_attached_opt(score_map.box) 258 259 if relative_box.shape == score_map.shape: 260 return score_map 261 262 return attrs.evolve( 263 score_map, 264 mat=relative_box.extract_np_array(score_map.mat), 265 box=new_score_map_box, 266 ) 267 268 def extract_image(self, image: 'Image'): 269 relative_box, new_image_box = self.get_boxes_for_box_attached_opt(image.box) 270 271 if relative_box.shape == image.shape: 272 return image 273 274 return attrs.evolve( 275 image, 276 mat=relative_box.extract_np_array(image.mat), 277 box=new_image_box, 278 ) 279 280 def prep_mat_and_value( 281 self, 282 mat: np.ndarray, 283 value: Union[np.ndarray, Tuple[float, ...], float], 284 ): 285 mat_shape_before_extraction = (mat.shape[0], mat.shape[1]) 286 if mat_shape_before_extraction != self.shape: 287 mat = self.extract_np_array(mat) 288 289 if isinstance(value, np.ndarray): 290 value_shape_before_extraction = (value.shape[0], value.shape[1]) 291 if value_shape_before_extraction != (mat.shape[0], mat.shape[1]): 292 assert value_shape_before_extraction == mat_shape_before_extraction 293 value = self.extract_np_array(value) 294 295 if value.dtype != mat.dtype: 296 value = value.astype(mat.dtype) 297 298 return mat, value 299 300 @classmethod 301 def get_np_mask_from_element_mask(cls, element_mask: Optional[Union['Mask', np.ndarray]]): 302 np_mask = None 303 if element_mask: 304 if isinstance(element_mask, Mask): 305 # NOTE: Mask.box is ignored. 306 np_mask = element_mask.np_mask 307 else: 308 np_mask = element_mask 309 return np_mask 310 311 def fill_np_array( 312 self, 313 mat: np.ndarray, 314 value: Union[np.ndarray, Tuple[float, ...], float], 315 np_mask: Optional[np.ndarray] = None, 316 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 317 keep_max_value: bool = False, 318 keep_min_value: bool = False, 319 ): 320 mat, value = self.prep_mat_and_value(mat, value) 321 322 if isinstance(alpha, ScoreMap): 323 # NOTE: 324 # 1. Place before np_mask to simplify ScoreMap opts. 325 # 2. ScoreMap.box is ignored. 326 assert alpha.is_prob 327 alpha = alpha.mat 328 329 if np_mask is None and isinstance(alpha, np.ndarray): 330 # For optimizing sparse alpha matrix. 331 np_mask = (alpha > 0.0) 332 333 fill_np_array( 334 mat=mat, 335 value=value, 336 np_mask=np_mask, 337 alpha=alpha, 338 keep_max_value=keep_max_value, 339 keep_min_value=keep_min_value, 340 ) 341 342 def fill_mask( 343 self, 344 mask: 'Mask', 345 value: Union['Mask', np.ndarray, int] = 1, 346 mask_mask: Optional[Union['Mask', np.ndarray]] = None, 347 keep_max_value: bool = False, 348 keep_min_value: bool = False, 349 ): 350 relative_box, _ = self.get_boxes_for_box_attached_opt(mask.box) 351 352 if isinstance(value, Mask): 353 if value.shape != self.shape: 354 value = self.extract_mask(value) 355 value = value.mat 356 357 np_mask = self.get_np_mask_from_element_mask(mask_mask) 358 359 with mask.writable_context: 360 relative_box.fill_np_array( 361 mask.mat, 362 value, 363 np_mask=np_mask, 364 keep_max_value=keep_max_value, 365 keep_min_value=keep_min_value, 366 ) 367 368 def fill_score_map( 369 self, 370 score_map: 'ScoreMap', 371 value: Union['ScoreMap', np.ndarray, float], 372 score_map_mask: Optional[Union['Mask', np.ndarray]] = None, 373 keep_max_value: bool = False, 374 keep_min_value: bool = False, 375 ): 376 relative_box, _ = self.get_boxes_for_box_attached_opt(score_map.box) 377 378 if isinstance(value, ScoreMap): 379 if value.shape != self.shape: 380 value = self.extract_score_map(value) 381 value = value.mat 382 383 np_mask = self.get_np_mask_from_element_mask(score_map_mask) 384 385 with score_map.writable_context: 386 relative_box.fill_np_array( 387 score_map.mat, 388 value, 389 np_mask=np_mask, 390 keep_max_value=keep_max_value, 391 keep_min_value=keep_min_value, 392 ) 393 394 def fill_image( 395 self, 396 image: 'Image', 397 value: Union['Image', np.ndarray, Tuple[int, ...], int], 398 image_mask: Optional[Union['Mask', np.ndarray]] = None, 399 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 400 ): 401 relative_box, _ = self.get_boxes_for_box_attached_opt(image.box) 402 403 if isinstance(value, Image): 404 if value.shape != self.shape: 405 value = self.extract_image(value) 406 value = value.mat 407 408 np_mask = self.get_np_mask_from_element_mask(image_mask) 409 410 with image.writable_context: 411 relative_box.fill_np_array( 412 image.mat, 413 value, 414 np_mask=np_mask, 415 alpha=alpha, 416 ) 417 418 419class BoxOverlappingValidator: 420 421 def __init__(self, boxes: Iterable[Box]): 422 geoms = tuple(box.to_shapely_polygon() for box in boxes) 423 self.strtree = STRtree(geoms) 424 425 def is_overlapped(self, box: Box): 426 shapely_polygon = box.to_shapely_polygon() 427 # NOTE: No need to test intersection, since the extent (bounding box) of a box is itself. 428 return any(True for _ in self.strtree.query(shapely_polygon)) 429 430 431def generate_fill_by_boxes_mask( 432 shape: Tuple[int, int], 433 boxes: Iterable[Box], 434 mode: ElementSetOperationMode, 435): 436 if mode == ElementSetOperationMode.UNION: 437 return None 438 else: 439 return Mask.from_boxes(shape, boxes, mode) 440 441 442# Cyclic dependency, by design. 443from .point import Point, PointList, PointTuple # noqa: E402 444from .polygon import Polygon # noqa: E402 445from .mask import Mask # noqa: E402 446from .score_map import ScoreMap # noqa: E402 447from .image import Image # noqa: E402
34class Box(Shapable): 35 # By design, smooth positioning is not supported in Box. 36 up: int 37 down: int 38 left: int 39 right: int 40 41 ############### 42 # Constructor # 43 ############### 44 @classmethod 45 def from_shape(cls, shape: Tuple[int, int]): 46 height, width = shape 47 return cls( 48 up=0, 49 down=height - 1, 50 left=0, 51 right=width - 1, 52 ) 53 54 @classmethod 55 def from_shapable(cls, shapable: Shapable): 56 return cls.from_shape(shapable.shape) 57 58 @classmethod 59 def from_boxes(cls, boxes: Iterable['Box']): 60 # Build a bounding box. 61 boxes_iter = iter(boxes) 62 63 first_box = next(boxes_iter) 64 up = first_box.up 65 down = first_box.down 66 left = first_box.left 67 right = first_box.right 68 69 for box in boxes_iter: 70 up = min(up, box.up) 71 down = max(down, box.down) 72 left = min(left, box.left) 73 right = max(right, box.right) 74 75 return cls(up=up, down=down, left=left, right=right) 76 77 ############ 78 # Property # 79 ############ 80 @property 81 def height(self): 82 return self.down + 1 - self.up 83 84 @property 85 def width(self): 86 return self.right + 1 - self.left 87 88 @property 89 def valid(self): 90 return (0 <= self.up <= self.down) and (0 <= self.left <= self.right) 91 92 ############## 93 # Conversion # 94 ############## 95 def to_polygon(self, step: Optional[int] = None): 96 if self.up == self.down or self.left == self.right: 97 raise RuntimeError(f'Cannot convert box={self} to polygon.') 98 99 # NOTE: Up left -> up right -> down right -> down left 100 # Char-level labelings are generated based on this ordering. 101 if step is None: 102 points = PointTuple.from_xy_pairs(( 103 (self.left, self.up), 104 (self.right, self.up), 105 (self.right, self.down), 106 (self.left, self.down), 107 )) 108 109 else: 110 assert step > 0 111 112 xs = list(range(self.left, self.right + 1, step)) 113 if xs[-1] < self.right: 114 xs.append(self.right) 115 116 ys = list(range(self.up, self.down + 1, step)) 117 if ys[-1] == self.down: 118 # NOTE: check first to avoid oob error. 119 ys.pop() 120 ys.pop(0) 121 122 points = PointList() 123 # Up. 124 for x in xs: 125 points.append(Point.create(y=self.up, x=x)) 126 # Right. 127 for y in ys: 128 points.append(Point.create(y=y, x=self.right)) 129 # Down. 130 for x in reversed(xs): 131 points.append(Point.create(y=self.down, x=x)) 132 # Left. 133 for y in reversed(ys): 134 points.append(Point.create(y=y, x=self.left)) 135 136 return Polygon.create(points=points) 137 138 def to_shapely_polygon(self): 139 return build_shapely_polygon_as_box( 140 miny=self.up, 141 maxy=self.down, 142 minx=self.left, 143 maxx=self.right, 144 ) 145 146 ############ 147 # Operator # 148 ############ 149 def get_center_point(self): 150 return Point.create(y=(self.up + self.down) / 2, x=(self.left + self.right) / 2) 151 152 def to_clipped_box(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]): 153 height, width = extract_shape_from_shapable_or_shape(shapable_or_shape) 154 return Box( 155 up=clip_val(self.up, height), 156 down=clip_val(self.down, height), 157 left=clip_val(self.left, width), 158 right=clip_val(self.right, width), 159 ) 160 161 def to_conducted_resized_box( 162 self, 163 shapable_or_shape: Union[Shapable, Tuple[int, int]], 164 resized_height: Optional[int] = None, 165 resized_width: Optional[int] = None, 166 ): 167 ( 168 height, 169 width, 170 resized_height, 171 resized_width, 172 ) = generate_shape_and_resized_shape( 173 shapable_or_shape=shapable_or_shape, 174 resized_height=resized_height, 175 resized_width=resized_width 176 ) 177 return Box( 178 up=round(resize_val(self.up, height, resized_height)), 179 down=round(resize_val(self.down, height, resized_height)), 180 left=round(resize_val(self.left, width, resized_width)), 181 right=round(resize_val(self.right, width, resized_width)), 182 ) 183 184 def to_resized_box( 185 self, 186 resized_height: Optional[int] = None, 187 resized_width: Optional[int] = None, 188 ): 189 return self.to_conducted_resized_box( 190 shapable_or_shape=self, 191 resized_height=resized_height, 192 resized_width=resized_width, 193 ) 194 195 def to_shifted_box(self, offset_y: int = 0, offset_x: int = 0): 196 return Box( 197 up=self.up + offset_y, 198 down=self.down + offset_y, 199 left=self.left + offset_x, 200 right=self.right + offset_x, 201 ) 202 203 def to_relative_box(self, origin_y: int, origin_x: int): 204 return self.to_shifted_box(offset_y=-origin_y, offset_x=-origin_x) 205 206 def to_dilated_box(self, ratio: float, clip_long_side: bool = False): 207 expand_vert = math.ceil(self.height * ratio / 2) 208 expand_hori = math.ceil(self.width * ratio / 2) 209 210 if clip_long_side: 211 expand_min = min(expand_vert, expand_hori) 212 expand_vert = expand_min 213 expand_hori = expand_min 214 215 return Box( 216 up=self.up - expand_vert, 217 down=self.down + expand_vert, 218 left=self.left - expand_hori, 219 right=self.right + expand_hori, 220 ) 221 222 def get_boxes_for_box_attached_opt(self, element_box: Optional['Box']): 223 if element_box is None: 224 relative_box = self 225 new_element_box = None 226 227 else: 228 assert element_box.up <= self.up <= self.down <= element_box.down 229 assert element_box.left <= self.left <= self.right <= element_box.right 230 231 # NOTE: Some shape, implicitly. 232 relative_box = self.to_relative_box( 233 origin_y=element_box.up, 234 origin_x=element_box.left, 235 ) 236 new_element_box = self 237 238 return relative_box, new_element_box 239 240 def extract_np_array(self, mat: np.ndarray) -> np.ndarray: 241 assert 0 <= self.up <= self.down <= mat.shape[0] 242 assert 0 <= self.left <= self.right <= mat.shape[1] 243 return mat[self.up:self.down + 1, self.left:self.right + 1] 244 245 def extract_mask(self, mask: 'Mask'): 246 relative_box, new_mask_box = self.get_boxes_for_box_attached_opt(mask.box) 247 248 if relative_box.shape == mask.shape: 249 return mask 250 251 return attrs.evolve( 252 mask, 253 mat=relative_box.extract_np_array(mask.mat), 254 box=new_mask_box, 255 ) 256 257 def extract_score_map(self, score_map: 'ScoreMap'): 258 relative_box, new_score_map_box = self.get_boxes_for_box_attached_opt(score_map.box) 259 260 if relative_box.shape == score_map.shape: 261 return score_map 262 263 return attrs.evolve( 264 score_map, 265 mat=relative_box.extract_np_array(score_map.mat), 266 box=new_score_map_box, 267 ) 268 269 def extract_image(self, image: 'Image'): 270 relative_box, new_image_box = self.get_boxes_for_box_attached_opt(image.box) 271 272 if relative_box.shape == image.shape: 273 return image 274 275 return attrs.evolve( 276 image, 277 mat=relative_box.extract_np_array(image.mat), 278 box=new_image_box, 279 ) 280 281 def prep_mat_and_value( 282 self, 283 mat: np.ndarray, 284 value: Union[np.ndarray, Tuple[float, ...], float], 285 ): 286 mat_shape_before_extraction = (mat.shape[0], mat.shape[1]) 287 if mat_shape_before_extraction != self.shape: 288 mat = self.extract_np_array(mat) 289 290 if isinstance(value, np.ndarray): 291 value_shape_before_extraction = (value.shape[0], value.shape[1]) 292 if value_shape_before_extraction != (mat.shape[0], mat.shape[1]): 293 assert value_shape_before_extraction == mat_shape_before_extraction 294 value = self.extract_np_array(value) 295 296 if value.dtype != mat.dtype: 297 value = value.astype(mat.dtype) 298 299 return mat, value 300 301 @classmethod 302 def get_np_mask_from_element_mask(cls, element_mask: Optional[Union['Mask', np.ndarray]]): 303 np_mask = None 304 if element_mask: 305 if isinstance(element_mask, Mask): 306 # NOTE: Mask.box is ignored. 307 np_mask = element_mask.np_mask 308 else: 309 np_mask = element_mask 310 return np_mask 311 312 def fill_np_array( 313 self, 314 mat: np.ndarray, 315 value: Union[np.ndarray, Tuple[float, ...], float], 316 np_mask: Optional[np.ndarray] = None, 317 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 318 keep_max_value: bool = False, 319 keep_min_value: bool = False, 320 ): 321 mat, value = self.prep_mat_and_value(mat, value) 322 323 if isinstance(alpha, ScoreMap): 324 # NOTE: 325 # 1. Place before np_mask to simplify ScoreMap opts. 326 # 2. ScoreMap.box is ignored. 327 assert alpha.is_prob 328 alpha = alpha.mat 329 330 if np_mask is None and isinstance(alpha, np.ndarray): 331 # For optimizing sparse alpha matrix. 332 np_mask = (alpha > 0.0) 333 334 fill_np_array( 335 mat=mat, 336 value=value, 337 np_mask=np_mask, 338 alpha=alpha, 339 keep_max_value=keep_max_value, 340 keep_min_value=keep_min_value, 341 ) 342 343 def fill_mask( 344 self, 345 mask: 'Mask', 346 value: Union['Mask', np.ndarray, int] = 1, 347 mask_mask: Optional[Union['Mask', np.ndarray]] = None, 348 keep_max_value: bool = False, 349 keep_min_value: bool = False, 350 ): 351 relative_box, _ = self.get_boxes_for_box_attached_opt(mask.box) 352 353 if isinstance(value, Mask): 354 if value.shape != self.shape: 355 value = self.extract_mask(value) 356 value = value.mat 357 358 np_mask = self.get_np_mask_from_element_mask(mask_mask) 359 360 with mask.writable_context: 361 relative_box.fill_np_array( 362 mask.mat, 363 value, 364 np_mask=np_mask, 365 keep_max_value=keep_max_value, 366 keep_min_value=keep_min_value, 367 ) 368 369 def fill_score_map( 370 self, 371 score_map: 'ScoreMap', 372 value: Union['ScoreMap', np.ndarray, float], 373 score_map_mask: Optional[Union['Mask', np.ndarray]] = None, 374 keep_max_value: bool = False, 375 keep_min_value: bool = False, 376 ): 377 relative_box, _ = self.get_boxes_for_box_attached_opt(score_map.box) 378 379 if isinstance(value, ScoreMap): 380 if value.shape != self.shape: 381 value = self.extract_score_map(value) 382 value = value.mat 383 384 np_mask = self.get_np_mask_from_element_mask(score_map_mask) 385 386 with score_map.writable_context: 387 relative_box.fill_np_array( 388 score_map.mat, 389 value, 390 np_mask=np_mask, 391 keep_max_value=keep_max_value, 392 keep_min_value=keep_min_value, 393 ) 394 395 def fill_image( 396 self, 397 image: 'Image', 398 value: Union['Image', np.ndarray, Tuple[int, ...], int], 399 image_mask: Optional[Union['Mask', np.ndarray]] = None, 400 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 401 ): 402 relative_box, _ = self.get_boxes_for_box_attached_opt(image.box) 403 404 if isinstance(value, Image): 405 if value.shape != self.shape: 406 value = self.extract_image(value) 407 value = value.mat 408 409 np_mask = self.get_np_mask_from_element_mask(image_mask) 410 411 with image.writable_context: 412 relative_box.fill_np_array( 413 image.mat, 414 value, 415 np_mask=np_mask, 416 alpha=alpha, 417 )
Box(up: int, down: int, left: int, right: int)
2def __init__(self, up, down, left, right): 3 _setattr = _cached_setattr_get(self) 4 _setattr('up', up) 5 _setattr('down', down) 6 _setattr('left', left) 7 _setattr('right', right)
Method generated by attrs for class Box.
58 @classmethod 59 def from_boxes(cls, boxes: Iterable['Box']): 60 # Build a bounding box. 61 boxes_iter = iter(boxes) 62 63 first_box = next(boxes_iter) 64 up = first_box.up 65 down = first_box.down 66 left = first_box.left 67 right = first_box.right 68 69 for box in boxes_iter: 70 up = min(up, box.up) 71 down = max(down, box.down) 72 left = min(left, box.left) 73 right = max(right, box.right) 74 75 return cls(up=up, down=down, left=left, right=right)
def
to_polygon(self, step: Union[int, NoneType] = None):
95 def to_polygon(self, step: Optional[int] = None): 96 if self.up == self.down or self.left == self.right: 97 raise RuntimeError(f'Cannot convert box={self} to polygon.') 98 99 # NOTE: Up left -> up right -> down right -> down left 100 # Char-level labelings are generated based on this ordering. 101 if step is None: 102 points = PointTuple.from_xy_pairs(( 103 (self.left, self.up), 104 (self.right, self.up), 105 (self.right, self.down), 106 (self.left, self.down), 107 )) 108 109 else: 110 assert step > 0 111 112 xs = list(range(self.left, self.right + 1, step)) 113 if xs[-1] < self.right: 114 xs.append(self.right) 115 116 ys = list(range(self.up, self.down + 1, step)) 117 if ys[-1] == self.down: 118 # NOTE: check first to avoid oob error. 119 ys.pop() 120 ys.pop(0) 121 122 points = PointList() 123 # Up. 124 for x in xs: 125 points.append(Point.create(y=self.up, x=x)) 126 # Right. 127 for y in ys: 128 points.append(Point.create(y=y, x=self.right)) 129 # Down. 130 for x in reversed(xs): 131 points.append(Point.create(y=self.down, x=x)) 132 # Left. 133 for y in reversed(ys): 134 points.append(Point.create(y=y, x=self.left)) 135 136 return Polygon.create(points=points)
152 def to_clipped_box(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]): 153 height, width = extract_shape_from_shapable_or_shape(shapable_or_shape) 154 return Box( 155 up=clip_val(self.up, height), 156 down=clip_val(self.down, height), 157 left=clip_val(self.left, width), 158 right=clip_val(self.right, width), 159 )
def
to_conducted_resized_box( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]], resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
161 def to_conducted_resized_box( 162 self, 163 shapable_or_shape: Union[Shapable, Tuple[int, int]], 164 resized_height: Optional[int] = None, 165 resized_width: Optional[int] = None, 166 ): 167 ( 168 height, 169 width, 170 resized_height, 171 resized_width, 172 ) = generate_shape_and_resized_shape( 173 shapable_or_shape=shapable_or_shape, 174 resized_height=resized_height, 175 resized_width=resized_width 176 ) 177 return Box( 178 up=round(resize_val(self.up, height, resized_height)), 179 down=round(resize_val(self.down, height, resized_height)), 180 left=round(resize_val(self.left, width, resized_width)), 181 right=round(resize_val(self.right, width, resized_width)), 182 )
def
to_resized_box( self, resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
def
to_dilated_box(self, ratio: float, clip_long_side: bool = False):
206 def to_dilated_box(self, ratio: float, clip_long_side: bool = False): 207 expand_vert = math.ceil(self.height * ratio / 2) 208 expand_hori = math.ceil(self.width * ratio / 2) 209 210 if clip_long_side: 211 expand_min = min(expand_vert, expand_hori) 212 expand_vert = expand_min 213 expand_hori = expand_min 214 215 return Box( 216 up=self.up - expand_vert, 217 down=self.down + expand_vert, 218 left=self.left - expand_hori, 219 right=self.right + expand_hori, 220 )
222 def get_boxes_for_box_attached_opt(self, element_box: Optional['Box']): 223 if element_box is None: 224 relative_box = self 225 new_element_box = None 226 227 else: 228 assert element_box.up <= self.up <= self.down <= element_box.down 229 assert element_box.left <= self.left <= self.right <= element_box.right 230 231 # NOTE: Some shape, implicitly. 232 relative_box = self.to_relative_box( 233 origin_y=element_box.up, 234 origin_x=element_box.left, 235 ) 236 new_element_box = self 237 238 return relative_box, new_element_box
257 def extract_score_map(self, score_map: 'ScoreMap'): 258 relative_box, new_score_map_box = self.get_boxes_for_box_attached_opt(score_map.box) 259 260 if relative_box.shape == score_map.shape: 261 return score_map 262 263 return attrs.evolve( 264 score_map, 265 mat=relative_box.extract_np_array(score_map.mat), 266 box=new_score_map_box, 267 )
269 def extract_image(self, image: 'Image'): 270 relative_box, new_image_box = self.get_boxes_for_box_attached_opt(image.box) 271 272 if relative_box.shape == image.shape: 273 return image 274 275 return attrs.evolve( 276 image, 277 mat=relative_box.extract_np_array(image.mat), 278 box=new_image_box, 279 )
def
prep_mat_and_value( self, mat: numpy.ndarray, value: Union[numpy.ndarray, Tuple[float, ...], float]):
281 def prep_mat_and_value( 282 self, 283 mat: np.ndarray, 284 value: Union[np.ndarray, Tuple[float, ...], float], 285 ): 286 mat_shape_before_extraction = (mat.shape[0], mat.shape[1]) 287 if mat_shape_before_extraction != self.shape: 288 mat = self.extract_np_array(mat) 289 290 if isinstance(value, np.ndarray): 291 value_shape_before_extraction = (value.shape[0], value.shape[1]) 292 if value_shape_before_extraction != (mat.shape[0], mat.shape[1]): 293 assert value_shape_before_extraction == mat_shape_before_extraction 294 value = self.extract_np_array(value) 295 296 if value.dtype != mat.dtype: 297 value = value.astype(mat.dtype) 298 299 return mat, value
@classmethod
def
get_np_mask_from_element_mask( cls, element_mask: Union[vkit.element.mask.Mask, numpy.ndarray, NoneType]):
301 @classmethod 302 def get_np_mask_from_element_mask(cls, element_mask: Optional[Union['Mask', np.ndarray]]): 303 np_mask = None 304 if element_mask: 305 if isinstance(element_mask, Mask): 306 # NOTE: Mask.box is ignored. 307 np_mask = element_mask.np_mask 308 else: 309 np_mask = element_mask 310 return np_mask
def
fill_np_array( self, mat: numpy.ndarray, value: Union[numpy.ndarray, Tuple[float, ...], float], np_mask: Union[numpy.ndarray, NoneType] = None, alpha: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0, keep_max_value: bool = False, keep_min_value: bool = False):
312 def fill_np_array( 313 self, 314 mat: np.ndarray, 315 value: Union[np.ndarray, Tuple[float, ...], float], 316 np_mask: Optional[np.ndarray] = None, 317 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 318 keep_max_value: bool = False, 319 keep_min_value: bool = False, 320 ): 321 mat, value = self.prep_mat_and_value(mat, value) 322 323 if isinstance(alpha, ScoreMap): 324 # NOTE: 325 # 1. Place before np_mask to simplify ScoreMap opts. 326 # 2. ScoreMap.box is ignored. 327 assert alpha.is_prob 328 alpha = alpha.mat 329 330 if np_mask is None and isinstance(alpha, np.ndarray): 331 # For optimizing sparse alpha matrix. 332 np_mask = (alpha > 0.0) 333 334 fill_np_array( 335 mat=mat, 336 value=value, 337 np_mask=np_mask, 338 alpha=alpha, 339 keep_max_value=keep_max_value, 340 keep_min_value=keep_min_value, 341 )
def
fill_mask( self, mask: vkit.element.mask.Mask, value: Union[vkit.element.mask.Mask, numpy.ndarray, int] = 1, mask_mask: Union[vkit.element.mask.Mask, numpy.ndarray, NoneType] = None, keep_max_value: bool = False, keep_min_value: bool = False):
343 def fill_mask( 344 self, 345 mask: 'Mask', 346 value: Union['Mask', np.ndarray, int] = 1, 347 mask_mask: Optional[Union['Mask', np.ndarray]] = None, 348 keep_max_value: bool = False, 349 keep_min_value: bool = False, 350 ): 351 relative_box, _ = self.get_boxes_for_box_attached_opt(mask.box) 352 353 if isinstance(value, Mask): 354 if value.shape != self.shape: 355 value = self.extract_mask(value) 356 value = value.mat 357 358 np_mask = self.get_np_mask_from_element_mask(mask_mask) 359 360 with mask.writable_context: 361 relative_box.fill_np_array( 362 mask.mat, 363 value, 364 np_mask=np_mask, 365 keep_max_value=keep_max_value, 366 keep_min_value=keep_min_value, 367 )
def
fill_score_map( self, score_map: vkit.element.score_map.ScoreMap, value: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float], score_map_mask: Union[vkit.element.mask.Mask, numpy.ndarray, NoneType] = None, keep_max_value: bool = False, keep_min_value: bool = False):
369 def fill_score_map( 370 self, 371 score_map: 'ScoreMap', 372 value: Union['ScoreMap', np.ndarray, float], 373 score_map_mask: Optional[Union['Mask', np.ndarray]] = None, 374 keep_max_value: bool = False, 375 keep_min_value: bool = False, 376 ): 377 relative_box, _ = self.get_boxes_for_box_attached_opt(score_map.box) 378 379 if isinstance(value, ScoreMap): 380 if value.shape != self.shape: 381 value = self.extract_score_map(value) 382 value = value.mat 383 384 np_mask = self.get_np_mask_from_element_mask(score_map_mask) 385 386 with score_map.writable_context: 387 relative_box.fill_np_array( 388 score_map.mat, 389 value, 390 np_mask=np_mask, 391 keep_max_value=keep_max_value, 392 keep_min_value=keep_min_value, 393 )
def
fill_image( self, image: vkit.element.image.Image, value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], image_mask: Union[vkit.element.mask.Mask, numpy.ndarray, NoneType] = None, alpha: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0):
395 def fill_image( 396 self, 397 image: 'Image', 398 value: Union['Image', np.ndarray, Tuple[int, ...], int], 399 image_mask: Optional[Union['Mask', np.ndarray]] = None, 400 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 401 ): 402 relative_box, _ = self.get_boxes_for_box_attached_opt(image.box) 403 404 if isinstance(value, Image): 405 if value.shape != self.shape: 406 value = self.extract_image(value) 407 value = value.mat 408 409 np_mask = self.get_np_mask_from_element_mask(image_mask) 410 411 with image.writable_context: 412 relative_box.fill_np_array( 413 image.mat, 414 value, 415 np_mask=np_mask, 416 alpha=alpha, 417 )
class
BoxOverlappingValidator:
420class BoxOverlappingValidator: 421 422 def __init__(self, boxes: Iterable[Box]): 423 geoms = tuple(box.to_shapely_polygon() for box in boxes) 424 self.strtree = STRtree(geoms) 425 426 def is_overlapped(self, box: Box): 427 shapely_polygon = box.to_shapely_polygon() 428 # NOTE: No need to test intersection, since the extent (bounding box) of a box is itself. 429 return any(True for _ in self.strtree.query(shapely_polygon))
BoxOverlappingValidator(boxes: Iterable[vkit.element.box.Box])
def
generate_fill_by_boxes_mask( shape: Tuple[int, int], boxes: Iterable[vkit.element.box.Box], mode: vkit.element.type.ElementSetOperationMode):