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 vkit.engine.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(self, run_config: SealImpressionEngineRunConfig, rng: RandomGenerator):
676        alpha, color = self.sample_alpha_and_color(rng)
677        text_line_slots, ellipse_inner_shape = self.generate_text_line_slots(
678            height=run_config.height,
679            width=run_config.width,
680            rng=rng,
681        )
682        background_mask, internal_text_line_box = self.generate_background(
683            height=run_config.height,
684            width=run_config.width,
685            ellipse_inner_shape=ellipse_inner_shape,
686            rng=rng,
687        )
688        return SealImpression(
689            alpha=alpha,
690            color=color,
691            background_mask=background_mask,
692            text_line_slots=text_line_slots,
693            internal_text_line_box=internal_text_line_box,
694        )
695
696
697seal_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.

Inherited Members
enum.Enum
name
value
class SealImpressionEllipseTextLineMode(enum.Enum):
100class SealImpressionEllipseTextLineMode(Enum):
101    ONE = 'one'
102    TWO = 'two'

An enumeration.

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.

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.

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(self, run_config: SealImpressionEngineRunConfig, rng: RandomGenerator):
677        alpha, color = self.sample_alpha_and_color(rng)
678        text_line_slots, ellipse_inner_shape = self.generate_text_line_slots(
679            height=run_config.height,
680            width=run_config.width,
681            rng=rng,
682        )
683        background_mask, internal_text_line_box = self.generate_background(
684            height=run_config.height,
685            width=run_config.width,
686            ellipse_inner_shape=ellipse_inner_shape,
687            rng=rng,
688        )
689        return SealImpression(
690            alpha=alpha,
691            color=color,
692            background_mask=background_mask,
693            text_line_slots=text_line_slots,
694            internal_text_line_box=internal_text_line_box,
695        )

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            })
@classmethod
def get_type_name(cls) -> str:
134    @classmethod
135    def get_type_name(cls) -> str:
136        return 'ellipse'
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: numpy.random._generator.Generator):
676    def run(self, run_config: SealImpressionEngineRunConfig, rng: RandomGenerator):
677        alpha, color = self.sample_alpha_and_color(rng)
678        text_line_slots, ellipse_inner_shape = self.generate_text_line_slots(
679            height=run_config.height,
680            width=run_config.width,
681            rng=rng,
682        )
683        background_mask, internal_text_line_box = self.generate_background(
684            height=run_config.height,
685            width=run_config.width,
686            ellipse_inner_shape=ellipse_inner_shape,
687            rng=rng,
688        )
689        return SealImpression(
690            alpha=alpha,
691            color=color,
692            background_mask=background_mask,
693            text_line_slots=text_line_slots,
694            internal_text_line_box=internal_text_line_box,
695        )