Исправьте дыры и недостающие контуры вашей таблицы с помощью OpenCV / Python

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

Большинство алгоритмов распознавания клеток основаны на линиях и клеточной структуре. Отсутствие линий приводит к плохому распознаванию из-за «забытых» ячеек. То же самое и с этим подходом. Линии необходимы. Если в вашей таблице нет четко различимых линий, это не сработает. А теперь взглянем на код!

Во-первых, нам нужно выполнить импорт. В этом случае он ограничен только двумя импортами: OpenCV и NumPy.

import cv2
import numpy as np

Затем нам нужно загрузить изображение / документ, содержащий таблицу. Если это целый документ с текстом, окружающим таблицу, вам нужно сначала распознать таблицу и обрезать изображение до размера таблицы. (Чтобы узнать больше о распознавании таблиц и обрезке до размеров таблицы, нажмите здесь.)

# Load the image
image = cv2.imread(‘/Users/marius/Desktop/holes.png’, -1)

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

Теперь нам нужно получить размер изображения (высоту и ширину) и сохранить его в переменных hei и wid.

(hei,wid,_) = image.shape 

Следующим шагом является масштабирование серого и размытие с помощью фильтра Гаусса. Это помогает распознавать линии. Для получения дополнительной информации о градациях серого щелкните здесь.

#Grayscale and blur the image
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)

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

#Threshold the image
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

Затем используется алгоритм findContours OpenCV для получения положения всех контуров. Для всех контуров рисуется ограничивающий прямоугольник для создания прямоугольников / ячеек таблицы. Затем поля сохраняются в списке с четырьмя значениями x, y, шириной, высотой.

#Retrieve contours 
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
#Create box-list
box = []
# Get position (x,y), width and height for every contour 
for c in contours:
    x, y, w, h = cv2.boundingRect(c)
    box.append([x,y,w,h])

Затем все значения высоты, ширины, x и y отдельно сохраняются в списках, и вычисляются минимальные значения высоты, ширины, а также x и y. Кроме того, необходимы максимальные y и x.

#Create separate lists for all values
heights=[]
widths=[]
xs=[]
ys=[]
#Store values in lists
for b in box:
    heights.append(b[3])
    widths.append(b[2])
    xs.append(b[0])
    ys.append(b[1])
#Retrieve minimum and maximum of lists
min_height = np.min(heights)
min_width = np.min(widths)
min_x = np.min(xs)
min_y = np.min(ys)
max_y = np.max(ys)
max_x = np.max(xs)

Сохраненные значения теперь используются, чтобы понять, где находится таблица. Минимальное значение y можно использовать для получения самой верхней строки таблицы, которую можно рассматривать как начальную точку таблицы. Минимальное значение x - это левый край таблицы. Чтобы получить приблизительный размер, нам нужно получить максимальное значение y, которое является ячейкой или строкой в ​​нижней части таблицы. Y-значение последней строки представляет верхний край ячейки, а не нижнюю часть ячейки. Чтобы учитывать полный размер ячейки и таблицы, необходимо прибавить высоту ячейки последних строк к максимальному значению y, чтобы получить полную высоту таблицы. Максимальный x будет последним столбцом и, соответственно, самой правой ячейкой / строкой таблицы. Значение x - это левый край каждой ячейки, и последовательно нам нужно добавить ширину последнего столбца к максимальному значению x, чтобы получить полную ширину таблицы.

#Retrieve height where y is maximum (edge at bottom, last row of table)
for b in box:
    if b[1] == max_y:
        max_y_height = b[3]
#Retrieve width where x is maximum (rightmost edge, last column of table)
for b in box:
    if b[0] == max_x:
        max_x_width = b[2]

На следующем этапе все горизонтальные и вертикальные линии извлекаются и сохраняются отдельно. Это делается путем создания ядра, которое устанавливает пороговые значения и применяет морфологические операции. Горизонтальное ядро ​​имеет размер (50,1). Вы можете поиграть с размером в зависимости от размера вашего изображения. Вертикальное ядро ​​имеет размер (1,50).

Морфологические операции осуществляют трансформации обнаруженных структур в зависимости от их геометрии (Soille, с.50, 1998). Дилатация - одна из самых распространенных и основных морфологических операций. Если хотя бы один пиксель под ядром белый, рассматриваемый пиксель исходного изображения будет считаться белым. Следовательно, белые области увеличиваются. Имейте в виду, что из-за инверсии фон черный, а передний - белый, что означает, что строки таблицы в настоящее время белые. Расширение можно рассматривать как наиболее важный шаг. Теперь дыры и ломаные линии исправлены, и для дальнейшего распознавания таблицы будут рассмотрены все ячейки.

# Obtain horizontal lines mask
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50,1))
horizontal_mask = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=1)
horizontal_mask = cv2.dilate(horizontal_mask, horizontal_kernel, iterations=9)
# Obtain vertical lines mask
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,50))
vertical_mask = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=1)
vertical_mask= cv2.dilate(vertical_mask, vertical_kernel, iterations=9)

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

# Bitwise-and masks together
result = 255 — cv2.bitwise_or(vertical_mask, horizontal_mask)

Если таблица окружена текстом и не стоит отдельно (в моем примере она не окружена), мы вырезаем ее и устанавливаем на белом фоне. Теперь нам нужен размер ранее полученной таблицы. Мы обрезаем окончательное изображение до размера таблицы, используя минимальное значение y (край наверху), максимальное значение y + высота максимальных ячеек y (край внизу), минимальное значение x (которое равно левый край) и максимальный x + ширина максимальных x ячеек (который является правым краем). Затем изображение обрезается до размера таблицы. Будет создан новый фон исходного размера документа, полностью заполненный белыми пикселями. Извлекается центр изображения, а восстановленная таблица объединяется с белым фоном и помещается прямо в центр изображения.

#Cropping the image to the table size
crop_img = result[(min_y+5):(max_y+max_y_height), (min_x):(max_x+max_x_width+5)]
#Creating a new image and filling it with white background
img_white = np.zeros((hei, wid), np.uint8)
img_white[:, 0:wid] = (255)
#Retrieve the coordinates of the center of the image
x_offset = int((wid — crop_img.shape[1])/2)
y_offset = int((hei — crop_img.shape[0])/2)
#Placing the cropped and repaired table into the white background
img_white[ y_offset:y_offset+crop_img.shape[0], x_offset:x_offset+crop_img.shape[1]] = crop_img
#Viewing the result
cv2.imshow(‘Result’, img_white)
cv2.waitKey()

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

Надеюсь, вам понравилось мое руководство, и вы можете использовать его в своих проектах.

Хотите прочитать больше подобных историй?

"Начать"