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

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

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

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

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

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

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

Рис.1 пример изображений (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 пример определение диапазона символа
Рис. 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
Рис.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
Рис.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]&lt; 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
Рис.5 Результат фон и часть шума имеют значения 255

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

Рис.6 Разделение символов по определенном диапазонам
Рис.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-го символа
Рис.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) &amp; (img_char1&lt;116)
img_char1[~mask_char1] = 255 # где не символ (полная область определения) img_char1[mask_char1] = 0 # где символ
Рис.8 Отображение результата в виде pandas dataframe
Рис.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-го символов
Рис.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 Корректировка данных, заполнение пропусков
Рис.10 Корректировка данных, заполнение пропусков

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

Рис.11 Расположение символов в средине матрицы
Рис.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 Пример подобранного текста и размещение символов в матрицах
Рис.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()

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

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])

После тренировки сохраняются веса модели с наилучшими показателями valaccuracy, доли верных ответов. Дальнейшая работа программы: на вход подается изображение, которое обрабатывается ранее описанным алгоритмом. Результат обработки — 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% (точность). На примере разбора алгоритма распознавания символов можно заметить, что основную долю разработки занимает подготовка, обработка и генерация данных. Выбор архитектуры и обучение нейронной сети является важней частью задачи, но не самой затратной по времени. Вариантов решения задачи распознавания цифр, букв, изображений предметов и т.п. множество, в данной статье приведен лишь один из примеров решения, показаны этапы решения, трудности, с которыми можно столкнуться в результате работы и примеры их преодоления. А как Вы работаете с каптчами?