[Из песочницы] Распознавание русского алфавита: от сбора датасета до создания GUI

Привет, Хабр! В этой статье я бы хотел рассказать как я сделал распознавалку русских букв и прикрутил к этому небольшой графический интерфейс.

Спойлер: в результате должно получиться вот так:

image


Скачиваем датасет

Итак, начнём! Первое, что нам нужно это набор данных. В качестве датасета я выбрал CoMNIST. Скачиваем набор данных, распаковываем и удаляем папку с названием I, так как эта буква не входит в русский алфавит.


Обработка данных

Как мы можем увидеть, данные изображения имеют четыре канала. Четвёртый канал — альфа-канал, который нам не нужен и мы его удаляем:

def make_background():
    image=' '
    file_without_extension = image.split('.')[0]
    image = cv2.imread(image, cv2.IMREAD_UNCHANGED)
    trans_mask = image[:, :, 3] == 0
    image[trans_mask] = [255, 255, 255, 255]
    new_img = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR)
    cv2.imwrite(file_without_extension + '.jpeg', new_img)

То есть на входе у нас было изображение, которое представлено слева, а на выходе должно получиться изображение, которое представлено справа:
image

Cледующее, что мы можем заметить, это то, что количество данных у нас маловато (350 — 450 изображений на класс). Поэтому, нам нужно «раздуть» наши данные. Для этих целей я использовал следующие приёмы: сдвиги, это когда мы перемещаем нашу букву на какое-то значение вверх/вниз и влево/вправо по осям, и повороты, когда поворачиваем нашу букву на какой-то определённый градус.

Сдвиги

image

def shift():
            image = ''
            img = cv2.imread(image)
            file_without_extension = image.split('.')[0]
            arr_translation = [[15, -15], [-15, 15], [-15, -15],
                               [15, 15]]
            arr_caption=['15-15','-1515','-15-15','1515']
            for i in range(4):
                transform = AffineTransform(
                    translation=tuple(arr_translation[i]))
                warp_image = warp(img, transform, mode="wrap")
                img_convert = cv2.convertScaleAbs(warp_image,
                                                  alpha=(255.0))
                cv2.imwrite(file_without_extension +
                         arr_caption[i] + '.jpeg', img_convert)

Повороты

image

def rotate():
    image = ''
    img = Image.open(image)
    file_without_extension = image.split('.')[0]
    angles = np.ndarray((2,),
        buffer=np.array([-13, 13]), dtype=int)
    for angle in angles:
        transformed_image = transform.rotate(np.array(img),
        angle, cval=255, preserve_range=True).astype(np.uint8)
        cv2.imwrite(file_without_extension +
                    str(angle) + '.jpeg', transformed_image)

Балансировка данных

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

def balancing():
    arr_len_files = []
    for path in root_path:
        name_path = name_root_path+path+'/'
        files=os.listdir(name_path)
        arr_len_files.append(len(files))

    min_value=min(arr_len_files)
    for path in root_path:
        folder = name_root_path+path
        arr = []
        for the_file in os.listdir(folder):
            arr.append(folder + '/' + the_file)
        d = 0
        k = len(arr)
        for i in arr:
            os.remove(i)
            d += 1
            if d == k - min_value:
                break

В результате чего, для каждой буквы количество изображений должно быть одинаково. Далее следует самостоятельно разделить данные на тренировочные, тестовые и валидационные примерно в соотношении 70%, 20% и 10% соответственно.


Обучение нейронной сети

Переходим к самому продолжительному этапу — обучению сети. В качестве нейронной сети я выбрал CNN, так как она хороша для классификации объектов. Сам процесс обучения занимает 2–2,5 часа и точность составила примерно 94%, что довольно хорошо. Ниже представлен код для обучения сети.

import tensorflow as tf
ImageDataGenerator = tf.keras.preprocessing.image.ImageDataGenerator
TRAINING_DIR = "path/to/train/dataset"
train_datagen = ImageDataGenerator(rescale=1.0 / 255.)
train_generator = train_datagen.flow_from_directory(TRAINING_DIR,
                              batch_size=40,
                              class_mode='binary',
                              target_size=(278,278))

VALIDATION_DIR = "path/to/test/dataset"
validation_datagen = ImageDataGenerator(rescale=1.0 / 255.)
validation_generator = validation_datagen.flow_from_directory(VALIDATION_DIR,
                                      batch_size=40,
                                      class_mode='binary',
                                      target_size=(278,278))

model = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(16, (3, 3), activation='relu', 
                           input_shape=(278,278, 3)),
    tf.keras.layers.MaxPooling2D(2, 2),
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2, 2),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2, 2),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(512, activation='relu'),
    tf.keras.layers.Dense(33, activation='softmax')
])
model.compile(optimizer='adam', 
loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.summary()
history = model.fit_generator(train_generator,
                           epochs=2,
                           verbose=1,
                           validation_data=validation_generator)

model.save('model.h5')

Дальше следует проверить нашу нейронную сеть на валидационных изображениях, для того, чтобы убедиться, что точность на тестовых данных соответствует реальности:


prediction.py
def print_letter(result):
    if result == 1:
        return 'А'

    elif result == 2:
        return 'Б'

    elif result == 3:
        return 'В'

    elif result == 4:
        return 'Г'

    elif result == 5:
        return 'Д'

    elif result == 6:
        return 'Е'

    elif result == 7:
        return 'Ж'

    elif result == 0:
        return 'Ё'

    elif result == 8:
        return 'З'

    elif result == 9:
        return 'И'

    elif result == 10:
        return 'Й'

    elif result == 11:
        return 'К'

    elif result == 12:
        return 'Л'

    elif result ==13:
        return 'М'

    elif result == 14:
        return 'Н'

    elif result == 15:
        return 'О'

    elif result == 16:
        return 'П'

    elif result == 17:
        return 'Р'

    elif result == 18:
        return 'С'

    elif result == 19:
        return 'Т'

    elif result == 20:
        return 'У'

    elif result == 21:
        return 'Ф'

    elif result == 22:
        return 'Х'

    elif result == 23:
        return 'Ц'

    elif result == 24:
        return 'Ч'

    elif result == 25:
        return 'Ш'

    elif result == 26:
        return 'Щ'

    elif result == 27:
        return 'Ъ'

    elif result == 28:
        return 'Ы'

    elif result == 29:
        return 'Ь'

    elif result == 30:
        return 'Э'

    elif result == 31:
        return 'Ю'

    elif result == 32:
        return 'Я'

    else:
        print('ERROR!')

def predicting(path_to_image):
    image = keras.preprocessing.image
    model = keras.models.load_model('path/to/model')

    img = image.load_img(path_to_image, target_size=(278, 278))
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)
    images = np.vstack([x])
    classes = model.predict(images, batch_size=1)
    result = int(np.argmax(classes))
    result = print_letter(result)
    print(result)


Прикручиваем GUI

И последнее, что нам предстоит сделать это прикрутить графический интерфейс с минимальными возможностями: холст, на котором можно будет рисовать цифры, поле, куда будет выводиться распознанная буква, кнопка для распознавания того, что находится на холсте, кнопка для очистки холста и объеденить это всё воедино.


