网站排版,淄博服装网站建设,设计软件库,镇江网站制作哪家好文章目录1 背景简述2 camelot中的方法2.1 二值化2.2 腐蚀膨胀2.3 轮廓检测2.4 结果展示3 基于霍夫直线检测的方法3.1 霍夫直线检测原理3.2 概率霍夫直线检测3.3 霍夫直线应用参考资料1 背景简述
图像中的表格结构化是一个比较热门的话题#xff0c;其输入是一张图片#xff…
文章目录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_backgroundFalse, blocksize15, c-2):Thresholds an image using OpenCVs 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 OpenCVs 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 OpenCVs 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_backgroundFalse,blocksize11,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]], dtypeuint8)情况一 我们先构造一个数值方向长度不足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, regionsNone, directionhorizontal, line_scale15, iterations0
):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 OpenCVs 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, iterationsiterations)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,regionsregions,directionvertical,line_scalevertical_line_scale,iterationsiterations,
)# 横线检测
iterations 0
horizontal_line_scale 50
regions None
horizontal_mask, horizontal_segments find_lines(threshold,regionsregions,directionhorizontal,line_scalehorizontal_line_scale,iterationsiterations,
)通过控制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唯一确定为ykxbykxbykxb。我们再构造一个kb坐标系横轴为kkk纵轴为bbb。那么这个坐标系上的任意一点(ki,bi)(k_i, b_i)(ki,bi)就是xy坐标系上的一条直线。再说一遍kb坐标系上的一个点就代表了xy坐标系上的一条直线。
那么好了过(x0,y0)(x_0, y_0)(x0,y0)的所有直线在kb坐标系上就是直线
y0kx0b→{k−1x0by0x0ifx0≠0by0ifx00(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} y0kx0b→{k−x01bx0y0by0if x00if x00(3-1)
kb坐标系上的一条直线就是过xy坐标系上的某个点的所有直线。
同理假设有另一个点(x1,y1)(x_1, y_1)(x1,y1)过该点的所有直线在kb坐标系上的直线为y1kx1by_1kx_1by1kx1b。
方程组(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)这两点所确定的直线。
{y0kx0by1kx1b(3-2)\begin{cases} y_0 kx_0 b \\ y_1kx_1b \end{cases} \tag{3-2} {y0kx0by1kx1b(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空间中的一条曲线rxicosθyisinθr x_i cos\theta y_i sin\thetarxicosθyisinθ。
图3-2 xy和极坐标空间映射图如果这里想不明白为啥rxicosθyisinθr x_i cos\theta y_i sin\thetarxicosθyisinθ可以表示xy空间过(xi,yi)(x_i, y_i)(xi,yi)的所有直线。不妨这样想一下某条直线过图3-2中的点(x2,y2)(x_2, y_2)(x2,y2)刚开始是和y轴平行的即θ0\theta0θ0然后开始绕(x2,y2)(x_2, y_2)(x2,y2)旋转θ\thetaθ不断变大直到转过2π2\pi2π。这个转动的过程遍历了所有过(x2,y2)(x_2, y_2)(x2,y2)的直线而且动手画辅助线算算看的话会发现rrr一直满足rx2cosθy2sinθr x_2 cos\theta y_2 sin\thetarx2cosθy2sinθ。
与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, apertureSize3)
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