在前文《camelot是怎么做表格抽取的(一)—— camelot框架概览》中已经对线框类表格,也就是lattice
的步骤进行了简单的介绍,主要包含以下几步:
- 把pdf页面转换成图像
- 通过图像处理的方式,从页面中检测出水平方向和竖直方向可能用于构成表格的直线。
- 根据检测出的直线,生成候选表格的bounding box
- 找出候选表格区域中水平和竖直直线的交点
- 确定表格各行、列的区域
- 根据各行、列的区域,水平、竖直方向的表格线以及页面文本内容,解析出表格结构,填充单元格内容,最终形成表格对象
接下来本文将对上述各个步骤进行更细致的梳理。
PDF转图像
抽取线框类表格的算法主要封装在camelot/parsers/lattice.py
中的Lattice
类中,该类通过extract_tables
方法对单页的pdf文档(camelot会把整个pdf文档拆分成一个个单页的pdf文档,每一页单独保存成一个pdf文档)进行表格抽取,该方法的源码如下所示:
1 | def extract_tables(self, filename, suppress_stdout=False, layout_kwargs={}): |
_generate_layout
方法使用pdfminer
库对每一页对应的pdf文档进行加载和layout analysis,把页面上的字符组织成一个个水平/竖直方向连续的文本。在PDF转图像这一部分,用不到这些文本内容。
由于lattice
是通过图像处理的方式检测表格线框的,所以第一步需要把pdf转换成图像,在Lattice
类中,这一功能是通过_generate_image
实现的,下面是该方法的源码:
1 | def _generate_image(self): |
该方法会调用ghostscript
命令行工具,把单页的pdf文档转换单页的page图像,所以在安装camelot之前需要安装ghostscript
这个转换工具,具体的安装指南可以参考camelot的官方文档:https://camelot-py.readthedocs.io/en/master/user/install-deps.html,有关`ghostscript`方法这里也就不具体展开了,其实我也不是太了解^_^。
直线检测
在lattice
模式下,想要检测出表格区域,首先要检测出构成表格线框的直线。在Lattice
类的_generate_table_bbox
方法中,通过调用camelot/image_procesing.py
中的find_lines
函数在图像中检测直线,find_lines
的源码如下:
1 | def find_lines( |
find_lines
中的入参threshold
是二值化的图像,camelot
采用的是自适应二值化。
find_lines
检测直线的算法主要包含以下几步:
形态学操作。通过腐蚀、膨胀操作先去除掉那些水平方向/竖直方向长度短于页面长度/宽度一定比例的区域。
1
2
3threshold = cv2.erode(threshold, el)
threshold = cv2.dilate(threshold, el)
dmask = cv2.dilate(threshold, el, iterations=iterations)找出图像中剩下的连通区域的外部轮廓。
1
2
3
4
5
6
7
8try:
_, 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得到连通区域的bounding box,然后根据方向对bbox的坐标取平均得到检测到的直线。
1
2
3
4
5
6
7
8
9
10for c in contours:
x, y, w, h = cv2.boundingRect(c)
x1, x2 = x, x + w
y1, y2 = y, y + h
if direction == "vertical":
lines.append(((x1 + x2) // 2, y2, (x1 + x2) // 2, y1))
elif direction == "horizontal":
lines.append((x1, (y1 + y2) // 2, x2, (y1 + y2) // 2)
return dmask, lines
表格区域检测
从页面图像检测出水平和竖直方向的直线区域后,Lattice
类的_generate_table_bbox
方法通过调用camelot/image_processing.py
中的find_contours
函数生成候选表格所在的矩形区域。find_contours
的源码如下所示:
1 | def find_contours(vertical, horizontal): |
find_contours
生成表格区域主要分为以下几步:
将水平和竖直方向的直线区域叠加到一起,形成一些由水平、竖直直线区域相交形成的连通区域。
1
mask = vertical + horizontal
找到上述叠加图像中各连通区域的外轮廓。
1
2
3
4
5
6
7
8
9try:
__, contours, __ = cv2.findContours(
mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
except ValueError:
# for opencv backward compatibility
contours, __ = cv2.findContours(
mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)按照面积对得到的连通区域的轮廓进行排序,保留面积最大的前十个轮廓。
1
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10]
把包围连通区域的矩形区域作为候选表格的矩形区域
1
2
3
4
5
6cont = []
for c in contours:
c_poly = cv2.approxPolyDP(c, 3, True) # 用折线对轮廓进行近似
x, y, w, h = cv2.boundingRect(c_poly)
cont.append((x, y, w, h))
return cont
找到水平和竖直方向直线的交点
由于lattice
模式是通过水平和竖直方向的交点识别表格的行、列区域的,所以在找到候选表格的矩形区域之后,Lattice
类中的_generate_table_bbox
方法会调用camelot/image_processing.py
中的find_joints
函数生成交点。find_joints
的源码如下所示:
1 | def find_joints(contours, vertical, horizontal): |
返回的结果是一个字典,其中key是每个候选表格的bounding box,value是位于每个候选表区域中的交点。
表格行列区域识别
得到了候选表格的矩形区域之后,Lattice
类的extract_tables
方法通过调用自身的_generate_columns_and_rows
方法识别出行和列所处的位置。_generate_columns_and_cols
方法的源码如下:
1 | def _generate_columns_and_rows(self, table_idx, tk): |
_generate_columns_and_rows
通过以下几步生成行和列:
根据表格区域内的交点,生成候选的水平、竖直表格线(根据交点的x坐标生成候选的竖直表格线,根据交点的y坐标生成候选的水平表格线)。
1
2cols, rows = zip(*self.table_bbox[tk])
cols, rows = list(cols), list(rows)根据表格区域的bounding box,生成可能位于表格边界上的表格线
1
2cols.extend([tk[0], tk[2]])
rows.extend([tk[1], tk[3]])合并重合在一起的表格线
1
2cols = merge_close_lines(sorted(cols), line_tol=self.line_tol)
rows = merge_close_lines(sorted(rows, reverse=True), line_tol=self.line_tol)两条相邻的水平/竖直直线形成一个行/列区域
1
2cols = [(cols[i], cols[i + 1]) for i in range(0, len(cols) - 1)]
rows = [(rows[i], rows[i + 1]) for i in range(0, len(rows) - 1)]
表格对象构建
在得到候选表格区域及其对应的行列区域后,Lattice
类的extract_tables
方法通过调用自身的_generate_table
方法创建表格对象。由于代码较多,这里就不贴相关代码了,只在下面大体说一下相关的东西,如果以后有机会,可能会把这部分展开具体剖析下。
这些生成的表格对象以单元格为基本单位组织表格内容,但是只包含最基本的单元格对象,不包含“合并单元格”对象,即每一个单元格对象都只对应一行和一列。虽然表格对象内不包含合并单元格对象,但是每个单元格对象通过hspan和vspan属性来表示其自身是否在水平或竖直方向与其它单元格合并到了一起。
因为多数情况下每个单元格在水平和竖直方向上,分别都有两个“邻居”,所以仅仅根据hspan和vspan属性还无法直接确定“合并单元格”的具体组成情况,所以对于某些需要用到“合并单元格”的情况可能不太方便。