Язык программирования Python

Распознавание рукописных чисел на Python с использованием OpenCV

Простой метод распознавания рукописных цифр с помощью OpenCV

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

Найти ответ на вычисление типа 3847*7943 относительно просто, при условии, что вас зовут “Человек дождя”.

Поскольку часто это не так, мы склонны обращаться к машине (калькулятору, компьютеру, смартфону и т.д.), которая способна сделать это почти мгновенно.

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

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

Представление и конфигурация

OpenCV (для Open Computer Vision) – это бесплатная графическая библиотека, специализирующаяся на обработке изображений в реальном времени. Она содержит более 2500 алгоритмов, оптимизированных для этого.

Установка OpenCV

Я компилировал из исходников версии 4.2.0, но вам не обязательно делать то же самое. Поэтому я предлагаю простой метод – сделать это с помощью pip.

pip

Этот метод довольно быстрый, но предварительно убедитесь, что pip у вас установлен.

python3 -m pip install --user -U opencv-python opencv-contrib-python

Подготовка к обработке

Прежде чем мы начнем писать алгоритм, который позволит компьютеру распознать число, мы должны подготовить что-то, что он сможет распознать, когда ему его дадут. Для этого давайте начнем с набора изображений цифр 0-9.

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

Эта функция принимает 4 параметра:

  • text: в нашем случае это будут цифры от 0 до 9;
  • fontFace: начертание текста, список возможных значений здесь ;
  • fontScale: коэффициент масштабирования шрифта, который умножается на базовый размер шрифта.;
  • thickness: толщина линии рисунка.

Давайте создадим файл digit.py и с помощью функции putText() начнем генерировать наши изображения:

#!/usr/bin/env python3 import cv2 import numpy as np SCALE = 3 THICK = 5 WHITE = (255, 255, 255) digits = [] for digit in map(str, range(10)): (width, height), bline = cv2.getTextSize(digit, cv2.FONT_HERSHEY_SIMPLEX, SCALE, THICK) digits.append(np.zeros((height + bline, width), np.uint8)) cv2.putText(digits[-1], digit, (0, height), cv2.FONT_HERSHEY_SIMPLEX, SCALE, WHITE, THICK) x0, y0, w, h = cv2.boundingRect(digits[-1]) digits[-1] = digits[-1][y0:y0+h, x0:x0+w] cv2.imshow(digit, digits[-1])
Code language: JavaScript (javascript)

Теперь наш список цифр содержит 10 изображений чисел от 0 до 9:

Позже мы сможем использовать эти образцы для сравнения со своим почерком.

Обработка изображений

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

Для теста используется следующее изображение:

Чтение изображения

Первый шаг – загрузить изображение и преобразовать его в черно-белое, поскольку в нашем случае цвет не имеет значения и даже может быть помехой. Это наша первая обработка.

Необходимо придерживаться файла digit.py:

color_test_image = cv2.imread('/test/image.png', cv2.IMREAD_COLOR) gray_test_image = cv2.cvtColor(color_test_image, cv2.COLOR_BGR2GRAY) cv2.imshow('test_image', gray_test_image)
Code language: JavaScript (javascript)

У вас должно получиться следующее:

Очистка изображения

Поскольку мы хотим извлечь только числа из нашей начальной картинки, мы выполним некоторую обработку с помощью бинарного порога, чтобы выделить числа. Цифры написаны черными чернилами на белой бумаге, но те, которые мы сгенерировали, написаны белым цветом на черном фоне.

Поэтому мы будем использовать то же поведение, что и в первой части, благодаря функции threshold(), которая также принимает 4 параметра:

  • src : исходное изображение (test_image) ;
  • thresh : пороговое значение;
  • maxval : максимальное значение для использования с THRESH_BINARY и THRESH_BINARY_INV ;
  • type : тип порога, который будет использоваться (список типов здесь).

Поэтому мы можем написать:

# выполняем обратное бинарное пороговое выделение, чтобы цифры выделялись белым на черном ret, thresh = cv2.threshold(gray_test_image, 170, 255, cv2.THRESH_BINARY_INV)
Code language: PHP (php)

Теперь изображение должно выглядеть следующим образом:

