Всем привет, это моя первая статья на Хабре. В этой статье я хочу рассказать, как сгенерировать датасет печатных букв с помощью .ttf файла и кода на Python в 170 строк.

Зачем?

Для начала выясним, зачем нужно генерировать датасет. В моем случае стоял пользовательский интерес в распознавании шифров с конструкторских документов при сканировании чертежей. Для распознавания нужна нейросеть, чтобы обучить нейросеть нужен датасет. В общем, нейросеть без датасета как машина без колес, а значит необходимо иметь возможность генерировать датасет. При попытке найти датасет по типу EMNIST было обнаружено, что печатные шрифты никто не выкладывает, по крайней мере, на Кириллице. А если нужно больше данных чем есть, или нужно поменять искажения текста. Так и было принято решение написать генератор датасета по типу EMNIST. Но с печатными буквами русского алфавита.

Создание изображения

Начнем с библиотек, нам потребуется следующий инструментарий:

from PIL import Image, ImageDraw, ImageFont, ImageFilter

import os

from glob import glob

import numpy as np

PIL - он же Pillow, позволит нам создавать изображения и работать с ними.
OS - позволит работать с директориями.
glob - для поиска изображений в папках.
numpy - для работы с массивами.

Я работаю в PyCharm, поэтому не буду затрагивать тему установки библиотек. Об этом можно почитать на официальном сайте Python.

Что мы имеем на входе?

На входе нам даны следующие параметры:

FONT_PATH = "GOST2304A.ttf"  # Путь к файлу шрифта (поддерживающему кириллицу)

FONT_SIZE = 36  # Размер шрифта 

IMAGE_SIZE = (28, 28)  # Размер выходного изображения

BACKGROUND_COLOR = (255, 255, 255)  # Белый фон

TEXT_COLOR = (0, 0, 0)  # Черный цвет текста

NUM_IMAGES = 10  # Количество изображений на букву

# Список русских букв

RUSSIAN_LETTERS = [

    'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ё', 'Ж', 'З', 'И', 'Й',

    'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф',

    'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я'

]

Как видно из кода, мы можем регулировать следующие параметры:

  • шрифт

  • размер шрифта

  • размер картинки (в пикселях)

  • цвет фона (в RGB)

  • цвет текста (в RGB)

  • количество экземпляров каждой буквы в нашем будущем датасете.

  • Алфавит для генерации

3. Дальше напишем функцию для генерации картинок, на вход она будет принимать только букву и путь до шрифта:

def create_letter_image(letter, font_path):

    """Создает изображение с наклонной буквой"""

    # Загружаем шрифт

    try:

        base_font = ImageFont.truetype(font_path, FONT_SIZE)

    except OSError as e:

        print(f"Ошибка загрузки шрифта: {font_path}")

        print(e)

        return

    # Создаем временное изображение для измерения текста

    temp_image = Image.new('RGB', IMAGE_SIZE, BACKGROUND_COLOR)

    draw = ImageDraw.Draw(temp_image)

    text_width, text_height = draw.textbbox((0, 0), letter, font=base_font)[2:]

    # Создаем основное изображение с учетом наклона

    image = Image.new('RGB',

                      (int(IMAGE_SIZE[0]), IMAGE_SIZE[1]),

                      BACKGROUND_COLOR)

    draw = ImageDraw.Draw(image)

    # Рисуем текст с наклоном

    x = (image.width - text_width) / 2

    y = (image.height - text_height) / 2

    draw.text((x, y), letter, font=base_font, fill=TEXT_COLOR)

    return image

Сначала проверяем наличие шрифта в нашем проекте.

Создаем новое изображение с помощью Image.new. Задаем цветовой канал, размер изображения и цвет фона.

С помощью метода textbbox создадим поле для написания текста и зададим высоту и ширину поля.

Задаем координаты центра буквы и заполняем текстовое поле взяв букву из нашего алфавита.

Добавляем шумы

Для создания уникальных изображений и во избежание повторов при генерации и как следствии получении не репрезентативной выборки создадим функцию, которая будет генерировать шум на изображении случайно добавляя пиксели разной яркости (и здесь мы будем конвертировать нашего изображение из RGB в оттенки серого).

def add_noise(image):

    """

    Добавляет гауссов шум на изображение в оттенках серого

    (шум одинаков для всех каналов, сохраняя изображение серым)

    """

    # Конвертируем в numpy array и преобразуем в grayscale

    img_array = np.array(image.convert('L'))  # 'L' - режим оттенков серого

    height, width = img_array.shape

    noisy = img_array.copy()

    # Параметры гауссова шума

    mean = 0

    var = np.random.uniform(0.001, 0.02)  # Диапазон дисперсии

    sigma = var ** 0.5

    # Генерируем шум (один канал)

    gauss = np.random.normal(mean, sigma, (height, width))

    # Применяем шум и обрезаем значения

    noisy = np.clip(noisy + gauss * 255, 0, 255).astype(np.uint8)

    # Конвертируем обратно в RGB (но сохраняем оттенки серого)

    return Image.fromarray(noisy).convert('RGB')

Было использовано гауссовское распределение для отрисовки серых пикселей случайной яркости.

Итоговая функция

Итоговая функция будет выглядеть следующим образом:

def main():

    # Загружаем шрифт

    try:

        font = ImageFont.truetype(FONT_PATH, FONT_SIZE)

    except IOError:

        print(f"Ошибка: Шрифт по пути '{FONT_PATH}' не найден")

        return

    # Создаем корневую директорию

    for letter in RUSSIAN_LETTERS:

        # Создаем директорию для буквы

        letter_dir = f"Generated_images/Letter_{letter}"

        os.makedirs(letter_dir, exist_ok=True)

        # Генерируем эталонное изображение

        # Сохраняем NUM_IMAGES копий

        for i in range(NUM_IMAGES):

            img = create_letter_image(letter, FONT_PATH)

            img = add_noise(img)

            file_name = f"Letter_{letter}_{i:04d}.png"

            file_path = os.path.join(letter_dir, file_name)

            img.save(file_path)

        print(f"Сгенерировано {NUM_IMAGES} изображений для буквы '{letter}'")

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

Итог

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

Комментарии (1)


  1. CrazyElf
    04.07.2025 12:54

    Хотел в своё время с чем-то таким поэкспериментировать, да так руки и не дошли. По идее это что-то близкое к аугументации. Имеет смысл попробовать не только зашумление, но и всякие другие искажения - небольшие повороты, наклоны, изменение яркости, много там чего можно придумать. Но степень полезности искажений будет зависеть от конкретной дальнейшей задачи, конечно - смотря какие там могут встретиться искажения.