文章目录

  • 1 背景简述
  • 2 camelot中的方法
    • 2.1 二值化
    • 2.2 腐蚀膨胀
    • 2.3 轮廓检测
    • 2.4 结果展示
  • 3 基于霍夫直线检测的方法
    • 3.1 霍夫直线检测原理
    • 3.2 概率霍夫直线检测
    • 3.3 霍夫直线应用
  • 参考资料

1 背景简述

图像中的表格结构化是一个比较热门的话题,其输入是一张图片,输出是结构化过的所有表格,也可以认为输出的是一个excel。目前市面上也没有哪家做的比较完美,因为表格总是千奇百怪的。不过对于一些简单规整的有线表或者多线表,还是可以做到比较好的结构化的。

图像表格检测的一般流程为

图1-1 图像表格检测流程图

【表格检测】是为了找到图像上的表格位置,同时分开一些挨得比较近的表,需要训练一个图像检测的模型,这个标一批数据硬train就行了,Yolov5等一般的图像检测模型效果都不错。不过对于只有一行的表,检测效果不太好,这个得要传统方法的辅助。

【水平线和垂直线检测】是为了检测表格中的分割线,对表格结构化有很大的参考意义。某些单行表也可以通过这步的结果来判断。或者说四边有线的,就可以用这里的结果来判断表格的位置。这篇讲的就是这一步。

【OCR】是为了得到表格中每个单元格的文本。

【表格结构化】是结合前三个模块的结果,得到结构化的表格,这里根据要处理的业务场景中表格的多样性程度,会有不同代码量的规则。我处理的场景得要写了几千行的规则了。

这篇只讲讲怎么把图像中的表格线给检测出来。

方案主要有两种:
(1)二值化+腐蚀膨胀+轮廓检测,这是camelot中使用的方法。
(2)边缘检测+霍夫直线检测,这是网上见到比较多的方法。

下面就来说说这两种方法,所使用的图片就是

图1-2 测试图片样例

取这张图片是因为图中的表格又有实线,又有虚线。方便比较不同方法的效果。

2 camelot中的方法

2.1 二值化

二值化之用了opencv当中的cv2.adaptiveThreshold,这种二值化的方法可以根据局部的色差来自适应调整阈值,比较符合表格背景色花里胡哨的场景。

def adaptive_threshold(imagename, process_background=False, blocksize=15, c=-2):"""Thresholds an image using OpenCV's adaptiveThreshold.Parameters----------imagename : stringPath to image file.process_background : bool, optional (default: False)Whether or not to process lines that are in background.blocksize : int, optional (default: 15)Size of a pixel neighborhood that is used to calculate athreshold value for the pixel: 3, 5, 7, and so on.For more information, refer `OpenCV's adaptiveThreshold <https://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html#adaptivethreshold>`_.c : int, optional (default: -2)Constant subtracted from the mean or weighted mean.Normally, it is positive but may be zero or negative as well.For more information, refer `OpenCV's adaptiveThreshold <https://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html#adaptivethreshold>`_.Returns-------img : objectnumpy.ndarray representing the original image.threshold : objectnumpy.ndarray representing the thresholded image."""img = cv2.imread(imagename)gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)if process_background:threshold = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blocksize, c)else:threshold = cv2.adaptiveThreshold(np.invert(gray),255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,blocksize,c,)return img, threshold

使用的时候,直接用就行

image, threshold = adaptive_threshold(image_path,process_background=False,blocksize=11,c=-2,
)

这里的threshold就是二值化之后的图像。

2.2 腐蚀膨胀

腐蚀膨胀的目的是把沿水平和竖直方向的长线给找出来。腐蚀是当kernel范围内有0时,就全部置0,滤掉了不连续的像素点;膨胀是把kernel中心为255的点膨胀成kernel的大小,把原来线段上被腐蚀的点给还原回来。

举个例子,我们先构造一个垂直方向长度为5的kernel。

import cv2
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 5))

kernel为

array([[1],[1],[1],[1],[1]], dtype=uint8)

情况一:
我们先构造一个数值方向长度不足5的直线,并用kernel腐蚀一下

import numpy as np
img = np.zeros((10, 5))
img[1:5, 0] = 255
erode_img = cv2.erode(img, kernel)

img为

