vkit.engine.seal_impression.ellipse
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 List, Tuple, Optional, Sequence 15from enum import Enum, unique 16 17import attrs 18from numpy.random import Generator as RandomGenerator 19import numpy as np 20import cv2 as cv 21 22from vkit.utility import normalize_to_keys_and_probs, rng_choice 23from vkit.element import Point, PointList, Box, Mask, ImageMode 24from ..interface import ( 25 NoneTypeEngineInitResource, 26 Engine, 27 EngineExecutorFactory, 28) 29from vkit.engine.image import image_selector_engine_executor_factory 30from .type import ( 31 SealImpressionEngineRunConfig, 32 CharSlot, 33 TextLineSlot, 34 SealImpression, 35) 36 37 38@attrs.define 39class SealImpressionEllipseEngineInitConfig: 40 # Color & Transparency. 41 color_rgb_min: int = 128 42 color_rgb_max: int = 255 43 weight_color_grayscale: float = 5 44 weight_color_red: float = 10 45 weight_color_green: float = 1 46 weight_color_blue: float = 1 47 alpha_min: float = 0.25 48 alpha_max: float = 0.75 49 50 # Border. 51 border_thickness_ratio_min: float = 0.0 52 border_thickness_ratio_max: float = 0.03 53 border_thickness_min: int = 2 54 weight_border_style_solid_line = 3 55 weight_border_style_double_lines = 1 56 57 # Char slots. 58 # NOTE: the ratio is relative to the height of seal impression. 59 pad_ratio_min: float = 0.03 60 pad_ratio_max: float = 0.08 61 text_line_height_ratio_min: float = 0.075 62 text_line_height_ratio_max: float = 0.2 63 weight_text_line_mode_one: float = 1 64 weight_text_line_mode_two: float = 1 65 text_line_mode_one_gap_ratio_min: float = 0.1 66 text_line_mode_one_gap_ratio_max: float = 0.55 67 text_line_mode_two_gap_ratio_min: float = 0.1 68 text_line_mode_two_gap_ratio_max: float = 0.4 69 char_aspect_ratio_min: float = 0.4 70 char_aspect_ratio_max: float = 0.9 71 char_space_ratio_min: float = 0.05 72 char_space_ratio_max: float = 0.25 73 angle_step_min: int = 10 74 75 # Icon. 76 icon_image_folders: Optional[Sequence[str]] = None 77 icon_image_grayscale_min: int = 127 78 prob_add_icon: float = 0.9 79 icon_height_ratio_min: float = 0.35 80 icon_height_ratio_max: float = 0.75 81 icon_width_ratio_min: float = 0.35 82 icon_width_ratio_max: float = 0.75 83 84 # Internal text line. 85 prob_add_internal_text_line: float = 0.5 86 internal_text_line_height_ratio_min: float = 0.075 87 internal_text_line_height_ratio_max: float = 0.15 88 internal_text_line_width_ratio_min: float = 0.22 89 internal_text_line_width_ratio_max: float = 0.5 90 91 92@unique 93class SealImpressionEllipseBorderStyle(Enum): 94 SOLID_LINE = 'solid_line' 95 DOUBLE_LINES = 'double_lines' 96 97 98@unique 99class SealImpressionEllipseTextLineMode(Enum): 100 ONE = 'one' 101 TWO = 'two' 102 103 104@unique 105class SealImpressionEllipseColorMode(Enum): 106 GRAYSCALE = 'grayscale' 107 RED = 'red' 108 GREEN = 'green' 109 BLUE = 'blue' 110 111 112@attrs.define 113class TextLineRoughPlacement: 114 ellipse_outer_height: int 115 ellipse_outer_width: int 116 ellipse_inner_height: int 117 ellipse_inner_width: int 118 text_line_height: int 119 angle_begin: int 120 angle_end: int 121 clockwise: bool 122 123 124class SealImpressionEllipseEngine( 125 Engine[ 126 SealImpressionEllipseEngineInitConfig, 127 NoneTypeEngineInitResource, 128 SealImpressionEngineRunConfig, 129 SealImpression, 130 ] 131): # yapf: disable 132 133 @classmethod 134 def get_type_name(cls) -> str: 135 return 'ellipse' 136 137 def __init__( 138 self, 139 init_config: SealImpressionEllipseEngineInitConfig, 140 init_resource: Optional[NoneTypeEngineInitResource] = None 141 ): 142 super().__init__(init_config, init_resource) 143 144 self.border_styles, self.border_styles_probs = normalize_to_keys_and_probs([ 145 ( 146 SealImpressionEllipseBorderStyle.SOLID_LINE, 147 self.init_config.weight_border_style_solid_line, 148 ), 149 ( 150 SealImpressionEllipseBorderStyle.DOUBLE_LINES, 151 self.init_config.weight_border_style_double_lines, 152 ), 153 ]) 154 self.text_line_modes, self.text_line_modes_probs = normalize_to_keys_and_probs([ 155 ( 156 SealImpressionEllipseTextLineMode.ONE, 157 self.init_config.weight_text_line_mode_one, 158 ), 159 ( 160 SealImpressionEllipseTextLineMode.TWO, 161 self.init_config.weight_text_line_mode_two, 162 ), 163 ]) 164 self.color_modes, self.color_modes_probs = normalize_to_keys_and_probs([ 165 ( 166 SealImpressionEllipseColorMode.GRAYSCALE, 167 self.init_config.weight_color_grayscale, 168 ), 169 ( 170 SealImpressionEllipseColorMode.RED, 171 self.init_config.weight_color_red, 172 ), 173 ( 174 SealImpressionEllipseColorMode.GREEN, 175 self.init_config.weight_color_green, 176 ), 177 ( 178 SealImpressionEllipseColorMode.BLUE, 179 self.init_config.weight_color_blue, 180 ), 181 ]) 182 self.icon_image_selector = None 183 if self.init_config.icon_image_folders: 184 self.icon_image_selector = image_selector_engine_executor_factory.create({ 185 'image_folders': self.init_config.icon_image_folders, 186 'target_image_mode': ImageMode.GRAYSCALE, 187 'force_resize': True, 188 }) 189 190 def sample_alpha_and_color(self, rng: RandomGenerator): 191 alpha = float(rng.uniform( 192 self.init_config.alpha_min, 193 self.init_config.alpha_max, 194 )) 195 196 color_mode = rng_choice(rng, self.color_modes, probs=self.color_modes_probs) 197 rgb_value = int( 198 rng.integers( 199 self.init_config.color_rgb_min, 200 self.init_config.color_rgb_max + 1, 201 ) 202 ) 203 if color_mode == SealImpressionEllipseColorMode.GRAYSCALE: 204 color = (rgb_value,) * 3 205 else: 206 if color_mode == SealImpressionEllipseColorMode.RED: 207 color = (rgb_value, 0, 0) 208 elif color_mode == SealImpressionEllipseColorMode.GREEN: 209 color = (0, rgb_value, 0) 210 elif color_mode == SealImpressionEllipseColorMode.BLUE: 211 color = (0, 0, rgb_value) 212 else: 213 raise NotImplementedError() 214 215 return alpha, color 216 217 @classmethod 218 def sample_ellipse_points( 219 cls, 220 ellipse_height: int, 221 ellipse_width: int, 222 ellipse_offset_y: int, 223 ellipse_offset_x: int, 224 angle_begin: int, 225 angle_end: int, 226 angle_step: int, 227 keep_last_oob: bool, 228 ): 229 # Sample points in unit circle. 230 unit_circle_xy_pairs: List[Tuple[float, float]] = [] 231 angle = angle_begin 232 while angle <= angle_end or (keep_last_oob and angle - angle_end < angle_step): 233 theta = angle / 180 * np.pi 234 # Shifted. 235 x = float(np.cos(theta)) 236 y = float(np.sin(theta)) 237 unit_circle_xy_pairs.append((x, y)) 238 # Move forward. 239 angle += angle_step 240 241 # Reshape to ellipse. 242 points = PointList() 243 half_ellipse_height = ellipse_height / 2 244 half_ellipse_width = ellipse_width / 2 245 for x, y in unit_circle_xy_pairs: 246 points.append( 247 Point.create( 248 y=y * half_ellipse_height + ellipse_offset_y, 249 x=x * half_ellipse_width + ellipse_offset_x, 250 ) 251 ) 252 return points 253 254 @classmethod 255 def sample_char_slots( 256 cls, 257 ellipse_up_height: int, 258 ellipse_up_width: int, 259 ellipse_down_height: int, 260 ellipse_down_width: int, 261 ellipse_offset_y: int, 262 ellipse_offset_x: int, 263 angle_begin: int, 264 angle_end: int, 265 angle_step: int, 266 rng: RandomGenerator, 267 reverse: float = False, 268 ): 269 char_slots: List[CharSlot] = [] 270 271 keep_last_oob = (rng.random() < 0.5) 272 273 point_ups = cls.sample_ellipse_points( 274 ellipse_height=ellipse_up_height, 275 ellipse_width=ellipse_up_width, 276 ellipse_offset_y=ellipse_offset_y, 277 ellipse_offset_x=ellipse_offset_x, 278 angle_begin=angle_begin, 279 angle_end=angle_end, 280 angle_step=angle_step, 281 keep_last_oob=keep_last_oob, 282 ) 283 point_downs = cls.sample_ellipse_points( 284 ellipse_height=ellipse_down_height, 285 ellipse_width=ellipse_down_width, 286 ellipse_offset_y=ellipse_offset_y, 287 ellipse_offset_x=ellipse_offset_x, 288 angle_begin=angle_begin, 289 angle_end=angle_end, 290 angle_step=angle_step, 291 keep_last_oob=keep_last_oob, 292 ) 293 for point_up, point_down in zip(point_ups, point_downs): 294 char_slots.append(CharSlot.build(point_up=point_up, point_down=point_down)) 295 296 if reverse: 297 char_slots = list(reversed(char_slots)) 298 299 return char_slots 300 301 def sample_curved_text_line_rough_placements( 302 self, 303 height: int, 304 width: int, 305 rng: RandomGenerator, 306 ): 307 # Shared outer ellipse. 308 pad_ratio = float( 309 rng.uniform( 310 self.init_config.pad_ratio_min, 311 self.init_config.pad_ratio_max, 312 ) 313 ) 314 315 pad = round(pad_ratio * height) 316 ellipse_outer_height = height - 2 * pad 317 ellipse_outer_width = width - 2 * pad 318 assert ellipse_outer_height > 0 and ellipse_outer_width > 0 319 320 # Rough placements. 321 rough_placements: List[TextLineRoughPlacement] = [] 322 323 # Place text line one. 324 half_gap = None 325 text_line_mode = rng_choice(rng, self.text_line_modes, probs=self.text_line_modes_probs) 326 327 if text_line_mode == SealImpressionEllipseTextLineMode.ONE: 328 gap_ratio = float( 329 rng.uniform( 330 self.init_config.text_line_mode_one_gap_ratio_min, 331 self.init_config.text_line_mode_one_gap_ratio_max, 332 ) 333 ) 334 angle_gap = round(gap_ratio * 360) 335 angle_range = 360 - angle_gap 336 text_line_one_angle_begin = 90 + angle_gap // 2 337 text_line_one_angle_end = text_line_one_angle_begin + angle_range - 1 338 339 elif text_line_mode == SealImpressionEllipseTextLineMode.TWO: 340 gap_ratio = float( 341 rng.uniform( 342 self.init_config.text_line_mode_two_gap_ratio_min, 343 self.init_config.text_line_mode_two_gap_ratio_max, 344 ) 345 ) 346 half_gap = round(gap_ratio * 360 / 2) 347 348 text_line_one_angle_begin = 180 + half_gap 349 text_line_one_angle_end = 360 - half_gap 350 351 else: 352 raise NotImplementedError() 353 354 text_line_one_height_ratio = float( 355 rng.uniform( 356 self.init_config.text_line_height_ratio_min, 357 self.init_config.text_line_height_ratio_max, 358 ) 359 ) 360 text_line_one_height = round(text_line_one_height_ratio * height) 361 assert text_line_one_height > 0 362 ellipse_inner_one_height = ellipse_outer_height - 2 * text_line_one_height 363 ellipse_inner_one_width = ellipse_outer_width - 2 * text_line_one_height 364 assert ellipse_inner_one_height > 0 and ellipse_inner_one_width > 0 365 366 rough_placements.append( 367 TextLineRoughPlacement( 368 ellipse_outer_height=ellipse_outer_height, 369 ellipse_outer_width=ellipse_outer_width, 370 ellipse_inner_height=ellipse_inner_one_height, 371 ellipse_inner_width=ellipse_inner_one_width, 372 text_line_height=text_line_one_height, 373 angle_begin=text_line_one_angle_begin, 374 angle_end=text_line_one_angle_end, 375 clockwise=True, 376 ) 377 ) 378 379 # Now for the text line two. 380 if text_line_mode == SealImpressionEllipseTextLineMode.TWO: 381 assert half_gap 382 383 text_line_two_height_ratio = float( 384 rng.uniform( 385 self.init_config.text_line_height_ratio_min, 386 self.init_config.text_line_height_ratio_max, 387 ) 388 ) 389 text_line_two_height = round(text_line_two_height_ratio * height) 390 assert text_line_two_height > 0 391 ellipse_inner_two_height = ellipse_outer_height - 2 * text_line_two_height 392 ellipse_inner_two_width = ellipse_outer_width - 2 * text_line_two_height 393 assert ellipse_inner_two_height > 0 and ellipse_inner_two_width > 0 394 395 text_line_two_angle_begin = half_gap 396 text_line_two_angle_end = 180 - half_gap 397 398 rough_placements.append( 399 TextLineRoughPlacement( 400 ellipse_outer_height=ellipse_outer_height, 401 ellipse_outer_width=ellipse_outer_width, 402 ellipse_inner_height=ellipse_inner_two_height, 403 ellipse_inner_width=ellipse_inner_two_width, 404 text_line_height=text_line_two_height, 405 angle_begin=text_line_two_angle_begin, 406 angle_end=text_line_two_angle_end, 407 clockwise=False, 408 ) 409 ) 410 411 return rough_placements 412 413 def generate_text_line_slots_based_on_rough_placements( 414 self, 415 height: int, 416 width: int, 417 rough_placements: Sequence[TextLineRoughPlacement], 418 rng: RandomGenerator, 419 ): 420 ellipse_offset_y = height // 2 421 ellipse_offset_x = width // 2 422 423 text_line_slots: List[TextLineSlot] = [] 424 425 for rough_placement in rough_placements: 426 char_aspect_ratio = float( 427 rng.uniform( 428 self.init_config.char_aspect_ratio_min, 429 self.init_config.char_aspect_ratio_max, 430 ) 431 ) 432 char_width_ref = max(1, round(rough_placement.text_line_height * char_aspect_ratio)) 433 434 char_space_ratio = float( 435 rng.uniform( 436 self.init_config.char_space_ratio_min, 437 self.init_config.char_space_ratio_max, 438 ) 439 ) 440 char_space_ref = max(1, round(rough_placement.text_line_height * char_space_ratio)) 441 442 radius_ref = max(1, ellipse_offset_y) 443 angle_step = max( 444 self.init_config.angle_step_min, 445 round(360 * (char_width_ref + char_space_ref) / (2 * np.pi * radius_ref)), 446 ) 447 448 if rough_placement.clockwise: 449 char_slots = self.sample_char_slots( 450 ellipse_up_height=rough_placement.ellipse_outer_height, 451 ellipse_up_width=rough_placement.ellipse_outer_width, 452 ellipse_down_height=rough_placement.ellipse_inner_height, 453 ellipse_down_width=rough_placement.ellipse_inner_width, 454 ellipse_offset_y=ellipse_offset_y, 455 ellipse_offset_x=ellipse_offset_x, 456 angle_begin=rough_placement.angle_begin, 457 angle_end=rough_placement.angle_end, 458 angle_step=angle_step, 459 rng=rng, 460 ) 461 462 else: 463 char_slots = self.sample_char_slots( 464 ellipse_up_height=rough_placement.ellipse_inner_height, 465 ellipse_up_width=rough_placement.ellipse_inner_width, 466 ellipse_down_height=rough_placement.ellipse_outer_height, 467 ellipse_down_width=rough_placement.ellipse_outer_width, 468 ellipse_offset_y=ellipse_offset_y, 469 ellipse_offset_x=ellipse_offset_x, 470 angle_begin=rough_placement.angle_begin, 471 angle_end=rough_placement.angle_end, 472 angle_step=angle_step, 473 rng=rng, 474 reverse=True, 475 ) 476 477 text_line_slots.append( 478 TextLineSlot( 479 text_line_height=rough_placement.text_line_height, 480 char_aspect_ratio=char_aspect_ratio, 481 char_slots=char_slots, 482 ) 483 ) 484 485 return text_line_slots 486 487 def generate_text_line_slots(self, height: int, width: int, rng: RandomGenerator): 488 rough_placements = self.sample_curved_text_line_rough_placements( 489 height=height, 490 width=width, 491 rng=rng, 492 ) 493 text_line_slots = self.generate_text_line_slots_based_on_rough_placements( 494 height=height, 495 width=width, 496 rough_placements=rough_placements, 497 rng=rng, 498 ) 499 ellipse_inner_shape = ( 500 min(rough_placement.ellipse_inner_height for rough_placement in rough_placements), 501 min(rough_placement.ellipse_inner_width for rough_placement in rough_placements), 502 ) 503 return text_line_slots, ellipse_inner_shape 504 505 def sample_icon_box( 506 self, 507 height: int, 508 width: int, 509 ellipse_inner_shape: Tuple[int, int], 510 rng: RandomGenerator, 511 ): 512 ellipse_inner_height, ellipse_inner_width = ellipse_inner_shape 513 514 box_height_ratio = rng.uniform( 515 self.init_config.icon_height_ratio_min, 516 self.init_config.icon_height_ratio_max, 517 ) 518 box_height = round(ellipse_inner_height * box_height_ratio) 519 520 box_width_ratio = rng.uniform( 521 self.init_config.icon_width_ratio_min, 522 self.init_config.icon_width_ratio_max, 523 ) 524 box_width = round(ellipse_inner_width * box_width_ratio) 525 526 up = (height - box_height) // 2 527 down = up + box_height - 1 528 left = (width - box_width) // 2 529 right = left + box_width - 1 530 return Box(up=up, down=down, left=left, right=right) 531 532 def sample_internal_text_line_box( 533 self, 534 height: int, 535 width: int, 536 ellipse_inner_shape: Tuple[int, int], 537 icon_box_down: Optional[int], 538 rng: RandomGenerator, 539 ): 540 ellipse_inner_height, ellipse_inner_width = ellipse_inner_shape 541 if ellipse_inner_height > ellipse_inner_width: 542 # Not supported yet. 543 return None 544 545 # Vert. 546 box_height_ratio = rng.uniform( 547 self.init_config.internal_text_line_height_ratio_min, 548 self.init_config.internal_text_line_height_ratio_max, 549 ) 550 box_height = round(ellipse_inner_height * box_height_ratio) 551 552 half_height = height // 2 553 up = half_height 554 if icon_box_down: 555 up = icon_box_down + 1 556 down = min( 557 height - 1, 558 half_height + ellipse_inner_height // 2 - 1, 559 up + box_height - 1, 560 ) 561 562 if up > down: 563 return None 564 565 # Hori. 566 ellipse_h = down + 1 - half_height 567 ellipse_a = ellipse_inner_width / 2 568 ellipse_b = ellipse_inner_height / 2 569 box_width_max = round(2 * ellipse_b * np.sqrt(ellipse_a**2 - ellipse_h**2) / ellipse_a) 570 571 box_width_ratio = rng.uniform( 572 self.init_config.internal_text_line_width_ratio_min, 573 self.init_config.internal_text_line_width_ratio_max, 574 ) 575 box_width = round(ellipse_inner_width * box_width_ratio) 576 box_width = max(box_width_max, box_width) 577 578 left = (width - box_width) // 2 579 right = left + box_width - 1 580 581 if left > right: 582 return None 583 584 return Box(up=up, down=down, left=left, right=right) 585 586 def generate_background( 587 self, 588 height: int, 589 width: int, 590 ellipse_inner_shape: Tuple[int, int], 591 rng: RandomGenerator, 592 ): 593 background_mask = Mask.from_shape((height, width)) 594 595 border_style = rng_choice(rng, self.border_styles, probs=self.border_styles_probs) 596 597 # Will generate solid line first. 598 border_thickness_ratio = float( 599 rng.uniform( 600 self.init_config.border_thickness_ratio_min, 601 self.init_config.border_thickness_ratio_max, 602 ) 603 ) 604 border_thickness = round(height * border_thickness_ratio) 605 border_thickness = max(self.init_config.border_thickness_min, border_thickness) 606 607 center = (width // 2, height // 2) 608 # NOTE: minus 1 to make sure the border is inbound. 609 axes = (width // 2 - border_thickness - 1, height // 2 - border_thickness - 1) 610 cv.ellipse( 611 background_mask.mat, 612 center=center, 613 axes=axes, 614 angle=0, 615 startAngle=0, 616 endAngle=360, 617 color=1, 618 thickness=border_thickness, 619 ) 620 621 if border_thickness > 2 * self.init_config.border_thickness_min + 1 \ 622 and border_style == SealImpressionEllipseBorderStyle.DOUBLE_LINES: 623 # Remove the middle part to generate double lines. 624 border_thickness_empty = int( 625 rng.integers( 626 1, 627 border_thickness - 2 * self.init_config.border_thickness_min, 628 ) 629 ) 630 cv.ellipse( 631 background_mask.mat, 632 center=center, 633 # NOTE: I don't know why, but this works as expected. 634 # Probably `axes` points to the center of border. 635 axes=axes, 636 angle=0, 637 startAngle=0, 638 endAngle=360, 639 color=0, 640 thickness=border_thickness_empty, 641 ) 642 643 icon_box_down = None 644 if self.icon_image_selector and rng.random() < self.init_config.prob_add_icon: 645 icon_box = self.sample_icon_box( 646 height=height, 647 width=width, 648 ellipse_inner_shape=ellipse_inner_shape, 649 rng=rng, 650 ) 651 icon_box_down = icon_box.down 652 icon_grayscale_image = self.icon_image_selector.run( 653 { 654 'height': icon_box.height, 655 'width': icon_box.width 656 }, 657 rng, 658 ) 659 icon_mask_mat = (icon_grayscale_image.mat > self.init_config.icon_image_grayscale_min) 660 icon_mask = Mask(mat=icon_mask_mat.astype(np.uint8)) 661 icon_box.fill_mask(background_mask, icon_mask) 662 663 internal_text_line_box = None 664 if rng.random() < self.init_config.prob_add_internal_text_line: 665 internal_text_line_box = self.sample_internal_text_line_box( 666 height=height, 667 width=width, 668 ellipse_inner_shape=ellipse_inner_shape, 669 icon_box_down=icon_box_down, 670 rng=rng, 671 ) 672 673 return background_mask, internal_text_line_box 674 675 def run( 676 self, 677 run_config: SealImpressionEngineRunConfig, 678 rng: Optional[RandomGenerator] = None, 679 ): 680 assert rng is not None 681 682 alpha, color = self.sample_alpha_and_color(rng) 683 text_line_slots, ellipse_inner_shape = self.generate_text_line_slots( 684 height=run_config.height, 685 width=run_config.width, 686 rng=rng, 687 ) 688 background_mask, internal_text_line_box = self.generate_background( 689 height=run_config.height, 690 width=run_config.width, 691 ellipse_inner_shape=ellipse_inner_shape, 692 rng=rng, 693 ) 694 return SealImpression( 695 alpha=alpha, 696 color=color, 697 background_mask=background_mask, 698 text_line_slots=text_line_slots, 699 internal_text_line_box=internal_text_line_box, 700 ) 701 702 703seal_impression_ellipse_engine_executor_factory = EngineExecutorFactory(SealImpressionEllipseEngine)
class
SealImpressionEllipseEngineInitConfig:
40class SealImpressionEllipseEngineInitConfig: 41 # Color & Transparency. 42 color_rgb_min: int = 128 43 color_rgb_max: int = 255 44 weight_color_grayscale: float = 5 45 weight_color_red: float = 10 46 weight_color_green: float = 1 47 weight_color_blue: float = 1 48 alpha_min: float = 0.25 49 alpha_max: float = 0.75 50 51 # Border. 52 border_thickness_ratio_min: float = 0.0 53 border_thickness_ratio_max: float = 0.03 54 border_thickness_min: int = 2 55 weight_border_style_solid_line = 3 56 weight_border_style_double_lines = 1 57 58 # Char slots. 59 # NOTE: the ratio is relative to the height of seal impression. 60 pad_ratio_min: float = 0.03 61 pad_ratio_max: float = 0.08 62 text_line_height_ratio_min: float = 0.075 63 text_line_height_ratio_max: float = 0.2 64 weight_text_line_mode_one: float = 1 65 weight_text_line_mode_two: float = 1 66 text_line_mode_one_gap_ratio_min: float = 0.1 67 text_line_mode_one_gap_ratio_max: float = 0.55 68 text_line_mode_two_gap_ratio_min: float = 0.1 69 text_line_mode_two_gap_ratio_max: float = 0.4 70 char_aspect_ratio_min: float = 0.4 71 char_aspect_ratio_max: float = 0.9 72 char_space_ratio_min: float = 0.05 73 char_space_ratio_max: float = 0.25 74 angle_step_min: int = 10 75 76 # Icon. 77 icon_image_folders: Optional[Sequence[str]] = None 78 icon_image_grayscale_min: int = 127 79 prob_add_icon: float = 0.9 80 icon_height_ratio_min: float = 0.35 81 icon_height_ratio_max: float = 0.75 82 icon_width_ratio_min: float = 0.35 83 icon_width_ratio_max: float = 0.75 84 85 # Internal text line. 86 prob_add_internal_text_line: float = 0.5 87 internal_text_line_height_ratio_min: float = 0.075 88 internal_text_line_height_ratio_max: float = 0.15 89 internal_text_line_width_ratio_min: float = 0.22 90 internal_text_line_width_ratio_max: float = 0.5
SealImpressionEllipseEngineInitConfig( color_rgb_min: int = 128, color_rgb_max: int = 255, weight_color_grayscale: float = 5, weight_color_red: float = 10, weight_color_green: float = 1, weight_color_blue: float = 1, alpha_min: float = 0.25, alpha_max: float = 0.75, border_thickness_ratio_min: float = 0.0, border_thickness_ratio_max: float = 0.03, border_thickness_min: int = 2, pad_ratio_min: float = 0.03, pad_ratio_max: float = 0.08, text_line_height_ratio_min: float = 0.075, text_line_height_ratio_max: float = 0.2, weight_text_line_mode_one: float = 1, weight_text_line_mode_two: float = 1, text_line_mode_one_gap_ratio_min: float = 0.1, text_line_mode_one_gap_ratio_max: float = 0.55, text_line_mode_two_gap_ratio_min: float = 0.1, text_line_mode_two_gap_ratio_max: float = 0.4, char_aspect_ratio_min: float = 0.4, char_aspect_ratio_max: float = 0.9, char_space_ratio_min: float = 0.05, char_space_ratio_max: float = 0.25, angle_step_min: int = 10, icon_image_folders: Union[Sequence[str], NoneType] = None, icon_image_grayscale_min: int = 127, prob_add_icon: float = 0.9, icon_height_ratio_min: float = 0.35, icon_height_ratio_max: float = 0.75, icon_width_ratio_min: float = 0.35, icon_width_ratio_max: float = 0.75, prob_add_internal_text_line: float = 0.5, internal_text_line_height_ratio_min: float = 0.075, internal_text_line_height_ratio_max: float = 0.15, internal_text_line_width_ratio_min: float = 0.22, internal_text_line_width_ratio_max: float = 0.5)
2def __init__(self, color_rgb_min=attr_dict['color_rgb_min'].default, color_rgb_max=attr_dict['color_rgb_max'].default, weight_color_grayscale=attr_dict['weight_color_grayscale'].default, weight_color_red=attr_dict['weight_color_red'].default, weight_color_green=attr_dict['weight_color_green'].default, weight_color_blue=attr_dict['weight_color_blue'].default, alpha_min=attr_dict['alpha_min'].default, alpha_max=attr_dict['alpha_max'].default, border_thickness_ratio_min=attr_dict['border_thickness_ratio_min'].default, border_thickness_ratio_max=attr_dict['border_thickness_ratio_max'].default, border_thickness_min=attr_dict['border_thickness_min'].default, pad_ratio_min=attr_dict['pad_ratio_min'].default, pad_ratio_max=attr_dict['pad_ratio_max'].default, text_line_height_ratio_min=attr_dict['text_line_height_ratio_min'].default, text_line_height_ratio_max=attr_dict['text_line_height_ratio_max'].default, weight_text_line_mode_one=attr_dict['weight_text_line_mode_one'].default, weight_text_line_mode_two=attr_dict['weight_text_line_mode_two'].default, text_line_mode_one_gap_ratio_min=attr_dict['text_line_mode_one_gap_ratio_min'].default, text_line_mode_one_gap_ratio_max=attr_dict['text_line_mode_one_gap_ratio_max'].default, text_line_mode_two_gap_ratio_min=attr_dict['text_line_mode_two_gap_ratio_min'].default, text_line_mode_two_gap_ratio_max=attr_dict['text_line_mode_two_gap_ratio_max'].default, char_aspect_ratio_min=attr_dict['char_aspect_ratio_min'].default, char_aspect_ratio_max=attr_dict['char_aspect_ratio_max'].default, char_space_ratio_min=attr_dict['char_space_ratio_min'].default, char_space_ratio_max=attr_dict['char_space_ratio_max'].default, angle_step_min=attr_dict['angle_step_min'].default, icon_image_folders=attr_dict['icon_image_folders'].default, icon_image_grayscale_min=attr_dict['icon_image_grayscale_min'].default, prob_add_icon=attr_dict['prob_add_icon'].default, icon_height_ratio_min=attr_dict['icon_height_ratio_min'].default, icon_height_ratio_max=attr_dict['icon_height_ratio_max'].default, icon_width_ratio_min=attr_dict['icon_width_ratio_min'].default, icon_width_ratio_max=attr_dict['icon_width_ratio_max'].default, prob_add_internal_text_line=attr_dict['prob_add_internal_text_line'].default, internal_text_line_height_ratio_min=attr_dict['internal_text_line_height_ratio_min'].default, internal_text_line_height_ratio_max=attr_dict['internal_text_line_height_ratio_max'].default, internal_text_line_width_ratio_min=attr_dict['internal_text_line_width_ratio_min'].default, internal_text_line_width_ratio_max=attr_dict['internal_text_line_width_ratio_max'].default): 3 self.color_rgb_min = color_rgb_min 4 self.color_rgb_max = color_rgb_max 5 self.weight_color_grayscale = weight_color_grayscale 6 self.weight_color_red = weight_color_red 7 self.weight_color_green = weight_color_green 8 self.weight_color_blue = weight_color_blue 9 self.alpha_min = alpha_min 10 self.alpha_max = alpha_max 11 self.border_thickness_ratio_min = border_thickness_ratio_min 12 self.border_thickness_ratio_max = border_thickness_ratio_max 13 self.border_thickness_min = border_thickness_min 14 self.pad_ratio_min = pad_ratio_min 15 self.pad_ratio_max = pad_ratio_max 16 self.text_line_height_ratio_min = text_line_height_ratio_min 17 self.text_line_height_ratio_max = text_line_height_ratio_max 18 self.weight_text_line_mode_one = weight_text_line_mode_one 19 self.weight_text_line_mode_two = weight_text_line_mode_two 20 self.text_line_mode_one_gap_ratio_min = text_line_mode_one_gap_ratio_min 21 self.text_line_mode_one_gap_ratio_max = text_line_mode_one_gap_ratio_max 22 self.text_line_mode_two_gap_ratio_min = text_line_mode_two_gap_ratio_min 23 self.text_line_mode_two_gap_ratio_max = text_line_mode_two_gap_ratio_max 24 self.char_aspect_ratio_min = char_aspect_ratio_min 25 self.char_aspect_ratio_max = char_aspect_ratio_max 26 self.char_space_ratio_min = char_space_ratio_min 27 self.char_space_ratio_max = char_space_ratio_max 28 self.angle_step_min = angle_step_min 29 self.icon_image_folders = icon_image_folders 30 self.icon_image_grayscale_min = icon_image_grayscale_min 31 self.prob_add_icon = prob_add_icon 32 self.icon_height_ratio_min = icon_height_ratio_min 33 self.icon_height_ratio_max = icon_height_ratio_max 34 self.icon_width_ratio_min = icon_width_ratio_min 35 self.icon_width_ratio_max = icon_width_ratio_max 36 self.prob_add_internal_text_line = prob_add_internal_text_line 37 self.internal_text_line_height_ratio_min = internal_text_line_height_ratio_min 38 self.internal_text_line_height_ratio_max = internal_text_line_height_ratio_max 39 self.internal_text_line_width_ratio_min = internal_text_line_width_ratio_min 40 self.internal_text_line_width_ratio_max = internal_text_line_width_ratio_max
Method generated by attrs for class SealImpressionEllipseEngineInitConfig.
class
SealImpressionEllipseBorderStyle(enum.Enum):
94class SealImpressionEllipseBorderStyle(Enum): 95 SOLID_LINE = 'solid_line' 96 DOUBLE_LINES = 'double_lines'
An enumeration.
SOLID_LINE =
<SealImpressionEllipseBorderStyle.SOLID_LINE: 'solid_line'>
DOUBLE_LINES =
<SealImpressionEllipseBorderStyle.DOUBLE_LINES: 'double_lines'>
Inherited Members
- enum.Enum
- name
- value
class
SealImpressionEllipseTextLineMode(enum.Enum):
An enumeration.
ONE =
<SealImpressionEllipseTextLineMode.ONE: 'one'>
TWO =
<SealImpressionEllipseTextLineMode.TWO: 'two'>
Inherited Members
- enum.Enum
- name
- value
class
SealImpressionEllipseColorMode(enum.Enum):
106class SealImpressionEllipseColorMode(Enum): 107 GRAYSCALE = 'grayscale' 108 RED = 'red' 109 GREEN = 'green' 110 BLUE = 'blue'
An enumeration.
GRAYSCALE =
<SealImpressionEllipseColorMode.GRAYSCALE: 'grayscale'>
RED =
<SealImpressionEllipseColorMode.RED: 'red'>
GREEN =
<SealImpressionEllipseColorMode.GREEN: 'green'>
BLUE =
<SealImpressionEllipseColorMode.BLUE: 'blue'>
Inherited Members
- enum.Enum
- name
- value
class
TextLineRoughPlacement:
114class TextLineRoughPlacement: 115 ellipse_outer_height: int 116 ellipse_outer_width: int 117 ellipse_inner_height: int 118 ellipse_inner_width: int 119 text_line_height: int 120 angle_begin: int 121 angle_end: int 122 clockwise: bool
TextLineRoughPlacement( ellipse_outer_height: int, ellipse_outer_width: int, ellipse_inner_height: int, ellipse_inner_width: int, text_line_height: int, angle_begin: int, angle_end: int, clockwise: bool)
2def __init__(self, ellipse_outer_height, ellipse_outer_width, ellipse_inner_height, ellipse_inner_width, text_line_height, angle_begin, angle_end, clockwise): 3 self.ellipse_outer_height = ellipse_outer_height 4 self.ellipse_outer_width = ellipse_outer_width 5 self.ellipse_inner_height = ellipse_inner_height 6 self.ellipse_inner_width = ellipse_inner_width 7 self.text_line_height = text_line_height 8 self.angle_begin = angle_begin 9 self.angle_end = angle_end 10 self.clockwise = clockwise
Method generated by attrs for class TextLineRoughPlacement.
class
SealImpressionEllipseEngine(vkit.engine.interface.Engine[vkit.engine.seal_impression.ellipse.SealImpressionEllipseEngineInitConfig, vkit.engine.interface.NoneTypeEngineInitResource, vkit.engine.seal_impression.type.SealImpressionEngineRunConfig, vkit.engine.seal_impression.type.SealImpression]):
125class SealImpressionEllipseEngine( 126 Engine[ 127 SealImpressionEllipseEngineInitConfig, 128 NoneTypeEngineInitResource, 129 SealImpressionEngineRunConfig, 130 SealImpression, 131 ] 132): # yapf: disable 133 134 @classmethod 135 def get_type_name(cls) -> str: 136 return 'ellipse' 137 138 def __init__( 139 self, 140 init_config: SealImpressionEllipseEngineInitConfig, 141 init_resource: Optional[NoneTypeEngineInitResource] = None 142 ): 143 super().__init__(init_config, init_resource) 144 145 self.border_styles, self.border_styles_probs = normalize_to_keys_and_probs([ 146 ( 147 SealImpressionEllipseBorderStyle.SOLID_LINE, 148 self.init_config.weight_border_style_solid_line, 149 ), 150 ( 151 SealImpressionEllipseBorderStyle.DOUBLE_LINES, 152 self.init_config.weight_border_style_double_lines, 153 ), 154 ]) 155 self.text_line_modes, self.text_line_modes_probs = normalize_to_keys_and_probs([ 156 ( 157 SealImpressionEllipseTextLineMode.ONE, 158 self.init_config.weight_text_line_mode_one, 159 ), 160 ( 161 SealImpressionEllipseTextLineMode.TWO, 162 self.init_config.weight_text_line_mode_two, 163 ), 164 ]) 165 self.color_modes, self.color_modes_probs = normalize_to_keys_and_probs([ 166 ( 167 SealImpressionEllipseColorMode.GRAYSCALE, 168 self.init_config.weight_color_grayscale, 169 ), 170 ( 171 SealImpressionEllipseColorMode.RED, 172 self.init_config.weight_color_red, 173 ), 174 ( 175 SealImpressionEllipseColorMode.GREEN, 176 self.init_config.weight_color_green, 177 ), 178 ( 179 SealImpressionEllipseColorMode.BLUE, 180 self.init_config.weight_color_blue, 181 ), 182 ]) 183 self.icon_image_selector = None 184 if self.init_config.icon_image_folders: 185 self.icon_image_selector = image_selector_engine_executor_factory.create({ 186 'image_folders': self.init_config.icon_image_folders, 187 'target_image_mode': ImageMode.GRAYSCALE, 188 'force_resize': True, 189 }) 190 191 def sample_alpha_and_color(self, rng: RandomGenerator): 192 alpha = float(rng.uniform( 193 self.init_config.alpha_min, 194 self.init_config.alpha_max, 195 )) 196 197 color_mode = rng_choice(rng, self.color_modes, probs=self.color_modes_probs) 198 rgb_value = int( 199 rng.integers( 200 self.init_config.color_rgb_min, 201 self.init_config.color_rgb_max + 1, 202 ) 203 ) 204 if color_mode == SealImpressionEllipseColorMode.GRAYSCALE: 205 color = (rgb_value,) * 3 206 else: 207 if color_mode == SealImpressionEllipseColorMode.RED: 208 color = (rgb_value, 0, 0) 209 elif color_mode == SealImpressionEllipseColorMode.GREEN: 210 color = (0, rgb_value, 0) 211 elif color_mode == SealImpressionEllipseColorMode.BLUE: 212 color = (0, 0, rgb_value) 213 else: 214 raise NotImplementedError() 215 216 return alpha, color 217 218 @classmethod 219 def sample_ellipse_points( 220 cls, 221 ellipse_height: int, 222 ellipse_width: int, 223 ellipse_offset_y: int, 224 ellipse_offset_x: int, 225 angle_begin: int, 226 angle_end: int, 227 angle_step: int, 228 keep_last_oob: bool, 229 ): 230 # Sample points in unit circle. 231 unit_circle_xy_pairs: List[Tuple[float, float]] = [] 232 angle = angle_begin 233 while angle <= angle_end or (keep_last_oob and angle - angle_end < angle_step): 234 theta = angle / 180 * np.pi 235 # Shifted. 236 x = float(np.cos(theta)) 237 y = float(np.sin(theta)) 238 unit_circle_xy_pairs.append((x, y)) 239 # Move forward. 240 angle += angle_step 241 242 # Reshape to ellipse. 243 points = PointList() 244 half_ellipse_height = ellipse_height / 2 245 half_ellipse_width = ellipse_width / 2 246 for x, y in unit_circle_xy_pairs: 247 points.append( 248 Point.create( 249 y=y * half_ellipse_height + ellipse_offset_y, 250 x=x * half_ellipse_width + ellipse_offset_x, 251 ) 252 ) 253 return points 254 255 @classmethod 256 def sample_char_slots( 257 cls, 258 ellipse_up_height: int, 259 ellipse_up_width: int, 260 ellipse_down_height: int, 261 ellipse_down_width: int, 262 ellipse_offset_y: int, 263 ellipse_offset_x: int, 264 angle_begin: int, 265 angle_end: int, 266 angle_step: int, 267 rng: RandomGenerator, 268 reverse: float = False, 269 ): 270 char_slots: List[CharSlot] = [] 271 272 keep_last_oob = (rng.random() < 0.5) 273 274 point_ups = cls.sample_ellipse_points( 275 ellipse_height=ellipse_up_height, 276 ellipse_width=ellipse_up_width, 277 ellipse_offset_y=ellipse_offset_y, 278 ellipse_offset_x=ellipse_offset_x, 279 angle_begin=angle_begin, 280 angle_end=angle_end, 281 angle_step=angle_step, 282 keep_last_oob=keep_last_oob, 283 ) 284 point_downs = cls.sample_ellipse_points( 285 ellipse_height=ellipse_down_height, 286 ellipse_width=ellipse_down_width, 287 ellipse_offset_y=ellipse_offset_y, 288 ellipse_offset_x=ellipse_offset_x, 289 angle_begin=angle_begin, 290 angle_end=angle_end, 291 angle_step=angle_step, 292 keep_last_oob=keep_last_oob, 293 ) 294 for point_up, point_down in zip(point_ups, point_downs): 295 char_slots.append(CharSlot.build(point_up=point_up, point_down=point_down)) 296 297 if reverse: 298 char_slots = list(reversed(char_slots)) 299 300 return char_slots 301 302 def sample_curved_text_line_rough_placements( 303 self, 304 height: int, 305 width: int, 306 rng: RandomGenerator, 307 ): 308 # Shared outer ellipse. 309 pad_ratio = float( 310 rng.uniform( 311 self.init_config.pad_ratio_min, 312 self.init_config.pad_ratio_max, 313 ) 314 ) 315 316 pad = round(pad_ratio * height) 317 ellipse_outer_height = height - 2 * pad 318 ellipse_outer_width = width - 2 * pad 319 assert ellipse_outer_height > 0 and ellipse_outer_width > 0 320 321 # Rough placements. 322 rough_placements: List[TextLineRoughPlacement] = [] 323 324 # Place text line one. 325 half_gap = None 326 text_line_mode = rng_choice(rng, self.text_line_modes, probs=self.text_line_modes_probs) 327 328 if text_line_mode == SealImpressionEllipseTextLineMode.ONE: 329 gap_ratio = float( 330 rng.uniform( 331 self.init_config.text_line_mode_one_gap_ratio_min, 332 self.init_config.text_line_mode_one_gap_ratio_max, 333 ) 334 ) 335 angle_gap = round(gap_ratio * 360) 336 angle_range = 360 - angle_gap 337 text_line_one_angle_begin = 90 + angle_gap // 2 338 text_line_one_angle_end = text_line_one_angle_begin + angle_range - 1 339 340 elif text_line_mode == SealImpressionEllipseTextLineMode.TWO: 341 gap_ratio = float( 342 rng.uniform( 343 self.init_config.text_line_mode_two_gap_ratio_min, 344 self.init_config.text_line_mode_two_gap_ratio_max, 345 ) 346 ) 347 half_gap = round(gap_ratio * 360 / 2) 348 349 text_line_one_angle_begin = 180 + half_gap 350 text_line_one_angle_end = 360 - half_gap 351 352 else: 353 raise NotImplementedError() 354 355 text_line_one_height_ratio = float( 356 rng.uniform( 357 self.init_config.text_line_height_ratio_min, 358 self.init_config.text_line_height_ratio_max, 359 ) 360 ) 361 text_line_one_height = round(text_line_one_height_ratio * height) 362 assert text_line_one_height > 0 363 ellipse_inner_one_height = ellipse_outer_height - 2 * text_line_one_height 364 ellipse_inner_one_width = ellipse_outer_width - 2 * text_line_one_height 365 assert ellipse_inner_one_height > 0 and ellipse_inner_one_width > 0 366 367 rough_placements.append( 368 TextLineRoughPlacement( 369 ellipse_outer_height=ellipse_outer_height, 370 ellipse_outer_width=ellipse_outer_width, 371 ellipse_inner_height=ellipse_inner_one_height, 372 ellipse_inner_width=ellipse_inner_one_width, 373 text_line_height=text_line_one_height, 374 angle_begin=text_line_one_angle_begin, 375 angle_end=text_line_one_angle_end, 376 clockwise=True, 377 ) 378 ) 379 380 # Now for the text line two. 381 if text_line_mode == SealImpressionEllipseTextLineMode.TWO: 382 assert half_gap 383 384 text_line_two_height_ratio = float( 385 rng.uniform( 386 self.init_config.text_line_height_ratio_min, 387 self.init_config.text_line_height_ratio_max, 388 ) 389 ) 390 text_line_two_height = round(text_line_two_height_ratio * height) 391 assert text_line_two_height > 0 392 ellipse_inner_two_height = ellipse_outer_height - 2 * text_line_two_height 393 ellipse_inner_two_width = ellipse_outer_width - 2 * text_line_two_height 394 assert ellipse_inner_two_height > 0 and ellipse_inner_two_width > 0 395 396 text_line_two_angle_begin = half_gap 397 text_line_two_angle_end = 180 - half_gap 398 399 rough_placements.append( 400 TextLineRoughPlacement( 401 ellipse_outer_height=ellipse_outer_height, 402 ellipse_outer_width=ellipse_outer_width, 403 ellipse_inner_height=ellipse_inner_two_height, 404 ellipse_inner_width=ellipse_inner_two_width, 405 text_line_height=text_line_two_height, 406 angle_begin=text_line_two_angle_begin, 407 angle_end=text_line_two_angle_end, 408 clockwise=False, 409 ) 410 ) 411 412 return rough_placements 413 414 def generate_text_line_slots_based_on_rough_placements( 415 self, 416 height: int, 417 width: int, 418 rough_placements: Sequence[TextLineRoughPlacement], 419 rng: RandomGenerator, 420 ): 421 ellipse_offset_y = height // 2 422 ellipse_offset_x = width // 2 423 424 text_line_slots: List[TextLineSlot] = [] 425 426 for rough_placement in rough_placements: 427 char_aspect_ratio = float( 428 rng.uniform( 429 self.init_config.char_aspect_ratio_min, 430 self.init_config.char_aspect_ratio_max, 431 ) 432 ) 433 char_width_ref = max(1, round(rough_placement.text_line_height * char_aspect_ratio)) 434 435 char_space_ratio = float( 436 rng.uniform( 437 self.init_config.char_space_ratio_min, 438 self.init_config.char_space_ratio_max, 439 ) 440 ) 441 char_space_ref = max(1, round(rough_placement.text_line_height * char_space_ratio)) 442 443 radius_ref = max(1, ellipse_offset_y) 444 angle_step = max( 445 self.init_config.angle_step_min, 446 round(360 * (char_width_ref + char_space_ref) / (2 * np.pi * radius_ref)), 447 ) 448 449 if rough_placement.clockwise: 450 char_slots = self.sample_char_slots( 451 ellipse_up_height=rough_placement.ellipse_outer_height, 452 ellipse_up_width=rough_placement.ellipse_outer_width, 453 ellipse_down_height=rough_placement.ellipse_inner_height, 454 ellipse_down_width=rough_placement.ellipse_inner_width, 455 ellipse_offset_y=ellipse_offset_y, 456 ellipse_offset_x=ellipse_offset_x, 457 angle_begin=rough_placement.angle_begin, 458 angle_end=rough_placement.angle_end, 459 angle_step=angle_step, 460 rng=rng, 461 ) 462 463 else: 464 char_slots = self.sample_char_slots( 465 ellipse_up_height=rough_placement.ellipse_inner_height, 466 ellipse_up_width=rough_placement.ellipse_inner_width, 467 ellipse_down_height=rough_placement.ellipse_outer_height, 468 ellipse_down_width=rough_placement.ellipse_outer_width, 469 ellipse_offset_y=ellipse_offset_y, 470 ellipse_offset_x=ellipse_offset_x, 471 angle_begin=rough_placement.angle_begin, 472 angle_end=rough_placement.angle_end, 473 angle_step=angle_step, 474 rng=rng, 475 reverse=True, 476 ) 477 478 text_line_slots.append( 479 TextLineSlot( 480 text_line_height=rough_placement.text_line_height, 481 char_aspect_ratio=char_aspect_ratio, 482 char_slots=char_slots, 483 ) 484 ) 485 486 return text_line_slots 487 488 def generate_text_line_slots(self, height: int, width: int, rng: RandomGenerator): 489 rough_placements = self.sample_curved_text_line_rough_placements( 490 height=height, 491 width=width, 492 rng=rng, 493 ) 494 text_line_slots = self.generate_text_line_slots_based_on_rough_placements( 495 height=height, 496 width=width, 497 rough_placements=rough_placements, 498 rng=rng, 499 ) 500 ellipse_inner_shape = ( 501 min(rough_placement.ellipse_inner_height for rough_placement in rough_placements), 502 min(rough_placement.ellipse_inner_width for rough_placement in rough_placements), 503 ) 504 return text_line_slots, ellipse_inner_shape 505 506 def sample_icon_box( 507 self, 508 height: int, 509 width: int, 510 ellipse_inner_shape: Tuple[int, int], 511 rng: RandomGenerator, 512 ): 513 ellipse_inner_height, ellipse_inner_width = ellipse_inner_shape 514 515 box_height_ratio = rng.uniform( 516 self.init_config.icon_height_ratio_min, 517 self.init_config.icon_height_ratio_max, 518 ) 519 box_height = round(ellipse_inner_height * box_height_ratio) 520 521 box_width_ratio = rng.uniform( 522 self.init_config.icon_width_ratio_min, 523 self.init_config.icon_width_ratio_max, 524 ) 525 box_width = round(ellipse_inner_width * box_width_ratio) 526 527 up = (height - box_height) // 2 528 down = up + box_height - 1 529 left = (width - box_width) // 2 530 right = left + box_width - 1 531 return Box(up=up, down=down, left=left, right=right) 532 533 def sample_internal_text_line_box( 534 self, 535 height: int, 536 width: int, 537 ellipse_inner_shape: Tuple[int, int], 538 icon_box_down: Optional[int], 539 rng: RandomGenerator, 540 ): 541 ellipse_inner_height, ellipse_inner_width = ellipse_inner_shape 542 if ellipse_inner_height > ellipse_inner_width: 543 # Not supported yet. 544 return None 545 546 # Vert. 547 box_height_ratio = rng.uniform( 548 self.init_config.internal_text_line_height_ratio_min, 549 self.init_config.internal_text_line_height_ratio_max, 550 ) 551 box_height = round(ellipse_inner_height * box_height_ratio) 552 553 half_height = height // 2 554 up = half_height 555 if icon_box_down: 556 up = icon_box_down + 1 557 down = min( 558 height - 1, 559 half_height + ellipse_inner_height // 2 - 1, 560 up + box_height - 1, 561 ) 562 563 if up > down: 564 return None 565 566 # Hori. 567 ellipse_h = down + 1 - half_height 568 ellipse_a = ellipse_inner_width / 2 569 ellipse_b = ellipse_inner_height / 2 570 box_width_max = round(2 * ellipse_b * np.sqrt(ellipse_a**2 - ellipse_h**2) / ellipse_a) 571 572 box_width_ratio = rng.uniform( 573 self.init_config.internal_text_line_width_ratio_min, 574 self.init_config.internal_text_line_width_ratio_max, 575 ) 576 box_width = round(ellipse_inner_width * box_width_ratio) 577 box_width = max(box_width_max, box_width) 578 579 left = (width - box_width) // 2 580 right = left + box_width - 1 581 582 if left > right: 583 return None 584 585 return Box(up=up, down=down, left=left, right=right) 586 587 def generate_background( 588 self, 589 height: int, 590 width: int, 591 ellipse_inner_shape: Tuple[int, int], 592 rng: RandomGenerator, 593 ): 594 background_mask = Mask.from_shape((height, width)) 595 596 border_style = rng_choice(rng, self.border_styles, probs=self.border_styles_probs) 597 598 # Will generate solid line first. 599 border_thickness_ratio = float( 600 rng.uniform( 601 self.init_config.border_thickness_ratio_min, 602 self.init_config.border_thickness_ratio_max, 603 ) 604 ) 605 border_thickness = round(height * border_thickness_ratio) 606 border_thickness = max(self.init_config.border_thickness_min, border_thickness) 607 608 center = (width // 2, height // 2) 609 # NOTE: minus 1 to make sure the border is inbound. 610 axes = (width // 2 - border_thickness - 1, height // 2 - border_thickness - 1) 611 cv.ellipse( 612 background_mask.mat, 613 center=center, 614 axes=axes, 615 angle=0, 616 startAngle=0, 617 endAngle=360, 618 color=1, 619 thickness=border_thickness, 620 ) 621 622 if border_thickness > 2 * self.init_config.border_thickness_min + 1 \ 623 and border_style == SealImpressionEllipseBorderStyle.DOUBLE_LINES: 624 # Remove the middle part to generate double lines. 625 border_thickness_empty = int( 626 rng.integers( 627 1, 628 border_thickness - 2 * self.init_config.border_thickness_min, 629 ) 630 ) 631 cv.ellipse( 632 background_mask.mat, 633 center=center, 634 # NOTE: I don't know why, but this works as expected. 635 # Probably `axes` points to the center of border. 636 axes=axes, 637 angle=0, 638 startAngle=0, 639 endAngle=360, 640 color=0, 641 thickness=border_thickness_empty, 642 ) 643 644 icon_box_down = None 645 if self.icon_image_selector and rng.random() < self.init_config.prob_add_icon: 646 icon_box = self.sample_icon_box( 647 height=height, 648 width=width, 649 ellipse_inner_shape=ellipse_inner_shape, 650 rng=rng, 651 ) 652 icon_box_down = icon_box.down 653 icon_grayscale_image = self.icon_image_selector.run( 654 { 655 'height': icon_box.height, 656 'width': icon_box.width 657 }, 658 rng, 659 ) 660 icon_mask_mat = (icon_grayscale_image.mat > self.init_config.icon_image_grayscale_min) 661 icon_mask = Mask(mat=icon_mask_mat.astype(np.uint8)) 662 icon_box.fill_mask(background_mask, icon_mask) 663 664 internal_text_line_box = None 665 if rng.random() < self.init_config.prob_add_internal_text_line: 666 internal_text_line_box = self.sample_internal_text_line_box( 667 height=height, 668 width=width, 669 ellipse_inner_shape=ellipse_inner_shape, 670 icon_box_down=icon_box_down, 671 rng=rng, 672 ) 673 674 return background_mask, internal_text_line_box 675 676 def run( 677 self, 678 run_config: SealImpressionEngineRunConfig, 679 rng: Optional[RandomGenerator] = None, 680 ): 681 assert rng is not None 682 683 alpha, color = self.sample_alpha_and_color(rng) 684 text_line_slots, ellipse_inner_shape = self.generate_text_line_slots( 685 height=run_config.height, 686 width=run_config.width, 687 rng=rng, 688 ) 689 background_mask, internal_text_line_box = self.generate_background( 690 height=run_config.height, 691 width=run_config.width, 692 ellipse_inner_shape=ellipse_inner_shape, 693 rng=rng, 694 ) 695 return SealImpression( 696 alpha=alpha, 697 color=color, 698 background_mask=background_mask, 699 text_line_slots=text_line_slots, 700 internal_text_line_box=internal_text_line_box, 701 )
Abstract base class for generic types.
A generic type is typically declared by inheriting from this class parameterized with one or more type variables. For example, a generic mapping type might be defined as::
class Mapping(Generic[KT, VT]): def __getitem__(self, key: KT) -> VT: ... # Etc.
This class can then be used as follows::
def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT: try: return mapping[key] except KeyError: return default
SealImpressionEllipseEngine( init_config: vkit.engine.seal_impression.ellipse.SealImpressionEllipseEngineInitConfig, init_resource: Union[vkit.engine.interface.NoneTypeEngineInitResource, NoneType] = None)
138 def __init__( 139 self, 140 init_config: SealImpressionEllipseEngineInitConfig, 141 init_resource: Optional[NoneTypeEngineInitResource] = None 142 ): 143 super().__init__(init_config, init_resource) 144 145 self.border_styles, self.border_styles_probs = normalize_to_keys_and_probs([ 146 ( 147 SealImpressionEllipseBorderStyle.SOLID_LINE, 148 self.init_config.weight_border_style_solid_line, 149 ), 150 ( 151 SealImpressionEllipseBorderStyle.DOUBLE_LINES, 152 self.init_config.weight_border_style_double_lines, 153 ), 154 ]) 155 self.text_line_modes, self.text_line_modes_probs = normalize_to_keys_and_probs([ 156 ( 157 SealImpressionEllipseTextLineMode.ONE, 158 self.init_config.weight_text_line_mode_one, 159 ), 160 ( 161 SealImpressionEllipseTextLineMode.TWO, 162 self.init_config.weight_text_line_mode_two, 163 ), 164 ]) 165 self.color_modes, self.color_modes_probs = normalize_to_keys_and_probs([ 166 ( 167 SealImpressionEllipseColorMode.GRAYSCALE, 168 self.init_config.weight_color_grayscale, 169 ), 170 ( 171 SealImpressionEllipseColorMode.RED, 172 self.init_config.weight_color_red, 173 ), 174 ( 175 SealImpressionEllipseColorMode.GREEN, 176 self.init_config.weight_color_green, 177 ), 178 ( 179 SealImpressionEllipseColorMode.BLUE, 180 self.init_config.weight_color_blue, 181 ), 182 ]) 183 self.icon_image_selector = None 184 if self.init_config.icon_image_folders: 185 self.icon_image_selector = image_selector_engine_executor_factory.create({ 186 'image_folders': self.init_config.icon_image_folders, 187 'target_image_mode': ImageMode.GRAYSCALE, 188 'force_resize': True, 189 })
def
sample_alpha_and_color(self, rng: numpy.random._generator.Generator):
191 def sample_alpha_and_color(self, rng: RandomGenerator): 192 alpha = float(rng.uniform( 193 self.init_config.alpha_min, 194 self.init_config.alpha_max, 195 )) 196 197 color_mode = rng_choice(rng, self.color_modes, probs=self.color_modes_probs) 198 rgb_value = int( 199 rng.integers( 200 self.init_config.color_rgb_min, 201 self.init_config.color_rgb_max + 1, 202 ) 203 ) 204 if color_mode == SealImpressionEllipseColorMode.GRAYSCALE: 205 color = (rgb_value,) * 3 206 else: 207 if color_mode == SealImpressionEllipseColorMode.RED: 208 color = (rgb_value, 0, 0) 209 elif color_mode == SealImpressionEllipseColorMode.GREEN: 210 color = (0, rgb_value, 0) 211 elif color_mode == SealImpressionEllipseColorMode.BLUE: 212 color = (0, 0, rgb_value) 213 else: 214 raise NotImplementedError() 215 216 return alpha, color
@classmethod
def
sample_ellipse_points( cls, ellipse_height: int, ellipse_width: int, ellipse_offset_y: int, ellipse_offset_x: int, angle_begin: int, angle_end: int, angle_step: int, keep_last_oob: bool):
218 @classmethod 219 def sample_ellipse_points( 220 cls, 221 ellipse_height: int, 222 ellipse_width: int, 223 ellipse_offset_y: int, 224 ellipse_offset_x: int, 225 angle_begin: int, 226 angle_end: int, 227 angle_step: int, 228 keep_last_oob: bool, 229 ): 230 # Sample points in unit circle. 231 unit_circle_xy_pairs: List[Tuple[float, float]] = [] 232 angle = angle_begin 233 while angle <= angle_end or (keep_last_oob and angle - angle_end < angle_step): 234 theta = angle / 180 * np.pi 235 # Shifted. 236 x = float(np.cos(theta)) 237 y = float(np.sin(theta)) 238 unit_circle_xy_pairs.append((x, y)) 239 # Move forward. 240 angle += angle_step 241 242 # Reshape to ellipse. 243 points = PointList() 244 half_ellipse_height = ellipse_height / 2 245 half_ellipse_width = ellipse_width / 2 246 for x, y in unit_circle_xy_pairs: 247 points.append( 248 Point.create( 249 y=y * half_ellipse_height + ellipse_offset_y, 250 x=x * half_ellipse_width + ellipse_offset_x, 251 ) 252 ) 253 return points
@classmethod
def
sample_char_slots( cls, ellipse_up_height: int, ellipse_up_width: int, ellipse_down_height: int, ellipse_down_width: int, ellipse_offset_y: int, ellipse_offset_x: int, angle_begin: int, angle_end: int, angle_step: int, rng: numpy.random._generator.Generator, reverse: float = False):
255 @classmethod 256 def sample_char_slots( 257 cls, 258 ellipse_up_height: int, 259 ellipse_up_width: int, 260 ellipse_down_height: int, 261 ellipse_down_width: int, 262 ellipse_offset_y: int, 263 ellipse_offset_x: int, 264 angle_begin: int, 265 angle_end: int, 266 angle_step: int, 267 rng: RandomGenerator, 268 reverse: float = False, 269 ): 270 char_slots: List[CharSlot] = [] 271 272 keep_last_oob = (rng.random() < 0.5) 273 274 point_ups = cls.sample_ellipse_points( 275 ellipse_height=ellipse_up_height, 276 ellipse_width=ellipse_up_width, 277 ellipse_offset_y=ellipse_offset_y, 278 ellipse_offset_x=ellipse_offset_x, 279 angle_begin=angle_begin, 280 angle_end=angle_end, 281 angle_step=angle_step, 282 keep_last_oob=keep_last_oob, 283 ) 284 point_downs = cls.sample_ellipse_points( 285 ellipse_height=ellipse_down_height, 286 ellipse_width=ellipse_down_width, 287 ellipse_offset_y=ellipse_offset_y, 288 ellipse_offset_x=ellipse_offset_x, 289 angle_begin=angle_begin, 290 angle_end=angle_end, 291 angle_step=angle_step, 292 keep_last_oob=keep_last_oob, 293 ) 294 for point_up, point_down in zip(point_ups, point_downs): 295 char_slots.append(CharSlot.build(point_up=point_up, point_down=point_down)) 296 297 if reverse: 298 char_slots = list(reversed(char_slots)) 299 300 return char_slots
def
sample_curved_text_line_rough_placements( self, height: int, width: int, rng: numpy.random._generator.Generator):
302 def sample_curved_text_line_rough_placements( 303 self, 304 height: int, 305 width: int, 306 rng: RandomGenerator, 307 ): 308 # Shared outer ellipse. 309 pad_ratio = float( 310 rng.uniform( 311 self.init_config.pad_ratio_min, 312 self.init_config.pad_ratio_max, 313 ) 314 ) 315 316 pad = round(pad_ratio * height) 317 ellipse_outer_height = height - 2 * pad 318 ellipse_outer_width = width - 2 * pad 319 assert ellipse_outer_height > 0 and ellipse_outer_width > 0 320 321 # Rough placements. 322 rough_placements: List[TextLineRoughPlacement] = [] 323 324 # Place text line one. 325 half_gap = None 326 text_line_mode = rng_choice(rng, self.text_line_modes, probs=self.text_line_modes_probs) 327 328 if text_line_mode == SealImpressionEllipseTextLineMode.ONE: 329 gap_ratio = float( 330 rng.uniform( 331 self.init_config.text_line_mode_one_gap_ratio_min, 332 self.init_config.text_line_mode_one_gap_ratio_max, 333 ) 334 ) 335 angle_gap = round(gap_ratio * 360) 336 angle_range = 360 - angle_gap 337 text_line_one_angle_begin = 90 + angle_gap // 2 338 text_line_one_angle_end = text_line_one_angle_begin + angle_range - 1 339 340 elif text_line_mode == SealImpressionEllipseTextLineMode.TWO: 341 gap_ratio = float( 342 rng.uniform( 343 self.init_config.text_line_mode_two_gap_ratio_min, 344 self.init_config.text_line_mode_two_gap_ratio_max, 345 ) 346 ) 347 half_gap = round(gap_ratio * 360 / 2) 348 349 text_line_one_angle_begin = 180 + half_gap 350 text_line_one_angle_end = 360 - half_gap 351 352 else: 353 raise NotImplementedError() 354 355 text_line_one_height_ratio = float( 356 rng.uniform( 357 self.init_config.text_line_height_ratio_min, 358 self.init_config.text_line_height_ratio_max, 359 ) 360 ) 361 text_line_one_height = round(text_line_one_height_ratio * height) 362 assert text_line_one_height > 0 363 ellipse_inner_one_height = ellipse_outer_height - 2 * text_line_one_height 364 ellipse_inner_one_width = ellipse_outer_width - 2 * text_line_one_height 365 assert ellipse_inner_one_height > 0 and ellipse_inner_one_width > 0 366 367 rough_placements.append( 368 TextLineRoughPlacement( 369 ellipse_outer_height=ellipse_outer_height, 370 ellipse_outer_width=ellipse_outer_width, 371 ellipse_inner_height=ellipse_inner_one_height, 372 ellipse_inner_width=ellipse_inner_one_width, 373 text_line_height=text_line_one_height, 374 angle_begin=text_line_one_angle_begin, 375 angle_end=text_line_one_angle_end, 376 clockwise=True, 377 ) 378 ) 379 380 # Now for the text line two. 381 if text_line_mode == SealImpressionEllipseTextLineMode.TWO: 382 assert half_gap 383 384 text_line_two_height_ratio = float( 385 rng.uniform( 386 self.init_config.text_line_height_ratio_min, 387 self.init_config.text_line_height_ratio_max, 388 ) 389 ) 390 text_line_two_height = round(text_line_two_height_ratio * height) 391 assert text_line_two_height > 0 392 ellipse_inner_two_height = ellipse_outer_height - 2 * text_line_two_height 393 ellipse_inner_two_width = ellipse_outer_width - 2 * text_line_two_height 394 assert ellipse_inner_two_height > 0 and ellipse_inner_two_width > 0 395 396 text_line_two_angle_begin = half_gap 397 text_line_two_angle_end = 180 - half_gap 398 399 rough_placements.append( 400 TextLineRoughPlacement( 401 ellipse_outer_height=ellipse_outer_height, 402 ellipse_outer_width=ellipse_outer_width, 403 ellipse_inner_height=ellipse_inner_two_height, 404 ellipse_inner_width=ellipse_inner_two_width, 405 text_line_height=text_line_two_height, 406 angle_begin=text_line_two_angle_begin, 407 angle_end=text_line_two_angle_end, 408 clockwise=False, 409 ) 410 ) 411 412 return rough_placements
def
generate_text_line_slots_based_on_rough_placements( self, height: int, width: int, rough_placements: Sequence[vkit.engine.seal_impression.ellipse.TextLineRoughPlacement], rng: numpy.random._generator.Generator):
414 def generate_text_line_slots_based_on_rough_placements( 415 self, 416 height: int, 417 width: int, 418 rough_placements: Sequence[TextLineRoughPlacement], 419 rng: RandomGenerator, 420 ): 421 ellipse_offset_y = height // 2 422 ellipse_offset_x = width // 2 423 424 text_line_slots: List[TextLineSlot] = [] 425 426 for rough_placement in rough_placements: 427 char_aspect_ratio = float( 428 rng.uniform( 429 self.init_config.char_aspect_ratio_min, 430 self.init_config.char_aspect_ratio_max, 431 ) 432 ) 433 char_width_ref = max(1, round(rough_placement.text_line_height * char_aspect_ratio)) 434 435 char_space_ratio = float( 436 rng.uniform( 437 self.init_config.char_space_ratio_min, 438 self.init_config.char_space_ratio_max, 439 ) 440 ) 441 char_space_ref = max(1, round(rough_placement.text_line_height * char_space_ratio)) 442 443 radius_ref = max(1, ellipse_offset_y) 444 angle_step = max( 445 self.init_config.angle_step_min, 446 round(360 * (char_width_ref + char_space_ref) / (2 * np.pi * radius_ref)), 447 ) 448 449 if rough_placement.clockwise: 450 char_slots = self.sample_char_slots( 451 ellipse_up_height=rough_placement.ellipse_outer_height, 452 ellipse_up_width=rough_placement.ellipse_outer_width, 453 ellipse_down_height=rough_placement.ellipse_inner_height, 454 ellipse_down_width=rough_placement.ellipse_inner_width, 455 ellipse_offset_y=ellipse_offset_y, 456 ellipse_offset_x=ellipse_offset_x, 457 angle_begin=rough_placement.angle_begin, 458 angle_end=rough_placement.angle_end, 459 angle_step=angle_step, 460 rng=rng, 461 ) 462 463 else: 464 char_slots = self.sample_char_slots( 465 ellipse_up_height=rough_placement.ellipse_inner_height, 466 ellipse_up_width=rough_placement.ellipse_inner_width, 467 ellipse_down_height=rough_placement.ellipse_outer_height, 468 ellipse_down_width=rough_placement.ellipse_outer_width, 469 ellipse_offset_y=ellipse_offset_y, 470 ellipse_offset_x=ellipse_offset_x, 471 angle_begin=rough_placement.angle_begin, 472 angle_end=rough_placement.angle_end, 473 angle_step=angle_step, 474 rng=rng, 475 reverse=True, 476 ) 477 478 text_line_slots.append( 479 TextLineSlot( 480 text_line_height=rough_placement.text_line_height, 481 char_aspect_ratio=char_aspect_ratio, 482 char_slots=char_slots, 483 ) 484 ) 485 486 return text_line_slots
def
generate_text_line_slots( self, height: int, width: int, rng: numpy.random._generator.Generator):
488 def generate_text_line_slots(self, height: int, width: int, rng: RandomGenerator): 489 rough_placements = self.sample_curved_text_line_rough_placements( 490 height=height, 491 width=width, 492 rng=rng, 493 ) 494 text_line_slots = self.generate_text_line_slots_based_on_rough_placements( 495 height=height, 496 width=width, 497 rough_placements=rough_placements, 498 rng=rng, 499 ) 500 ellipse_inner_shape = ( 501 min(rough_placement.ellipse_inner_height for rough_placement in rough_placements), 502 min(rough_placement.ellipse_inner_width for rough_placement in rough_placements), 503 ) 504 return text_line_slots, ellipse_inner_shape
def
sample_icon_box( self, height: int, width: int, ellipse_inner_shape: Tuple[int, int], rng: numpy.random._generator.Generator):
506 def sample_icon_box( 507 self, 508 height: int, 509 width: int, 510 ellipse_inner_shape: Tuple[int, int], 511 rng: RandomGenerator, 512 ): 513 ellipse_inner_height, ellipse_inner_width = ellipse_inner_shape 514 515 box_height_ratio = rng.uniform( 516 self.init_config.icon_height_ratio_min, 517 self.init_config.icon_height_ratio_max, 518 ) 519 box_height = round(ellipse_inner_height * box_height_ratio) 520 521 box_width_ratio = rng.uniform( 522 self.init_config.icon_width_ratio_min, 523 self.init_config.icon_width_ratio_max, 524 ) 525 box_width = round(ellipse_inner_width * box_width_ratio) 526 527 up = (height - box_height) // 2 528 down = up + box_height - 1 529 left = (width - box_width) // 2 530 right = left + box_width - 1 531 return Box(up=up, down=down, left=left, right=right)
def
sample_internal_text_line_box( self, height: int, width: int, ellipse_inner_shape: Tuple[int, int], icon_box_down: Union[int, NoneType], rng: numpy.random._generator.Generator):
533 def sample_internal_text_line_box( 534 self, 535 height: int, 536 width: int, 537 ellipse_inner_shape: Tuple[int, int], 538 icon_box_down: Optional[int], 539 rng: RandomGenerator, 540 ): 541 ellipse_inner_height, ellipse_inner_width = ellipse_inner_shape 542 if ellipse_inner_height > ellipse_inner_width: 543 # Not supported yet. 544 return None 545 546 # Vert. 547 box_height_ratio = rng.uniform( 548 self.init_config.internal_text_line_height_ratio_min, 549 self.init_config.internal_text_line_height_ratio_max, 550 ) 551 box_height = round(ellipse_inner_height * box_height_ratio) 552 553 half_height = height // 2 554 up = half_height 555 if icon_box_down: 556 up = icon_box_down + 1 557 down = min( 558 height - 1, 559 half_height + ellipse_inner_height // 2 - 1, 560 up + box_height - 1, 561 ) 562 563 if up > down: 564 return None 565 566 # Hori. 567 ellipse_h = down + 1 - half_height 568 ellipse_a = ellipse_inner_width / 2 569 ellipse_b = ellipse_inner_height / 2 570 box_width_max = round(2 * ellipse_b * np.sqrt(ellipse_a**2 - ellipse_h**2) / ellipse_a) 571 572 box_width_ratio = rng.uniform( 573 self.init_config.internal_text_line_width_ratio_min, 574 self.init_config.internal_text_line_width_ratio_max, 575 ) 576 box_width = round(ellipse_inner_width * box_width_ratio) 577 box_width = max(box_width_max, box_width) 578 579 left = (width - box_width) // 2 580 right = left + box_width - 1 581 582 if left > right: 583 return None 584 585 return Box(up=up, down=down, left=left, right=right)
def
generate_background( self, height: int, width: int, ellipse_inner_shape: Tuple[int, int], rng: numpy.random._generator.Generator):
587 def generate_background( 588 self, 589 height: int, 590 width: int, 591 ellipse_inner_shape: Tuple[int, int], 592 rng: RandomGenerator, 593 ): 594 background_mask = Mask.from_shape((height, width)) 595 596 border_style = rng_choice(rng, self.border_styles, probs=self.border_styles_probs) 597 598 # Will generate solid line first. 599 border_thickness_ratio = float( 600 rng.uniform( 601 self.init_config.border_thickness_ratio_min, 602 self.init_config.border_thickness_ratio_max, 603 ) 604 ) 605 border_thickness = round(height * border_thickness_ratio) 606 border_thickness = max(self.init_config.border_thickness_min, border_thickness) 607 608 center = (width // 2, height // 2) 609 # NOTE: minus 1 to make sure the border is inbound. 610 axes = (width // 2 - border_thickness - 1, height // 2 - border_thickness - 1) 611 cv.ellipse( 612 background_mask.mat, 613 center=center, 614 axes=axes, 615 angle=0, 616 startAngle=0, 617 endAngle=360, 618 color=1, 619 thickness=border_thickness, 620 ) 621 622 if border_thickness > 2 * self.init_config.border_thickness_min + 1 \ 623 and border_style == SealImpressionEllipseBorderStyle.DOUBLE_LINES: 624 # Remove the middle part to generate double lines. 625 border_thickness_empty = int( 626 rng.integers( 627 1, 628 border_thickness - 2 * self.init_config.border_thickness_min, 629 ) 630 ) 631 cv.ellipse( 632 background_mask.mat, 633 center=center, 634 # NOTE: I don't know why, but this works as expected. 635 # Probably `axes` points to the center of border. 636 axes=axes, 637 angle=0, 638 startAngle=0, 639 endAngle=360, 640 color=0, 641 thickness=border_thickness_empty, 642 ) 643 644 icon_box_down = None 645 if self.icon_image_selector and rng.random() < self.init_config.prob_add_icon: 646 icon_box = self.sample_icon_box( 647 height=height, 648 width=width, 649 ellipse_inner_shape=ellipse_inner_shape, 650 rng=rng, 651 ) 652 icon_box_down = icon_box.down 653 icon_grayscale_image = self.icon_image_selector.run( 654 { 655 'height': icon_box.height, 656 'width': icon_box.width 657 }, 658 rng, 659 ) 660 icon_mask_mat = (icon_grayscale_image.mat > self.init_config.icon_image_grayscale_min) 661 icon_mask = Mask(mat=icon_mask_mat.astype(np.uint8)) 662 icon_box.fill_mask(background_mask, icon_mask) 663 664 internal_text_line_box = None 665 if rng.random() < self.init_config.prob_add_internal_text_line: 666 internal_text_line_box = self.sample_internal_text_line_box( 667 height=height, 668 width=width, 669 ellipse_inner_shape=ellipse_inner_shape, 670 icon_box_down=icon_box_down, 671 rng=rng, 672 ) 673 674 return background_mask, internal_text_line_box
def
run( self, run_config: vkit.engine.seal_impression.type.SealImpressionEngineRunConfig, rng: Union[numpy.random._generator.Generator, NoneType] = None):
676 def run( 677 self, 678 run_config: SealImpressionEngineRunConfig, 679 rng: Optional[RandomGenerator] = None, 680 ): 681 assert rng is not None 682 683 alpha, color = self.sample_alpha_and_color(rng) 684 text_line_slots, ellipse_inner_shape = self.generate_text_line_slots( 685 height=run_config.height, 686 width=run_config.width, 687 rng=rng, 688 ) 689 background_mask, internal_text_line_box = self.generate_background( 690 height=run_config.height, 691 width=run_config.width, 692 ellipse_inner_shape=ellipse_inner_shape, 693 rng=rng, 694 ) 695 return SealImpression( 696 alpha=alpha, 697 color=color, 698 background_mask=background_mask, 699 text_line_slots=text_line_slots, 700 internal_text_line_box=internal_text_line_box, 701 )