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