Обнаружение горизонтальной линии с помощью OpenCV
Я пытаюсь найти горизонтальные и вертикальные линии на изображении, которое пришло из "документа". Документы - это отсканированные страницы из контрактов, поэтому строки выглядят так, как вы видите в таблице или в блоке контракта.
Я пытался OpenCV для работы. Реализация преобразования Хафа в OpenCV казалась полезной для этой работы, но я не смог найти какую-либо комбинацию параметров, которая позволила бы ему точно находить вертикальные и горизонтальные линии. Я пытался с и без обнаружения края. Неудачно. Если кто-то сделал что-то подобное, мне интересно знать, как.
Смотрите здесь изображение моего до и после экспериментов с HoughP в OpenCV. Это лучшее, что я мог сделать, http://dl.dropbox.com/u/3787481/Untitled%201.png
Так что теперь мне интересно, есть ли другой вид преобразования, который я мог бы использовать, который позволил бы мне надежно находить горизонтальные и вертикальные линии (и, желательно, штриховые линии тоже).
Я знаю, что эта проблема разрешима, потому что у меня есть инструменты Nuance и ABBYY OCR, которые могут надежно извлекать горизонтальные и вертикальные линии и возвращать мне ограничивающую рамку линий.
Спасибо! Патрик.
6 ответов
Вы видели пример кода из документации по функции HoughLinesP?
Я думаю, что вы можете использовать его в качестве отправной точки для вашего алгоритма. Чтобы выбрать горизонтальные вертикальные линии, вам просто нужно отфильтровать другие линии по углу наклона.
ОБНОВИТЬ:
Как я вижу, вам нужно найти не линии, а горизонтальные и вертикальные края на странице. Для этой задачи вам нужно объединить несколько этапов обработки, чтобы получить хорошие результаты.
Для вашего изображения я могу получить хорошие результаты, комбинируя обнаружение краев Canny с HoughLinesP. Вот мой код (я использовал Python, но я думаю, что вы видите идею):
img = cv2.imread("C:/temp/1.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 80, 120)
lines = cv2.HoughLinesP(edges, 1, math.pi/2, 2, None, 30, 1);
for line in lines[0]:
pt1 = (line[0],line[1])
pt2 = (line[2],line[3])
cv2.line(img, pt1, pt2, (0,0,255), 3)
cv2.imwrite("C:/temp/2.png", img)
Результат выглядит так:
Вот полное решение OpenCV с использованием морфологических операций.
- Получить бинарный образ
- Создать горизонтальное ядро и обнаружить горизонтальные линии
- Создать вертикальное ядро и обнаружить вертикальные линии
Вот визуализация процесса. Используя это входное изображение:
Двоичное изображение
import cv2
# Load image, convert to grayscale, Otsu's threshold
image = cv2.imread('1.png')
result = image.copy()
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
Обнаруженные горизонтальные линии выделены зеленым цветом
# Detect horizontal lines
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40,1))
detect_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
cv2.drawContours(result, [c], -1, (36,255,12), 2)
Обнаруженные вертикальные линии выделены зеленым цветом
# Detect vertical lines
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,10))
detect_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
cnts = cv2.findContours(detect_vertical, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
cv2.drawContours(result, [c], -1, (36,255,12), 2)
Результат
Вот результат с использованием другого входного изображения
Ввод ->
Двоичный ->
Обнаружено по горизонтали ->
Обнаружен вертикальный ->
Результат
Примечание. В зависимости от образа вам может потребоваться изменить размер ядра. Например, чтобы захватить более длинные горизонтальные линии, может потребоваться увеличить горизонтальное ядро от(40, 1)
сказать (80, 1)
. Если вы хотите обнаружить более толстые горизонтальные линии, вы можете увеличить ширину ядра, чтобы сказать(80, 2)
. Кроме того, вы можете увеличить количество итераций при выполненииcv2.morphologyEx()
. Точно так же вы можете изменить вертикальные ядра, чтобы обнаруживать более или менее вертикальные линии. При увеличении или уменьшении размера ядра есть компромисс, так как вы можете захватить больше или меньше строк. Опять же, все зависит от входного изображения.
Полный код для полноты
import cv2
# Load image, convert to grayscale, Otsu's threshold
image = cv2.imread('1.png')
result = image.copy()
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Detect horizontal lines
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40,1))
detect_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
cv2.drawContours(result, [c], -1, (36,255,12), 2)
# Detect vertical lines
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,10))
detect_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
cnts = cv2.findContours(detect_vertical, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
cv2.drawContours(result, [c], -1, (36,255,12), 2)
cv2.imshow('result', result)
cv2.waitKey()
Если вы просто хотите использовать "линии", а не "линейные сегменты", я бы не стал использовать Canny, Hough, FindContours или любую другую подобную функцию на тот случай, если вы захотите увеличить скорость своего кода. Если ваши изображения не повернуты и то, что вы хотите найти, всегда вертикальное или горизонтальное, я бы просто использовал cv::Sobel (одно для вертикального, а другое для горизонтального) и создавал массивы накопления для столбцов и строк. Затем вы можете искать максимумы в таких скоплениях или профилях, например, установив порог, и вы узнаете строку или столбец, в котором есть вертикальные или горизонтальные граничные линии.
Возможно, вы решите оставить обнаружение линий Хафа, так как этот метод ищет "глобальные" линии, а не обязательно отрезки. Недавно я реализовал приложение, которое идентифицировало "параллелограммы" - по существу квадраты, которые можно поворачивать и перспективу укорачивать из-за угла обзора. Вы могли бы рассмотреть что-то подобное. Мой трубопровод был:
- Конвертировать из RGB в оттенки серого (cvCvtColor)
- Smooth (cvSmooth)
- Порог (cvThreshold)
- Определить края (cvCanny)
- Найти контуры (cvFindContours)
- Приблизительные контуры с линейными элементами (cvApproxPoly)
В вашем приложении результирующий список контуров, вероятно, будет большим (в зависимости от "агрессивности" сглаживания и улучшения характеристик детектора контуров Canny. Вы можете сократить этот список по различным параметрам: количество точек, возвращаемых из искателя контура площадь контура (cvContourArea) и т. д. Исходя из моего опыта, я ожидал бы, что "допустимые" линии в вашем приложении будут иметь четко определенные свойства площади и количества вершин. Кроме того, вы можете отфильтровать контуры на основе расстояния между конечными точками. точки, угол, определяемый линией, соединяющей конечные точки и т. д.
В зависимости от того, сколько процессорного времени у вас есть, вы всегда можете соединить алгоритм Хафа с алгоритмом, подобным приведенному выше, для надежного определения горизонтальных и вертикальных линий.
Не конвертируйте RGB в оттенки серого. Иногда разные цвета в RGB могут быть объединены в одно и то же значение в градациях серого, поэтому он может пропустить некоторые контуры. Вы должны проанализировать каждый из каналов RGB отдельно.
Вот подход, который накапливает массивы для столбцов и строк. Затем можно искать максимумы в таких скоплениях (выше определенного порога) и делать вывод, в какой строке или столбце находится вертикальная или горизонтальная линия.
Если вы хотите быстро протестировать код, используйте следующий блокнот Google Colab.
Блокнот Google Colab
import numpy as np
import cv2
import scipy
from scipy.signal import find_peaks
from matplotlib import pyplot as plt
url = "https://i.stack.imgur.com/S00ap.png"
!wget $url -q -O input.jpg
fileName = 'input.jpg'
img = cv2.imread(fileName)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
tmp = img.copy()
gray = cv2.cvtColor(tmp, cv2.COLOR_BGR2GRAY)
blurred = cv2.bilateralFilter(gray, 11, 61, 39)
edges = cv2.Canny(blurred, 0, 255)
v_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,3))
h_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7,1))
v_morphed = cv2.morphologyEx(edges, cv2.MORPH_OPEN, v_kernel, iterations=2)
v_morphed = cv2.dilate(v_morphed, None)
h_morphed = cv2.morphologyEx(edges, cv2.MORPH_OPEN, h_kernel, iterations=2)
h_morphed = cv2.dilate(h_morphed, None)
v_acc = cv2.reduce(v_morphed, 0, cv2.REDUCE_SUM, dtype=cv2.CV_32S)
h_acc = cv2.reduce(h_morphed, 1, cv2.REDUCE_SUM, dtype=cv2.CV_32S)
def smooth(y, box_pts):
box = np.ones(box_pts)/box_pts
y_smooth = np.convolve(y, box, mode='same')
return y_smooth
s_v_acc = smooth(v_acc[0,:],9)
s_h_acc = smooth(h_acc[:,0],9)
v_peaks, v_props = find_peaks(s_v_acc, 0.70*np.max(np.max(s_v_acc)))
h_peaks, h_props = find_peaks(s_h_acc, 0.70*np.max(np.max(s_h_acc)))
for peak_index in v_peaks:
cv2.line(tmp, (peak_index, 0), (peak_index, img.shape[0]), (255, 0, 0),2)
for peak_index in h_peaks:
cv2.line(tmp, (0, peak_index), (img.shape[1], peak_index), (0, 0, 255),2)
v_height = v_props['peak_heights'] #list of the heights of the peaks
h_height = h_props['peak_heights'] #list of the heights of the peaks
def align_axis_x(ax, ax_target):
"""Make x-axis of `ax` aligned with `ax_target` in figure"""
posn_old, posn_target = ax.get_position(), ax_target.get_position()
ax.set_position([posn_target.x0, posn_old.y0, posn_target.width, posn_old.height])
def align_axis_y(ax, ax_target):
"""Make y-axis of `ax` aligned with `ax_target` in figure"""
posn_old, posn_target = ax.get_position(), ax_target.get_position()
ax.set_position([posn_old.x0, posn_target.y0, posn_old.width, posn_target.height])
fig = plt.figure(constrained_layout=False, figsize=(24,16))
spec = fig.add_gridspec(ncols=4, nrows=2, height_ratios=[1, 1])
ax1 = fig.add_subplot(spec[0,0])
ax1.imshow(tmp)
ax2 = fig.add_subplot(spec[0, 1])
ax2.imshow(v_morphed)
ax3 = fig.add_subplot(spec[0, 2])
ax3.imshow(h_morphed)
ax4 = fig.add_subplot(spec[0, 3], sharey=ax3)
ax4.plot(h_acc[:,0], np.arange(len(h_acc[:,0])), 'y', marker="o", ms=1, mfc="k", mec="k")
ax4.plot(s_h_acc, np.arange(len(s_h_acc)), 'r', lw=1)
ax4.plot(h_height, h_peaks, "x", lw="5")
ax5 = fig.add_subplot(spec[1, 1], sharex=ax2)
ax5.plot(np.arange(len(v_acc[0,:])), v_acc[0,:], 'y', marker="o", ms=1, mfc="k", mec="k")
ax5.plot(np.arange(len(s_v_acc)), s_v_acc, 'r', lw=2)
ax5.plot(v_peaks, v_height, "x", lw="5")
plt.tight_layout()
align_axis_y(ax4,ax3)
align_axis_x(ax5,ax2)