Время прочтения: 12 мин.

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

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

— анализ изображений;

— подготовка данных;

— генерация данных;

— тренировка нейронной сети, предсказание ответов.

Рис.1 пример изображений (CAPTCHA)

Проводим предварительный анализ 100 изображений в формате «.png». Изображения состоят из 29 уникальных символов «12345789абвгдежзиклмнопрстуфя». Символы могут быть повернуты на определенный угол (от -1° –+15°), могут быть смещены по горизонтали и вертикали, символы могут накладываться друг на друга. Символы представлены в цвете, все три символа могут иметь различные цвета. На фоне имеются более мелкие символы в светлых оттенках (шум на изображении). Для анализа, вывода и обработки изображений понадобятся библиотеки языка python 3 opencv, matplotlib, pillow. Определим положение символов на изображении путем подбора разделяющих линий:

import cv2 # импортируем библиотеку для раб.с графикой
image = cv2.imread('.\Captcha.png') # читаем изоб. Результат numpy array
# рисуем линию (img, (x1, y1), (x2, y2), (255, 255, 255), 4) – 
    # изображение на котором рисуем, точка начала линии, точка конца линии, 
    # цвет линии цветовая модель BGR, толщина линии.
image = cv2.line(image, (14, 0), (14, 50), (0, 0, 255), 1)
    …
# для вывода можно задать функцию, параметры прочитанное иозбр., имя окна
def view_image(image, name_wind='default'):
    cv2.namedWindow(name_wind, cv2.WINDOW_NORMAL) # # создаем окно вывода
    cv2.imshow(name_wind, image) # # в окно передаем изображение image
    cv2.waitKey(0) # # ждем нажатия любой клавиши, 0 нет таймера.
    cv2.destroyAllWindows() # # уничтожение (закрытие) всех окон
view_image(image)  # # вызов функции вывода с передачей прочитанного файла
Рис. 2 пример определение диапазона символа

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

Перебором изображений с расстановкой разделяющих линий, определим место положение символов. На изображениях каждый символ находится в своем диапазоне. Для всех символов верхняя граница находится на 3 строке, нижняя на 47. Вертикальные границы (за которые символы не выходят): для первого символа 14–44 колонка, для второго символа: 32–62 колонка, для третьего символа: 48 –72. Изображение при чтении библиотекой opencv представляется в виде тензора numpy array, размерностью (50, 100, 3). Изображение представлено в 3 массивах, состоящих из 50 строк и 100 столбцов. Каждый из трех массивов отвечает за свой цвет BGR (blue синий, green зеленый, red красный), каждый из   3-х массивов находится в диапазоне от 0-255.

Рис.3 Цветовая модель RGB

Данные такого вида не совсем удобны для дальнейшей обработки цветов. Так как символ имеет не четкий цвет, а сумму цветов, к краям более светлый тон, сумма цветов меняет свои значения. Для выделения символа определенного цвета необходимо будет указывать диапазоны для трех цветов B(n-m) G(k-l) R(y-z). Вместо этого проще представить изображение в другой цветовой модели HSV (Hue, Saturation, Value — тон, насыщенность, яркость). В библиотеке opencv единицы измерения Heu 0 – 179, S 0 – 255, V 0 –255. При данной цветовой модели достаточно указать сектор цвета Heu и для всех символов указать постоянные значения S 10 – 255, V 0 – 234, отсекая тем самым фон и шумовые изображения, представленные в более светлых тонах.

Рис.4 Цветовые модели RGB (BGR) и HSV
# # преобразование из BGR цветовой модели в HSV
image = cv2.imread('.\captcha_png')
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

HSV представляет собой тензор (50, 100, 3) (3 матрицы numpy array размерностью (50, 100), 50 одномерных массивов, содержащие 100 значений). Индексы матриц — [:, :, 0] Hue, [:, :, 1] Saturation, [:, :, 2] Value.

Исходное изображение

Отобразим матрицы в градиентах серого (0 – черный 255 – белый).

[:,:, 0] Тон, отсутствует белый т.к. диапазон до 179, приблизительный разброс цветов 160 – 179 и 0~30 красный цвет, 60 ~ 100 зеленый, 110 ~ 150 синий. На первом изображении цифра 9 более светлая т.к. оттенок красного попал в диапазон от 160, на втором изображении буква «м» черная т.к. оттенок попал в диапазон 0~30

[:,:, 1] Насыщенность, фон и шум на изображении представлен в светлых, пастельных тонах значения 0~10, символы имеют более выраженный цвет >10

[:,:, 2] Яркость, изображение светлое следовательно яркость фона 240 ~ 255, символы более темнее имеют значения < 240.

На основании матриц S и V (насыщенность и яркость), можно получить фильтры для отделения символов от фона и от шума в матрице Hue (тон).

mask_S = image[:, :, 1]< 10; mask_V = image[:, :, 1] > 240

Результат: две матрицы булевых значений (50, 100) [[True, False, ..,], …, [..]]. Значению истина соответствует фон и шум. Если применить эти фильтры к матрице тон (Hue) и придать значениям, соответствующим истина 255, матрица будет содержать значения 0-179, 255 – удобно для дальнейшей фильтрации (дальнейшая работа только с матрицей тон, Hue).

# значение 255 не попадает в диапазон матрицы 0 - 179
image[:, :, 0][mask_S] = 255 ; image[:, :, 0][mask_V] = 255
Рис.5 Результат фон и часть шума имеют значения 255

Следующий этап — отделение символов друг от друга. Задаем диапазон, в котором находится символ.

Рис.6 Разделение символов по определенном диапазонам
img_char1 = image[3: 47, 14: 44, 0].copy()
img_char2 = image[3: 47, 32: 62, 0].copy()
img_char3 = image[3: 47, 48: 78, 0].copy()

Для выделения каждого символа и удаления части соседнего символа, отсекаем область нахождения соседнего символа, находим количество значений соответствующих тонам в матрице (максимальное количество значений имеет число 255 фон (500 – 800 значений в матрице), следующее по количеству значений будет основной тон символа, единичные значения будет иметь оставшийся не отфильтрованный шум). На основании основного тона символа находим диапазон оттенков N -10, N + 10.

Рис.7 Определение областей 1 и 3 символа, где нет данных 2-го символа

Оставляем часть 1 и 3 символа не попадающее в область 2 символа. Получаем матрицы, отвечающие за тон.

# преобразуем матрицу в массив значений, посчитаем количество значений
val_count_1 = img_char1[3: 47, 14: 32, 0].copy().reshape(-1) 
val_color_hue_1 = pd.Series(val_count_1).value_counts()
# val_color_hue_1 ->255 – 741, 106 – 11, 104 – 11, 20 – 1, 99 – 1.
val_color_hue_1 = pd.Series(val_count_1).value_counts().index[1] 
# числа тонов являются индексами, устанавливаем сектор тона Hue -10, +10.
val_color_char_hue_1_min = val_base_hue_1 – 10 = 106 - 10 = 96
val_color_char_hue_1_max = val_base_hue_1 + 10 = 106+ 10 = 116

На матрицы Hue для символов 1, 3 накладываем фильтр, значения где тон попадает в указанный диапазон равны 0, иначе 255.

mask_char1 = (img_char1> 96) & (img_char1<116)
img_char1[~mask_char1] = 255 # где не символ (полная область определения) img_char1[mask_char1] = 0 # где символ
Рис.8 Отображение результата в виде pandas dataframe

Необходимо привести значения к 0 и 1 и инвертировать матрицу.

img_char1[img_char1 == 0] = 1; img_char1[img_char1 == 255] = 0

Из 2-го символа, находящегося в центре изображения, удаляем части 1 и 3 символа путем определения частей матриц, пересекающих 2-й символ и приравнивании к 255 тех значений матрицы 2, где матрицы 1 и 3 не равны нулю.

Рис.9 Удаление из 2-го символа данных 1 и 3-го символов

Проводим аналогичные преобразования матрицы символа 2. Результат матрицы символов 1, 2, 3 – со значениями 0, 1. При выводе изображения имеют пропуски, остатки шума. Проводим фильтрацию,  дополнительное преобразование инструментами opencv, указываем ядро (окно матрицы) которое проходит по матрице символов и при нахождении рядом заполненных пикселей и пропусков, заполняет пропуски

kernel = np.ones((3, 3), np.uint8)
closing = cv2.morphologyEx(np_matrix, cv2.MORPH_CLOSE, kernel)
Рис.10 Корректировка данных, заполнение пропусков

Определяем центры символов, перемещаем символы в центр массива добавляя и удаляя строки, столбцы, заполненные нулями.

Рис.11 Расположение символов в средине матрицы

Разработан алгоритм выделения символов из изображения. Для дальнейшего определения значения символа построим и обучим нейронную сеть. Для обучения нейронной сети скачиваем ~100 экземпляров картинок выделяем из них символы, размечаем картинки вручную. Получаем 300 экземпляров размеченных данных (массивы 44×30 содержащие числа 0 и 1). Этого количества данных недостаточно. Определяем и скачиваем шрифт, которым отображаются символы на картинках. Воспользовавшись библиотекой pillow языка python, размещаем символы шрифта на изображении 44×30, задаем смещение и поворот символов случайным выбором из заданных значений, преобразуем в массив nympy array. Формируем выборку данных из сгенерированных данных и данных размеченных вручную.

shift_x = [1, 1, -1, -1, -2, 2, 0, 0, 0]
shift_y = [1, 1, -1, -1, -2, 2, 0, 0, 0]
rotor_char = [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1]
char = '12345789абвгдежзиклмнопрстуфя'
# в цикле генерируем данные – ~10_000 – 60_000
shift _x_r = random.choice(shift_x)
shift _y_r = random.choice(shift_y)
rotor_r = random.choice(rotor_char)
char_r = random.choice(char)
Рис.13 Пример подобранного текста и размещение символов в матрицах
train_x = []
train_x.append(char)
train_x = np.array(train_x)
train_x = train_x.reshape(train_x.shape[0], train_x[1], train_x[2], 1)