array([[  0.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.]])

erode_img为

array([[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.]])

情况二:
我们再构造一个数值方向长度足够5的直线,并用kernel腐蚀一下

import numpy as np
img = np.zeros((10, 5))
img[1:6, 0] = 255
erode_img = cv2.erode(img, kernel)

img为

array([[  0.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[255.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.],[  0.,   0.,   0.,   0.,   0.]])

erode_img为

array([[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[255., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.],[0., 0., 0., 0., 0.]])

膨胀的话,就可以把没有完全被腐蚀掉的点,恢复回线,这里就不举例啰嗦了。

2.3 轮廓检测

轮廓检测部分是把腐蚀膨胀得到的线给找出来,这个和腐蚀膨胀在同一个函数里

def find_lines(threshold, regions=None, direction="horizontal", line_scale=15, iterations=0
):"""Finds horizontal and vertical lines by applying morphologicaltransformations on an image.Parameters----------threshold : objectnumpy.ndarray representing the thresholded image.regions : list, optional (default: None)List of page regions that may contain tables of the form x1,y1,x2,y2where (x1, y1) -> left-top and (x2, y2) -> right-bottomin image coordinate space.direction : string, optional (default: 'horizontal')Specifies whether to find vertical or horizontal lines.line_scale : int, optional (default: 15)Factor by which the page dimensions will be divided to getsmallest length of lines that should be detected.The larger this value, smaller the detected lines. Making ittoo large will lead to text being detected as lines.iterations : int, optional (default: 0)Number of times for erosion/dilation is applied.For more information, refer `OpenCV's dilate <https://docs.opencv.org/2.4/modules/imgproc/doc/filtering.html#dilate>`_.Returns-------dmask : objectnumpy.ndarray representing pixels where vertical/horizontallines lie.lines : listList of tuples representing vertical/horizontal lines withcoordinates relative to a left-top origin inimage coordinate space."""lines = []if direction == "vertical":size = threshold.shape[0] // line_scaleif size < 2:size = threshold.shape[0]el = cv2.getStructuringElement(cv2.MORPH_RECT, (1, size))elif direction == "horizontal":size = threshold.shape[1] // line_scaleif size < 2:size = threshold.shape[1]el = cv2.getStructuringElement(cv2.MORPH_RECT, (size, 1))elif direction is None:raise ValueError("Specify direction as either 'vertical' or 'horizontal'")if regions is not None:region_mask = np.zeros(threshold.shape)for region in regions:x, y, w, h = regionregion_mask[y : y + h, x : x + w] = 1threshold = np.multiply(threshold, region_mask)threshold = cv2.erode(threshold, el)threshold = cv2.dilate(threshold, el)dmask = cv2.dilate(threshold, el, iterations=iterations)try:contours, _ = cv2.findContours(threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)except ValueError:# for opencv backward compatibility_, contours, _ = cv2.findContours(threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)for c in contours:x, y, w, h = cv2.boundingRect(c)x1, x2 = x, x + wy1, y2 = y, y + hif direction == "vertical":lines.append(((x1 + x2) // 2, y1, (x1 + x2) // 2, y2))elif direction == "horizontal":lines.append((x1, (y1 + y2) // 2, x2, (y1 + y2) // 2))return dmask, lines

横线和竖线的检测代码为

# 竖线检测
iterations = 0
vertical_line_scale = 60
regions = None
vertical_mask, vertical_segments = find_lines(threshold,regions=regions,direction="vertical",line_scale=vertical_line_scale,iterations=iterations,
)# 横线检测
iterations = 0
horizontal_line_scale = 50
regions = None
horizontal_mask, horizontal_segments = find_lines(threshold,regions=regions,direction="horizontal",line_scale=horizontal_line_scale,iterations=iterations,
)

通过控制vertical_line_scale和horizontal_line_scale可以控制最小线段长度。vertical_mask和horizontal_mask是二值图像,vertical_segments和horizontal_segments是线段的位置。

2.4 结果展示

按这种方法检测出来的线段如下图2-1所示。

图2-1 结果展示图

不难看出,这种方法下,需要的实线都找到了,但是某些大字上的笔画也被认为是线段,更严重的是,虚线检测不出来。

在表格中没有虚线的场景下,这其实是一个简单快捷准确的方案。

3 基于霍夫直线检测的方法

为了解决虚线没法检测出来的问题,就想到了霍夫直线检测。这里简单说明一下霍夫直线检测是怎么回事。

3.1 霍夫直线检测原理

霍夫直线检测想明白了很简单,初中的知识就能解了。

要想明白这个问题,首先得要知道过xy坐标系上的某个点(x0,y0)(x_0, y_0)(x0​,y0​)的所有直线如何表示。我们都知道,一条直线可以用斜率kkk和截距bbb唯一确定为y=kx+by=kx+by=kx+b。我们再构造一个kb坐标系,横轴为kkk,纵轴为bbb。那么这个坐标系上的任意一点(ki,bi)(k_i, b_i)(ki​,bi​)就是xy坐标系上的一条直线。再说一遍,kb坐标系上的一个点,就代表了xy坐标系上的一条直线

那么好了,过(x0,y0)(x_0, y_0)(x0​,y0​)的所有直线在kb坐标系上就是直线

y0=kx0+b→{k=−1x0b+y0x0ifx0≠0b=y0ifx0=0(3-1)y_0 = kx_0 + b \rightarrow \begin{cases} k = -\frac{1}{x_0}b + \frac{y_0}{x_0} &if\ x_0 \ne 0\\ b = y_0 &if\ x_0 =0 \end{cases} \tag{3-1} y0​=kx0​+b→{k=−x0​1​b+x0​y0​​b=y0​​if x0​​=0if x0​=0​(3-1)

kb坐标系上的一条直线,就是过xy坐标系上的某个点的所有直线

同理,假设有另一个点(x1,y1)(x_1, y_1)(x1​,y1​),过该点的所有直线在kb坐标系上的直线为y1=kx1+by_1=kx_1+by1​=kx1​+b。

方程组(3−2)(3-2)(3−2)的解(k∗,b∗)(k^*, b^*)(k∗,b∗),就是过(x0,y0)(x_0, y_0)(x0​,y0​)和(x1,y1)(x_1, y_1)(x1​,y1​)这两点所确定的直线。

{y0=kx0+by1=kx1+b(3-2)\begin{cases} y_0 = kx_0 + b \\ y_1=kx_1+b \end{cases} \tag{3-2} {y0​=kx0​+by1​=kx1​+b​(3-2)

xy坐标系同一直线上的所有点的所有直线的表示,在kb坐标系上必定都过同一个点,如下图3-1所示。图是从参考资料[2]借过来,所以符号不一致,懒得画了。

图3-1 xy和kb空间映射图

这样以来,我们就可以根据kb空间上某个点被多少条直线穿过来判断在xy坐标系上有多少个点在这条直线上。

不过映射到kb坐标系会有一个问题,当xy坐标系上的直线接近于平行y轴时,k也会接近于无穷大,无穷大就没法算了。为了解决这个问题,就提出了把xy空间映射到极坐标θr\theta rθr空间。

映射方法如下图3-2所示,这图是借的参考资料[3]的。xy坐标系中的每条直线都θr\theta rθr空间中的一个点(θ,r)(\theta, r)(θ,r),rrr为xy坐标系中原点距离直线的距离,θ\thetaθ表示原点到直线的垂线与x轴的夹角;xy坐标系中过某个点的所有直线都对应于θr\theta rθr空间中的一条曲线r=xicosθ+yisinθr = x_i cos\theta + y_i sin\thetar=xi​cosθ+yi​sinθ

图3-2 xy和极坐标空间映射图

如果这里想不明白为啥r=xicosθ+yisinθr = x_i cos\theta + y_i sin\thetar=xi​cosθ+yi​sinθ可以表示xy空间过(xi,yi)(x_i, y_i)(xi​,yi​)的所有直线。不妨这样想一下,某条直线过图3-2中的点(x2,y2)(x_2, y_2)(x2​,y2​),刚开始是和y轴平行的,即θ=0\theta=0θ=0,然后开始绕(x2,y2)(x_2, y_2)(x2​,y2​)旋转,θ\thetaθ不断变大,直到转过2π2\pi2π。这个转动的过程遍历了所有过(x2,y2)(x_2, y_2)(x2​,y2​)的直线,而且动手画辅助线算算看的话,会发现rrr一直满足r=x2cosθ+y2sinθr = x_2 cos\theta + y_2 sin\thetar=x2​cosθ+y2​sinθ。

与kb坐标系不同,这里θ\thetaθ就是[0,2π)[0, 2\pi)[0,2π)的取值范围,rrr只要点(xi,yi)(x_i, y_i)(xi​,yi​)离原点是有限距离的就行,这个在实际场景都能满足。不会产生无限大的值。

至于怎么找直线,也是和kb坐标系一样,在θr\theta rθr空间找很多条曲线相交的那个点,就是xy空间的直线。点数量设置一个阈值,不让太短的线进来就行。

霍夫直线的好处是可以找到虚线。

3.2 概率霍夫直线检测

霍夫直线检测一般需要先把图像过边缘检测(比如canny),然后再将所有的边缘点映射到θr\theta rθr空间后寻找被超过一定数量的曲线相交的那些点。这样有两个缺点,一是计算量太大,二是不知道线段的真实长度。所以就有了概率霍夫直线检测。

概率霍夫会取边缘点的一个子集,来进行θr\theta rθr空间交点的统计,有一个累加器(Hough accumulator)会统计候选点被曲线穿过的次数。这大大减小了计算量,根据参考资料[6]说的,只要2%的边缘点,就有比较好的效果了。由于使用的是子集,所以点数量的阈值也要相应地调小。

根据阈值确定了候选点之后,概率霍夫会去边缘点的全集上找还有哪些点也在这条直线上,并发间隔太大的点过滤掉,这样以来就可以找到一条线段上的所有点了。

这样以来计算量大和不知道线段真实长度的问题就都解决了。

3.3 霍夫直线应用

霍夫直线检测在opencv中对应于cv2.HoughLines这个函数,只返回θ\thetaθ和rrr。概率霍夫在opencv中对应于cv2.HoughLinesP这个函数,返回线段的起始点和终止点坐标。这两个函数的参数说明可以参考参考资料[7],这里就不说了。

直接上代码,总的来说就两步,先Canny边缘检测,再概率霍夫。

import cv2im = cv2.imread(image_path)
gray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 150, 200, apertureSize=3)
img = im.copy()
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 100, minLineLength = 100, maxLineGap = 10)

这样得到的结果如下图3-3所示。

图3-3 概率霍夫结果图

不难看出,虚线出来了,但是有三个问题,一是文字的笔画也被认为是直线了,二是有挺多接近于重合的直线,三是有斜线出现。这几个问题都可以通过后处理来解决。

ocr的结果可以提供文字的位置,那些文字上的直线可以用这个信息过滤掉;接近于重合的直线可以根据直线的距离过滤掉;斜线根据斜率过滤掉即可,用表格检测的检测框也能过滤掉一大波线,因为我们只要表格里的表格线。

整体来说,方法总比困难多。

参考资料

[1] https://github.com/atlanhq/camelot
[2] https://blog.csdn.net/lkj345/article/details/50699981
[3] https://congleetea.github.io/blog/2018/09/28/hough-transform/
[4] https://stackoverflow.com/questions/59340367/how-does-the-probabilistic-hough-transform-compute-the-end-points-of-lines
[5] https://blog.csdn.net/zhaocj/article/details/40047397
[6] https://jayrambhia.com/blog/probabilistic-hough-transform
[7] https://www.cxyzjd.com/article/hihell/113670582

图像表格实线和虚线检测相关推荐

  1. html表格边框为一条虚的实线,表格边框变成虚线怎么回事 线条样式中点击”实线“...

    excel表格全部怎么变成虚线了 在弹出的界面中,选择"边框",选择虚线命令,分别为表格中横向和竖向添加.添加完毕后点击确定. 如何将EXCEL表格的实线变为虚线? EXCEL表格 ...

  2. 遥感图像中的小样本目标检测:Few-shot Object Detection on Remote SensingImages

    论文下载:https://arxiv.org/pdf/2006.07826v2.pdf Abstract 在本文中,我们处理遥感图像上的目标检测问题.以前的方法已经发展了许多基于深度卷积的遥感图像目标 ...

  3. html合并单元格后有虚线,excle单元格中间出现虚线/Excel表格里出现虚线,是怎么回事?...

    excel表格框中有一纵向虚线是什么? 网友回 2014-10-13 一般情况下,是"页面的分页线",做过打印预览的,都会出现. 如果想容打印到一张A4纸上以把要打印的内容先&qu ...

  4. 2021-IEEE论文-深度神经网络在文档图像表格识别中的应用现状及性能分析

    2021年5月12日收到, 2021年6月4日接受, 出版日期2021年6月9日, 当前版本日期2021年6月24日. 原论文下载地址 摘要 - Abstract   表格识别的第一阶段是检测文档中的 ...

  5. 在c语言中用什么表示虚线,ERWin ERStudio图里的实线和虚线的含义[转]

    注: ERWin 与 ERStudio 中这一点的描述方法是一样的. ERWin里面线代表实体间的三种关系:决定关系(Identifying Relationship),非决定关系(None-Iden ...

  6. html中设置表格单实线,css实现表格实线的方法

    css实现表格实线的方法 发布时间:2020-08-21 14:46:18 来源:亿速云 阅读:147 作者:小新 这篇文章主要介绍了css实现表格实线的方法,具有一定借鉴价值,需要的朋友可以参考下. ...

  7. PointPillar:利用伪图像高效实现3D目标检测

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 文章导读 计算机视觉任务中,2D卷积神经网络得益于极其出色的性能不断刷新着各大视觉任务的榜单,可谓是Y ...

  8. 谷歌最新论文:从图像中进行3-D目标检测

    关注上方"深度学习技术前沿",选择"星标公众号", 资源干货,第一时间送达! 作者:黄浴 知乎链接:https://zhuanlan.zhihu.com/p/1 ...

  9. OpenCV图像中的人脸界标检测

    OpenCV图像中的人脸界标检测 图像中的人脸界标检测 简介 命令参数的描述{tutorial_face_training_parameters} 源代码 检测结果 图像中的人脸界标检测 简介 此应用 ...

  10. 两个一样的图像相除会怎么样_【壮凌自动化分析】一种动力电池生产中基于图像运动模糊的速度检测方法...

    一种动力电池生产中基于图像运动模糊的速度检测方法 1.西南大学 电子信息工程学院,重庆 400715) 2.非线性电路与智能信息处理重庆市重点实验室,重庆 400715) 1.当前背景与成熟方法介绍 ...

最新文章

  1. LabviewRS232串口通信数据格式问题解析
  2. 学习笔记(十九)——Python与数据库交互(mysql、redis)
  3. 【rabbitmq】Authentication failed (rejected by the remote node), please check the Erlang cookie
  4. php 设置curl不超时时间,curl命令的超时时间
  5. 0day的NFO文件名的含义大全
  6. Linux下常用文本处理命令
  7. JS中调用bignumber处理高精度小数运算
  8. ubuntu下解决wireshark权限问题
  9. 2017.3.31 棋盘制作 失败总结
  10. quartus仿真6:74194构建线性反馈移位寄存器计数器LFSR
  11. 干货 :如何系统地学习数据挖掘
  12. 对于网络文学而言 计算机叙事,90年代文学的“增量”
  13. win7左上角白杠一直闪_win10换win7
  14. KubeVela解读
  15. Java 实现数据库数据 导入 导出成Excel文档的功能
  16. 小米Android 13 应用适配指南公告
  17. Echarts 图表制作建议指南思维导图,教你什么数据画什么图
  18. IM即时通讯仿微信软件平台源码搭建开发解决方案
  19. 中国智能音箱争夺战,国外巨头缺席BAT各有心思
  20. Java - 静态代理

热门文章

  1. 每日一生信--blast2go本地化(终极版)
  2. 机器学习(时间序列):线性回归之虚拟变量 dummy variables
  3. 陈强教授《机器学习及R应用》课程 第十章作业
  4. 4199 公约数(求解约数 + 最大公约数 + 二分)
  5. 邱跃鹏:互联网下半场,腾讯云要做信息能源发动机
  6. 计算机技术三大领域,量化投资策略的运用
  7. c#推箱子小游戏代码_C# 简单推箱子游戏源码
  8. Super-pack按键精灵安卓离线打包发布
  9. OpenCV-python 自制图片画框脚本
  10. windows系统设置定时开关机的方法