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