From 1b5fe6cbfe3f4bcee7ca3cde69168f2e9d6be384 Mon Sep 17 00:00:00 2001 From: m13891290332 Date: Thu, 27 Nov 2025 01:59:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BC=80=E5=A4=B4=E6=89=B0?= =?UTF-8?q?=E5=8A=A8=E5=8F=82=E6=95=B0left=5Fmargin=5Fsigma=E3=80=81?= =?UTF-8?q?=E6=B7=B7=E4=B9=B1=E5=BA=A6=E7=9A=84=E6=AD=A3=E6=80=81=E5=88=86?= =?UTF-8?q?=E5=B8=83=E5=8F=82=E6=95=B0all=5Ftext=5Fsigma=5Fsigma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handright/_core.py | 112 +++++++++++++++++++++++++++++++---------- handright/_template.py | 44 ++++++++++++++-- 2 files changed, 124 insertions(+), 32 deletions(-) diff --git a/handright/_core.py b/handright/_core.py index 0a0cb77..4de267b 100644 --- a/handright/_core.py +++ b/handright/_core.py @@ -49,24 +49,59 @@ def handwrite( templates = (template,) else: templates = template - pages = _draft(text, templates, seed) - renderer = _Renderer(templates, seed) + + total_pages = None + if any(t.get_all_text_sigma_sigma() > 1 for t in templates): + total_pages = _count_pages(text, templates, seed) + + pages = _draft(text, templates, seed, total_pages) + renderer = _Renderer(templates, seed, total_pages) return mapper(renderer, pages) -def _draft(text, templates, seed=None) -> Iterator[Page]: +def _count_pages(text, templates, seed=None) -> int: text = _preprocess_text(text) template_iter = itertools.cycle(templates) - num_iter = itertools.count() rand = random.Random(x=seed) start = 0 + count = 0 while start < len(text): template = next(template_iter) - page = Page(_INTERNAL_MODE, template.get_size(), _BLACK, next(num_iter)) + page = DummyPage(template.get_size()) start = _draw_page(page, text, start, template, rand) + count += 1 + return count + + +def _draft(text, templates, seed=None, total_pages=None) -> Iterator[Page]: + text = _preprocess_text(text) + template_iter = itertools.cycle(templates) + num_iter = itertools.count() + rand = random.Random(x=seed) + start = 0 + while start < len(text): + template = next(template_iter) + num = next(num_iter) + page = Page(_INTERNAL_MODE, template.get_size(), _BLACK, num) + multiplier = _get_multiplier(num, total_pages, template.get_all_text_sigma_sigma()) + start = _draw_page(page, text, start, template, rand, multiplier) yield page +def _get_multiplier(page_num, total_pages, sigma_sigma): + if total_pages is None or total_pages <= 1 or sigma_sigma <= 1: + return 1.0 + + center = (total_pages - 1) / 2 + sigma_curve = (total_pages - 1) / 6 + if sigma_curve == 0: + return 1.0 + + delta = page_num - center + factor = math.exp(-(delta ** 2) / (2 * sigma_curve ** 2)) + return 1 + (sigma_sigma - 1) * factor + + def _preprocess_text(text: str) -> str: return text.replace(_CRLF, _LF).replace(_CR, _LF) @@ -89,7 +124,7 @@ def _check_template(page, tpl) -> None: def _draw_page( - page, text, start: int, tpl: Template, rand: random.Random + page, text, start: int, tpl: Template, rand: random.Random, multiplier: float = 1.0 ) -> int: _check_template(page, tpl) @@ -107,7 +142,7 @@ def _draw_page( draw = page.draw() y = top_margin + line_spacing - font_size while y <= height - bottom_margin - font_size: - x = left_margin + x = gauss(rand, left_margin, tpl.get_left_margin_sigma() * multiplier) while True: if text[start] == _LF: start += 1 @@ -121,9 +156,9 @@ def _draw_page( and text[start] not in end_chars): break if Feature.GRID_LAYOUT in tpl.get_features(): - x = _grid_layout(draw, x, y, text[start], tpl, rand) + x = _grid_layout(draw, x, y, text[start], tpl, rand, multiplier) else: - x = _flow_layout(draw, x, y, text[start], tpl, rand) + x = _flow_layout(draw, x, y, text[start], tpl, rand, multiplier) start += 1 if start == len(text): return start @@ -132,34 +167,34 @@ def _draw_page( def _flow_layout( - draw, x, y, char, tpl: Template, rand: random.Random + draw, x, y, char, tpl: Template, rand: random.Random, multiplier: float = 1.0 ) -> float: - xy = (round(x), round(gauss(rand, y, tpl.get_line_spacing_sigma()))) - font = _get_font(tpl, rand) + xy = (round(x), round(gauss(rand, y, tpl.get_line_spacing_sigma() * multiplier))) + font = _get_font(tpl, rand, multiplier) offset = _draw_char(draw, char, xy, font) x += gauss( rand, tpl.get_word_spacing() + offset, - tpl.get_word_spacing_sigma() + tpl.get_word_spacing_sigma() * multiplier ) return x def _grid_layout( - draw, x, y, char, tpl: Template, rand: random.Random + draw, x, y, char, tpl: Template, rand: random.Random, multiplier: float = 1.0 ) -> float: - xy = (round(gauss(rand, x, tpl.get_word_spacing_sigma())), - round(gauss(rand, y, tpl.get_line_spacing_sigma()))) - font = _get_font(tpl, rand) + xy = (round(gauss(rand, x, tpl.get_word_spacing_sigma() * multiplier)), + round(gauss(rand, y, tpl.get_line_spacing_sigma() * multiplier))) + font = _get_font(tpl, rand, multiplier) _ = _draw_char(draw, char, xy, font) x += tpl.get_word_spacing() + tpl.get_font().size return x -def _get_font(tpl: Template, rand: random.Random): +def _get_font(tpl: Template, rand: random.Random, multiplier: float = 1.0): font = tpl.get_font() actual_font_size = max(round( - gauss(rand, font.size, tpl.get_font_size_sigma()) + gauss(rand, font.size, tpl.get_font_size_sigma() * multiplier) ), 0) if actual_font_size != font.size: return font.font_variant(size=actual_font_size) @@ -182,14 +217,16 @@ class _Renderer(object): "_templates", "_rand", "_hashed_seed", + "_total_pages", ) - def __init__(self, templates, seed=None) -> None: + def __init__(self, templates, seed=None, total_pages=None) -> None: self._templates = _to_picklable(templates) self._rand = random.Random() self._hashed_seed = None if seed is not None: self._hashed_seed = hash(seed) + self._total_pages = total_pages def __call__(self, page) -> PIL.Image.Image: if self._hashed_seed is None: @@ -206,7 +243,8 @@ def _perturb_and_merge(self, page) -> PIL.Image.Image: if bbox is None: return canvas strokes = _extract_strokes(page.matrix(), bbox) - _draw_strokes(canvas.load(), strokes, template, self._rand) + multiplier = _get_multiplier(page.num, self._total_pages, template.get_all_text_sigma_sigma()) + _draw_strokes(canvas.load(), strokes, template, self._rand, multiplier) return canvas @@ -261,7 +299,7 @@ def _extract_stroke( stack.append((x + 1, y)) -def _draw_strokes(bitmap, strokes, tpl, rand) -> None: +def _draw_strokes(bitmap, strokes, tpl, rand, multiplier=1.0) -> None: stroke = [] min_x = _MAX_INT16_VALUE min_y = _MAX_INT16_VALUE @@ -270,7 +308,7 @@ def _draw_strokes(bitmap, strokes, tpl, rand) -> None: for xy in strokes: if xy == _STROKE_END: center = ((min_x + max_x) / 2, (min_y + max_y) / 2) - _draw_stroke(bitmap, stroke, tpl, center, rand) + _draw_stroke(bitmap, stroke, tpl, center, rand, multiplier) min_x = _MAX_INT16_VALUE min_y = _MAX_INT16_VALUE max_x = 0 @@ -290,11 +328,12 @@ def _draw_stroke( stroke: Sequence[Tuple[int, int]], tpl: Template, center: Tuple[float, float], - rand + rand, + multiplier: float = 1.0 ) -> None: - dx = gauss(rand, 0, tpl.get_perturb_x_sigma()) - dy = gauss(rand, 0, tpl.get_perturb_y_sigma()) - theta = gauss(rand, 0, tpl.get_perturb_theta_sigma()) + dx = gauss(rand, 0, tpl.get_perturb_x_sigma() * multiplier) + dy = gauss(rand, 0, tpl.get_perturb_y_sigma() * multiplier) + theta = gauss(rand, 0, tpl.get_perturb_theta_sigma() * multiplier) for x, y in stroke: new_x, new_y = _rotate(center, x, y, theta) new_x = round(new_x + dx) @@ -324,3 +363,22 @@ def _xy(x: int, y: int) -> int: def _x_y(xy: int) -> Tuple[int, int]: return xy >> 16, xy & 0xFFFF + + +class DummyDraw(object): + def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs): + pass + + +class DummyPage(object): + def __init__(self, size): + self._size = size + + def width(self): + return self._size[0] + + def height(self): + return self._size[1] + + def draw(self): + return DummyDraw() diff --git a/handright/_template.py b/handright/_template.py index 7d97697..1b1b419 100644 --- a/handright/_template.py +++ b/handright/_template.py @@ -27,12 +27,14 @@ class Template(object): "_line_spacing_sigma", "_font_size_sigma", "_word_spacing_sigma", + "_left_margin_sigma", "_start_chars", "_end_chars", "_perturb_x_sigma", "_perturb_y_sigma", "_perturb_theta_sigma", "_features", + "_all_text_sigma_sigma", ) _DEFAULT_WORD_SPACING = 0 @@ -47,6 +49,8 @@ class Template(object): _DEFAULT_PERTURB_THETA_SIGMA = 0.07 + _DEFAULT_ALL_TEXT_SIGMA_SIGMA = 1.0 + _DEFAULT_FEATURES = frozenset() def __init__( @@ -63,12 +67,14 @@ def __init__( line_spacing_sigma: Optional[float] = None, font_size_sigma: Optional[float] = None, word_spacing_sigma: Optional[float] = None, + left_margin_sigma: Optional[float] = None, start_chars: str = _DEFAULT_START_CHARS, end_chars: str = _DEFAULT_END_CHARS, perturb_x_sigma: Optional[float] = None, perturb_y_sigma: Optional[float] = None, perturb_theta_sigma: float = _DEFAULT_PERTURB_THETA_SIGMA, features: Set = _DEFAULT_FEATURES, + all_text_sigma_sigma: float = _DEFAULT_ALL_TEXT_SIGMA_SIGMA, ): """Note that, all the Integer parameters are in pixels. @@ -85,9 +91,9 @@ def __init__( `word_spacing` can be less than `0`, but must be greater than `-font.size // 2`. - `line_spacing_sigma`, `font_size_sigma` and `word_spacing_sigma` are the - sigmas of the gauss distributions of line spacing, font size and word - spacing, respectively. + `line_spacing_sigma`, `font_size_sigma`, `word_spacing_sigma` and + `left_margin_sigma` are the sigmas of the gauss distributions of line + spacing, font size, word spacing and left margin, respectively. `start_chars` is the collection of Chars which should not be placed in the end of a line. `end_chars`, by contrast, is the collection of Chars @@ -97,6 +103,9 @@ def __init__( sigmas of the gauss distributions of the horizontal position, the vertical position and the rotation of strokes, respectively. + `all_text_sigma_sigma` is the overall sigma multiplier for all + perturbation parameters. It must be greater than or equal to 1. + **EXPERIMENT** Use `features` to turn on the extra features, see `handright.Feature`. """ @@ -112,12 +121,14 @@ def __init__( self.set_line_spacing_sigma(line_spacing_sigma) self.set_font_size_sigma(font_size_sigma) self.set_word_spacing_sigma(word_spacing_sigma) + self.set_left_margin_sigma(left_margin_sigma) self.set_start_chars(start_chars) self.set_end_chars(end_chars) self.set_perturb_x_sigma(perturb_x_sigma) self.set_perturb_y_sigma(perturb_y_sigma) self.set_perturb_theta_sigma(perturb_theta_sigma) self.set_features(features) + self.set_all_text_sigma_sigma(all_text_sigma_sigma) def __eq__(self, other) -> bool: return (isinstance(other, Template) @@ -134,11 +145,13 @@ def __eq__(self, other) -> bool: and self._word_spacing == other._word_spacing and self._features == other._features and self._word_spacing_sigma == other._word_spacing_sigma + and self._left_margin_sigma == other._left_margin_sigma and self._start_chars == other._start_chars and self._end_chars == other._end_chars and self._perturb_x_sigma == other._perturb_x_sigma and self._perturb_y_sigma == other._perturb_y_sigma - and self._perturb_theta_sigma == other._perturb_theta_sigma) + and self._perturb_theta_sigma == other._perturb_theta_sigma + and self._all_text_sigma_sigma == other._all_text_sigma_sigma) def set_background(self, background: PIL.Image.Image) -> None: self._background = background @@ -186,6 +199,11 @@ def set_word_spacing( def set_features(self, features: Set = _DEFAULT_FEATURES) -> None: self._features = features + def set_all_text_sigma_sigma( + self, all_text_sigma_sigma: float = _DEFAULT_ALL_TEXT_SIGMA_SIGMA + ) -> None: + self._all_text_sigma_sigma = all_text_sigma_sigma + def set_line_spacing_sigma( self, line_spacing_sigma: Optional[float] = None ) -> None: @@ -210,6 +228,14 @@ def set_word_spacing_sigma( else: self._word_spacing_sigma = word_spacing_sigma + def set_left_margin_sigma( + self, left_margin_sigma: Optional[float] = None + ) -> None: + if left_margin_sigma is None: + self._left_margin_sigma = self._font.size / 32 + else: + self._left_margin_sigma = left_margin_sigma + def set_start_chars(self, start_chars: str = _DEFAULT_START_CHARS) -> None: self._start_chars = start_chars @@ -267,6 +293,9 @@ def get_word_spacing(self) -> int: def get_features(self) -> Set: return self._features + def get_all_text_sigma_sigma(self) -> float: + return self._all_text_sigma_sigma + def get_line_spacing_sigma(self) -> float: return self._line_spacing_sigma @@ -276,6 +305,9 @@ def get_font_size_sigma(self) -> float: def get_word_spacing_sigma(self) -> float: return self._word_spacing_sigma + def get_left_margin_sigma(self) -> float: + return self._left_margin_sigma + def get_start_chars(self) -> str: return self._start_chars @@ -316,11 +348,13 @@ def __repr__(self) -> str: "line_spacing_sigma={self._line_spacing_sigma}, " "font_size_sigma={self._font_size_sigma}, " "word_spacing_sigma={self._word_spacing_sigma}, " + "left_margin_sigma={self._left_margin_sigma}, " "start_chars={self._start_chars}, " "end_chars={self._end_chars}, " "perturb_x_sigma={self._perturb_x_sigma}, " "perturb_y_sigma={self._perturb_y_sigma}, " - "perturb_theta_sigma={self._perturb_theta_sigma})" + "perturb_theta_sigma={self._perturb_theta_sigma}, " + "all_text_sigma_sigma={self._all_text_sigma_sigma})" ).format(class_name=class_name, self=self)