ai_learning/modules/12_generative_handwriting/notes/03_conditional_glyphs.md

13 KiB
Raw Blame History

Условная генерация глифов, композиция строк и читаемость

Глава связывает генератор отдельных символов с построением строки, измерением читаемости и ограничениями безопасного использования синтетического почерка.

Мотивация и декомпозиция

Генерировать «рукописный текст» одной картинкой трудно: модель должна одновременно соблюдать содержание, стиль, интервалы, baseline и границы. Практичный baseline разбивает задачу:

  1. генератор создаёт глиф по символу и style code;
  2. layout engine задаёт размеры, kerning и baseline;
  3. compositor размещает изображения с перекрытием;
  4. постобработка имитирует бумагу/чернила;
  5. recognizer и человек проверяют читаемость.

Декомпозиция прозрачна и управляема, но посимвольная генерация часто выглядит неестественно: настоящий курсив содержит связи и контекстные формы. Следующий уровень — генерация коротких grapheme chunks или всей строки с sequence encoder.

Условная модель

Пусть c — идентификатор символа, \mathbf{s}\in\mathbb{R}^{d_s} — стиль, \mathbf{z}\sim\mathcal{N}(0,\mathbf{I}) — локальная вариация:


\hat{\mathbf{x}}=G_{\boldsymbol{\theta}}(\mathbf{z},\mathbf{e}_c,\mathbf{s}),

где \mathbf{e}_c — learned embedding. Content должен меняться с c, общий style code — сохраняться между глифами строки, а локальный \mathbf{z}_i — добавлять небольшую естественную вариативность. Если семплировать новый style для каждой буквы, строка выглядит написанной разными людьми.

Content/style не разделяются автоматически. Возможны:

  • conditional VAE: encoder получает глиф и label, decoder — z,c;
  • conditional GAN/diffusion;
  • style encoder по нескольким reference glyphs;
  • losses распознавания, consistency и contrastive style representation.

Recognizer loss -\log p_{\rm OCR}(c\mid G(z,c,s)) улучшает различимость, но generator может эксплуатировать особенности конкретного OCR. Нужны независимая модель и human evaluation. Для unseen символов требуется compositional representation штрихов или few-shot reference; embedding закрытого словаря не экстраполирует.

Подготовка данных

Каждый пример хранит raster, Unicode code point/grapheme, writer ID, источник и лицензию. Нужны единые polarity, alpha, canvas и диапазон, но сохраняйте метаданные bounding box, baseline, ascender/descender. Split выполняют по writers, а для zero-shot content — дополнительно по characters.

Class imbalance приводит к хорошим частым буквам и плохим редким. Используйте balanced sampler или class weights, но отчёт давайте per-character. Augmentation не должна превращать букву в другую: horizontal flip почти всегда недопустим, сильный rotation/erosion меняет читаемость.

Геометрия композиции

Для глифа i известны bounding box, advance a_i, bearing и baseline offset. Позиция пера:


x_{i+1}=x_i+a_i+k(c_i,c_{i+1})+\delta_i,

где k — pair-dependent kerning, \delta_i — малая случайная вариация. Вертикальная позиция:


y_i=y_{\rm base}-b_i+\eta_i,

где b_i связывает bitmap с baseline, \eta_i — медленный jitter. Независимый сильный jitter разрушает строку; лучше коррелированный low-frequency drift.

При alpha compositing foreground F, background B, opacity \alpha\in[0,1]:


I=\alpha F+(1-\alpha)B.

Для grayscale «чёрные чернила на белом» часто удобнее хранить ink mask M=1-I и объединять перекрытия как M=1-\prod_i(1-M_i), а не суммировать с переполнением. Resize должен сохранять aspect ratio; thresholding после каждого масштабирования создаёт jagged edges.

Перенос строк учитывает ширину слова до размещения. Для длинных слов нужен fallback: hyphenation, уменьшение масштаба в допустимых пределах или расширение canvas. Пунктуация имеет собственные bearings; пробел — advance без bitmap.

Разобранный пример

Пусть два глифа имеют advances 18 и 16 px, pair kerning k=-2 px, jitter \delta=1 px. Если x_1=10, то x_2=10+18-2+1=27 px. Bounding boxes могут перекрываться — это допустимо для связного письма, если alpha/ink compositing корректно.

Для строки шириной 100 px следующее слово шириной 42 px при текущем pen 70 и пробеле 6 не помещается: 70+6+42=118. Слово целиком переносится, а не режется посередине глифа.

Метрики читаемости и качества

