Создаем датасет печатных букв с любым шрифтом за 170 строк

от автора

Всем привет, это моя первая статья на Хабре. В этой статье я хочу рассказать, как сгенерировать датасет печатных букв с помощью .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.


ссылка на оригинал статьи https://habr.com/ru/articles/925022/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *