Привет, Хабр! В этой статье я расскажу о том, как восстановить структуру таблицы и  извлечь рукописные числа из отсканированного документа такого плана:  Допустим, у нас есть документ , который представлен выше, и в нём  необходимо извлечь данные из тех ячеек, в которых имеются числа. Ячейки, в которых могут содержаться числа выделены красным цветом:   Для того, чтобы распознать информацию в ячейках, необходимо проделать следующие шаги:  Коррекция перекоса таблицы — в нашем случае это не так критично, так как у нас угол перекоса не очень большой, но если угол перекоса будет достаточно большой(от 10 градусов), в таком случае может возникнуть много трудностей при распознавании информации.  Восстановление структуры таблицы, то есть нахождения ограничительных рамок каждой ячейки.  Поиск нужных полей в таблице, то есть тех полей, в которых есть числа.  Распознавание рукописных чисел в ячейках.  Первым делом мы переводим наше изображение в оттенки серого и преобразуем серое изображение в бинарное с помощью функции threshold(), про которую я подробно писал тут .  Теперь в  функцию where() мы передаём чёрно-белое изображение, и берём координаты тех пикселей, у которых значения равны 255 . Эта функция возвращает нам два массива — координаты по оси y и x соответственно, поэтому с помощью функции column_stack() мы складываем два одномерных массива в виде столбцов, чтобы получить один двумерный массив. Далее этот массив мы передаём в функцию minAreaRect(), которая находит минимальную площадь повёрнутого прямоугольника и возвращает последним аргументом угол поворота. Теперь нам известен угол наклона, поэтому мы применяем аффинное преобразование для исправления перекоса:    Мы передаём координаты точки вокруг которой будем вращаться, в нашем случае, это координаты центра, угол наклона и коэффициент масштабирования в функцию getRotationMatrix2D(). Данная функция возвращает массив numpy, который мы передаём вторым аргументом в функцию warpAffine(), которая выполняет преобразование. Сверху — изображение до, снизу — изображение после:   Для того, чтобы восстановить структуру таблицы, нам необходимо воспользоваться методами математической морфологии такими как эрозия и наращивание:  Эрозия(erode): прилагаем к каждой точке объекта на оригинальном изображении структурный элемент так, чтобы совпадал центр структурного элемента и точка на изображении, если структурный элемент полностью принадлежит  объекту, то такая точка остается => удаляются детали, которые меньше чем структурный элемент и объект становится тоньше.  Наращивание (dilate): на каждую точку объекта накладывается структурный элемент, а недостающие точки дорисовываются => закрашиваются дырки меньшие, чем структурный элемент, а объект в целом становится толще.  В результате у нас получились следующие вертикальные и горизонтальные линии соответственно:  Далее полученные линии складываем и применяем к полученном изображению функцию dilate():  В итоге  восстановить структуру таблицы удалось:  Теперь создадим словарь, где в качестве ключа будет выступать название колонки, а в качестве значения-  True/False , True — те ячейки, в которых могут содержаться числа, False — это соответственно те поля, в которых информацию мы не собираемся распознавать. В дальнейшем для ключей, у которых значение True, информацию будем обновлять распознанными числами:  Далее необходимо найти контуры каждой колонки, что мы и  делаем  с помощью функции findContours(). Полученные контуры нам необходимо отсортировать в таком порядке, в котором идут пары «ключ-значение» в словаре, то есть сверху вниз и слева направо:  Сперва  с помощью функции boundingRect() находим ограничительные рамки каждого контура: начальные x и y координаты, за которыми следуют ширина и высота рамки. В 3 строчке кода мы сортируем контуры с помощью функции custom_tuple_sorting(), реализацию которой Вы можете посмотреть на github . Данная функция сортирует контуры сначала по у, а потом по x. В качестве первых двух аргументов функция  принимает координаты контура, а последним — смещение. Дело в том, что ячейки могут располагаться в одной строке, но координаты по y могут немного отличаться, поэтому, если разница между двумя координатами меньше смещения, то мы считаем такие координаты равными между собой. То есть, если смещение равно 3, то 34=35=36.   Следующим шагом мы вырезаем только ту ячейку с основного изображения, у которой значение в словаре True:    Находим ограничительные рамки чисел:  Как мы можем увидеть, у нас появилось две проблемы:  Нашлись не все контуры чисел. Это связано с тем, что числа были написаны слишком близко к нижней линии ограничительной рамки.  У последней цифры образовалось два контура.  Для того, чтобы решить первую проблему  необходимо найти и заменить все пиксели ограничительной рамки ячейки белыми пикселями. Для этого мы вырезаем такой же кусок изображения только с изображения, которое нам вернула функция merge_lines(), где линии ограничительной рамки белого цвета:  Данное изображение мы передаём в функцию find_cell_contours(), где в качестве первого аргумента выступает предыдущее изображение, а вторым аргументом — изображение, которое было вырезано с основного изображения. В первой строчке кода мы находим координаты всех белых пикселей у изображения, которое было передано первым в функцию  и в цикле заменяем цвет на белый у изображения, которое было передано в качестве второго аргумента, только для тех пикселей, которые были белыми у первого изображения:  Для того, чтобы решить вторую проблему, мы с помощью функции permutations() модуля itertools перебираем все возможные комбинации контуров и если оно соответствует условию в 3 строчке кода => контур находится внутри контура и мы такой контур удаляем:  После всех вышеперечисленных действий удалось решить проблему и получилось следующее изображение: Распознавание рукописных чисел в ячейках  На этом этапе я не буду останавливаться подробно. Была обучена свёрточная нейронная сеть . Датасет был взят отсюда .  В конечном итоге, для изображения, которое было приведено в начале статьи у нас получился вот такой словарик:    Я надеюсь, вам понравилась моя статья, а знания пригодятся для разработки программы по анализу собственных документов. Код, как всегда, доступен на github.