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.bool8) 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 def to_bounding_rectangular_polygon(self, shape: Tuple[int, int]): 306 shapely_polygon = self.to_smooth_shapely_polygon() 307 308 assert isinstance(shapely_polygon.minimum_rotated_rectangle, ShapelyPolygon) 309 polygon = self.from_shapely_polygon(shapely_polygon.minimum_rotated_rectangle) 310 assert polygon.num_points == 4 311 312 # NOTE: Could be out-of-bound. 313 polygon = polygon.to_clipped_polygon(shape) 314 315 return polygon 316 317 def to_bounding_box(self): 318 return self.bounding_box 319 320 def fill_np_array( 321 self, 322 mat: np.ndarray, 323 value: Union[np.ndarray, Tuple[float, ...], float], 324 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 325 keep_max_value: bool = False, 326 keep_min_value: bool = False, 327 ): 328 self.mask.fill_np_array( 329 mat=mat, 330 value=value, 331 alpha=alpha, 332 keep_max_value=keep_max_value, 333 keep_min_value=keep_min_value, 334 ) 335 336 def extract_mask(self, mask: 'Mask'): 337 return self.mask.extract_mask(mask) 338 339 def fill_mask( 340 self, 341 mask: 'Mask', 342 value: Union['Mask', np.ndarray, int] = 1, 343 keep_max_value: bool = False, 344 keep_min_value: bool = False, 345 ): 346 self.mask.fill_mask( 347 mask=mask, 348 value=value, 349 keep_max_value=keep_max_value, 350 keep_min_value=keep_min_value, 351 ) 352 353 def extract_score_map(self, score_map: 'ScoreMap'): 354 return self.mask.extract_score_map(score_map) 355 356 def fill_score_map( 357 self, 358 score_map: 'ScoreMap', 359 value: Union['ScoreMap', np.ndarray, float], 360 keep_max_value: bool = False, 361 keep_min_value: bool = False, 362 ): 363 self.mask.fill_score_map( 364 score_map=score_map, 365 value=value, 366 keep_max_value=keep_max_value, 367 keep_min_value=keep_min_value, 368 ) 369 370 def extract_image(self, image: 'Image'): 371 return self.mask.extract_image(image) 372 373 def fill_image( 374 self, 375 image: 'Image', 376 value: Union['Image', np.ndarray, Tuple[int, ...], int], 377 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 378 ): 379 self.mask.fill_image( 380 image=image, 381 value=value, 382 alpha=alpha, 383 ) 384 385 @classmethod 386 def remove_duplicated_xy_pairs(cls, xy_pairs: Sequence[Tuple[int, int]]): 387 xy_pairs = tuple(map(tuple, xy_pairs)) 388 unique_xy_pairs = [] 389 390 idx = 0 391 while idx < len(xy_pairs): 392 unique_xy_pairs.append(xy_pairs[idx]) 393 394 next_idx = idx + 1 395 while next_idx < len(xy_pairs) and xy_pairs[idx] == xy_pairs[next_idx]: 396 next_idx += 1 397 idx = next_idx 398 399 # Check head & tail. 400 if len(unique_xy_pairs) > 1 and unique_xy_pairs[0] == unique_xy_pairs[-1]: 401 unique_xy_pairs.pop() 402 403 assert len(unique_xy_pairs) >= 3 404 return unique_xy_pairs 405 406 def to_vatti_clipped_polygon(self, ratio: float, shrink: bool): 407 assert 0.0 <= ratio <= 1.0 408 if ratio == 1.0: 409 return self, 0.0 410 411 xy_pairs = self.to_smooth_xy_pairs() 412 413 shapely_polygon = ShapelyPolygon(xy_pairs) 414 if shapely_polygon.area == 0: 415 logger.warning('shapely_polygon.area == 0, this breaks vatti_clip.') 416 417 distance: float = shapely_polygon.area * (1 - np.power(ratio, 2)) / shapely_polygon.length 418 if shrink: 419 distance *= -1 420 421 clipper = pyclipper.PyclipperOffset() # type: ignore 422 clipper.AddPath(xy_pairs, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) # type: ignore 423 424 clipped_paths = clipper.Execute(distance) 425 assert clipped_paths 426 clipped_path: Sequence[Tuple[int, int]] = clipped_paths[0] 427 428 clipped_xy_pairs = self.remove_duplicated_xy_pairs(clipped_path) 429 clipped_polygon = self.from_xy_pairs(clipped_xy_pairs) 430 431 return clipped_polygon, distance 432 433 def to_shrank_polygon( 434 self, 435 ratio: float, 436 no_exception: bool = True, 437 ): 438 try: 439 shrank_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=True) 440 441 shrank_bounding_box = shrank_polygon.bounding_box 442 vert_contains = ( 443 self.bounding_box.up <= shrank_bounding_box.up 444 and shrank_bounding_box.down <= self.bounding_box.down 445 ) 446 hori_contains = ( 447 self.bounding_box.left <= shrank_bounding_box.left 448 and shrank_bounding_box.right <= self.bounding_box.right 449 ) 450 if not (shrank_bounding_box.valid and vert_contains and hori_contains): 451 logger.warning('Invalid shrank_polygon bounding box. Fallback to NOP.') 452 return self 453 454 if 0 < shrank_polygon.area <= self.area: 455 return shrank_polygon 456 else: 457 logger.warning('Invalid shrank_polygon.area. Fallback to NOP.') 458 return self 459 460 except Exception: 461 if no_exception: 462 logger.exception('Failed to shrink. Fallback to NOP.') 463 return self 464 else: 465 raise 466 467 def to_dilated_polygon( 468 self, 469 ratio: float, 470 no_exception: bool = True, 471 ): 472 try: 473 dilated_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=False) 474 475 dilated_bounding_box = dilated_polygon.bounding_box 476 vert_contains = ( 477 dilated_bounding_box.up <= self.bounding_box.up 478 and self.bounding_box.down <= dilated_bounding_box.down 479 ) 480 hori_contains = ( 481 dilated_bounding_box.left <= self.bounding_box.left 482 and self.bounding_box.right <= dilated_bounding_box.right 483 ) 484 if not (dilated_bounding_box.valid and vert_contains and hori_contains): 485 logger.warning('Invalid dilated_polygon bounding box. Fallback to NOP.') 486 return self 487 488 if dilated_polygon.area >= self.area: 489 return dilated_polygon 490 else: 491 logger.warning('Invalid dilated_polygon.area. Fallback to NOP.') 492 return self 493 494 except Exception: 495 if no_exception: 496 logger.exception('Failed to dilate. Fallback to NOP.') 497 return self 498 else: 499 raise 500 501 502# Experimental operations. 503# TODO: Might add to Polygon class. 504def get_line_lengths(shapely_polygon: ShapelyPolygon): 505 assert shapely_polygon.exterior is not None 506 points = tuple(shapely_polygon.exterior.coords) 507 for idx, p0 in enumerate(points): 508 p1 = points[(idx + 1) % len(points)] 509 length = math.sqrt((p0[0] - p1[0])**2 + (p0[1] - p1[1])**2) 510 yield length 511 512 513def estimate_shapely_polygon_height(shapely_polygon: ShapelyPolygon): 514 length = max(get_line_lengths(shapely_polygon)) 515 return float(shapely_polygon.area) / length 516 517 518def calculate_patch_buffer_eps(shapely_polygon: ShapelyPolygon): 519 return estimate_shapely_polygon_height(shapely_polygon) / 10 520 521 522def patch_unionized_unionized_shapely_polygon(unionized_shapely_polygon: ShapelyPolygon): 523 eps = calculate_patch_buffer_eps(unionized_shapely_polygon) 524 unionized_shapely_polygon = unionized_shapely_polygon.buffer( 525 eps, 526 cap_style=CAP_STYLE.round, 527 join_style=JOIN_STYLE.round, 528 ) # type: ignore 529 unionized_shapely_polygon = unionized_shapely_polygon.buffer( 530 -eps, 531 cap_style=CAP_STYLE.round, 532 join_style=JOIN_STYLE.round, 533 ) # type: ignore 534 return unionized_shapely_polygon 535 536 537def unionize_polygons(polygons: Iterable[Polygon]): 538 shapely_polygons = [polygon.to_smooth_shapely_polygon() for polygon in polygons] 539 540 unionized_shapely_polygons = [] 541 542 # Patch unary_union. 543 unary_union_output = unary_union(shapely_polygons) 544 if not isinstance(unary_union_output, ShapelyMultiPolygon): 545 assert isinstance(unary_union_output, ShapelyPolygon) 546 unary_union_output = [unary_union_output] 547 548 for unionized_shapely_polygon in unary_union_output: 549 unionized_shapely_polygon = patch_unionized_unionized_shapely_polygon( 550 unionized_shapely_polygon 551 ) 552 unionized_shapely_polygons.append(unionized_shapely_polygon) 553 554 unionized_polygons = [ 555 Polygon.from_xy_pairs(unionized_shapely_polygon.exterior.coords) 556 for unionized_shapely_polygon in unionized_shapely_polygons 557 ] 558 559 scatter_indices: List[int] = [] 560 for shapely_polygon in shapely_polygons: 561 best_unionized_polygon_idx = None 562 best_area = 0.0 563 conflict = False 564 565 for unionized_polygon_idx, unionized_shapely_polygon in enumerate( 566 unionized_shapely_polygons 567 ): 568 if not unionized_shapely_polygon.intersects(shapely_polygon): 569 continue 570 area = unionized_shapely_polygon.intersection(shapely_polygon).area 571 if area > best_area: 572 best_area = area 573 best_unionized_polygon_idx = unionized_polygon_idx 574 conflict = False 575 elif area == best_area: 576 conflict = True 577 578 assert not conflict 579 assert best_unionized_polygon_idx is not None 580 scatter_indices.append(best_unionized_polygon_idx) 581 582 return unionized_polygons, scatter_indices 583 584 585def generate_fill_by_polygons_mask( 586 shape: Tuple[int, int], 587 polygons: Iterable[Polygon], 588 mode: ElementSetOperationMode, 589): 590 if mode == ElementSetOperationMode.UNION: 591 return None 592 else: 593 return Mask.from_polygons(shape, polygons, mode) 594 595 596# Cyclic dependency, by design. 597from .point import Point, PointList, PointTuple # noqa: E402 598from .box import Box # noqa: E402 599from .mask import Mask # noqa: E402 600from .score_map import ScoreMap # noqa: E402 601from .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.bool8) 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 def to_bounding_rectangular_polygon(self, shape: Tuple[int, int]): 307 shapely_polygon = self.to_smooth_shapely_polygon() 308 309 assert isinstance(shapely_polygon.minimum_rotated_rectangle, ShapelyPolygon) 310 polygon = self.from_shapely_polygon(shapely_polygon.minimum_rotated_rectangle) 311 assert polygon.num_points == 4 312 313 # NOTE: Could be out-of-bound. 314 polygon = polygon.to_clipped_polygon(shape) 315 316 return polygon 317 318 def to_bounding_box(self): 319 return self.bounding_box 320 321 def fill_np_array( 322 self, 323 mat: np.ndarray, 324 value: Union[np.ndarray, Tuple[float, ...], float], 325 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 326 keep_max_value: bool = False, 327 keep_min_value: bool = False, 328 ): 329 self.mask.fill_np_array( 330 mat=mat, 331 value=value, 332 alpha=alpha, 333 keep_max_value=keep_max_value, 334 keep_min_value=keep_min_value, 335 ) 336 337 def extract_mask(self, mask: 'Mask'): 338 return self.mask.extract_mask(mask) 339 340 def fill_mask( 341 self, 342 mask: 'Mask', 343 value: Union['Mask', np.ndarray, int] = 1, 344 keep_max_value: bool = False, 345 keep_min_value: bool = False, 346 ): 347 self.mask.fill_mask( 348 mask=mask, 349 value=value, 350 keep_max_value=keep_max_value, 351 keep_min_value=keep_min_value, 352 ) 353 354 def extract_score_map(self, score_map: 'ScoreMap'): 355 return self.mask.extract_score_map(score_map) 356 357 def fill_score_map( 358 self, 359 score_map: 'ScoreMap', 360 value: Union['ScoreMap', np.ndarray, float], 361 keep_max_value: bool = False, 362 keep_min_value: bool = False, 363 ): 364 self.mask.fill_score_map( 365 score_map=score_map, 366 value=value, 367 keep_max_value=keep_max_value, 368 keep_min_value=keep_min_value, 369 ) 370 371 def extract_image(self, image: 'Image'): 372 return self.mask.extract_image(image) 373 374 def fill_image( 375 self, 376 image: 'Image', 377 value: Union['Image', np.ndarray, Tuple[int, ...], int], 378 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 379 ): 380 self.mask.fill_image( 381 image=image, 382 value=value, 383 alpha=alpha, 384 ) 385 386 @classmethod 387 def remove_duplicated_xy_pairs(cls, xy_pairs: Sequence[Tuple[int, int]]): 388 xy_pairs = tuple(map(tuple, xy_pairs)) 389 unique_xy_pairs = [] 390 391 idx = 0 392 while idx < len(xy_pairs): 393 unique_xy_pairs.append(xy_pairs[idx]) 394 395 next_idx = idx + 1 396 while next_idx < len(xy_pairs) and xy_pairs[idx] == xy_pairs[next_idx]: 397 next_idx += 1 398 idx = next_idx 399 400 # Check head & tail. 401 if len(unique_xy_pairs) > 1 and unique_xy_pairs[0] == unique_xy_pairs[-1]: 402 unique_xy_pairs.pop() 403 404 assert len(unique_xy_pairs) >= 3 405 return unique_xy_pairs 406 407 def to_vatti_clipped_polygon(self, ratio: float, shrink: bool): 408 assert 0.0 <= ratio <= 1.0 409 if ratio == 1.0: 410 return self, 0.0 411 412 xy_pairs = self.to_smooth_xy_pairs() 413 414 shapely_polygon = ShapelyPolygon(xy_pairs) 415 if shapely_polygon.area == 0: 416 logger.warning('shapely_polygon.area == 0, this breaks vatti_clip.') 417 418 distance: float = shapely_polygon.area * (1 - np.power(ratio, 2)) / shapely_polygon.length 419 if shrink: 420 distance *= -1 421 422 clipper = pyclipper.PyclipperOffset() # type: ignore 423 clipper.AddPath(xy_pairs, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) # type: ignore 424 425 clipped_paths = clipper.Execute(distance) 426 assert clipped_paths 427 clipped_path: Sequence[Tuple[int, int]] = clipped_paths[0] 428 429 clipped_xy_pairs = self.remove_duplicated_xy_pairs(clipped_path) 430 clipped_polygon = self.from_xy_pairs(clipped_xy_pairs) 431 432 return clipped_polygon, distance 433 434 def to_shrank_polygon( 435 self, 436 ratio: float, 437 no_exception: bool = True, 438 ): 439 try: 440 shrank_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=True) 441 442 shrank_bounding_box = shrank_polygon.bounding_box 443 vert_contains = ( 444 self.bounding_box.up <= shrank_bounding_box.up 445 and shrank_bounding_box.down <= self.bounding_box.down 446 ) 447 hori_contains = ( 448 self.bounding_box.left <= shrank_bounding_box.left 449 and shrank_bounding_box.right <= self.bounding_box.right 450 ) 451 if not (shrank_bounding_box.valid and vert_contains and hori_contains): 452 logger.warning('Invalid shrank_polygon bounding box. Fallback to NOP.') 453 return self 454 455 if 0 < shrank_polygon.area <= self.area: 456 return shrank_polygon 457 else: 458 logger.warning('Invalid shrank_polygon.area. Fallback to NOP.') 459 return self 460 461 except Exception: 462 if no_exception: 463 logger.exception('Failed to shrink. Fallback to NOP.') 464 return self 465 else: 466 raise 467 468 def to_dilated_polygon( 469 self, 470 ratio: float, 471 no_exception: bool = True, 472 ): 473 try: 474 dilated_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=False) 475 476 dilated_bounding_box = dilated_polygon.bounding_box 477 vert_contains = ( 478 dilated_bounding_box.up <= self.bounding_box.up 479 and self.bounding_box.down <= dilated_bounding_box.down 480 ) 481 hori_contains = ( 482 dilated_bounding_box.left <= self.bounding_box.left 483 and self.bounding_box.right <= dilated_bounding_box.right 484 ) 485 if not (dilated_bounding_box.valid and vert_contains and hori_contains): 486 logger.warning('Invalid dilated_polygon bounding box. Fallback to NOP.') 487 return self 488 489 if dilated_polygon.area >= self.area: 490 return dilated_polygon 491 else: 492 logger.warning('Invalid dilated_polygon.area. Fallback to NOP.') 493 return self 494 495 except Exception: 496 if no_exception: 497 logger.exception('Failed to dilate. Fallback to NOP.') 498 return self 499 else: 500 raise
Polygon(points: vkit.element.point.PointTuple)
2def __init__(self, points): 3 _setattr(self, 'points', points) 4 _setattr(self, '_internals', attr_dict['_internals'].default) 5 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):
def
to_bounding_rectangular_polygon(self, shape: Tuple[int, int]):
306 def to_bounding_rectangular_polygon(self, shape: Tuple[int, int]): 307 shapely_polygon = self.to_smooth_shapely_polygon() 308 309 assert isinstance(shapely_polygon.minimum_rotated_rectangle, ShapelyPolygon) 310 polygon = self.from_shapely_polygon(shapely_polygon.minimum_rotated_rectangle) 311 assert polygon.num_points == 4 312 313 # NOTE: Could be out-of-bound. 314 polygon = polygon.to_clipped_polygon(shape) 315 316 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):
321 def fill_np_array( 322 self, 323 mat: np.ndarray, 324 value: Union[np.ndarray, Tuple[float, ...], float], 325 alpha: Union['ScoreMap', np.ndarray, float] = 1.0, 326 keep_max_value: bool = False, 327 keep_min_value: bool = False, 328 ): 329 self.mask.fill_np_array( 330 mat=mat, 331 value=value, 332 alpha=alpha, 333 keep_max_value=keep_max_value, 334 keep_min_value=keep_min_value, 335 )
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):
340 def fill_mask( 341 self, 342 mask: 'Mask', 343 value: Union['Mask', np.ndarray, int] = 1, 344 keep_max_value: bool = False, 345 keep_min_value: bool = False, 346 ): 347 self.mask.fill_mask( 348 mask=mask, 349 value=value, 350 keep_max_value=keep_max_value, 351 keep_min_value=keep_min_value, 352 )
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):
357 def fill_score_map( 358 self, 359 score_map: 'ScoreMap', 360 value: Union['ScoreMap', np.ndarray, float], 361 keep_max_value: bool = False, 362 keep_min_value: bool = False, 363 ): 364 self.mask.fill_score_map( 365 score_map=score_map, 366 value=value, 367 keep_max_value=keep_max_value, 368 keep_min_value=keep_min_value, 369 )
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]]):
386 @classmethod 387 def remove_duplicated_xy_pairs(cls, xy_pairs: Sequence[Tuple[int, int]]): 388 xy_pairs = tuple(map(tuple, xy_pairs)) 389 unique_xy_pairs = [] 390 391 idx = 0 392 while idx < len(xy_pairs): 393 unique_xy_pairs.append(xy_pairs[idx]) 394 395 next_idx = idx + 1 396 while next_idx < len(xy_pairs) and xy_pairs[idx] == xy_pairs[next_idx]: 397 next_idx += 1 398 idx = next_idx 399 400 # Check head & tail. 401 if len(unique_xy_pairs) > 1 and unique_xy_pairs[0] == unique_xy_pairs[-1]: 402 unique_xy_pairs.pop() 403 404 assert len(unique_xy_pairs) >= 3 405 return unique_xy_pairs
def
to_vatti_clipped_polygon(self, ratio: float, shrink: bool):
407 def to_vatti_clipped_polygon(self, ratio: float, shrink: bool): 408 assert 0.0 <= ratio <= 1.0 409 if ratio == 1.0: 410 return self, 0.0 411 412 xy_pairs = self.to_smooth_xy_pairs() 413 414 shapely_polygon = ShapelyPolygon(xy_pairs) 415 if shapely_polygon.area == 0: 416 logger.warning('shapely_polygon.area == 0, this breaks vatti_clip.') 417 418 distance: float = shapely_polygon.area * (1 - np.power(ratio, 2)) / shapely_polygon.length 419 if shrink: 420 distance *= -1 421 422 clipper = pyclipper.PyclipperOffset() # type: ignore 423 clipper.AddPath(xy_pairs, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) # type: ignore 424 425 clipped_paths = clipper.Execute(distance) 426 assert clipped_paths 427 clipped_path: Sequence[Tuple[int, int]] = clipped_paths[0] 428 429 clipped_xy_pairs = self.remove_duplicated_xy_pairs(clipped_path) 430 clipped_polygon = self.from_xy_pairs(clipped_xy_pairs) 431 432 return clipped_polygon, distance
def
to_shrank_polygon(self, ratio: float, no_exception: bool = True):
434 def to_shrank_polygon( 435 self, 436 ratio: float, 437 no_exception: bool = True, 438 ): 439 try: 440 shrank_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=True) 441 442 shrank_bounding_box = shrank_polygon.bounding_box 443 vert_contains = ( 444 self.bounding_box.up <= shrank_bounding_box.up 445 and shrank_bounding_box.down <= self.bounding_box.down 446 ) 447 hori_contains = ( 448 self.bounding_box.left <= shrank_bounding_box.left 449 and shrank_bounding_box.right <= self.bounding_box.right 450 ) 451 if not (shrank_bounding_box.valid and vert_contains and hori_contains): 452 logger.warning('Invalid shrank_polygon bounding box. Fallback to NOP.') 453 return self 454 455 if 0 < shrank_polygon.area <= self.area: 456 return shrank_polygon 457 else: 458 logger.warning('Invalid shrank_polygon.area. Fallback to NOP.') 459 return self 460 461 except Exception: 462 if no_exception: 463 logger.exception('Failed to shrink. Fallback to NOP.') 464 return self 465 else: 466 raise
def
to_dilated_polygon(self, ratio: float, no_exception: bool = True):
468 def to_dilated_polygon( 469 self, 470 ratio: float, 471 no_exception: bool = True, 472 ): 473 try: 474 dilated_polygon, _ = self.to_vatti_clipped_polygon(ratio, shrink=False) 475 476 dilated_bounding_box = dilated_polygon.bounding_box 477 vert_contains = ( 478 dilated_bounding_box.up <= self.bounding_box.up 479 and self.bounding_box.down <= dilated_bounding_box.down 480 ) 481 hori_contains = ( 482 dilated_bounding_box.left <= self.bounding_box.left 483 and self.bounding_box.right <= dilated_bounding_box.right 484 ) 485 if not (dilated_bounding_box.valid and vert_contains and hori_contains): 486 logger.warning('Invalid dilated_polygon bounding box. Fallback to NOP.') 487 return self 488 489 if dilated_polygon.area >= self.area: 490 return dilated_polygon 491 else: 492 logger.warning('Invalid dilated_polygon.area. Fallback to NOP.') 493 return self 494 495 except Exception: 496 if no_exception: 497 logger.exception('Failed to dilate. Fallback to NOP.') 498 return self 499 else: 500 raise
def
get_line_lengths(shapely_polygon: shapely.geometry.polygon.Polygon):
505def get_line_lengths(shapely_polygon: ShapelyPolygon): 506 assert shapely_polygon.exterior is not None 507 points = tuple(shapely_polygon.exterior.coords) 508 for idx, p0 in enumerate(points): 509 p1 = points[(idx + 1) % len(points)] 510 length = math.sqrt((p0[0] - p1[0])**2 + (p0[1] - p1[1])**2) 511 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):
523def patch_unionized_unionized_shapely_polygon(unionized_shapely_polygon: ShapelyPolygon): 524 eps = calculate_patch_buffer_eps(unionized_shapely_polygon) 525 unionized_shapely_polygon = unionized_shapely_polygon.buffer( 526 eps, 527 cap_style=CAP_STYLE.round, 528 join_style=JOIN_STYLE.round, 529 ) # type: ignore 530 unionized_shapely_polygon = unionized_shapely_polygon.buffer( 531 -eps, 532 cap_style=CAP_STYLE.round, 533 join_style=JOIN_STYLE.round, 534 ) # type: ignore 535 return unionized_shapely_polygon
538def unionize_polygons(polygons: Iterable[Polygon]): 539 shapely_polygons = [polygon.to_smooth_shapely_polygon() for polygon in polygons] 540 541 unionized_shapely_polygons = [] 542 543 # Patch unary_union. 544 unary_union_output = unary_union(shapely_polygons) 545 if not isinstance(unary_union_output, ShapelyMultiPolygon): 546 assert isinstance(unary_union_output, ShapelyPolygon) 547 unary_union_output = [unary_union_output] 548 549 for unionized_shapely_polygon in unary_union_output: 550 unionized_shapely_polygon = patch_unionized_unionized_shapely_polygon( 551 unionized_shapely_polygon 552 ) 553 unionized_shapely_polygons.append(unionized_shapely_polygon) 554 555 unionized_polygons = [ 556 Polygon.from_xy_pairs(unionized_shapely_polygon.exterior.coords) 557 for unionized_shapely_polygon in unionized_shapely_polygons 558 ] 559 560 scatter_indices: List[int] = [] 561 for shapely_polygon in shapely_polygons: 562 best_unionized_polygon_idx = None 563 best_area = 0.0 564 conflict = False 565 566 for unionized_polygon_idx, unionized_shapely_polygon in enumerate( 567 unionized_shapely_polygons 568 ): 569 if not unionized_shapely_polygon.intersects(shapely_polygon): 570 continue 571 area = unionized_shapely_polygon.intersection(shapely_polygon).area 572 if area > best_area: 573 best_area = area 574 best_unionized_polygon_idx = unionized_polygon_idx 575 conflict = False 576 elif area == best_area: 577 conflict = True 578 579 assert not conflict 580 assert best_unionized_polygon_idx is not None 581 scatter_indices.append(best_unionized_polygon_idx) 582 583 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):