如何在以编程方式保留文本的同时删除图像中的所有线条和边框?

How to remove all lines and borders in an image while keeping text programmatically?

我正在尝试使用 Tesseract OCR 从图像中提取文本。 目前,对于这个原始输入图像,输出质量非常差(大约 50%)。但是当我尝试使用 photoshop 删除所有线条和边框时,输出效果提高了很多 (~90%)。有什么方法可以使用 OpenCV、Imagemagick 或其他技术以编程方式删除图像中的所有线条和边框(保留文本)?

原图:

预期图像:

我有一个想法。但只有当你有绝对水平和垂直的线条时它才会起作用。您可以先对该图像进行二值化(如果尚未进行)。然后编写一些代码,同时遍历图像的每一行,检查是否存在包含超过某个阈值的黑色像素序列。例如,如果从第 100 个像素到第 150 个像素的某行中存在连续的黑点序列,则将这些像素设为白色。找到所有水平线后,您可以做同样的事情来摆脱垂直线。

在我的示例中,我认为黑色像素序列恰好从第 100 个像素开始到第 150 个像素结束,因为如果第 151 个像素中有另一个黑色像素,那么我也必须添加该像素。换句话说,尝试完全找到这些行。

如果你解决了这个问题,请告诉我)

不使用 OpenCV,只是终端中的一行 ImageMagick,但它可能会让您了解如何在 OpenCV 中执行此操作。 ImageMagick 安装在大多数 Linux 发行版上,可用于 OSX 和 Windows.

这个概念的关键是创建一个新图像,其中每个像素都设置为其左侧 100 个相邻像素和右侧 100 个相邻像素的中值。这样,具有许多黑色水平相邻像素(即水平黑线)的像素在输出图像中将是白色的。然后在垂直方向应用相同的处理以去除垂直线。

您在终端中输入的命令将是:

convert input.png                                                 \
   \( -clone 0 -threshold 50% -negate -statistic median 200x1 \)  \
   -compose lighten -composite                                    \
   \( -clone 0 -threshold 50% -negate -statistic median 1x200 \)  \
   -composite result.png

第一行说加载你的原始图像。

第二行开始一些"aside-processing"复制原始图像,对其进行阈值化和反转,然后计算每边100的所有相邻像素的中值。

然后第三行采用第二行的结果并将其合成到原始图像上,选择每个位置的较亮像素 - 即我的水平线蒙版变白的像素。

接下来的两行再次做同样的事情,但垂直线是垂直方向。

结果是这样的:

如果我将它与您的原始图像进行区别,就像这样,我可以看到它做了什么:

convert input.png result.png -compose difference -composite diff.png

我想,如果您想删除更多的线条,您实际上可以稍微模糊差异图像并将其应用于原始图像。当然,您也可以使用过滤器长度和阈值等。

你需要的是 Leptonica 和 Lept4j

在项目的源代码中有一个关于如何完成此操作的示例,在测试中 here: LineRemovalTest.java

输入:

输出:

您可以使用 Sobel/Laplacian/Canny 中的边缘检测算法并使用 Hough 变换来识别 OpenCV 中的线条并将它们涂成白色以移除线条:

laplacian = cv2.Laplacian(img,cv2.CV_8UC1) # Laplacian OR
edges = cv2.Canny(img,80,10,apertureSize = 3) # canny Edge OR
# Output dtype = cv2.CV_8U # Sobel
sobelx8u = cv2.Sobel(img,cv2.CV_8U,1,0,ksize=5)
# Output dtype = cv2.CV_64F. Then take its absolute and convert to cv2.CV_8U
sobelx64f = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=5)
abs_sobel64f = np.absolute(sobelx64f)
sobel_8u = np.uint8(abs_sobel64f)

# Hough's Probabilistic Line Transform 
minLineLength = 900
maxLineGap = 100
lines = cv2.HoughLinesP(edges,1,np.pi/180,100,minLineLength,maxLineGap)
for line in lines:
    for x1,y1,x2,y2 in line:
        cv2.line(img,(x1,y1),(x2,y2),(255,255,255),2)

cv2.imwrite('houghlines.jpg',img)

使用 ImageMagick 有更好的方法。

识别线条形状并将其移除

ImageMagick 有一个巧妙的功能,称为形状形态学。您可以使用它来识别像 table 线这样的形状并删除它们。

一个班轮

