Поваренная книга моей бабушки встречает машинное обучение, часть I

Моя бабушка была прекрасным поваром. Поэтому, когда я недавно наткнулся на ее старую кулинарную книгу, я попытался прочитать некоторые рецепты, надеясь, что смогу воссоздать некоторые из блюд, которые мне нравились в детстве. Однако это оказалось труднее, чем ожидалось, поскольку книга была напечатана примерно в 1911 году шрифтом под названием f raktur. К сожалению, шрифт fraktur в некоторых случаях отличается от современных шрифтов. Например, буква A выглядит как U во фрактуре, и каждый раз, когда я вижу Z во фрактуре, я читаю 3 (см. рисунок 2).

Так возникла идея разработать конвейер, который будет создавать живой перевод букв fraktur в современный шрифт. Имея в руках такой инструмент, мы могли бы легко прочитать рецепты и сосредоточиться на приготовлении пищи вместо того, чтобы разбирать книгу. К счастью, существует множество отличных инструментов с открытым исходным кодом, которые помогут нам разработать такой конвейер. Однако некоторые аспекты этого проекта нужно создавать с нуля. В основном нам нужен набор данных для обучения алгоритмов машинного обучения. И именно на этом мы остановимся в этой статье. Но прежде чем мы начнем с набора данных, давайте кратко рассмотрим все задачи, которые стоят перед нами:

  1. Обнаружение отдельных букв на изображении
  2. Создайте набор обучающих данных из этих букв
  3. Обучите алгоритм классификации букв
  4. Используйте обученный алгоритм для классификации отдельных букв (онлайн)

Мы рассмотрим первые две темы в этой статье и продолжим темы 3 и 4 во второй и третьей статьях. Это должно дать нам достаточно места для подробного изучения каждой задачи.

Также в качестве общего замечания: в этих статьях мы не будем сосредотачиваться на том, как реализовать каждый алгоритм с нуля. Вместо этого мы увидим, как мы можем соединить различные инструменты, чтобы перевести поваренную книгу в современный шрифт. Если вас больше интересует код, чем объяснения, вы также можете перейти непосредственно к Блокнотам Jupyter на Github.

Обнаружение букв на изображении

Итак, первая задача - найти способ извлекать отдельные буквы со страниц поваренной книги. Это основа для всего, что будет дальше. На следующем этапе мы можем создать набор данных из извлеченных букв и, наконец, обучить на нем классификатор.

Входными данными для нашего конвейера всегда будут изображения страниц из поваренной книги, подобные показанной на рисунке 3 выше. Эти входы могут быть либо одиночными изображениями с высоким разрешением с камеры смартфона, либо потоком изображений с веб-камеры. Мы должны гарантировать, что каждое изображение, независимо от его источника, обрабатывается таким образом, чтобы алгоритм обнаружения мог найти все отдельные буквы. Прежде всего следует помнить, что цифровые камеры хранят изображения в трех отдельных каналах: R ed, G reen и B lue ( RGB). Но в нашем случае эти три канала содержат избыточную информацию, поскольку буквы можно идентифицировать в каждом из этих трех каналов отдельно. Поэтому сначала мы конвертируем все изображения в шкалу серого. В результате вместо трех каналов мы имеем дело только с одним каналом. Кроме того, мы также сократили объем данных до 1/3, что должно улучшить производительность. Но наш алгоритм обнаружения столкнется с другой проблемой: меняющимися условиями молнии. Это затрудняет отделение букв от фона, поскольку контрастность изменяется по всему изображению. Чтобы решить эту проблему, мы будем использовать метод, называемый адаптивным пороговым значением, который использует близкие пиксели для создания локальных пороговых значений, которые затем используются для преобразования изображения в двоичную форму. В результате обработанное изображение будет состоять только из черных и белых пикселей; больше нет серого. Затем мы можем дополнительно оптимизировать изображение для обнаружения букв, подавив его с помощью фильтра медианного размытия. В приведенном ниже коде описана функция Python, которая выполняет преобразование изображения из RGB в черно-белое с помощью библиотеки openCV. Результат этого этапа обработки дополнительно проиллюстрирован на рисунке 4.

# Define a function that converts an image to thresholded image
def convert_image(img, blur=3):
    # Convert to grayscale
    conv_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # Adaptive thresholding to binarize the image
    conv_img = cv2.adaptiveThreshold(conv_img, 255,   
               cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
               cv2.THRESH_BINARY, 11, 4)
    # Blur the image to reduce noise
    conv_img = cv2.medianBlur(conv_img, blur) 
    
    return conv_img

Хорошо, теперь, когда мы обработали изображение, пора обнаружить буквы. Для этого мы можем использовать метод findContours библиотеки openCV. Код сводится к одной строке, которая вызывается функцией ниже. Затем мы можем сопоставить ограничивающие прямоугольники контуров, найденных этой функцией, с исходным изображением RGB, чтобы увидеть, что на самом деле было обнаружено (рисунок 5).

# Define a function that detects contours in the converted image
def extract_char(conv_img):
    # Find contours
    _, ctrs, _ = cv2.findContours(conv_img, cv2.RETR_TREE,  
                 cv2.CHAIN_APPROX_SIMPLE)
    return ctrs