Тренировочный набор данных представляет собой тензор размерностью (50000, 44, 30, 1), дополнительная размерность (1) для нейронной сети.

Каждому экземпляру тренировочной выборки соответствует экземпляр из массива ответов: char_y = [0, 4, …, 29] – 50_000 (цифры 0-29 ключи словаря символов

char = '12345789абвгдежзиклмнопрстуфя' # 29 позиций
dict_char = {char[i]: i for i in range(len(char))}
dict_char_reverse = {i[1]: i[0] for i in dict_char.items()}

Приведем данные предсказания к виду унитарный код (one-hot encoding). Преобразуем численные значения в массив, имеющий длину 29. Массив состоит из нулей и одной единицы. Позиция единицы соответствует кодируемому символу. Например, буква «а» будет иметь вид ‘000000000100000000000000000000’.

Img_y = utils.to_categorical(Img_y)
# пример 1 -> (array( [1, 0, 0, 0, …, 0, 0],  dtype=float32)
# пример 2 -> (array( [0, 1, 0, 0, …, 0, 0],  dtype=float32)

Разбиваем полученные данные на тренировочный набор и тестовый.

x_train, x_test, y_train, y_test = sklearn.train_test_split(
                             out_train_x_rsh, out_train_y_sh, 
                             test_size=0.1, shuffle=True)

Для распознавания символов необходимо построить нейронную сеть, так как данные похожи на учебный набор mnist (рукописные цифры 28×28) на ресурсе kaggle находим примеры архитектуры нейронной сети дающие хороший результат. Архитектура нейронной сети:

# Определим простую модель
Import tensorflow as tf

def model_detection():
    model=tf.keras.models.Sequential([
        tf.keras.layers.Conv2D(input_shape=(44,30, 1), filters=32, 
                kernel_size=(5, 5), padding='same', activation='relu'),
        tf.keras.layers.Conv2D( filters=32, kernel_size=(5, 5), 
                               padding='same', activation='relu'),
        tf.keras.layers.MaxPool2D(pool_size=(2, 2)),
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Conv2D( filters=64, kernel_size=(3, 3), 
                padding='same', activation='relu'),
        tf.keras.layers.Conv2D( filters=64, kernel_size=(3, 3), 
                padding='same', activation='relu'),
        tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=(2, 2)),
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(256, activation='relu'),
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Dense(29, activation=tf.nn.softmax)])

    model.compile(optimizer='adam', loss='categorical_crossentropy',
                   metrics=['accuracy'])
    returnmodel

# создадим экземпляр модели
model = model_detection()

Зададим параметр, при улучшении которого модель сохраняется (val_accuracy).

checkpoint = ModelCheckpoint('captcha_1.hdf5', monitor='val_accuracy',
                                        save_best_only=True, verbose=1)

model.fit(x_train, y_train, epochs=5, validation_data=(x_test, y_test), 
          verbose=1, callbacks=[checkpoint])

После тренировки сохраняются веса модели с наилучшими показателями val_accuracy, доли верных ответов. Дальнейшая работа программы: на вход подается изображение, которое обрабатывается ранее описанным алгоритмом. Результат обработки — numpy array (массив). Далее инициализируется модель, загружаются веса ранее обученной модели. На вход модели подают данные (1, 2, 3 символ). В результате модель выдает вероятность того или иного символа. Приняв за верный ответ позицию с наибольшей вероятностью из словаря «значение – символ» получаем символьное предсказание модели.

model2 = model_detection() # инициализируем
model2.load_weights('captcha_1.hdf5') # загружаем веса
prediction_ch_1 = model2.predict(char_1) # массив 29 значений вероятностей
# позиция на которой наибольшая вероятность, ключ к словарю ответов
prediction_ch_1 = np.argmax(prediction_ch_1, axis=1)
# из словаря ключ число, значение цифра или буква получаем ответ
dict_char_reverse[prediction_ch_1]

Данный алгоритм обрабатывает цветные изображения, на которых находятся символы букв и цифр, результат распознавания символов нейронной сетью 95% (точность), распознавание каптчи 82% (точность). На примере разбора алгоритма распознавания символов можно заметить, что основную долю разработки занимает подготовка, обработка и генерация данных. Выбор архитектуры и обучение нейронной сети является важней частью задачи, но не самой затратной по времени. Вариантов решения задачи распознавания цифр, букв, изображений предметов и т.п. множество, в данной статье приведен лишь один из примеров решения, показаны этапы решения, трудности, с которыми можно столкнуться в результате работы и примеры их преодоления. А как Вы работаете с каптчами?