app.py
from PyQt5.QtWidgets import QMainWindow, QApplication, QMenu, QMenuBar, QAction, QFileDialog, QPushButton, QTextBrowser
from PyQt5.QtGui import QIcon, QImage, QPainter, QPen, QBrush
from PyQt5.QtCore import Qt, QPoint
import sys
from PyQt5.QtWidgets import QMainWindow, QTextEdit, QAction, QApplication
from PyQt5.QtWidgets import (QWidget, QLabel, QLineEdit, QTextEdit, QGridLayout, QApplication)
import numpy as np
from tensorflow import keras

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        title = "recognition cyrillic letter"
        top = 200
        left = 200
        width = 540
        height = 340

        self.drawing = False
        self.brushSize = 8
        self.brushColor = Qt.black
        self.lastPoint = QPoint()

        self.image = QImage(278, 278, QImage.Format_RGB32)
        self.image.fill(Qt.white)

        self.nameLabel = QLabel(self)
        self.nameLabel.setText('RES:')
        self.line = QLineEdit(self)

        self.line.move(360, 168)
        self.line.resize(80, 33)
        self.nameLabel.move(290, 170)

        prediction_button = QPushButton('RECOGNITION', self)
        prediction_button.move(290, 30)
        prediction_button.resize(230, 33)
        prediction_button.clicked.connect(self.save)
        prediction_button.clicked.connect(self.predicting)

        clean_button = QPushButton('CLEAN', self)
        clean_button.move(290, 100)
        clean_button.resize(230, 33)
        clean_button.clicked.connect(self.clear)

        self.setWindowTitle(title)
        self.setGeometry(top, left, width, height)

    def print_letter(self, result):
        if result == 1:
            self.line.setText('А')
            return 'А'

        elif result == 2:
            self.line.setText('Б')
            return 'Б'

        elif result == 3:
            self.line.setText('В')
            return 'В'

        elif result == 4:
            self.line.setText('Г')
            return 'Г'

        elif result == 5:
            self.line.setText('Д')
            return 'Д'

        elif result == 6:
            self.line.setText('Е')
            return 'Е'

        elif result == 7:
            self.line.setText('Ж')
            return 'Ж'

        elif result == 0:
            self.line.setText('Ё')
            return 'Ё'

        elif result == 8:
            self.line.setText('З')
            return 'З'

        elif result == 9:
            self.line.setText('И')
            return 'И'

        elif result == 10:
            self.line.setText('Й')
            return 'Й'

        elif result == 11:
            self.line.setText('К')
            return 'К'

        elif result == 12:
            self.line.setText('Л')
            return 'Л'

        elif result == 13:
            self.line.setText('М')
            return 'М'

        elif result == 14:
            self.line.setText('Н')
            return 'Н'

        elif result == 15:
            self.line.setText('О')
            return 'О'

        elif result == 16:
            self.line.setText('П')
            return 'П'

        elif result == 17:
            self.line.setText('Р')
            return 'Р'

        elif result == 18:
            self.line.setText('С')
            return 'С'

        elif result == 19:
            self.line.setText('Т')
            return 'Т'

        elif result == 20:
            self.line.setText('У')
            return 'У'

        elif result == 21:
            self.line.setText('Ф')
            return 'Ф'

        elif result == 22:
            self.line.setText('Х')
            return 'Х'

        elif result == 23:
            self.line.setText('Ц')
            return 'Ц'

        elif result == 24:
            self.line.setText('Ч')
            return 'Ч'

        elif result == 25:
            self.line.setText('Ш')
            return 'Ш'

        elif result == 26:
            self.line.setText('Щ')
            return 'Щ'

        elif result == 27:
            self.line.setText('Ъ')
            return 'Ъ'

        elif result == 28:
            self.line.setText('Ы')
            return 'Ы'

        elif result == 29:
            self.line.setText('Ь')
            return 'Ь'

        elif result == 30:
            self.line.setText('Э')
            return 'Э'

        elif result == 31:
            self.line.setText('Ю')
            return 'Ю'

        elif result == 32:
            self.line.setText('Я')
            return 'Я'

        else:
            print('ERROR!')

    def predicting(self):
        image = keras.preprocessing.image
        model = keras.models.load_model('model/cyrillic_model.h5')
        img = image.load_img('res.jpeg', target_size=(278, 278))
        x = image.img_to_array(img)
        x = np.expand_dims(x, axis=0)
        images = np.vstack([x])
        classes = model.predict(images, batch_size=1)
        result = int(np.argmax(classes))
        self.print_letter(result)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.drawing = True
            self.lastPoint = event.pos()

    def mouseMoveEvent(self, event):
        if (event.buttons() & Qt.LeftButton) & self.drawing:
            painter = QPainter(self.image)
            painter.setPen(QPen(self.brushColor, self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
            painter.drawLine(self.lastPoint, event.pos())
            self.lastPoint = event.pos()
            self.update()

    def mouseReleaseEvent(self, event):

        if event.button() == Qt.LeftButton:
            self.drawing = False

    def paintEvent(self, event):
        canvasPainter = QPainter(self)
        canvasPainter.drawImage(0, 0, self.image)

    def save(self):
        self.image.save('res.jpeg')

    def clear(self):
        self.image.fill(Qt.white)
        self.update()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    app.exec()


Заключение

Как можно видеть, что казалось когда-то «магией», с помощью современных библиотек делается вполне несложно.

Поскольку Python является кроссплатформенным, работать код должен везде, на Windows, Linux и OSX. Я это всё делал на Ubuntu 18.04. Для желающих поэкспериментировать самостоятельно, исходный код я выложил на GitHub.

© Habrahabr.ru