Использование черно-белых изображений интересно еще и тем, что ими можно управлять как булевыми массивами (0 черный, 1 белый); поэтому мы можем использовать логические операции (AND, OR, XOR, NAND…) для поиска наибольшего соответствия.

Функция распознавания

Целью будет сравнить изображение и сопоставить его с нашими ранее сгенерированными известными фигурами.

Сопоставление:

Чем больше контрольная цифра стерта, тем выше будет совпадение. Вот почему мы начинаем с XOR (или исключения) наших двух изображений.

XOR будет иметь эффект сохранения того, что не является общим между двумя данными.

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

Чем слабее эта часть, тем более “надежным” является распознавание.

Например.
Если 4 элемент управления содержит 400 белых пикселей, а после XOR и AND он содержит только 74, мы можем сделать следующий расчет 100 – (74 / 400 * 100), что дает 81% соответствия.

def detect(img): # сравниваем полученную цифру с нашей базой percent_white_pix = 0 digit = -1 for i, d in enumerate(digits): scaled_img = cv2.resize(img, d.shape[:2][::-1]) # d AND (scaled_img XOR d) bitwise = cv2.bitwise_and(d, cv2.bitwise_xor(scaled_img, d)) # результат определяется наибольшей потерей белых пикселей before = np.sum(d == 255) matching = 100 - (np.sum(bitwise == 255) / before * 100) #cv2.imshow('digit_%d' % (9-i), bitwise) if percent_white_pix < matching: percent_white_pix = matching digit = i return digit
Code language: PHP (php)

Извлечение всех цифр

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

# находим контуры порогового изображения contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
Code language: PHP (php)

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

for cnt in contours: # проверяется размер контура, чтобы избежать обработки "дефекта". if cv2.contourArea(cnt) > 30: # получаем прямоугольник, окружающий число brect = cv2.boundingRect(cnt) x,y,w,h = brect roi = thresh[y:y+h, x:x+w] # определение digit = detect(roi) cv2.rectangle(color_test_image, brect, (0,255,0), 2) cv2.putText(color_test_image, str(digit), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (190, 123, 68), 2) cv2.imshow('resultat', color_test_image) cv2.waitKey(0) cv2.destroyAllWindows()
Code language: PHP (php)

Берем каждую извлеченную цифру, сравниваем ее с нашими тестовыми изображениями и возвращаем только самое лучшее совпадение.

Вот результат:

Мы получаем 80% успеха, что не так уж плохо. В зависимости от того, как вы пишете, вы можем получить очень разный процент успеха.

Алгоритм в цикле

При копировании и вставке приведенного выше кода не всегда очевидно, что происходит.

Наглядное изображение стоит тысячи слов:

Каждое число в извлеченной картинке сравнивается со всеми остальными следующим образом:

  • XOR изображения обнаруженной цифры с контрольной цифрой;
  • Результат XOR с контрольным числом;
  • Сравнивается количество белых пикселей на контрольной цифре до и после;
  • Если процент стирания/восстановления контрольного разряда выше, чем предыдущего, то он преобразуется.
Разброс в успешности результатов может быть обусловлен несколькими факторами, такими как качество фотографии, качество письма, ориентация изображения и т.д. Однако если вы хотите применить тот же процесс к фотографии, содержащей напечатанные цифры, вы должны получить более устойчивые результаты (если шрифт не слишком отличается от того, который был создан в первой части).

Пример использования

Распознавание одного числа может показаться незначительным результатом, но не стесняйтесь адаптировать и улучшать этот пример алгоритма.

Используя точно такую же логику, можно извлечь игральные карты из изображения и определить их достоинство и символ (♥, ♠, ♦ и ♣). Очевидно, с теми же недостатками, что описаны выше, но вы можете получить результат, схожий с этим:

Единственной ошибкой обнаружения на этом изображении является знак 🂪 в правом верхнем углу. При таком подходе очень трудно получить 100%, но всегда интересно увидеть методы, которые человек может легко понять, что далеко не так при использовании нейронных сетей, которые делают для нас метод “непрозрачным”.

Если вы хотите начать, я советую вам начать с простых примеров (обнаружение чисел), чтобы затем усложнять их и прийти, как в примере выше, к обнаружению более сложных элементов, таких как игральные карты.

Этот метод, очевидно, имеет свои ограничения, которые становятся очевидными довольно быстро.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *