vkit.element.polygon
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 cast, Optional, Tuple, Union, Sequence, Iterable, List 15import logging 16import math 17 18import attrs 19import numpy as np 20import cv2 as cv 21from shapely.geometry import ( 22 Polygon as ShapelyPolygon, 23 MultiPolygon as ShapelyMultiPolygon, 24 CAP_STYLE, 25 JOIN_STYLE, 26) 27from shapely.ops import unary_union 28import pyclipper 29 30from vkit.utility import attrs_lazy_field 31from .type import Shapable, ElementSetOperationMode 32 33logger = logging.getLogger(__name__) 34 35_T = Union[float, str] 36 37 38@attrs.define 39class PolygonInternals: 40 bounding_box: 'Box' 41 np_self_relative_points: np.ndarray 42 43 _area: Optional[float] = attrs_lazy_field() 44 _self_relative_polygon: Optional['Polygon'] = attrs_lazy_field() 45 _np_mask: Optional[np.ndarray] = attrs_lazy_field() 46 _mask: Optional['Mask'] = attrs_lazy_field() 47 48 def lazy_post_init_area(self): 49 if self._area is not None: 50 return self._area 51 52 self._area = float(ShapelyPolygon(self.np_self_relative_points).area) 53 return self._area 54 55 @property 56 def area(self): 57 return self.lazy_post_init_area() 58 59 def lazy_post_init_self_relative_polygon(self): 60 if self._self_relative_polygon is not None: 61 return self._self_relative_polygon 62 63 self._self_relative_polygon = Polygon.from_np_array(self.np_self_relative_points) 64 return self._self_relative_polygon 65 66 @property 67 def self_relative_polygon(self): 68 return self.lazy_post_init_self_relative_polygon() 69 70 def lazy_post_init_np_mask(self): 71 if self._np_mask is not None: 72 return self._np_mask 73 74 np_mask = np.zeros(self.bounding_box.shape, dtype=np.uint8) 75 cv.fillPoly(np_mask, [self.self_relative_polygon.to_np_array()], 1) 76 self._np_mask = np_mask.astype(np.bool_) 77 return self._np_mask 78 79 @property 80 def np_mask(self): 81 return self.lazy_post_init_np_mask() 82 83 def lazy_post_init_mask(self): 84 if self._mask is not None: 85 return self._mask 86 87 self._mask = Mask(mat=self.np_mask.astype(np.uint8)) 88 self._mask = self._mask.to_box_attached(self.bounding_box) 89 return self._mask 90 91 @property 92 def mask(self): 93 return self.lazy_post_init_mask() 94 95 96@attrs.define(frozen=True, eq=False) 97class Polygon: 98 points: 'PointTuple' 99 100 _internals: Optional[PolygonInternals] = attrs_lazy_field() 101 102 def __attrs_post_init__(self): 103 assert self.points 104 105 def lazy_post_init_internals(self): 106 if self._internals is not None: 107 return self._internals 108 109 np_self_relative_points = self.to_smooth_np_array() 110 111 y_min = np_self_relative_points[:, 1].min() 112 y_max = np_self_relative_points[:, 1].max() 113 114 x_min = np_self_relative_points[:, 0].min() 115 x_max = np_self_relative_points[:, 0].max() 116 117 np_self_relative_points[:, 0] -= x_min 118 np_self_relative_points[:, 1] -= y_min 119 120 bounding_box = Box( 121 up=round(y_min), 122 down=round(y_max), 123 left=round(x_min), 124 right=round(x_max), 125 ) 126 127 object.__setattr__( 128 self, 129 '_internals', 130 PolygonInternals( 131 bounding_box=bounding_box, 132 np_self_relative_points=np_self_relative_points, 133 ), 134 ) 135 return cast(PolygonInternals, self._internals) 136 137 ############### 138 # Constructor # 139 ############### 140 @classmethod 141 def create(cls, points: Union['PointList', 'PointTuple', Iterable['Point']]): 142 return cls(points=PointTuple(points)) 143 144 ############ 145 # Property # 146 ############ 147 @property 148 def num_points(self): 149 return len(self.points) 150 151 @property 152 def internals(self): 153 return self.lazy_post_init_internals() 154 155 @property 156 def area(self): 157 return self.internals.area 158 159 @property 160 def bounding_box(self): 161 return self.internals.bounding_box 162 163 @property 164 def self_relative_polygon(self): 165 return self.internals.self_relative_polygon 166 167 @property 168 def mask(self): 169 return self.internals.mask 170 171 ############## 172 # Conversion # 173 ############## 174 @classmethod 175 def from_xy_pairs(cls, xy_pairs: Iterable[Tuple[_T, _T]]): 176 return cls(points=PointTuple.from_xy_pairs(xy_pairs)) 177 178 def to_xy_pairs(self): 179 return self.points.to_xy_pairs() 180 181 def to_smooth_xy_pairs(self): 182 return self.points.to_smooth_xy_pairs() 183 184 @classmethod 185 def from_flatten_xy_pairs(cls, flatten_xy_pairs: Sequence[_T]): 186 return cls(points=PointTuple.from_flatten_xy_pairs(flatten_xy_pairs)) 187 188 def to_flatten_xy_pairs(self): 189 return self.points.to_flatten_xy_pairs() 190 191 def to_smooth_flatten_xy_pairs(self): 192 return self.points.to_smooth_flatten_xy_pairs() 193 194 @classmethod 195 def from_np_array(cls, np_points: np.ndarray): 196 return cls(points=PointTuple.from_np_array(np_points)) 197 198 def to_np_array(self): 199 return self.points.to_np_array() 200 201 def to_smooth_np_array(self): 202 return self.points.to_smooth_np_array() 203 204 @classmethod 205 def from_shapely_polygon(cls, shapely_polygon: ShapelyPolygon): 206 xy_pairs = cls.remove_duplicated_xy_pairs(shapely_polygon.exterior.coords) # type: ignore 207 return cls.from_xy_pairs(xy_pairs) 208 209 def to_shapely_polygon(self): 210 return ShapelyPolygon(self.to_xy_pairs()) 211 212 def to_smooth_shapely_polygon(self): 213 return ShapelyPolygon(self.to_smooth_xy_pairs()) 214 215 ############ 216 # Operator # 217 ############ 218 def get_center_point(self): 219 shapely_polygon = self.to_smooth_shapely_polygon() 220 centroid = shapely_polygon.centroid 221 x, y = centroid.coords[0] 222 return Point.create(y=y, x=x) 223 224 def get_rectangular_height(self): 225 # See Box.to_polygon. 226 assert self.num_points == 4 227 ( 228 point_up_left, 229 point_up_right, 230 point_down_right, 231 point_down_left, 232 ) = self.points 233 left_side_height = math.hypot( 234 point_up_left.smooth_y - point_down_left.smooth_y, 235 point_up_left.smooth_x - point_down_left.smooth_x, 236 ) 237 right_side_height = math.hypot( 238 point_up_right.smooth_y - point_down_right.smooth_y, 239 point_up_right.smooth_x - point_down_right.smooth_x, 240 ) 241 return (left_side_height + right_side_height) / 2 242 243 def get_rectangular_width(self): 244 # See Box.to_polygon. 245 assert self.num_points == 4 246 ( 247 point_up_left, 248 point_up_right, 249 point_down_right, 250 point_down_left, 251 ) = self.points 252 up_side_width = math.hypot( 253 point_up_left.smooth_y - point_up_right.smooth_y, 254 point_up_left.smooth_x - point_up_right.smooth_x, 255 ) 256 down_side_width = math.hypot( 257 point_down_left.smooth_y - point_down_right.smooth_y, 258 point_down_left.smooth_x - point_down_right.smooth_x, 259 ) 260 return (up_side_width + down_side_width) / 2 261 262 def to_clipped_points(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]): 263 return self.points.to_clipped_points(shapable_or_shape) 264 265 def to_clipped_polygon(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]): 266 return Polygon(points=self.to_clipped_points(shapable_or_shape)) 267 268 def to_shifted_points(self, offset_y: int = 0, offset_x: int = 0): 269 return self.points.to_shifted_points(offset_y=offset_y, offset_x=offset_x) 270 271 def to_relative_points(self, origin_y: int, origin_x: int): 272 return self.points.to_relative_points(origin_y=origin_y, origin_x=origin_x) 273 274 def to_shifted_polygon(self, offset_y: int = 0, offset_x: int = 0): 275 return Polygon(points=self.to_shifted_points(offset_y=offset_y, offset_x=offset_x)) 276 277 def to_relative_polygon(self, origin_y: int, origin_x: int): 278 return Polygon(points=self.to_relative_points(origin_y=origin_y, origin_x=origin_x)) 279 280 def to_conducted_resized_polygon( 281 self, 282 shapable_or_shape: Union[Shapable, Tuple[int, int]], 283 resized_height: Optional[int] = None, 284 resized_width: Optional[int] = None, 285 ): 286 return Polygon( 287 points=self.points.to_conducted_resized_points( 288 shapable_or_shape=shapable_or_shape, 289 resized_height=resized_height, 290 resized_width=resized_width, 291 ), 292 ) 293 294 def to_resized_polygon( 295 self, 296 resized_height: Optional[int] = None, 297 resized_width: Optional[int] = None, 298 ): 299 return self.to_conducted_resized_polygon( 300 shapable_or_shape=self.bounding_box.shape, 301 resized_height=resized_height, 302 resized_width=resized_width, 303 ) 304 305 @classmethod 306 def project_polygon_to_unit_vector(cls, np_points: np.ndarray, radian: float): 307 np_vector = np.asarray([math.cos(radian), math.sin(radian)]) 308 309 np_projected: np.ndarray = np.dot(np_points, np_vector.reshape(2, 1)).flatten() 310 scale_begin = float(np_projected.min()) 311 scale_end = float(np_projected.max()) 312 313 np_point_begin = np_vector * scale_begin 314 np_point_end = np_vector * scale_end 315 316 return np_point_begin, np_point_end 317 318 @classmethod 319 def calculate_lines_intersection_point( 320 cls, 321 np_point0: np.ndarray, 322 radian0: float, 323 np_point1: np.ndarray, 324 radian1: float, 325 ): 326 x0, y0 = np_point0 327 x1, y1 = np_point1 328 329 slope0 = np.tan(radian0) 330 slope1 = np.tan(radian1) 331 332 # Within pi / 2 + k * pi plus or minus 0.1 degree. 333 invalid_slope_abs = 572.9572133543033 334 335 if abs(slope0) > invalid_slope_abs and abs(slope1) > invalid_slope_abs: 336 raise RuntimeError('Lines are vertical.') 337 338 if abs(slope0) > invalid_slope_abs: 339 its_x = float(x0) 340 its_y = float(y1 + slope1 * (x0 - x1)) 341 342 elif abs(slope1) > invalid_slope_abs: 343 its_x = float(x1) 344 its_y = float(y0 + slope0 * (x1 - x0)) 345 346 else: 347 c0 = y0 - slope0 * x0 348 c1 = y1 - slope1 * x1 349 350 with np.errstate(divide='ignore', invalid='ignore'): 351 its_x = (c1 - c0) / (slope0 - slope1) 352 if its_x == np.inf: 353 raise RuntimeError('Lines not intersected.') 354 355 its_y = slope0 * its_x + c0 356 357 return Point.create(y=float(its_y), x=float(its_x)) 358 359 def to_bounding_rectangular_polygon( 360 self, 361 shape: Tuple[int, int], 362 angle: Optional[float] = None, 363 ): 364 if angle is None: 365 shapely_polygon = self.to_smooth_shapely_polygon() 366 367 assert isinstance(shapely_polygon.minimum_rotated_rectangle, ShapelyPolygon) 368 # NOTE: For unknown reasons, the polygon border COULD exceed the bounding box 369 # a little bit. Hence we cannot assume the bounding box contains the polygon. 370 polygon = self.from_shapely_polygon(shapely_polygon.minimum_rotated_rectangle) 371 assert polygon.num_points == 4 372 373 else: 374 # Make sure in range [0, 180). 375 angle = angle % 180 376 377 main_radian = math.radians(angle) 378 orthogonal_radian = math.radians(angle + 90) 379 380 # Project points. 381 np_smooth_points = self.to_smooth_np_array() 382 ( 383 np_main_point_begin, 384 np_main_point_end, 385 ) = self.project_polygon_to_unit_vector( 386 np_points=np_smooth_points, 387 radian=main_radian, 388 ) 389 ( 390 np_orthogonal_point_begin, 391 np_orthogonal_point_end, 392 ) = self.project_polygon_to_unit_vector( 393 np_points=np_smooth_points, 394 radian=orthogonal_radian, 395 ) 396 397 # Build polygon. 398 polygon = Polygon.create( 399 points=[ 400 # main begin, orthogonal begin. 401 self.calculate_lines_intersection_point( 402 np_point0=np_main_point_begin, 403 radian0=orthogonal_radian, 404 np_point1=np_orthogonal_point_begin, 405 radian1=main_radian, 406 ), 407 # main begin, orthogonal end. 408 self.calculate_lines_intersection_point( 409 np_point0=np_main_point_begin, 410 radian0=orthogonal_radian, 411 np_point1=np_orthogonal_point_end, 412 radian1=main_radian, 413 ), 414 # main end, orthogonal end. 415 self.calculate_lines_intersection_point( 416 np_point0=np_main_point_end, 417 radian0=orthogonal_radian, 418 np_point1=np_orthogonal_point_end, 419 radian1=main_radian, 420 ), 421 # main end, orthogonal begin. 422 self.calculate_lines_intersection_point( 423 np_point0=np_main_point_end, 424 radian0=orthogonal_radian, 425 np_point1=np_orthogonal_point_begin, 426 radian1=main_radian, 427 ), 428 ] 429 ) 430 431 # NOTE: Could be out-of-bound. 432 polygon = polygon.to_clipped_polygon(shape) 433 434 return polygon 435 436 def to_bounding_box(self): 437 return self.bounding_box 438 439 def fill_np_array( 440 self, 441 mat: np.ndarray, 442 value: Union[np.ndarray, Tuple[float, ...], float], 443 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 444 keep_max_value: bool = False, 445 keep_min_value: bool = False, 446 ): 447 self.mask.fill_np_array( 448 mat=mat, 449 value=value, 450 alpha=alpha, 451 keep_max_value=keep_max_value, 452 keep_min_value=keep_min_value, 453 ) 454 455 def extract_mask(self, mask: 'Mask'): 456 return self.mask.extract_mask(mask) 457 458 def fill_mask( 459 self, 460 mask: 'Mask', 461 value: Union['Mask', np.ndarray, int] = 1, 462 keep_max_value: bool = False, 463 keep_min_value: bool = False, 464 ): 465 self.mask.fill_mask( 466 mask=mask, 467 value=value, 468 keep_max_value=keep_max_value, 469 keep_min_value=keep_min_value, 470 ) 471 472 def extract_score_map(self, score_map: 'ScoreMap'): 473 return self.mask.extract_score_map(score_map) 474 475 def fill_score_map( 476 self, 477 score_map: 'ScoreMap', 478 value: Union['ScoreMap', np.ndarray, float], 479 keep_max_value: bool = False, 480 keep_min_value: bool = False, 481 ): 482 self.mask.fill_score_map( 483 score_map=score_map, 484 value=value, 485 keep_max_value=keep_max_value, 486 keep_min_value=keep_min_value, 487 ) 488 489 def extract_image(self, image: 'Image'): 490 return self.mask.extract_image(image) 491 492 def fill_image( 493 self, 494 image: 'Image', 495 value: Union['Image', np.ndarray, Tuple[int, ...], int], 496 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 497 ): 498 self.mask.fill_image( 499 image=image, 500 value=value, 501 alpha=alpha, 502 ) 503 504 @classmethod 505 def remove_duplicated_xy_pairs(cls, xy_pairs: Sequence[Tuple[int, int]]): 506 xy_pairs = tuple(map(tuple, xy_pairs)) 507 unique_xy_pairs = [] 508 509 idx = 0 510 while idx < len(xy_pairs): 511 unique_xy_pairs.append(xy_pairs[idx]) 512 513 next_idx = idx + 1 514 while next_idx < len(xy_pairs) and xy_pairs[idx] == xy_pairs[next_idx]: 515 next_idx += 1 516 idx = next_idx 517 518 # Check head & tail. 519 if len(unique_xy_pairs) > 1 and unique_xy_pairs[0] == unique_xy_pairs[-1]: 520 unique_xy_pairs.pop() 521 522 assert len(unique_xy_pairs) >= 3 523 return unique_xy_pairs 524 525 def to_vatti_clipped_polygon(self, ratio: float, shrink: bool): 526 assert 0.0 <= ratio <= 1.0 527 if ratio == 1.0: 528 return self, 0.0 529 530 xy_pairs = self.to_smooth_xy_pairs() 531 532 shapely_polygon = ShapelyPolygon(xy_pairs) 533 if shapely_polygon.area == 0: 534 logger.warning('shapely_polygon.area == 0, this breaks vatti_clip.') 535 536 distance: float = shapely_polygon.area * (1 - np.power(ratio, 2)) / shapely_polygon.length 537 if shrink: 538 distance *= -1 539 540 clipper = pyclipper.PyclipperOffset() # type: ignore 541 clipper.AddPath(xy_pairs, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) # type: ignore 542 543 clipped_paths = clipper.Execute(distance) 544 assert clipped_paths 545 clipped_path: Sequence[Tuple[int, int]] = clipped_paths[0] 546 547 clipped_xy_pairs = self.remove_duplicated_xy_pairs(clipped_path) 548 clipped_polygon = self.from_xy_pairs(clipped_xy_pairs) 549 550 return clipped_polygon, distance 551 552 def to_shrank_polygon( 553 self, 554 ratio: float, 555 no_exception: bool = True, 556 ): 557 try: 558 shrank_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=True) 559 560 shrank_bounding_box = shrank_polygon.bounding_box 561 vert_contains = ( 562 self.bounding_box.up <= shrank_bounding_box.up 563 and shrank_bounding_box.down <= self.bounding_box.down 564 ) 565 hori_contains = ( 566 self.bounding_box.left <= shrank_bounding_box.left 567 and shrank_bounding_box.right <= self.bounding_box.right 568 ) 569 if not (shrank_bounding_box.valid and vert_contains and hori_contains): 570 logger.warning('Invalid shrank_polygon bounding box. Fallback to NOP.') 571 return self 572 573 if 0 < shrank_polygon.area <= self.area: 574 return shrank_polygon 575 else: 576 logger.warning('Invalid shrank_polygon.area. Fallback to NOP.') 577 return self 578 579 except Exception: 580 if no_exception: 581 logger.exception('Failed to shrink. Fallback to NOP.') 582 return self 583 else: 584 raise 585 586 def to_dilated_polygon( 587 self, 588 ratio: float, 589 no_exception: bool = True, 590 ): 591 try: 592 dilated_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=False) 593 594 dilated_bounding_box = dilated_polygon.bounding_box 595 vert_contains = ( 596 dilated_bounding_box.up <= self.bounding_box.up 597 and self.bounding_box.down <= dilated_bounding_box.down 598 ) 599 hori_contains = ( 600 dilated_bounding_box.left <= self.bounding_box.left 601 and self.bounding_box.right <= dilated_bounding_box.right 602 ) 603 if not (dilated_bounding_box.valid and vert_contains and hori_contains): 604 logger.warning('Invalid dilated_polygon bounding box. Fallback to NOP.') 605 return self 606 607 if dilated_polygon.area >= self.area: 608 return dilated_polygon 609 else: 610 logger.warning('Invalid dilated_polygon.area. Fallback to NOP.') 611 return self 612 613 except Exception: 614 if no_exception: 615 logger.exception('Failed to dilate. Fallback to NOP.') 616 return self 617 else: 618 raise 619 620 621# Experimental operations. 622# TODO: Might add to Polygon class. 623def get_line_lengths(shapely_polygon: ShapelyPolygon): 624 assert shapely_polygon.exterior is not None 625 points = tuple(shapely_polygon.exterior.coords) 626 for idx, p0 in enumerate(points): 627 p1 = points[(idx + 1) % len(points)] 628 length = math.sqrt((p0[0] - p1[0])**2 + (p0[1] - p1[1])**2) 629 yield length 630 631 632def estimate_shapely_polygon_height(shapely_polygon: ShapelyPolygon): 633 length = max(get_line_lengths(shapely_polygon)) 634 return float(shapely_polygon.area) / length 635 636 637def calculate_patch_buffer_eps(shapely_polygon: ShapelyPolygon): 638 return estimate_shapely_polygon_height(shapely_polygon) / 10 639 640 641def patch_unionized_unionized_shapely_polygon(unionized_shapely_polygon: ShapelyPolygon): 642 eps = calculate_patch_buffer_eps(unionized_shapely_polygon) 643 unionized_shapely_polygon = unionized_shapely_polygon.buffer( 644 eps, 645 cap_style=CAP_STYLE.round, # type: ignore 646 join_style=JOIN_STYLE.round, # type: ignore 647 ) 648 unionized_shapely_polygon = unionized_shapely_polygon.buffer( 649 -eps, 650 cap_style=CAP_STYLE.round, # type: ignore 651 join_style=JOIN_STYLE.round, # type: ignore 652 ) 653 return unionized_shapely_polygon 654 655 656def unionize_polygons(polygons: Iterable[Polygon]): 657 shapely_polygons = [polygon.to_smooth_shapely_polygon() for polygon in polygons] 658 659 unionized_shapely_polygons = [] 660 661 # Patch unary_union. 662 unary_union_output = unary_union(shapely_polygons) 663 if not isinstance(unary_union_output, ShapelyMultiPolygon): 664 assert isinstance(unary_union_output, ShapelyPolygon) 665 unary_union_output = [unary_union_output] 666 667 for unionized_shapely_polygon in unary_union_output: # type: ignore 668 unionized_shapely_polygon = patch_unionized_unionized_shapely_polygon( 669 unionized_shapely_polygon 670 ) 671 unionized_shapely_polygons.append(unionized_shapely_polygon) 672 673 unionized_polygons = [ 674 Polygon.from_xy_pairs(unionized_shapely_polygon.exterior.coords) 675 for unionized_shapely_polygon in unionized_shapely_polygons 676 ] 677 678 scatter_indices: List[int] = [] 679 for shapely_polygon in shapely_polygons: 680 best_unionized_polygon_idx = None 681 best_area = 0.0 682 conflict = False 683 684 for unionized_polygon_idx, unionized_shapely_polygon in enumerate( 685 unionized_shapely_polygons 686 ): 687 if not unionized_shapely_polygon.intersects(shapely_polygon): 688 continue 689 area = unionized_shapely_polygon.intersection(shapely_polygon).area 690 if area > best_area: 691 best_area = area 692 best_unionized_polygon_idx = unionized_polygon_idx 693 conflict = False 694 elif area == best_area: 695 conflict = True 696 697 assert not conflict 698 assert best_unionized_polygon_idx is not None 699 scatter_indices.append(best_unionized_polygon_idx) 700 701 return unionized_polygons, scatter_indices 702 703 704def generate_fill_by_polygons_mask( 705 shape: Tuple[int, int], 706 polygons: Iterable[Polygon], 707 mode: ElementSetOperationMode, 708): 709 if mode == ElementSetOperationMode.UNION: 710 return None 711 else: 712 return Mask.from_polygons(shape, polygons, mode) 713 714 715# Cyclic dependency, by design. 716from .point import Point, PointList, PointTuple # noqa: E402 717from .box import Box # noqa: E402 718from .mask import Mask # noqa: E402 719from .score_map import ScoreMap # noqa: E402 720from .image import Image # noqa: E402
class
PolygonInternals:
40class PolygonInternals: 41 bounding_box: 'Box' 42 np_self_relative_points: np.ndarray 43 44 _area: Optional[float] = attrs_lazy_field() 45 _self_relative_polygon: Optional['Polygon'] = attrs_lazy_field() 46 _np_mask: Optional[np.ndarray] = attrs_lazy_field() 47 _mask: Optional['Mask'] = attrs_lazy_field() 48 49 def lazy_post_init_area(self): 50 if self._area is not None: 51 return self._area 52 53 self._area = float(ShapelyPolygon(self.np_self_relative_points).area) 54 return self._area 55 56 @property 57 def area(self): 58 return self.lazy_post_init_area() 59 60 def lazy_post_init_self_relative_polygon(self): 61 if self._self_relative_polygon is not None: 62 return self._self_relative_polygon 63 64 self._self_relative_polygon = Polygon.from_np_array(self.np_self_relative_points) 65 return self._self_relative_polygon 66 67 @property 68 def self_relative_polygon(self): 69 return self.lazy_post_init_self_relative_polygon() 70 71 def lazy_post_init_np_mask(self): 72 if self._np_mask is not None: 73 return self._np_mask 74 75 np_mask = np.zeros(self.bounding_box.shape, dtype=np.uint8) 76 cv.fillPoly(np_mask, [self.self_relative_polygon.to_np_array()], 1) 77 self._np_mask = np_mask.astype(np.bool_) 78 return self._np_mask 79 80 @property 81 def np_mask(self): 82 return self.lazy_post_init_np_mask() 83 84 def lazy_post_init_mask(self): 85 if self._mask is not None: 86 return self._mask 87 88 self._mask = Mask(mat=self.np_mask.astype(np.uint8)) 89 self._mask = self._mask.to_box_attached(self.bounding_box) 90 return self._mask 91 92 @property 93 def mask(self): 94 return self.lazy_post_init_mask()
PolygonInternals( bounding_box: vkit.element.box.Box, np_self_relative_points: numpy.ndarray)
2def __init__(self, bounding_box, np_self_relative_points): 3 self.bounding_box = bounding_box 4 self.np_self_relative_points = np_self_relative_points 5 self._area = attr_dict['_area'].default 6 self._self_relative_polygon = attr_dict['_self_relative_polygon'].default 7 self._np_mask = attr_dict['_np_mask'].default 8 self._mask = attr_dict['_mask'].default
Method generated by attrs for class PolygonInternals.
class
Polygon:
98class Polygon: 99 points: 'PointTuple' 100 101 _internals: Optional[PolygonInternals] = attrs_lazy_field() 102 103 def __attrs_post_init__(self): 104 assert self.points 105 106 def lazy_post_init_internals(self): 107 if self._internals is not None: 108 return self._internals 109 110 np_self_relative_points = self.to_smooth_np_array() 111 112 y_min = np_self_relative_points[:, 1].min() 113 y_max = np_self_relative_points[:, 1].max() 114 115 x_min = np_self_relative_points[:, 0].min() 116 x_max = np_self_relative_points[:, 0].max() 117 118 np_self_relative_points[:, 0] -= x_min 119 np_self_relative_points[:, 1] -= y_min 120 121 bounding_box = Box( 122 up=round(y_min), 123 down=round(y_max), 124 left=round(x_min), 125 right=round(x_max), 126 ) 127 128 object.__setattr__( 129 self, 130 '_internals', 131 PolygonInternals( 132 bounding_box=bounding_box, 133 np_self_relative_points=np_self_relative_points, 134 ), 135 ) 136 return cast(PolygonInternals, self._internals) 137 138 ############### 139 # Constructor # 140 ############### 141 @classmethod 142 def create(cls, points: Union['PointList', 'PointTuple', Iterable['Point']]): 143 return cls(points=PointTuple(points)) 144 145 ############ 146 # Property # 147 ############ 148 @property 149 def num_points(self): 150 return len(self.points) 151 152 @property 153 def internals(self): 154 return self.lazy_post_init_internals() 155 156 @property 157 def area(self): 158 return self.internals.area 159 160 @property 161 def bounding_box(self): 162 return self.internals.bounding_box 163 164 @property 165 def self_relative_polygon(self): 166 return self.internals.self_relative_polygon 167 168 @property 169 def mask(self): 170 return self.internals.mask 171 172 ############## 173 # Conversion # 174 ############## 175 @classmethod 176 def from_xy_pairs(cls, xy_pairs: Iterable[Tuple[_T, _T]]): 177 return cls(points=PointTuple.from_xy_pairs(xy_pairs)) 178 179 def to_xy_pairs(self): 180 return self.points.to_xy_pairs() 181 182 def to_smooth_xy_pairs(self): 183 return self.points.to_smooth_xy_pairs() 184 185 @classmethod 186 def from_flatten_xy_pairs(cls, flatten_xy_pairs: Sequence[_T]): 187 return cls(points=PointTuple.from_flatten_xy_pairs(flatten_xy_pairs)) 188 189 def to_flatten_xy_pairs(self): 190 return self.points.to_flatten_xy_pairs() 191 192 def to_smooth_flatten_xy_pairs(self): 193 return self.points.to_smooth_flatten_xy_pairs() 194 195 @classmethod 196 def from_np_array(cls, np_points: np.ndarray): 197 return cls(points=PointTuple.from_np_array(np_points)) 198 199 def to_np_array(self): 200 return self.points.to_np_array() 201 202 def to_smooth_np_array(self): 203 return self.points.to_smooth_np_array() 204 205 @classmethod 206 def from_shapely_polygon(cls, shapely_polygon: ShapelyPolygon): 207 xy_pairs = cls.remove_duplicated_xy_pairs(shapely_polygon.exterior.coords) # type: ignore 208 return cls.from_xy_pairs(xy_pairs) 209 210 def to_shapely_polygon(self): 211 return ShapelyPolygon(self.to_xy_pairs()) 212 213 def to_smooth_shapely_polygon(self): 214 return ShapelyPolygon(self.to_smooth_xy_pairs()) 215 216 ############ 217 # Operator # 218 ############ 219 def get_center_point(self): 220 shapely_polygon = self.to_smooth_shapely_polygon() 221 centroid = shapely_polygon.centroid 222 x, y = centroid.coords[0] 223 return Point.create(y=y, x=x) 224 225 def get_rectangular_height(self): 226 # See Box.to_polygon. 227 assert self.num_points == 4 228 ( 229 point_up_left, 230 point_up_right, 231 point_down_right, 232 point_down_left, 233 ) = self.points 234 left_side_height = math.hypot( 235 point_up_left.smooth_y - point_down_left.smooth_y, 236 point_up_left.smooth_x - point_down_left.smooth_x, 237 ) 238 right_side_height = math.hypot( 239 point_up_right.smooth_y - point_down_right.smooth_y, 240 point_up_right.smooth_x - point_down_right.smooth_x, 241 ) 242 return (left_side_height + right_side_height) / 2 243 244 def get_rectangular_width(self): 245 # See Box.to_polygon. 246 assert self.num_points == 4 247 ( 248 point_up_left, 249 point_up_right, 250 point_down_right, 251 point_down_left, 252 ) = self.points 253 up_side_width = math.hypot( 254 point_up_left.smooth_y - point_up_right.smooth_y, 255 point_up_left.smooth_x - point_up_right.smooth_x, 256 ) 257 down_side_width = math.hypot( 258 point_down_left.smooth_y - point_down_right.smooth_y, 259 point_down_left.smooth_x - point_down_right.smooth_x, 260 ) 261 return (up_side_width + down_side_width) / 2 262 263 def to_clipped_points(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]): 264 return self.points.to_clipped_points(shapable_or_shape) 265 266 def to_clipped_polygon(self, shapable_or_shape: Union[Shapable, Tuple[int, int]]): 267 return Polygon(points=self.to_clipped_points(shapable_or_shape)) 268 269 def to_shifted_points(self, offset_y: int = 0, offset_x: int = 0): 270 return self.points.to_shifted_points(offset_y=offset_y, offset_x=offset_x) 271 272 def to_relative_points(self, origin_y: int, origin_x: int): 273 return self.points.to_relative_points(origin_y=origin_y, origin_x=origin_x) 274 275 def to_shifted_polygon(self, offset_y: int = 0, offset_x: int = 0): 276 return Polygon(points=self.to_shifted_points(offset_y=offset_y, offset_x=offset_x)) 277 278 def to_relative_polygon(self, origin_y: int, origin_x: int): 279 return Polygon(points=self.to_relative_points(origin_y=origin_y, origin_x=origin_x)) 280 281 def to_conducted_resized_polygon( 282 self, 283 shapable_or_shape: Union[Shapable, Tuple[int, int]], 284 resized_height: Optional[int] = None, 285 resized_width: Optional[int] = None, 286 ): 287 return Polygon( 288 points=self.points.to_conducted_resized_points( 289 shapable_or_shape=shapable_or_shape, 290 resized_height=resized_height, 291 resized_width=resized_width, 292 ), 293 ) 294 295 def to_resized_polygon( 296 self, 297 resized_height: Optional[int] = None, 298 resized_width: Optional[int] = None, 299 ): 300 return self.to_conducted_resized_polygon( 301 shapable_or_shape=self.bounding_box.shape, 302 resized_height=resized_height, 303 resized_width=resized_width, 304 ) 305 306 @classmethod 307 def project_polygon_to_unit_vector(cls, np_points: np.ndarray, radian: float): 308 np_vector = np.asarray([math.cos(radian), math.sin(radian)]) 309 310 np_projected: np.ndarray = np.dot(np_points, np_vector.reshape(2, 1)).flatten() 311 scale_begin = float(np_projected.min()) 312 scale_end = float(np_projected.max()) 313 314 np_point_begin = np_vector * scale_begin 315 np_point_end = np_vector * scale_end 316 317 return np_point_begin, np_point_end 318 319 @classmethod 320 def calculate_lines_intersection_point( 321 cls, 322 np_point0: np.ndarray, 323 radian0: float, 324 np_point1: np.ndarray, 325 radian1: float, 326 ): 327 x0, y0 = np_point0 328 x1, y1 = np_point1 329 330 slope0 = np.tan(radian0) 331 slope1 = np.tan(radian1) 332 333 # Within pi / 2 + k * pi plus or minus 0.1 degree. 334 invalid_slope_abs = 572.9572133543033 335 336 if abs(slope0) > invalid_slope_abs and abs(slope1) > invalid_slope_abs: 337 raise RuntimeError('Lines are vertical.') 338 339 if abs(slope0) > invalid_slope_abs: 340 its_x = float(x0) 341 its_y = float(y1 + slope1 * (x0 - x1)) 342 343 elif abs(slope1) > invalid_slope_abs: 344 its_x = float(x1) 345 its_y = float(y0 + slope0 * (x1 - x0)) 346 347 else: 348 c0 = y0 - slope0 * x0 349 c1 = y1 - slope1 * x1 350 351 with np.errstate(divide='ignore', invalid='ignore'): 352 its_x = (c1 - c0) / (slope0 - slope1) 353 if its_x == np.inf: 354 raise RuntimeError('Lines not intersected.') 355 356 its_y = slope0 * its_x + c0 357 358 return Point.create(y=float(its_y), x=float(its_x)) 359 360 def to_bounding_rectangular_polygon( 361 self, 362 shape: Tuple[int, int], 363 angle: Optional[float] = None, 364 ): 365 if angle is None: 366 shapely_polygon = self.to_smooth_shapely_polygon() 367 368 assert isinstance(shapely_polygon.minimum_rotated_rectangle, ShapelyPolygon) 369 # NOTE: For unknown reasons, the polygon border COULD exceed the bounding box 370 # a little bit. Hence we cannot assume the bounding box contains the polygon. 371 polygon = self.from_shapely_polygon(shapely_polygon.minimum_rotated_rectangle) 372 assert polygon.num_points == 4 373 374 else: 375 # Make sure in range [0, 180). 376 angle = angle % 180 377 378 main_radian = math.radians(angle) 379 orthogonal_radian = math.radians(angle + 90) 380 381 # Project points. 382 np_smooth_points = self.to_smooth_np_array() 383 ( 384 np_main_point_begin, 385 np_main_point_end, 386 ) = self.project_polygon_to_unit_vector( 387 np_points=np_smooth_points, 388 radian=main_radian, 389 ) 390 ( 391 np_orthogonal_point_begin, 392 np_orthogonal_point_end, 393 ) = self.project_polygon_to_unit_vector( 394 np_points=np_smooth_points, 395 radian=orthogonal_radian, 396 ) 397 398 # Build polygon. 399 polygon = Polygon.create( 400 points=[ 401 # main begin, orthogonal begin. 402 self.calculate_lines_intersection_point( 403 np_point0=np_main_point_begin, 404 radian0=orthogonal_radian, 405 np_point1=np_orthogonal_point_begin, 406 radian1=main_radian, 407 ), 408 # main begin, orthogonal end. 409 self.calculate_lines_intersection_point( 410 np_point0=np_main_point_begin, 411 radian0=orthogonal_radian, 412 np_point1=np_orthogonal_point_end, 413 radian1=main_radian, 414 ), 415 # main end, orthogonal end. 416 self.calculate_lines_intersection_point( 417 np_point0=np_main_point_end, 418 radian0=orthogonal_radian, 419 np_point1=np_orthogonal_point_end, 420 radian1=main_radian, 421 ), 422 # main end, orthogonal begin. 423 self.calculate_lines_intersection_point( 424 np_point0=np_main_point_end, 425 radian0=orthogonal_radian, 426 np_point1=np_orthogonal_point_begin, 427 radian1=main_radian, 428 ), 429 ] 430 ) 431 432 # NOTE: Could be out-of-bound. 433 polygon = polygon.to_clipped_polygon(shape) 434 435 return polygon 436 437 def to_bounding_box(self): 438 return self.bounding_box 439 440 def fill_np_array( 441 self, 442 mat: np.ndarray, 443 value: Union[np.ndarray, Tuple[float, ...], float], 444 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 445 keep_max_value: bool = False, 446 keep_min_value: bool = False, 447 ): 448 self.mask.fill_np_array( 449 mat=mat, 450 value=value, 451 alpha=alpha, 452 keep_max_value=keep_max_value, 453 keep_min_value=keep_min_value, 454 ) 455 456 def extract_mask(self, mask: 'Mask'): 457 return self.mask.extract_mask(mask) 458 459 def fill_mask( 460 self, 461 mask: 'Mask', 462 value: Union['Mask', np.ndarray, int] = 1, 463 keep_max_value: bool = False, 464 keep_min_value: bool = False, 465 ): 466 self.mask.fill_mask( 467 mask=mask, 468 value=value, 469 keep_max_value=keep_max_value, 470 keep_min_value=keep_min_value, 471 ) 472 473 def extract_score_map(self, score_map: 'ScoreMap'): 474 return self.mask.extract_score_map(score_map) 475 476 def fill_score_map( 477 self, 478 score_map: 'ScoreMap', 479 value: Union['ScoreMap', np.ndarray, float], 480 keep_max_value: bool = False, 481 keep_min_value: bool = False, 482 ): 483 self.mask.fill_score_map( 484 score_map=score_map, 485 value=value, 486 keep_max_value=keep_max_value, 487 keep_min_value=keep_min_value, 488 ) 489 490 def extract_image(self, image: 'Image'): 491 return self.mask.extract_image(image) 492 493 def fill_image( 494 self, 495 image: 'Image', 496 value: Union['Image', np.ndarray, Tuple[int, ...], int], 497 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 498 ): 499 self.mask.fill_image( 500 image=image, 501 value=value, 502 alpha=alpha, 503 ) 504 505 @classmethod 506 def remove_duplicated_xy_pairs(cls, xy_pairs: Sequence[Tuple[int, int]]): 507 xy_pairs = tuple(map(tuple, xy_pairs)) 508 unique_xy_pairs = [] 509 510 idx = 0 511 while idx < len(xy_pairs): 512 unique_xy_pairs.append(xy_pairs[idx]) 513 514 next_idx = idx + 1 515 while next_idx < len(xy_pairs) and xy_pairs[idx] == xy_pairs[next_idx]: 516 next_idx += 1 517 idx = next_idx 518 519 # Check head & tail. 520 if len(unique_xy_pairs) > 1 and unique_xy_pairs[0] == unique_xy_pairs[-1]: 521 unique_xy_pairs.pop() 522 523 assert len(unique_xy_pairs) >= 3 524 return unique_xy_pairs 525 526 def to_vatti_clipped_polygon(self, ratio: float, shrink: bool): 527 assert 0.0 <= ratio <= 1.0 528 if ratio == 1.0: 529 return self, 0.0 530 531 xy_pairs = self.to_smooth_xy_pairs() 532 533 shapely_polygon = ShapelyPolygon(xy_pairs) 534 if shapely_polygon.area == 0: 535 logger.warning('shapely_polygon.area == 0, this breaks vatti_clip.') 536 537 distance: float = shapely_polygon.area * (1 - np.power(ratio, 2)) / shapely_polygon.length 538 if shrink: 539 distance *= -1 540 541 clipper = pyclipper.PyclipperOffset() # type: ignore 542 clipper.AddPath(xy_pairs, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) # type: ignore 543 544 clipped_paths = clipper.Execute(distance) 545 assert clipped_paths 546 clipped_path: Sequence[Tuple[int, int]] = clipped_paths[0] 547 548 clipped_xy_pairs = self.remove_duplicated_xy_pairs(clipped_path) 549 clipped_polygon = self.from_xy_pairs(clipped_xy_pairs) 550 551 return clipped_polygon, distance 552 553 def to_shrank_polygon( 554 self, 555 ratio: float, 556 no_exception: bool = True, 557 ): 558 try: 559 shrank_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=True) 560 561 shrank_bounding_box = shrank_polygon.bounding_box 562 vert_contains = ( 563 self.bounding_box.up <= shrank_bounding_box.up 564 and shrank_bounding_box.down <= self.bounding_box.down 565 ) 566 hori_contains = ( 567 self.bounding_box.left <= shrank_bounding_box.left 568 and shrank_bounding_box.right <= self.bounding_box.right 569 ) 570 if not (shrank_bounding_box.valid and vert_contains and hori_contains): 571 logger.warning('Invalid shrank_polygon bounding box. Fallback to NOP.') 572 return self 573 574 if 0 < shrank_polygon.area <= self.area: 575 return shrank_polygon 576 else: 577 logger.warning('Invalid shrank_polygon.area. Fallback to NOP.') 578 return self 579 580 except Exception: 581 if no_exception: 582 logger.exception('Failed to shrink. Fallback to NOP.') 583 return self 584 else: 585 raise 586 587 def to_dilated_polygon( 588 self, 589 ratio: float, 590 no_exception: bool = True, 591 ): 592 try: 593 dilated_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=False) 594 595 dilated_bounding_box = dilated_polygon.bounding_box 596 vert_contains = ( 597 dilated_bounding_box.up <= self.bounding_box.up 598 and self.bounding_box.down <= dilated_bounding_box.down 599 ) 600 hori_contains = ( 601 dilated_bounding_box.left <= self.bounding_box.left 602 and self.bounding_box.right <= dilated_bounding_box.right 603 ) 604 if not (dilated_bounding_box.valid and vert_contains and hori_contains): 605 logger.warning('Invalid dilated_polygon bounding box. Fallback to NOP.') 606 return self 607 608 if dilated_polygon.area >= self.area: 609 return dilated_polygon 610 else: 611 logger.warning('Invalid dilated_polygon.area. Fallback to NOP.') 612 return self 613 614 except Exception: 615 if no_exception: 616 logger.exception('Failed to dilate. Fallback to NOP.') 617 return self 618 else: 619 raise
Polygon(points: vkit.element.point.PointTuple)
2def __init__(self, points): 3 _setattr = _cached_setattr_get(self) 4 _setattr('points', points) 5 _setattr('_internals', attr_dict['_internals'].default) 6 self.__attrs_post_init__()
Method generated by attrs for class Polygon.
def
lazy_post_init_internals(self):
106 def lazy_post_init_internals(self): 107 if self._internals is not None: 108 return self._internals 109 110 np_self_relative_points = self.to_smooth_np_array() 111 112 y_min = np_self_relative_points[:, 1].min() 113 y_max = np_self_relative_points[:, 1].max() 114 115 x_min = np_self_relative_points[:, 0].min() 116 x_max = np_self_relative_points[:, 0].max() 117 118 np_self_relative_points[:, 0] -= x_min 119 np_self_relative_points[:, 1] -= y_min 120 121 bounding_box = Box( 122 up=round(y_min), 123 down=round(y_max), 124 left=round(x_min), 125 right=round(x_max), 126 ) 127 128 object.__setattr__( 129 self, 130 '_internals', 131 PolygonInternals( 132 bounding_box=bounding_box, 133 np_self_relative_points=np_self_relative_points, 134 ), 135 ) 136 return cast(PolygonInternals, self._internals)
@classmethod
def
create( cls, points: Union[vkit.element.point.PointList, vkit.element.point.PointTuple, collections.abc.Iterable[vkit.element.point.Point]]):
@classmethod
def
from_xy_pairs(cls, xy_pairs: Iterable[Tuple[Union[float, str], Union[float, str]]]):
def
get_rectangular_height(self):
225 def get_rectangular_height(self): 226 # See Box.to_polygon. 227 assert self.num_points == 4 228 ( 229 point_up_left, 230 point_up_right, 231 point_down_right, 232 point_down_left, 233 ) = self.points 234 left_side_height = math.hypot( 235 point_up_left.smooth_y - point_down_left.smooth_y, 236 point_up_left.smooth_x - point_down_left.smooth_x, 237 ) 238 right_side_height = math.hypot( 239 point_up_right.smooth_y - point_down_right.smooth_y, 240 point_up_right.smooth_x - point_down_right.smooth_x, 241 ) 242 return (left_side_height + right_side_height) / 2
def
get_rectangular_width(self):
244 def get_rectangular_width(self): 245 # See Box.to_polygon. 246 assert self.num_points == 4 247 ( 248 point_up_left, 249 point_up_right, 250 point_down_right, 251 point_down_left, 252 ) = self.points 253 up_side_width = math.hypot( 254 point_up_left.smooth_y - point_up_right.smooth_y, 255 point_up_left.smooth_x - point_up_right.smooth_x, 256 ) 257 down_side_width = math.hypot( 258 point_down_left.smooth_y - point_down_right.smooth_y, 259 point_down_left.smooth_x - point_down_right.smooth_x, 260 ) 261 return (up_side_width + down_side_width) / 2
def
to_clipped_points( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]]):
def
to_clipped_polygon( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]]):
def
to_conducted_resized_polygon( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]], resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
281 def to_conducted_resized_polygon( 282 self, 283 shapable_or_shape: Union[Shapable, Tuple[int, int]], 284 resized_height: Optional[int] = None, 285 resized_width: Optional[int] = None, 286 ): 287 return Polygon( 288 points=self.points.to_conducted_resized_points( 289 shapable_or_shape=shapable_or_shape, 290 resized_height=resized_height, 291 resized_width=resized_width, 292 ), 293 )
def
to_resized_polygon( self, resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
@classmethod
def
project_polygon_to_unit_vector(cls, np_points: numpy.ndarray, radian: float):
306 @classmethod 307 def project_polygon_to_unit_vector(cls, np_points: np.ndarray, radian: float): 308 np_vector = np.asarray([math.cos(radian), math.sin(radian)]) 309 310 np_projected: np.ndarray = np.dot(np_points, np_vector.reshape(2, 1)).flatten() 311 scale_begin = float(np_projected.min()) 312 scale_end = float(np_projected.max()) 313 314 np_point_begin = np_vector * scale_begin 315 np_point_end = np_vector * scale_end 316 317 return np_point_begin, np_point_end
@classmethod
def
calculate_lines_intersection_point( cls, np_point0: numpy.ndarray, radian0: float, np_point1: numpy.ndarray, radian1: float):
319 @classmethod 320 def calculate_lines_intersection_point( 321 cls, 322 np_point0: np.ndarray, 323 radian0: float, 324 np_point1: np.ndarray, 325 radian1: float, 326 ): 327 x0, y0 = np_point0 328 x1, y1 = np_point1 329 330 slope0 = np.tan(radian0) 331 slope1 = np.tan(radian1) 332 333 # Within pi / 2 + k * pi plus or minus 0.1 degree. 334 invalid_slope_abs = 572.9572133543033 335 336 if abs(slope0) > invalid_slope_abs and abs(slope1) > invalid_slope_abs: 337 raise RuntimeError('Lines are vertical.') 338 339 if abs(slope0) > invalid_slope_abs: 340 its_x = float(x0) 341 its_y = float(y1 + slope1 * (x0 - x1)) 342 343 elif abs(slope1) > invalid_slope_abs: 344 its_x = float(x1) 345 its_y = float(y0 + slope0 * (x1 - x0)) 346 347 else: 348 c0 = y0 - slope0 * x0 349 c1 = y1 - slope1 * x1 350 351 with np.errstate(divide='ignore', invalid='ignore'): 352 its_x = (c1 - c0) / (slope0 - slope1) 353 if its_x == np.inf: 354 raise RuntimeError('Lines not intersected.') 355 356 its_y = slope0 * its_x + c0 357 358 return Point.create(y=float(its_y), x=float(its_x))
def
to_bounding_rectangular_polygon(self, shape: Tuple[int, int], angle: Union[float, NoneType] = None):
360 def to_bounding_rectangular_polygon( 361 self, 362 shape: Tuple[int, int], 363 angle: Optional[float] = None, 364 ): 365 if angle is None: 366 shapely_polygon = self.to_smooth_shapely_polygon() 367 368 assert isinstance(shapely_polygon.minimum_rotated_rectangle, ShapelyPolygon) 369 # NOTE: For unknown reasons, the polygon border COULD exceed the bounding box 370 # a little bit. Hence we cannot assume the bounding box contains the polygon. 371 polygon = self.from_shapely_polygon(shapely_polygon.minimum_rotated_rectangle) 372 assert polygon.num_points == 4 373 374 else: 375 # Make sure in range [0, 180). 376 angle = angle % 180 377 378 main_radian = math.radians(angle) 379 orthogonal_radian = math.radians(angle + 90) 380 381 # Project points. 382 np_smooth_points = self.to_smooth_np_array() 383 ( 384 np_main_point_begin, 385 np_main_point_end, 386 ) = self.project_polygon_to_unit_vector( 387 np_points=np_smooth_points, 388 radian=main_radian, 389 ) 390 ( 391 np_orthogonal_point_begin, 392 np_orthogonal_point_end, 393 ) = self.project_polygon_to_unit_vector( 394 np_points=np_smooth_points, 395 radian=orthogonal_radian, 396 ) 397 398 # Build polygon. 399 polygon = Polygon.create( 400 points=[ 401 # main begin, orthogonal begin. 402 self.calculate_lines_intersection_point( 403 np_point0=np_main_point_begin, 404 radian0=orthogonal_radian, 405 np_point1=np_orthogonal_point_begin, 406 radian1=main_radian, 407 ), 408 # main begin, orthogonal end. 409 self.calculate_lines_intersection_point( 410 np_point0=np_main_point_begin, 411 radian0=orthogonal_radian, 412 np_point1=np_orthogonal_point_end, 413 radian1=main_radian, 414 ), 415 # main end, orthogonal end. 416 self.calculate_lines_intersection_point( 417 np_point0=np_main_point_end, 418 radian0=orthogonal_radian, 419 np_point1=np_orthogonal_point_end, 420 radian1=main_radian, 421 ), 422 # main end, orthogonal begin. 423 self.calculate_lines_intersection_point( 424 np_point0=np_main_point_end, 425 radian0=orthogonal_radian, 426 np_point1=np_orthogonal_point_begin, 427 radian1=main_radian, 428 ), 429 ] 430 ) 431 432 # NOTE: Could be out-of-bound. 433 polygon = polygon.to_clipped_polygon(shape) 434 435 return polygon
def
fill_np_array( self, mat: numpy.ndarray, value: Union[numpy.ndarray, Tuple[float, ...], float], alpha: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0, keep_max_value: bool = False, keep_min_value: bool = False):
440 def fill_np_array( 441 self, 442 mat: np.ndarray, 443 value: Union[np.ndarray, Tuple[float, ...], float], 444 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 445 keep_max_value: bool = False, 446 keep_min_value: bool = False, 447 ): 448 self.mask.fill_np_array( 449 mat=mat, 450 value=value, 451 alpha=alpha, 452 keep_max_value=keep_max_value, 453 keep_min_value=keep_min_value, 454 )
def
fill_mask( self, mask: vkit.element.mask.Mask, value: Union[vkit.element.mask.Mask, numpy.ndarray, int] = 1, keep_max_value: bool = False, keep_min_value: bool = False):
459 def fill_mask( 460 self, 461 mask: 'Mask', 462 value: Union['Mask', np.ndarray, int] = 1, 463 keep_max_value: bool = False, 464 keep_min_value: bool = False, 465 ): 466 self.mask.fill_mask( 467 mask=mask, 468 value=value, 469 keep_max_value=keep_max_value, 470 keep_min_value=keep_min_value, 471 )
def
fill_score_map( self, score_map: vkit.element.score_map.ScoreMap, value: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float], keep_max_value: bool = False, keep_min_value: bool = False):
476 def fill_score_map( 477 self, 478 score_map: 'ScoreMap', 479 value: Union['ScoreMap', np.ndarray, float], 480 keep_max_value: bool = False, 481 keep_min_value: bool = False, 482 ): 483 self.mask.fill_score_map( 484 score_map=score_map, 485 value=value, 486 keep_max_value=keep_max_value, 487 keep_min_value=keep_min_value, 488 )
def
fill_image( self, image: vkit.element.image.Image, value: Union[vkit.element.image.Image, numpy.ndarray, Tuple[int, ...], int], alpha: Union[vkit.element.score_map.ScoreMap, numpy.ndarray, float] = 1.0):
@classmethod
def
remove_duplicated_xy_pairs(cls, xy_pairs: Sequence[Tuple[int, int]]):
505 @classmethod 506 def remove_duplicated_xy_pairs(cls, xy_pairs: Sequence[Tuple[int, int]]): 507 xy_pairs = tuple(map(tuple, xy_pairs)) 508 unique_xy_pairs = [] 509 510 idx = 0 511 while idx < len(xy_pairs): 512 unique_xy_pairs.append(xy_pairs[idx]) 513 514 next_idx = idx + 1 515 while next_idx < len(xy_pairs) and xy_pairs[idx] == xy_pairs[next_idx]: 516 next_idx += 1 517 idx = next_idx 518 519 # Check head & tail. 520 if len(unique_xy_pairs) > 1 and unique_xy_pairs[0] == unique_xy_pairs[-1]: 521 unique_xy_pairs.pop() 522 523 assert len(unique_xy_pairs) >= 3 524 return unique_xy_pairs
def
to_vatti_clipped_polygon(self, ratio: float, shrink: bool):
526 def to_vatti_clipped_polygon(self, ratio: float, shrink: bool): 527 assert 0.0 <= ratio <= 1.0 528 if ratio == 1.0: 529 return self, 0.0 530 531 xy_pairs = self.to_smooth_xy_pairs() 532 533 shapely_polygon = ShapelyPolygon(xy_pairs) 534 if shapely_polygon.area == 0: 535 logger.warning('shapely_polygon.area == 0, this breaks vatti_clip.') 536 537 distance: float = shapely_polygon.area * (1 - np.power(ratio, 2)) / shapely_polygon.length 538 if shrink: 539 distance *= -1 540 541 clipper = pyclipper.PyclipperOffset() # type: ignore 542 clipper.AddPath(xy_pairs, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) # type: ignore 543 544 clipped_paths = clipper.Execute(distance) 545 assert clipped_paths 546 clipped_path: Sequence[Tuple[int, int]] = clipped_paths[0] 547 548 clipped_xy_pairs = self.remove_duplicated_xy_pairs(clipped_path) 549 clipped_polygon = self.from_xy_pairs(clipped_xy_pairs) 550 551 return clipped_polygon, distance
def
to_shrank_polygon(self, ratio: float, no_exception: bool = True):
553 def to_shrank_polygon( 554 self, 555 ratio: float, 556 no_exception: bool = True, 557 ): 558 try: 559 shrank_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=True) 560 561 shrank_bounding_box = shrank_polygon.bounding_box 562 vert_contains = ( 563 self.bounding_box.up <= shrank_bounding_box.up 564 and shrank_bounding_box.down <= self.bounding_box.down 565 ) 566 hori_contains = ( 567 self.bounding_box.left <= shrank_bounding_box.left 568 and shrank_bounding_box.right <= self.bounding_box.right 569 ) 570 if not (shrank_bounding_box.valid and vert_contains and hori_contains): 571 logger.warning('Invalid shrank_polygon bounding box. Fallback to NOP.') 572 return self 573 574 if 0 < shrank_polygon.area <= self.area: 575 return shrank_polygon 576 else: 577 logger.warning('Invalid shrank_polygon.area. Fallback to NOP.') 578 return self 579 580 except Exception: 581 if no_exception: 582 logger.exception('Failed to shrink. Fallback to NOP.') 583 return self 584 else: 585 raise
def
to_dilated_polygon(self, ratio: float, no_exception: bool = True):
587 def to_dilated_polygon( 588 self, 589 ratio: float, 590 no_exception: bool = True, 591 ): 592 try: 593 dilated_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=False) 594 595 dilated_bounding_box = dilated_polygon.bounding_box 596 vert_contains = ( 597 dilated_bounding_box.up <= self.bounding_box.up 598 and self.bounding_box.down <= dilated_bounding_box.down 599 ) 600 hori_contains = ( 601 dilated_bounding_box.left <= self.bounding_box.left 602 and self.bounding_box.right <= dilated_bounding_box.right 603 ) 604 if not (dilated_bounding_box.valid and vert_contains and hori_contains): 605 logger.warning('Invalid dilated_polygon bounding box. Fallback to NOP.') 606 return self 607 608 if dilated_polygon.area >= self.area: 609 return dilated_polygon 610 else: 611 logger.warning('Invalid dilated_polygon.area. Fallback to NOP.') 612 return self 613 614 except Exception: 615 if no_exception: 616 logger.exception('Failed to dilate. Fallback to NOP.') 617 return self 618 else: 619 raise
def
get_line_lengths(shapely_polygon: shapely.geometry.polygon.Polygon):
624def get_line_lengths(shapely_polygon: ShapelyPolygon): 625 assert shapely_polygon.exterior is not None 626 points = tuple(shapely_polygon.exterior.coords) 627 for idx, p0 in enumerate(points): 628 p1 = points[(idx + 1) % len(points)] 629 length = math.sqrt((p0[0] - p1[0])**2 + (p0[1] - p1[1])**2) 630 yield length
def
estimate_shapely_polygon_height(shapely_polygon: shapely.geometry.polygon.Polygon):
def
calculate_patch_buffer_eps(shapely_polygon: shapely.geometry.polygon.Polygon):
def
patch_unionized_unionized_shapely_polygon(unionized_shapely_polygon: shapely.geometry.polygon.Polygon):
642def patch_unionized_unionized_shapely_polygon(unionized_shapely_polygon: ShapelyPolygon): 643 eps = calculate_patch_buffer_eps(unionized_shapely_polygon) 644 unionized_shapely_polygon = unionized_shapely_polygon.buffer( 645 eps, 646 cap_style=CAP_STYLE.round, # type: ignore 647 join_style=JOIN_STYLE.round, # type: ignore 648 ) 649 unionized_shapely_polygon = unionized_shapely_polygon.buffer( 650 -eps, 651 cap_style=CAP_STYLE.round, # type: ignore 652 join_style=JOIN_STYLE.round, # type: ignore 653 ) 654 return unionized_shapely_polygon
657def unionize_polygons(polygons: Iterable[Polygon]): 658 shapely_polygons = [polygon.to_smooth_shapely_polygon() for polygon in polygons] 659 660 unionized_shapely_polygons = [] 661 662 # Patch unary_union. 663 unary_union_output = unary_union(shapely_polygons) 664 if not isinstance(unary_union_output, ShapelyMultiPolygon): 665 assert isinstance(unary_union_output, ShapelyPolygon) 666 unary_union_output = [unary_union_output] 667 668 for unionized_shapely_polygon in unary_union_output: # type: ignore 669 unionized_shapely_polygon = patch_unionized_unionized_shapely_polygon( 670 unionized_shapely_polygon 671 ) 672 unionized_shapely_polygons.append(unionized_shapely_polygon) 673 674 unionized_polygons = [ 675 Polygon.from_xy_pairs(unionized_shapely_polygon.exterior.coords) 676 for unionized_shapely_polygon in unionized_shapely_polygons 677 ] 678 679 scatter_indices: List[int] = [] 680 for shapely_polygon in shapely_polygons: 681 best_unionized_polygon_idx = None 682 best_area = 0.0 683 conflict = False 684 685 for unionized_polygon_idx, unionized_shapely_polygon in enumerate( 686 unionized_shapely_polygons 687 ): 688 if not unionized_shapely_polygon.intersects(shapely_polygon): 689 continue 690 area = unionized_shapely_polygon.intersection(shapely_polygon).area 691 if area > best_area: 692 best_area = area 693 best_unionized_polygon_idx = unionized_polygon_idx 694 conflict = False 695 elif area == best_area: 696 conflict = True 697 698 assert not conflict 699 assert best_unionized_polygon_idx is not None 700 scatter_indices.append(best_unionized_polygon_idx) 701 702 return unionized_polygons, scatter_indices
def
generate_fill_by_polygons_mask( shape: Tuple[int, int], polygons: Iterable[vkit.element.polygon.Polygon], mode: vkit.element.type.ElementSetOperationMode):