Ни одной достаточной метрики нет. Используйте набор:

  • Character accuracy / CER независимого OCR на сгенерированном тексте;
  • word accuracy / WER для целых строк;
  • per-class confusion, особенно похожих и/п, ш/щ, ь/ъ;
  • coverage/diversity внутри одного условия;
  • style consistency между глифами одной строки и различимость между styles;
  • nearest-neighbor distance до train для обнаружения memorization;
  • layout metrics: clipping rate, overlap violations, baseline deviation, доля строк за canvas;
  • human transcription: слепая расшифровка несколькими людьми с agreement;
  • preference/legibility rating с заранее заданной шкалой.

OCR accuracy может быть высокой на неестественных adversarial patterns или низкой на красивом почерке вне домена OCR. FID не измеряет соответствие заданному тексту. Human raters должны видеть randomized samples, не знать метод и иметь право не оценивать чувствительный контент.

Runnable NumPy-код композиции

import numpy as np

def place_ink(canvas, glyph, x, y):
    """canvas/glyph: ink masks [0, 1], координаты могут выходить за canvas."""
    h, w = glyph.shape
    H, W = canvas.shape
    x0, y0, x1, y1 = max(x, 0), max(y, 0), min(x + w, W), min(y + h, H)
    if x0 >= x1 or y0 >= y1:
        return
    g = glyph[y0-y:y1-y, x0-x:x1-x]
    c = canvas[y0:y1, x0:x1]
    canvas[y0:y1, x0:x1] = 1 - (1 - c) * (1 - g)

canvas = np.zeros((40, 100), dtype=np.float32)
g1 = np.zeros((24, 16), dtype=np.float32)
g1[:, 6:9] = 0.8
g1[2:5, 2:14] = 0.8
g2 = np.fliplr(g1).copy()
place_ink(canvas, g1, 10, 8)
place_ink(canvas, g2, 23, 9)  # небольшое перекрытие
image = (255 * (1 - canvas)).round().astype(np.uint8)
print(image.shape, image.min(), image.max())

Функция делает clipping координат и устойчиво объединяет ink masks. Для цветных RGBA-слоёв используйте premultiplied alpha и единое цветовое пространство.

От глифа к production-pipeline

  1. Нормализовать Unicode осознанно: code point не всегда равен grapheme.
  2. Токенизировать текст, отдельно обработать пробелы, punctuation и неизвестные символы; не заменять их молча.
  3. Выбрать один style reference/code на строку и локальные seeds на glyph.
  4. Сгенерировать glyph masks и их геометрические метаданные.
  5. Рассчитать word-aware layout, затем rasterize/composite.
  6. Проверить clipping, пустые glyphs и диапазоны.
  7. Прогнать независимый OCR; пометить низкоуверенные строки для проверки человеком.
  8. Сохранить seed, model/version, условия и отметку «synthetic».

Для воспроизводимости генератор случайных чисел передают явно. Batch generation не должна случайно повторно использовать персональный style reference между пользователями.

Риски злоупотребления и меры

Система может использоваться для подделки подписей, документов, согласия, экзаменационных работ, harassment или имитации конкретного человека. Нельзя обучаться на почерке без прав и информированного согласия, особенно на подписях, медицинских и юридических формах.

Безопасные границы продукта:

  • не поддерживать signature generation и identity impersonation;
  • требовать право на style references и минимизировать их хранение;
  • добавлять видимую маркировку синтетики и, где уместно, provenance metadata;
  • ограничивать массовую генерацию официально выглядящих документов;
  • логировать согласованные abuse-relevant события без хранения лишнего текста;
  • проводить misuse review и red-team до публикации;
  • не обещать, что watermark абсолютно не удаляется.

Учебные примеры используйте на собственном или открыто лицензированном почерке и нейтральных фразах. Сходство со стилем человека не означает право выдавать результат за написанное им.

Типичные ошибки и ограничения

  • Менять style code для каждого глифа или фиксировать один noise code для всей строки.
  • Обрезать descenders из-за неверного baseline/canvas.
  • Масштабировать glyphs независимо по ширине и высоте.
  • Использовать только OCR, обученный вместе с generator.
  • Делить случайно по изображениям вместо writers.
  • Скрывать unknown characters или заменять их визуально похожими.
  • Выдавать synthetic handwriting за настоящий источник.
  • Считать watermark полной защитой от подделки.

Краткое резюме

  • Условный generator разделяет content label, общий style и локальную вариацию.
  • Хорошая строка требует typographic metrics, baseline, kerning и корректного alpha.
  • Читаемость оценивают OCR, per-class/layout метриками и слепой human transcription.
  • Writer-level split, provenance, согласие и запрет impersonation обязательны.

Связанные материалы