convert in.png                              \
-type Grayscale                             \
-negate                                     \
-define morphology:compose=darken           \
-morphology Thinning 'Rectangle:1x80+0+0<'  \
-negate                                     \
out.png

说明

  • 转换 in.png : 加载图片。
  • -type Grayscale:确保 ImageMagick 知道它是灰度图像。
  • -negate:反转图像颜色层(已通过设置灰度适当调整)。线条和字符将为白色,背景为黑色。
  • -define morphology:compose=darken:定义形态学识别的区域会变暗。
  • -morphology Thinning 'Rectangle:1x80+0+0<' 定义一个 1px x 80px 的矩形内核,用于识别线条形状。仅当此内核适合白色形状(记住我们 否定 颜色)这么大或更大时,它才会变暗。 < 标志允许它旋转。
  • -negate:第二次反转颜色。现在字符又变黑了,背景变白了。
  • out.png:要生成的输出文件。

结果图像

申请后

convert in.png -type Grayscale -negate -define morphology:compose=darken -morphology Thinning 'Rectangle:1x80+0+0<' -negate out.png

这是输出图像:

观察

  • 您应该选择一个比您的较大字符大小更大的矩形内核大小,以确保该矩形不适合字符。
  • 一些小虚线和小 table 细胞分裂仍然存在,但这是因为它们小于 80 像素。
  • 这种技术的优点是它比其他用户在这里提出的中值像素色差方法更好地保留了字符,尽管有点混乱,它仍然有更好的结果去除 table 行.

遇到了同样的问题。我觉得一个更合乎逻辑的解决方案可能是(参考:Extract Table Borders

//assuming, b_w is the binary image
inv = 255 - b_w    
horizontal_img = new_img
vertical_img = new_img

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (100,1))
horizontal_img = cv2.erode(horizontal_img, kernel, iterations=1)
horizontal_img = cv2.dilate(horizontal_img, kernel, iterations=1)


kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,100))
vertical_img = cv2.erode(vertical_img, kernel, iterations=1)
vertical_img = cv2.dilate(vertical_img, kernel, iterations=1)

mask_img = horizontal_img + vertical_img
no_border = np.bitwise_or(b_w, mask_img)

由于没有人发布完整的 OpenCV 解决方案,这里提供一个简单的方法

  1. 获取二值图像Load the image, convert to grayscale, and Otsu's threshold

  2. 去掉水平线。我们创建一个horizontal shaped kernel cv2.getStructuringElement() 然后 find contours 并删除带有 cv2.drawContours()

    的行
  3. 删除垂直线。我们做同样的操作,但使用垂直形状的内核


加载图片,转灰度,然后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]

现在我们创建一个水平内核来检测水平线 cv2.getStructuringElement() 并找到轮廓 cv2.findContours() .要删除水平线,我们使用 cv2.drawContours() 并用白色填充每个水平轮廓。这有效地“擦除”了水平线。这是检测到的绿色水平线

# Remove horizontal lines
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40,1))
remove_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
cnts = cv2.findContours(remove_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, (255,255,255), 5)

类似地,我们创建一个垂直内核来删除垂直线,找到轮廓,并用白色填充每个垂直轮廓。这是检测到的以绿色突出显示的垂直线

# Remove vertical lines
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,40))
remove_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
cnts = cv2.findContours(remove_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, (255,255,255), 5)

用白色填充水平和垂直线后,这是我们的结果


注意:根据图像,您可能需要修改内核大小。例如,要捕获更长的水平线,可能需要将水平内核从 (40, 1) 增加到 (80, 1)。如果你想检测更粗的水平线,那么你可以增加内核的宽度说 (80, 2)。此外,您可以在执行 cv2.morphologyEx() 时增加迭代次数。同样,您可以修改垂直内核以检测更多或更少的垂直线。增加或减少内核大小时需要权衡取舍,因为您可能会捕获更多或更少的行。同样,这一切都取决于输入图像

完整性的完整代码

import cv2

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]

# Remove horizontal lines
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40,1))
remove_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
cnts = cv2.findContours(remove_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, (255,255,255), 5)

# Remove vertical lines
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,40))
remove_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
cnts = cv2.findContours(remove_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, (255,255,255), 5)

cv2.imshow('thresh', thresh)
cv2.imshow('result', result)
cv2.imwrite('result.png', result)
cv2.waitKey()