Из рисунка 5 видно, что обнаружение работает достаточно хорошо. Однако в некоторых случаях буквы не обнаруживаются, например некоторые из i в конце строки 1. А в других случаях одна буква разделяется на две буквы, например b в конце последней строки. Еще одна вещь, на которую следует обратить внимание, - это то, что некоторые комбинации букв по существу становятся одной буквой и соответственно обнаруживаются алгоритмом, два примера из рисунка 5 - это ch и ck. Позже мы увидим, как решать эти проблемы. Но пока мы можем перейти к текущему результату. Итак, поскольку у нас есть ограничивающие рамки каждой буквы, мы можем вырезать их и сохранить как отдельные изображения (.png) в папке на нашем жестком диске. Если вам интересно, как это сделать, загляните в Блокнот Jupyter.

Создание набора данных

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

  1. Удалите изображения, не содержащие букв. Это могут быть артефакты всех видов, например мазок на одной из страниц или только на части письма, как мы видели на рисунке 5.
  2. Сгруппируйте все оставшиеся изображения. Это означает, что все буквы «A» попадают в одну папку, все буквы «B» - в другую папку и так далее.

В принципе, оба вышеперечисленных пункта решаются легко. Однако, поскольку мы извлекли несколько тысяч потенциальных писем со многих страниц книги, они представляют собой долгую и утомительную задачу. С другой стороны, мы можем автоматизировать первый раунд группировки, так что позже нам «только» нужно будет исправить результат этого этапа предварительной группировки. Но есть еще одна вещь, которую нужно сделать, прежде чем мы сможем приступить к этому этапу кластеризации: мы должны привести все изображения извлеченных букв к одинаковому размеру.

Причина этого в том, что алгоритмы, которые мы будем использовать для кластеризации, а также для классификации, ожидают фиксированного размера входных изображений. Но, как мы видим из ограничивающих рамок на рисунке 5, каждое изображение в настоящее время имеет разную форму. Чтобы преодолеть это разнообразие размеров изображений, мы будем использовать метод resize библиотеки openCV и привести все изображения к одному и тому же размеру. Затем мы сохраним изображения в массиве Numpy и нормализуем их, вычислив их z-баллы. Нормализация важна для следующего шага, который заключается в уменьшении количества измерений каждого изображения с помощью анализа главных компонентов (PCA). Затем оценки первых основных компонентов будут входными данными для алгоритма кластеризации K-средних, который будет выполнять предварительную кластеризацию букв за нас. Если вас интересуют подробности этой процедуры и алгоритма K-средних, вы можете проверить Блокнот Jupyter к этой статье или узнать о другом варианте использования здесь. Результаты кластеризации K-средних визуализированы на рисунке 6, где цвет каждой точки указывает на кластер, к которому она принадлежит. Глядя на рисунок 6 , кажется, что некоторые точки данных образуют группы, которые также были назначены одному и тому же кластеру с помощью алгоритма K-средних. Однако только по рисунку 6 сложно судить о том, насколько хорошо работала кластеризация. Лучший способ оценить результаты - переместить все изображения в кластере в отдельную папку, а затем просматривать содержимое каждого кластера. На рисунке 7 показаны изображения в папке в качестве примера, в которой кластеризация работала очень хорошо. Следующим шагом будет переименование этой папки в a.

В других случаях кластеризация работала не так хорошо. На рис. 8 показан пример кластера, который содержит разные типы букв. Хотя большинство букв - это «n», в кластере также есть «K» и «u». Однако мы можем легко это исправить, выполнив поиск кластеров «K» и «u» и переместив туда изображения. Впоследствии папку можно переименовать в «n».

Мы будем продолжать так, пока все кластеры не будут очищены и переименованы, как описано выше. Результат должен быть похож на рисунок 9, где заглавные буквы отмечены знаком «_».

Итак, очевидно, что потребовалась некоторая ручная работа, чтобы привести данные в форму. Однако нам удалось автоматизировать большую часть работы за счет предварительной кластеризации данных с помощью PCA и K-средних. Набор данных теперь очищен и упорядочен, но для эффективной работы нам нужно сохранить его более удобным способом, чем папки на жестком диске.

Преобразование набора данных в формат IDX

Таким образом, последний шаг, чтобы обернуть все это, - это преобразовать набор данных в формат данных IDX. Возможно, вы уже знакомы с этим форматом, так как знаменитый набор данных MNIST сохраняется таким же образом. Только здесь вместо чтения данных из файла IDX мы должны их записать.
Мы сделаем это с помощью idx_converter, который принимает файловую структуру, как мы установили выше, и напрямую сохраняет ее в формате IDX. Результатом будет два файла: один файл с изображениями и второй файл с метками.
Поскольку мы хотим позже обучить классификатор на данных, мы уже должны разделить изображения на обучающий и тестовый набор данных. Для этого мы переместим 30% писем в тестовую папку, а остальные письма останутся в тренировочной папке. Вы можете проверить код в Блокноте Jupyter для получения подробной информации о реализации этой процедуры.

Теперь, когда мы создали набор данных fraktur с нуля, мы можем перейти к следующей статье, в которой мы сравним производительность нескольких алгоритмов классификатора, чтобы выбрать один для живого перевода поваренной книги.

А пока вы можете посмотреть полный код этой статьи здесь, подписаться на меня в Twitter или подключиться через LinkedIn.