表格识别与内容提炼技术理解及研发趋势 - 合合技术团队 - 博客园

mikel阅读(351)

来源: 表格识别与内容提炼技术理解及研发趋势 – 合合技术团队 – 博客园

引言:

表格是各类文档中常见的对象,其结构化的组织形式方便人们进行信息理解和提取。表格的种类根据有无边框可以划分有线表、少线表、无线表。表格样式复杂多样,如存在背景填充、光照阴影、单元格行列合并等情况。大数据时代存在大量电子文档,应用表格识别技术能够减少表格处理时间,因此表格识别是文档理解领域的重要研究课题,也是合合信息这几年的技术突破点方向之一。

表格识别主要包括表格检测和表格结构识别两个子任务。

表格检测主要检测表格主体,即样本中表格区域。表格结构识别是对表格区域进行分析,提取表格中的数据与结构信息,得到行列分布与逻辑结构。未经特殊说明,以下表格识别专指表格结构识别。

 

研究现状与解决方案:

近年来,国内外专家学者对表格识别进行了大量研究,取得了丰富的研究成果。下面主要从传统图像处理方法和深度学习这两个方面做具体阐述。

传统方法

传统的表格识别工作是基于一些启发式的规则和图像处理方法,主要利用表格线或者文本块之间的空白分隔区域来确定单元格区域,通过腐蚀、膨胀,找连通区域,检测线段、直线,求交点,合并猜测框等。有自顶向下的方法(先检测表格区域,再不断对表格区域进行切割拆分得到单元格区域);还有自底向上的方法(先检测文本块,找到可能的表格线以及这些线的交点,确定单元格后还原出表格区域)

1、 OpenCV-检测并提取表格[1]

这是一种自顶向下的方法,先对图像进行二值化,然后使用霍夫变换,检测其中的直线,并在直线中,找到围成的一个矩形区域,最后将这块区域提取出来。

作者主要使用了四步操作
1、处理图像,灰度化,二值化。在灰度图的基础上运用adaptiveThreshold来达成自动阈值的二值化,取代Canny,这个算法在提取直线和文字比Canny有更好的效果
2、利用OpenCV里面的形态学函数,腐蚀erode膨胀dilate
3、交叉横纵线条,对点进行定位,通过bitwise_and函数获得横纵线条的交点,通过交点对表格区域进行提取
4、判断区域是否为表格。

虽然此方法可以相对完整的识别图片中的表格,但也存在几个问题:
1、图片倾斜不易识别
2、图片背景复杂会干扰识别
3、少线表情况,表格只有上下两条线的时候如何判断

 

2、pdfplumber解析表格[2]

pdfplumber是一款完全用python开发的pdf解析库,对于全线表,pdfminer能够实现较好的抽取效果,但对于少线表和无线表,效果就差了很多。

下面介绍pdfplumber中的表格抽取流程,这是一种自底向上的方法,首先找到可见的或猜测出不可见的候选表格线;然后根据候选表格线确定它们的交点;接着根据得到的交点,找到围成它们的最小单元格;最后把连通的单元格进行整合,生成检测出的表格对象。在表格生成的过程中,利用单元格的bbox坐标(四个角的坐标)判断单元格是否属于当前表格;同时对表格的左上角坐标进行排序,过滤掉小表格。

3、camelot表格抽取[3]

camelot是一个可以从可编辑的pdf文档中抽取表格的开源框架,与pdfplumber相比,其功能完备性差了点,除了表格抽取之外,并不能用它从pdf文档中解析出字符、单词、文本、线等较为低层次的对象。

 

camelot支持两种表格抽取模式:

 

一、lattice线框类表格抽取,步骤如下:
1、pdf转图像
2、图像处理算法检测出水平方向和竖直方向可能用于构成表格的直线
3、根据检测出的直线,生成可能表格的bounding box
4、确定表格各行、列的区域
5、根据各行、列的区域,水平、竖直方向的表格线以及页面文本内容,解析出表格结构,填充单元格内容,最终形成表格对象。

二、stream少线框类表格抽取,步骤如下:
1、通过pdfminer获取连续字符串
2、通过文本对齐的方式确定可能表格的bounding box
3、确定表格各行、列的区域
4、根据各行、列的区域以及页面上的文本字符串,解析表格结构,填充单元格内容,最终形成表格对象。

4、T-recs[4]

这是一种自底向上的方法,核心思想是对文本块区域进行聚类。

步骤如下:
1、从文本块中选择种子点
2、在种子点上下各一行分别去找与该种子点文本块之间是否水平方向有重合,如果有重合则将相应文本块和种子点块归到一起,并作为新的种子点
3、重复第1、2步,不断找与之水平方向有重合的文本块,直到所有的文本块都不能再合并下去。

此方法也存在诸多局限:
1、表头是跨单元格的,表头下面的文本块会被全部合并到一起
2、有时候上下几行文本确确实实是对齐的,但是和左右文本区域比较近,这种本不能分开的区域被错误的分开了
3、孤立的文本块会被切分成单独块。

因此,本方法后面大部分工作是针对这三种局限设定后处理规则,该方法认为表格之所以是表格是由文本块的分布决定的,而与分割带无关。加入后处理规则之后该方法方法具有较好的通用性,无论是对与PDF文档还是OCR的结果,都有比较好的效果。

深度学习方法

近年来人工智能技术飞速发展,研究人员将CV,NLP和图神经等成熟方法应用在表格识别任务中,取得很多不错的成果。深度学习表格识别主流方法包括语义分割,目标检测,序列预测和图神经等,下面我们对这些工作分别进行介绍。

1. 语义分割方法:

1.1 Rethinking Semantic Segmentation for Table Structure Recognition in Documents[5]

本文将表格结构的识别定义为语义分割问题,使用FCN网络框架,对表格的行和列分别进行预测。主要介绍了一种对预测结果进行切片的方法,降低了表格识别的复杂度。使用了FCN的Encoder和Decoder的结构,并加载了在ImageNet预训练好的模型。图片经过模型生成了与原图大小相同的特征,切片过程将特征按照行和列进行平均,将H*W*C(高*宽*Channel)的特征合并成了H*C和W*C大小特征;对这些特征进行卷积后,再进行复制,扩展为H*W*C的大小,再通过卷积层得到每个像素点的标签;最后进行后处理得到最终的结果。本文pipeline如图1所示。本文方法的局限在于本文所处理的表格对象中所有的单元格不存在跨行跨列,每行每列都从表格的最左侧和最上端开始,到最右侧和最下端结束。

图1

 

1.2 腾讯表格识别技术方案[6]

图像分割是对图像的每个像素点赋予标签,在表格识别任务中,每个像素可能属于横线、竖线、不可见横线、不可见竖线这几个标签。

解决方案流程如图2所示:
1、表格线标注:横向的线,竖向的线,横向的不可见线,竖向的不可见线。类别不互斥,考虑到单元格交点问题,即交点处的像素属于多个类别
2、几何分析提取连通区域,对连通区域拟合折线,合并形成框线;考虑图片弯曲、表格倾斜的情况,利用投影变换对原图矫正
3、调用ocr,识别文本内容,确定字符坐标
4、根据第二步的框线计算行列信息,判断单元格合并情况,得到每个单元格在途中的位置
5、根据单元格坐标和字符坐标,将字符嵌入到单元格,还原表格。

此方案专注于将页面拍照后进行表格识别,对于一般的表格效果还好,但现实场景太过纷杂,仍有很多问题亟待解决。

图2

2. 物体检测方法:

2.1 海康LGPMA方案[7]

此方案是ICDAR21比赛Table Recognition赛道的冠军,LGPMA将表格识别分为文本行检测、文字识别和表格结构识别三部分。文本检测模块是一个单行文本检测器,文字识别模块是一个基于attention 的识别器,这两部分用来获取表格图像中的文本信息。表格结构识别部分采用的是一种LGPMA的方案,基于Mask-RCNN同时出两个分割头,一个LPMA学习局部对齐边界,一个GPMA学习全局对齐边界,融合了自顶向下和自底向上两种思想。

如图3所示。在得到两路的soft mask之后,将LPMA和GPMA的对齐mask融合,之后对每个单元格边框进行精修。最后经过cell matching , empty cell searching 和 empty cell merging三个后处理步骤得到最终的表格结构。原方案采用较大的基础网络,训练推理对硬件及输出尺寸有一定约束,实际落地较为困难。

图3

2.2 角点表格检测法[8]

针对表格检测本文使用了“角点”来提升表格检测的精确度,对比基本模型在检测结果上能够得到进一步的检测与提升。

首先引入角点的概念,如图4所示:角点是表格四个顶点周围的一部份区域,这些区域大小相同,同一个表格的所有角点构成一个角点组,角点的检测与表格检测一样,可以使用目标检测模型来解决,作者使用Faster R-CNN模型,同时进行角点和表格的检测,检测结构如图5所示,使用角点组对对应的表格检测的横坐标进行校准,得到最终的表格区域。该方法与未加入角点的Faster R-CNN模型相比,结果有了较大的提升。

图4

图5

基于CenterNet的端到端表格识别方案[9]

目标检测识别往往是在图像上将目标用矩形框的形式框出,目标检测器都先穷举出潜在目标位置,然后对该位置进行分类,这种做法浪费时间,低效,还需要做额外的后处理。

CenterNet是将目标作为一个点(BBOX中心点),利用中心点回归其他目标属性,比如尺寸、位置、方向等。本文提出了一种基于CenterNet的表格识别方法,网络结构如图6所示,利用Cycle_Pairing模块和Pairing损失去学习相邻单元格的公共顶点信息,然后通过连结单元格获取一个完整的表格结构,最后使用相同的解析过程去获取行列信息。这篇文章解决户外场景图像的表格解析问题(TSP:table structure parsing),局限在于此方法仅适用于有线表格,无线表角点定义的歧义性使得本文方法不一定work。从本文的思路我们或许可以探索角点法在处理复杂场景的情况下是否比anchor-based的方法表现更优。

图6

3. 序列预测方法:

3.1 Latex标签序列预测[10]

基于图像的表格分析优势在于,它对表格类型具有鲁棒性,并不要求格式是页面扫描图像还是纯数字文档,它可用于多数文档类型,包括PDF、HTML、PowerPoint格式等。然而,非结构化数字文档中的表格数据,由于其结构和样式的复杂性及多样性,很难解析为结构化的机器可读格式。

在实践中,手工标注用于训练的数据集的成本和不灵活性是实际部署深度学习模型的关键瓶颈。本文是微软的一篇文章,利用互联网中存在大量的Word和Latex源文档,对这些在线文档应用一些弱监督来标注表格,创建TableBank数据集。对于word文档,可以修改内部的office xml代码,指定每个表格的边界线;对于latex文档,可以修改tex代码,代码已识别表格的边界框。表格检测使用基于不同配置的Faster R-CNN的架构,表格结构识别模型基于image-to-text的编码器-解码器架构。

本文的局限在于版式多样对表格分析任务的准确率具有负面影响,模型泛化能力差,某一领域的模型应用到其他领域效果不好,在TableBank数据集上的建模和学习具有很大改进空间。

3.2 HTML标签序列预测[11]

类似地,IBM公司开发并发布了数据集PubTabNet,此数据集中自动为每个表图像加上关于表的结构和每个单元格内的文本信息(HTML格式),如图7(a)所示。

作者等人提出了一种端到端的表格识别方案,是一种基于注意力的编码器-解码器(EDD)架构,它是由编码器、结构解码器和单元格解码器组成,可以将表格图像转化成HTML代码。编码器获得表格图像的视觉特征,两个独立的结构解码器⼀个输出表格结构,⼀个输出单元格内容。

图7(b)所示为EDD架构,不需要复杂的后处理即可得到表格结果。但end2end的方案在中文场景的落地还有很长的路要走,另外缺少表格物理结构的信息,EDD方法的纠错空间不多。同时,本文的另一个贡献是提出了一种新的基于树编辑距离的图像表格识别评价指标TEDS,将表格建模为树形结构,该指标比之前的基于precision、reacll、F1 score的评价指标更为规范。

图7(a)

图7(b)

3.3 TableMaster解决方案[12]

平安产险提出的TableMaster方案是ICDAR21比赛Table Recognition赛道的亚军,本方案基于文本识别算法Master[13]。TableMASTER算法采用多任务学习的模式,同时进行表格结构序列预测以及单元格位置回归,最后通过后处理匹配算法,融合表格结构序列和单元格文本内容,得到HTML代码。整个解决方案可以分为4个部分:表格结构序列识别,表格文本行检测,表格文本行识别,以及单元格与表格结构序列匹配。

表格结构序列识别部分,使用改进的文本识别算法MASTER,它与原生的MASTER在结构上的区别如图8所示。他们的特征提取器在结构上是大体一致的,采用的都是改进后的ResNet网络,TableMaster在解码阶段,经过一个TransformerLayer后,会分为两条分支。一条分支进行表格结构序列的监督学习,另一条分支进行表格中单元位置回归的监督学习。

表格文本行检测部分,采用的是经典的文本检测算法PSENet[14],表格文本行识别部分,采用的文本识别算法MASTER。

在单元格与表格结构序列匹配部分,作者团队定义了三种匹配规则,分别是Center Point Rule,IOU Rule,以及Distance Rule。这三种匹配规则的优先级顺序为Center Point Rule>IOU Rule>Distance Rule。

序列预测的表格识别结果受限于序列长度和类别数量,同时transformer结构推理速度有优化空间。

图8

4. 图卷积神经网络方法:

4.1 Rethinking Table Recognition using Graph Neural Networks[15]

近年来,越来越多的学者将深度学习技术应用到图数据领域,本文作者将表格结构识别问题描述为一个与图神经网络兼容的图问题,利用图神经网络解决这一问题。将每一个文本区域作为一个顶点,使用矩阵描述的三个图(单元格、行共享矩阵、列共享矩阵)定义为真值,若顶点共享一行,则对应的文本区域属于同一行,这些顶点视作彼此相邻。

模型分为四个流程:特征提取、信息交互、随机采样和分类,过程如图9所示。利用CNN提取视觉特征,输出端将ocr得到的顶点位置映射到特征图上,将视觉特征与位置特征结合形成聚集特征。得到所有的顶点特征后进行特征交互融合,得到每个顶点的结构特征。在分类部分使用DenseNet分别对定点对进行是否同行、同列、同单元格的结构关系分类。

图9

4.2 GFTE: Graph-based Financial Table Extraction[16]

本文[16]主要是为了主要解决金融类的、中文表格识别问题,提出一种新的基于图卷积神经网络的模型GFTE作为表格识别数据集(本文自己发布的中文数据集FinTab)中的基线方法。GFTE通过融合图像特征、位置特征和文本特征,能够对表格进行精确的边缘检测,取得较好的效果。

GFTE可以分为以下几个步骤:
1、构建基本事实,包括表格区域的图像、文本内容、文本位置和结构标签
2、基于单元格构造一个无向图G=<V, R, C>
3、使用GFTE来预测相邻关系,包括垂直和水平关系。

作者将表格中的每个单元格视作节点,每个节点包含三种类型的信息:文本内容、绝对位置和图像,节点与它的邻域理解成边,那么表格结构完全可以用一个图来表示,图结构如图10所示。

图11是GFTE的基本结构,首先将绝对位置转换成相对位置,然后使用这些位置生成图。然后将纯文本嵌入预先定义好的特征空间,使用LSTM获取语义特征。接着将位置特征与文本特征进行拼接,将他们传入两层的图卷积网络(GCN)。最后,利用节点的相对位置,计算网格,利用网格中国输入的像素位置计算输出,得到某一节点在某一点的图像特征。获得这三种不同的特征后,将生成图的一条边上的两个节点配对,并与三种不同的特征集合在一起,使用MLP预测这两个节点处于同行还是同列。

图神经网络通过合理建模,将表格中的不同特征进行融合,并取得了不错的效果,这种表格结构识别方法,为未来的工作提供了一些可能的发展道路。

图10

图11

————

结语:

目前,表格识别任务逐渐受到工业界和学术界的关注。表格识别对象从PDF文档,到文档图片,再到自然场景图片,复杂场景表格识越来越被重视。随着人工智能应用的成熟,表格识别方法已从传统图像算法转向到深度学习算法。目标检测、语义分割、序列预测、图神经网络等是目前表格识别的主要技术方向,它们各有优劣。作为文档理解的重要一环,一套在复杂场景中鲁棒性很强的表格识别方案仍有待探索。

对于表格识别及相关内容提炼,合合信息认为传统的表格识别方法设计起来较为复杂,对于版面布局分析和表格结构的提取,图像处理的方法依赖各种阈值和参数的选择,难以满足现实生活中复杂多样的表格场景,鲁棒性也相对较差。目前业界常用的表格识别方法多为深度学习算法,比较依赖算法工程师对于神经网络的精心设计,可以不用依赖阈值和参数,鲁棒性较强。

从公司角度来讲,算法落地的关键因素大致可分为计算力、数据、算法模型、业务场景等。计算力是人工智能技术的生产力,数据的价值在于给予解决某一场景问题足够的特征,算法模型是项目落地的承载体,业务场景是落地的关键。在技术落地中,应当一切以业务场景为核心,以最少的数据,最简单的模型,最少的计算力解决最实际的问题,达到最好的效果。对于表格识别这一技术来讲,一款推理速度快且在少样本情况下表现优异的表格识别算法会备受青睐。


参考文献:

[1] OpenCV-检测并提取表格:https://blog.csdn.net/yomo127/article/details/52045146

[2] pdfplumber解析表格:pdfplumber是怎么做表格抽取的(三) – 知乎

[3] camelot表格抽取:camelot是怎么做表格抽取的(三)—— 非线框类表格抽取 – 知乎

[4] T-recs:T. Kieninger and A. Dengel, “The T-Recs table recognition and analysis system,” in Document Analysis Systems: Theory and Practice, pp. 255–270, 1999.

[5] Siddiqui S A , Khan P I , Dengel A , et al. Rethinking Semantic Segmentation for Table Structure Recognition in Documents[C]// 2019 International Conference on Document Analysis and Recognition (ICDAR). 2019.

[6] 走进AI时代的文档识别技术之表格图像识别 – 云+社区 – 腾讯云 (tencent.com)

[7] LGPMA: Complicated Table Structure Recognition with Local and Global Pyramid Mask Alignment (arxiv.org)

[8] Sun N , Zhu Y , Hu X . Faster R-CNN Based Table Detection Combining Corner Locating[C]// 2019 International Conference on Document Analysis and Recognition (ICDAR). 2019.

[9] Long R , Wang W , Xue N , et al. Parsing Table Structures in the Wild. 2021.

[10] Zhong X , Shafieibavani E , Yepes A J . Image-based table recognition: data, model, and evaluation[J]. 2019.

[11] Li M , Cui L , Huang S , et al. TableBank: Table Benchmark for Image-based Table Detection and Recognition[J]. 2019.

[12] Ye J , Qi X , He Y , et al. PingAn-VCGroup’s Solution for ICDAR 2021 Competition on Scientific Literature Parsing Task B: Table Recognition to HTML[J]. 2021.

[13] Lu N , Yu W , Qi X , et al. MASTER: Multi-Aspect Non-local Network for Scene Text Recognition[J]. 2019.

[14]Li X , Wang W , Hou W , et al. Shape Robust Text Detection with Progressive Scale Expansion Network[J]. 2018.

[15] Qasim S R , Mahmood H , F Shafait. Rethinking Table Recognition using Graph Neural Networks. 2019.

[16] Li Y , Huang Z , Yan J , et al. GFTE: Graph-based Financial Table Extraction[J]. 2020.

表格识别与内容提炼技术理解及研发趋势 - 合合技术团队 - 博客园

mikel阅读(385)

来源: 表格识别与内容提炼技术理解及研发趋势 – 合合技术团队 – 博客园

引言:

表格是各类文档中常见的对象,其结构化的组织形式方便人们进行信息理解和提取。表格的种类根据有无边框可以划分有线表、少线表、无线表。表格样式复杂多样,如存在背景填充、光照阴影、单元格行列合并等情况。大数据时代存在大量电子文档,应用表格识别技术能够减少表格处理时间,因此表格识别是文档理解领域的重要研究课题,也是合合信息这几年的技术突破点方向之一。

表格识别主要包括表格检测和表格结构识别两个子任务。

表格检测主要检测表格主体,即样本中表格区域。表格结构识别是对表格区域进行分析,提取表格中的数据与结构信息,得到行列分布与逻辑结构。未经特殊说明,以下表格识别专指表格结构识别。

 

研究现状与解决方案:

近年来,国内外专家学者对表格识别进行了大量研究,取得了丰富的研究成果。下面主要从传统图像处理方法和深度学习这两个方面做具体阐述。

传统方法

传统的表格识别工作是基于一些启发式的规则和图像处理方法,主要利用表格线或者文本块之间的空白分隔区域来确定单元格区域,通过腐蚀、膨胀,找连通区域,检测线段、直线,求交点,合并猜测框等。有自顶向下的方法(先检测表格区域,再不断对表格区域进行切割拆分得到单元格区域);还有自底向上的方法(先检测文本块,找到可能的表格线以及这些线的交点,确定单元格后还原出表格区域)

1、 OpenCV-检测并提取表格[1]

这是一种自顶向下的方法,先对图像进行二值化,然后使用霍夫变换,检测其中的直线,并在直线中,找到围成的一个矩形区域,最后将这块区域提取出来。

作者主要使用了四步操作
1、处理图像,灰度化,二值化。在灰度图的基础上运用adaptiveThreshold来达成自动阈值的二值化,取代Canny,这个算法在提取直线和文字比Canny有更好的效果
2、利用OpenCV里面的形态学函数,腐蚀erode膨胀dilate
3、交叉横纵线条,对点进行定位,通过bitwise_and函数获得横纵线条的交点,通过交点对表格区域进行提取
4、判断区域是否为表格。

虽然此方法可以相对完整的识别图片中的表格,但也存在几个问题:
1、图片倾斜不易识别
2、图片背景复杂会干扰识别
3、少线表情况,表格只有上下两条线的时候如何判断

 

2、pdfplumber解析表格[2]

pdfplumber是一款完全用python开发的pdf解析库,对于全线表,pdfminer能够实现较好的抽取效果,但对于少线表和无线表,效果就差了很多。

下面介绍pdfplumber中的表格抽取流程,这是一种自底向上的方法,首先找到可见的或猜测出不可见的候选表格线;然后根据候选表格线确定它们的交点;接着根据得到的交点,找到围成它们的最小单元格;最后把连通的单元格进行整合,生成检测出的表格对象。在表格生成的过程中,利用单元格的bbox坐标(四个角的坐标)判断单元格是否属于当前表格;同时对表格的左上角坐标进行排序,过滤掉小表格。

3、camelot表格抽取[3]

camelot是一个可以从可编辑的pdf文档中抽取表格的开源框架,与pdfplumber相比,其功能完备性差了点,除了表格抽取之外,并不能用它从pdf文档中解析出字符、单词、文本、线等较为低层次的对象。

 

camelot支持两种表格抽取模式:

 

一、lattice线框类表格抽取,步骤如下:
1、pdf转图像
2、图像处理算法检测出水平方向和竖直方向可能用于构成表格的直线
3、根据检测出的直线,生成可能表格的bounding box
4、确定表格各行、列的区域
5、根据各行、列的区域,水平、竖直方向的表格线以及页面文本内容,解析出表格结构,填充单元格内容,最终形成表格对象。

二、stream少线框类表格抽取,步骤如下:
1、通过pdfminer获取连续字符串
2、通过文本对齐的方式确定可能表格的bounding box
3、确定表格各行、列的区域
4、根据各行、列的区域以及页面上的文本字符串,解析表格结构,填充单元格内容,最终形成表格对象。

4、T-recs[4]

这是一种自底向上的方法,核心思想是对文本块区域进行聚类。

步骤如下:
1、从文本块中选择种子点
2、在种子点上下各一行分别去找与该种子点文本块之间是否水平方向有重合,如果有重合则将相应文本块和种子点块归到一起,并作为新的种子点
3、重复第1、2步,不断找与之水平方向有重合的文本块,直到所有的文本块都不能再合并下去。

此方法也存在诸多局限:
1、表头是跨单元格的,表头下面的文本块会被全部合并到一起
2、有时候上下几行文本确确实实是对齐的,但是和左右文本区域比较近,这种本不能分开的区域被错误的分开了
3、孤立的文本块会被切分成单独块。

因此,本方法后面大部分工作是针对这三种局限设定后处理规则,该方法认为表格之所以是表格是由文本块的分布决定的,而与分割带无关。加入后处理规则之后该方法方法具有较好的通用性,无论是对与PDF文档还是OCR的结果,都有比较好的效果。

深度学习方法

近年来人工智能技术飞速发展,研究人员将CV,NLP和图神经等成熟方法应用在表格识别任务中,取得很多不错的成果。深度学习表格识别主流方法包括语义分割,目标检测,序列预测和图神经等,下面我们对这些工作分别进行介绍。

1. 语义分割方法:

1.1 Rethinking Semantic Segmentation for Table Structure Recognition in Documents[5]

本文将表格结构的识别定义为语义分割问题,使用FCN网络框架,对表格的行和列分别进行预测。主要介绍了一种对预测结果进行切片的方法,降低了表格识别的复杂度。使用了FCN的Encoder和Decoder的结构,并加载了在ImageNet预训练好的模型。图片经过模型生成了与原图大小相同的特征,切片过程将特征按照行和列进行平均,将H*W*C(高*宽*Channel)的特征合并成了H*C和W*C大小特征;对这些特征进行卷积后,再进行复制,扩展为H*W*C的大小,再通过卷积层得到每个像素点的标签;最后进行后处理得到最终的结果。本文pipeline如图1所示。本文方法的局限在于本文所处理的表格对象中所有的单元格不存在跨行跨列,每行每列都从表格的最左侧和最上端开始,到最右侧和最下端结束。

图1

 

1.2 腾讯表格识别技术方案[6]

图像分割是对图像的每个像素点赋予标签,在表格识别任务中,每个像素可能属于横线、竖线、不可见横线、不可见竖线这几个标签。

解决方案流程如图2所示:
1、表格线标注:横向的线,竖向的线,横向的不可见线,竖向的不可见线。类别不互斥,考虑到单元格交点问题,即交点处的像素属于多个类别
2、几何分析提取连通区域,对连通区域拟合折线,合并形成框线;考虑图片弯曲、表格倾斜的情况,利用投影变换对原图矫正
3、调用ocr,识别文本内容,确定字符坐标
4、根据第二步的框线计算行列信息,判断单元格合并情况,得到每个单元格在途中的位置
5、根据单元格坐标和字符坐标,将字符嵌入到单元格,还原表格。

此方案专注于将页面拍照后进行表格识别,对于一般的表格效果还好,但现实场景太过纷杂,仍有很多问题亟待解决。

图2

2. 物体检测方法:

2.1 海康LGPMA方案[7]

此方案是ICDAR21比赛Table Recognition赛道的冠军,LGPMA将表格识别分为文本行检测、文字识别和表格结构识别三部分。文本检测模块是一个单行文本检测器,文字识别模块是一个基于attention 的识别器,这两部分用来获取表格图像中的文本信息。表格结构识别部分采用的是一种LGPMA的方案,基于Mask-RCNN同时出两个分割头,一个LPMA学习局部对齐边界,一个GPMA学习全局对齐边界,融合了自顶向下和自底向上两种思想。

如图3所示。在得到两路的soft mask之后,将LPMA和GPMA的对齐mask融合,之后对每个单元格边框进行精修。最后经过cell matching , empty cell searching 和 empty cell merging三个后处理步骤得到最终的表格结构。原方案采用较大的基础网络,训练推理对硬件及输出尺寸有一定约束,实际落地较为困难。

图3

2.2 角点表格检测法[8]

针对表格检测本文使用了“角点”来提升表格检测的精确度,对比基本模型在检测结果上能够得到进一步的检测与提升。

首先引入角点的概念,如图4所示:角点是表格四个顶点周围的一部份区域,这些区域大小相同,同一个表格的所有角点构成一个角点组,角点的检测与表格检测一样,可以使用目标检测模型来解决,作者使用Faster R-CNN模型,同时进行角点和表格的检测,检测结构如图5所示,使用角点组对对应的表格检测的横坐标进行校准,得到最终的表格区域。该方法与未加入角点的Faster R-CNN模型相比,结果有了较大的提升。

图4

图5

基于CenterNet的端到端表格识别方案[9]

目标检测识别往往是在图像上将目标用矩形框的形式框出,目标检测器都先穷举出潜在目标位置,然后对该位置进行分类,这种做法浪费时间,低效,还需要做额外的后处理。

CenterNet是将目标作为一个点(BBOX中心点),利用中心点回归其他目标属性,比如尺寸、位置、方向等。本文提出了一种基于CenterNet的表格识别方法,网络结构如图6所示,利用Cycle_Pairing模块和Pairing损失去学习相邻单元格的公共顶点信息,然后通过连结单元格获取一个完整的表格结构,最后使用相同的解析过程去获取行列信息。这篇文章解决户外场景图像的表格解析问题(TSP:table structure parsing),局限在于此方法仅适用于有线表格,无线表角点定义的歧义性使得本文方法不一定work。从本文的思路我们或许可以探索角点法在处理复杂场景的情况下是否比anchor-based的方法表现更优。

图6

3. 序列预测方法:

3.1 Latex标签序列预测[10]

基于图像的表格分析优势在于,它对表格类型具有鲁棒性,并不要求格式是页面扫描图像还是纯数字文档,它可用于多数文档类型,包括PDF、HTML、PowerPoint格式等。然而,非结构化数字文档中的表格数据,由于其结构和样式的复杂性及多样性,很难解析为结构化的机器可读格式。

在实践中,手工标注用于训练的数据集的成本和不灵活性是实际部署深度学习模型的关键瓶颈。本文是微软的一篇文章,利用互联网中存在大量的Word和Latex源文档,对这些在线文档应用一些弱监督来标注表格,创建TableBank数据集。对于word文档,可以修改内部的office xml代码,指定每个表格的边界线;对于latex文档,可以修改tex代码,代码已识别表格的边界框。表格检测使用基于不同配置的Faster R-CNN的架构,表格结构识别模型基于image-to-text的编码器-解码器架构。

本文的局限在于版式多样对表格分析任务的准确率具有负面影响,模型泛化能力差,某一领域的模型应用到其他领域效果不好,在TableBank数据集上的建模和学习具有很大改进空间。

3.2 HTML标签序列预测[11]

类似地,IBM公司开发并发布了数据集PubTabNet,此数据集中自动为每个表图像加上关于表的结构和每个单元格内的文本信息(HTML格式),如图7(a)所示。

作者等人提出了一种端到端的表格识别方案,是一种基于注意力的编码器-解码器(EDD)架构,它是由编码器、结构解码器和单元格解码器组成,可以将表格图像转化成HTML代码。编码器获得表格图像的视觉特征,两个独立的结构解码器⼀个输出表格结构,⼀个输出单元格内容。

图7(b)所示为EDD架构,不需要复杂的后处理即可得到表格结果。但end2end的方案在中文场景的落地还有很长的路要走,另外缺少表格物理结构的信息,EDD方法的纠错空间不多。同时,本文的另一个贡献是提出了一种新的基于树编辑距离的图像表格识别评价指标TEDS,将表格建模为树形结构,该指标比之前的基于precision、reacll、F1 score的评价指标更为规范。

图7(a)

图7(b)

3.3 TableMaster解决方案[12]

平安产险提出的TableMaster方案是ICDAR21比赛Table Recognition赛道的亚军,本方案基于文本识别算法Master[13]。TableMASTER算法采用多任务学习的模式,同时进行表格结构序列预测以及单元格位置回归,最后通过后处理匹配算法,融合表格结构序列和单元格文本内容,得到HTML代码。整个解决方案可以分为4个部分:表格结构序列识别,表格文本行检测,表格文本行识别,以及单元格与表格结构序列匹配。

表格结构序列识别部分,使用改进的文本识别算法MASTER,它与原生的MASTER在结构上的区别如图8所示。他们的特征提取器在结构上是大体一致的,采用的都是改进后的ResNet网络,TableMaster在解码阶段,经过一个TransformerLayer后,会分为两条分支。一条分支进行表格结构序列的监督学习,另一条分支进行表格中单元位置回归的监督学习。

表格文本行检测部分,采用的是经典的文本检测算法PSENet[14],表格文本行识别部分,采用的文本识别算法MASTER。

在单元格与表格结构序列匹配部分,作者团队定义了三种匹配规则,分别是Center Point Rule,IOU Rule,以及Distance Rule。这三种匹配规则的优先级顺序为Center Point Rule>IOU Rule>Distance Rule。

序列预测的表格识别结果受限于序列长度和类别数量,同时transformer结构推理速度有优化空间。

图8

4. 图卷积神经网络方法:

4.1 Rethinking Table Recognition using Graph Neural Networks[15]

近年来,越来越多的学者将深度学习技术应用到图数据领域,本文作者将表格结构识别问题描述为一个与图神经网络兼容的图问题,利用图神经网络解决这一问题。将每一个文本区域作为一个顶点,使用矩阵描述的三个图(单元格、行共享矩阵、列共享矩阵)定义为真值,若顶点共享一行,则对应的文本区域属于同一行,这些顶点视作彼此相邻。

模型分为四个流程:特征提取、信息交互、随机采样和分类,过程如图9所示。利用CNN提取视觉特征,输出端将ocr得到的顶点位置映射到特征图上,将视觉特征与位置特征结合形成聚集特征。得到所有的顶点特征后进行特征交互融合,得到每个顶点的结构特征。在分类部分使用DenseNet分别对定点对进行是否同行、同列、同单元格的结构关系分类。

图9

4.2 GFTE: Graph-based Financial Table Extraction[16]

本文[16]主要是为了主要解决金融类的、中文表格识别问题,提出一种新的基于图卷积神经网络的模型GFTE作为表格识别数据集(本文自己发布的中文数据集FinTab)中的基线方法。GFTE通过融合图像特征、位置特征和文本特征,能够对表格进行精确的边缘检测,取得较好的效果。

GFTE可以分为以下几个步骤:
1、构建基本事实,包括表格区域的图像、文本内容、文本位置和结构标签
2、基于单元格构造一个无向图G=<V, R, C>
3、使用GFTE来预测相邻关系,包括垂直和水平关系。

作者将表格中的每个单元格视作节点,每个节点包含三种类型的信息:文本内容、绝对位置和图像,节点与它的邻域理解成边,那么表格结构完全可以用一个图来表示,图结构如图10所示。

图11是GFTE的基本结构,首先将绝对位置转换成相对位置,然后使用这些位置生成图。然后将纯文本嵌入预先定义好的特征空间,使用LSTM获取语义特征。接着将位置特征与文本特征进行拼接,将他们传入两层的图卷积网络(GCN)。最后,利用节点的相对位置,计算网格,利用网格中国输入的像素位置计算输出,得到某一节点在某一点的图像特征。获得这三种不同的特征后,将生成图的一条边上的两个节点配对,并与三种不同的特征集合在一起,使用MLP预测这两个节点处于同行还是同列。

图神经网络通过合理建模,将表格中的不同特征进行融合,并取得了不错的效果,这种表格结构识别方法,为未来的工作提供了一些可能的发展道路。

图10

图11

————

结语:

目前,表格识别任务逐渐受到工业界和学术界的关注。表格识别对象从PDF文档,到文档图片,再到自然场景图片,复杂场景表格识越来越被重视。随着人工智能应用的成熟,表格识别方法已从传统图像算法转向到深度学习算法。目标检测、语义分割、序列预测、图神经网络等是目前表格识别的主要技术方向,它们各有优劣。作为文档理解的重要一环,一套在复杂场景中鲁棒性很强的表格识别方案仍有待探索。

对于表格识别及相关内容提炼,合合信息认为传统的表格识别方法设计起来较为复杂,对于版面布局分析和表格结构的提取,图像处理的方法依赖各种阈值和参数的选择,难以满足现实生活中复杂多样的表格场景,鲁棒性也相对较差。目前业界常用的表格识别方法多为深度学习算法,比较依赖算法工程师对于神经网络的精心设计,可以不用依赖阈值和参数,鲁棒性较强。

从公司角度来讲,算法落地的关键因素大致可分为计算力、数据、算法模型、业务场景等。计算力是人工智能技术的生产力,数据的价值在于给予解决某一场景问题足够的特征,算法模型是项目落地的承载体,业务场景是落地的关键。在技术落地中,应当一切以业务场景为核心,以最少的数据,最简单的模型,最少的计算力解决最实际的问题,达到最好的效果。对于表格识别这一技术来讲,一款推理速度快且在少样本情况下表现优异的表格识别算法会备受青睐。


参考文献:

[1] OpenCV-检测并提取表格:https://blog.csdn.net/yomo127/article/details/52045146

[2] pdfplumber解析表格:pdfplumber是怎么做表格抽取的(三) – 知乎

[3] camelot表格抽取:camelot是怎么做表格抽取的(三)—— 非线框类表格抽取 – 知乎

[4] T-recs:T. Kieninger and A. Dengel, “The T-Recs table recognition and analysis system,” in Document Analysis Systems: Theory and Practice, pp. 255–270, 1999.

[5] Siddiqui S A , Khan P I , Dengel A , et al. Rethinking Semantic Segmentation for Table Structure Recognition in Documents[C]// 2019 International Conference on Document Analysis and Recognition (ICDAR). 2019.

[6] 走进AI时代的文档识别技术之表格图像识别 – 云+社区 – 腾讯云 (tencent.com)

[7] LGPMA: Complicated Table Structure Recognition with Local and Global Pyramid Mask Alignment (arxiv.org)

[8] Sun N , Zhu Y , Hu X . Faster R-CNN Based Table Detection Combining Corner Locating[C]// 2019 International Conference on Document Analysis and Recognition (ICDAR). 2019.

[9] Long R , Wang W , Xue N , et al. Parsing Table Structures in the Wild. 2021.

[10] Zhong X , Shafieibavani E , Yepes A J . Image-based table recognition: data, model, and evaluation[J]. 2019.

[11] Li M , Cui L , Huang S , et al. TableBank: Table Benchmark for Image-based Table Detection and Recognition[J]. 2019.

[12] Ye J , Qi X , He Y , et al. PingAn-VCGroup’s Solution for ICDAR 2021 Competition on Scientific Literature Parsing Task B: Table Recognition to HTML[J]. 2021.

[13] Lu N , Yu W , Qi X , et al. MASTER: Multi-Aspect Non-local Network for Scene Text Recognition[J]. 2019.

[14]Li X , Wang W , Hou W , et al. Shape Robust Text Detection with Progressive Scale Expansion Network[J]. 2018.

[15] Qasim S R , Mahmood H , F Shafait. Rethinking Table Recognition using Graph Neural Networks. 2019.

[16] Li Y , Huang Z , Yan J , et al. GFTE: Graph-based Financial Table Extraction[J]. 2020.
————————————————

常用的表格检测识别方法 - 表格区域检测方法(下) - 合合技术团队 - 博客园

mikel阅读(399)

来源: 常用的表格检测识别方法 – 表格区域检测方法(下) – 合合技术团队 – 博客园

Training

半监督网络的训练分两步进行:a)对标记数据独立训练学生模块,由教师模块生成伪标签;b)结合两个模块的训练,得到最终的预测结果。

 

伪标签框架

 

 

实验

 

数据集:

TableBank是文档分析领域中用于表识别问题的第二大数据集。该数据集有417,000个通过arXiv数据库爬虫过程注释。该数据集具有来自三类文档图像的表格:LaTeX图像(253,817)、Word图像(163,417),以及两者的组合(417,234)。它还包括一个用于识别表格的结构的数据集。在论文的实验中,只使用进行表检测的数据。

PubLayNet是一个大型公共数据集,训练集中有335,703张图像,验证集中有11,240张图像,测试集中有11,405张图像。它包括注释,如多边形分割和图形的边界框,列出标题、表格和来自研究论文和文章的图像文本。使用coco分析技术对该数据集进行了评估。在实验中,作者只使用了86,460个表注释中的102,514个。

DocBank是一个包含5000多个带注释的文档图像的大型数据集,旨在训练和评估诸如文本分类、实体识别和关系提取等任务。它包括标题、作者姓名、隶属关系、摘要、正文等方面的注释。

ICDAR-19:表检测和识别(cTDaR)竞赛于2019年由ICDAR组织。对于表格检测任务(TRACKA),在比赛中引入了两个新的数据集(现代和历史数据集)。为了与之前的最先进的方法进行直接比较,实验提供了在IoU阈值范围为0.5-0.9的现代数据集上的结果。

 

实验设置细节:

 

实验使用在ImageNet数据集上预先训练的ResNet-50为主干的可变形DETR作为检测框架,以评估半监督方法的有效性。在PubLayNet、ICDAR-19、DocBank和TableBank的三类数据集上进行训练。实验使用10%、30%和50%的标记数据,其余的作为未标记数据。伪标记的阈值设置为0.7。将所有实验的训练周期设置为150,在第120期的学习率降低了0.1倍。应用强增强作为水平翻转,调整大小,去除斑块,裁剪,灰度和高斯模糊。实验使用水平翻转来应用弱增强。可变形DETR解码器输入的query数的值N被设置为30,因为它能给出最好的结果。除非另有说明,实验都使用mAP(AP50:95)度量来评估结果。

实验结果讨论:

 

TableBank:

实验提供了对不同比例的标签数据的表库数据集的所有分割的实验结果。还比较了基于transformer的半监督方法与以前的基于深度学习的监督和半监督方法。此外,实验给出了10%标记数据的TableBank-both数据集在所有IoU阈值下的结果。表1提供了半监督方法在TableBank-latex, TableBank-word, 和TableBank-both数据集,分别10%、30%和50%标记数据时的实验结果。它表明,在10%标记数据时,TableBank-both数据集的AP50值最高,为95.8%,TableBank-latex为93.5%,TableBank-word有92.5%。

 

 

表格的半监督学习的定性分析如图5所示。图5的(b)部分有一个与行和列结构相似的矩阵,网络将该矩阵检测为一个表格,给出false positive检测结果。在这里,不正确的检测结果表明网络不能提供正确的表格区域检测。表2给出了这种半监督方法对10%标签数据上的所有数据集的不同IoU阈值的结果。在TableBank10%标记数据集上使用不同的ResNet-50骨干的半监督网络的准确率、召回率和f1-score的可视化比较如图6所示。

 

 

 

 

 

与以前的监督方法和半监督方法的比较

 

表3比较了ResNet-50主干上基于深度学习的监督网络和半监督网络。还将在10%、30%和50%TableBank-both数据集标签数据上训练的监督可变形DETR与使用可变形transformer的半监督方法进行了比较。结果表明,基于attention机制的半监督方法使用候选生成过程和后处理步骤,如非最大抑制(NMS),取得了可观的结果。

 

 

 

PubLayNet:

实验讨论了在PubLayNet表类数据集上对不同标记数据百分比的实验结果。还比较了基于transformer的半监督方法与以前的基于深度学习的监督和半监督方法。此外,实验给出了10%标记数据的PubLayNet数据集上的所有IoU阈值的结果。表4提供了半监督方法的结果,该方法对PubLayNet表类数据使用可变形transformer来处理标记数据的不同百分比。在这里,10%、30%和50%的标记数据的AP50值分别为98.5%、98.8%和98.8%

 

 

 

此外,半监督网络在10%的标记的PubLayNet数据集上,在不同的IoU阈值上进行训练。表5给出了半监督方法对10%标记数据上的PubLayNet表类的不同IoU阈值的结果。在PubLayNet表类的10%标记数据集上,在不同的IoU阈值上使用具有ResNet-50主干的可变形transformer网络的半监督网络的准确率、召回率和f1-score的可视化比较如图6(b)所示。这里,蓝色表示不同IoU阈值的准确率结果,红色表示不同IoU阈值的召回结果,绿色表示对不同IoU阈值的f1-score结果。

 

 

 

与以前的监督方法和半监督方法的比较

 

表6比较了使用ResNet-50骨干网的PubLayNet表类上基于深度学习的监督网络和半监督网络。还比较了在10%、30%和50%的PubLayNet表类标签数据上训练的有监督的可变形detr与使用可变形transformer的半监督方法。它表明,半监督方法不使用候选和后处理步骤,如非最大抑制(NMS),提供了有竞争力的结果。

 

 

 

 

DocBank:

 

实验讨论了在DocBank数据集上的不同标签百分比数据的实验结果。在表7中比较了基于transformer的半监督方法与以前的基于cnn的半监督方法。

 

 

 

此外,还比较了表8中对不同比例的标记数据的半监督方法与之前针对不同数据集的表格检测和文档分析方法。虽然不能直接比较作者的半监督方法与以前的监督文档分析方法。然而,可以观察到,即使有50%的标签数据,作者也获得了与以前的监督方法类似的结果。

 

ICDAR-19:

实验还评估了在Modern Track A数据集上的表格检测方法。作者总结了该方法在不同百分比的标签数据下的定量结果,并将其与表9中以前的监督表格检测方法进行了比较。在更高的IoU阈值0.8和0.9下评估结果。为了与以前的表格检测方法进行直接比较,作者还在100%的标签数据上评估了论文的方法。论文方法在100%标签数据的IoU阈值上获得了92.6%的准确率和91.3%的召回率。

 

 

 

 

消融实验:

 

伪标记置信阈值

 

阈值(称为置信阈值)在决定生成的伪标签的准确性和数量之间的平衡方面起着重要的作用。随着这个阈值的增加,通过过滤器的样本将会更少,但它们的质量将会更高。相反,较小的阈值将导致更多的样本通过,但false positive的可能性更高。从0.5到0.9的各种阈值的影响如表10所示。根据计算结果,确定最优阈值为0.7。

 

 

可学习query数量的影响

 

在分析中,作者研究了改变作为可变形DETR解码器中输入的query数量的影响。图7通过改变作为可变形DETR解码器中输入的对象query的数量来比较预测结果。当query数N设置为30时,达到最佳性能;偏离此值会导致性能下降。表11显示并分析了不同对象query数量的结果。为N选择一个较小的值可能会导致模型无法识别特定的对象,从而对其性能产生负面影响。另一方面,选择一个较大的N值可能会导致模型由于过拟合而表现不佳,因为它会错误地将某些区域分类为对象。此外,在师生模块中,该半监督自注意机制的训练复杂度依赖于对象query的数量,并通过最小化对象query的数量来降低复杂度而得到提高。

 

 

 

结论

 

本文介绍了一种利用可变形transformer对文档图像进行表格检测的半监督方法。该方法通过将伪标签生成框架集成到一个简化的机制中,减轻了对大规模注释数据的需要,并简化了该过程。同时生成伪标签产生了一个被称为“飞轮效应”的动态过程,随着训练的进行,一个模型不断改进另一个模型产生的伪边框。在该框架中,使用两个不同的模块学生和教师,对伪类标签和伪边界框进行了改进。这些模块通过EMA功能相互更新,以提供精确的分类和边界框预测。结果表明,当应用于TableBank和PubLayNet训练数据的10%、30%和50%时,该方法的性能超过了监督模型的性能。此外,当对PubLayNet的10%标记数据进行训练时,该模型的性能与当前基于cnn的半监督基线相比较。在未来,作者的目标是研究标记数据的比例对最终性能的影响,并开发出以最小数量的标记数据有效运行的模型。此外,作者还打算采用基于transformer的半监督学习机制来进行表结构识别任务。

 

 

 

 

参考文献:

Gao L C, Li Y B, Du L, Zhang X P, Zhu Z Y, Lu N, Jin L W, Huang Y S, Tang Z . 2022.A survey on table recognition technology. Journal of Image and Graphics, 27(6): 1898-1917.

M Kasem , A Abdallah, A Berendeyev,E Elkady , M Abdalla, M Mahmouda, M Hamada, D Nurseitovd, I Taj-Eddin.Deep learning for table detection and structure recognition: A survey.arXiv:2211.08469v1 [cs.CV] 15 Nov 2022

S A Siddiqui , M I Malik,S Agne , A Dengel and S Ahmed. DeCNT: Deep Deformable CNN for Table Detection. in IEEE Access, vol.6, pp.74151-74161, [DOI: 10.1109/ACCESS.2018.2880211]

T Shehzadi, K A Hashmi, D Stricker, M Liwicki , and M Z Afzal.Towards End-to-End Semi-Supervised Table Detection with Deformable Transformer.arXiv:2305.02769v2 [cs.CV] 7 May 2023

常用的表格检测识别方法——表格结构识别方法(上) - 合合技术团队 - 博客园

mikel阅读(433)

来源: 常用的表格检测识别方法——表格结构识别方法(上) – 合合技术团队 – 博客园

3.2表格结构识别方法

 

表格结构识别是表格区域检测之后的任务,其目标是识别出表格的布局结构、层次结构等,将表格视觉信息转换成可重建表格的结构描述信息。这些表格结构描述信息包括:单元格的具体位置、单元格之间的关系、单元格的行列位置等。

 

在当前的研究中,表格结构信息主要包括以下两类描述形式:1)单元格的列表(包含每个单元格的位置、单元格 的行列信息、单元格的内容);2)HTML代码或Latex代码(包含单元格的位置信息,有些也会包含单元格的内容)。

 

与表格区域检测任务类似,在早期的表格结构识别方法中,研究者们通常会根据数据集特点,设计启发式算法或者使用机器学习方法来完成表格结构识别任务。

 

Itonori(1993)根据表格中单元格的二维布局的 规律性,使用连通体分析抽取其中的文本块,然后 对每个文本块进行扩展对齐形成单元格,从而得到 每个单元格的物理坐标和行列位置。

 

Rahgozar等人 (1994)则根据行列来进行表格结构的识别,其先 识别出图片中的文本块,然后按照文本块的位置以及两个单元格中间的空白区域做行的聚类和列的聚类,之后通过行和列的交叉得到每个单元格的位 置和表格的结构。

 

Hirayama等人(1995)则从表格线出发,通过平行、垂直等几何分析得到表格的行和列,并使用动态规划匹配的方法对各个内容块进 行逻辑关系识别,来恢复表格的结构。

 

Zuyev(1997) 使用视觉特征进行表格的识别,使用行线和列线以及空白区域进行单元格分割。该算法已经应用到FineReader OCR产品之中。

 

Kieninger等人(1998) 提出了T-Recs(Table RECognition System)系统,以 词语区域的框作为输入,并通过聚类和列分解等启 发式方法,输出各个文本框对应的信息,恢复表格 的结构。随后,其又在此基础上提出了T-Recs++系 统(Kieninger等,2001),进一步提升了识别效果。

 

Amano等人(2001)创新性地引入了文本的语义信息,首先将文档分解为一组框,并将它们半自动地 分为四种类型:空白、插入、指示和解释。然后根据 文档结构语法中定义的语义和几何知识,分析表示 框与其关联条目之间的框关系。

 

Wang等人(2004) 将表格结构定义为一棵树,提出了一种基于优化方 法设计的表结构理解算法。该算法通过对训练集中 的几何分布进行学习来优化参数,得到表格的结构。 同样使用树结构定义表格结构的还有Ishitani等人 (2005),其使用了DOM(Document Object Model) 树来表示表格,从表格的输入图像中提取单元格特 征。然后对每个单元格进行分类,识别出不规则的 表格,并对其进行修改以形成规则的单元格排布。

 

Hassan(2007)、Shigarov(2016)等人则以PDF文档为表格识别的载体,从PDF文档中反解出表格视 觉信息。后者还提出了一种可配置的启发式方法框架。

 

国内的表格结构识别研究起步较晚,因此传统的启发式方法和机器学习方法较少。

 

在早期,Liu等 人(1995)提出了表格框线模板方法,使用表格的 框架线构成框架模板,可以从拓扑上或几何上反映 表格的结构。然后提出相应的项遍历算法来定位和 标记表格中的项。之后Li等人(2012)使用OCR引擎抽取表单中的文本内容和文本位置,使用关键词 来定位表头,然后将表头信息和表的投影信息结合 起来,得到列分隔符和行分隔符来得到表格结构。

 

总体来说,表格结构识别的传统方法可以归纳为以下四种:基于行和列的分割与后处理,基于文本的检测、扩展与后处理,基于文本块的分类和后处理,以及几类方法的融合。

 

随着神经网络的兴起,研究人员开始将它们应用于文档布局分析任务中。后来,随着更复杂的架构的发展,更多的工作被放到表列和整体结构识别中。

 

A Zucker提出了一种有效的方法CluSTi,是一种用于识别发票扫描图像中的表格结构的聚类方法。CluSTi有三个贡献。首先,它使用了一种聚类方法来消除表格图片中的高噪声。其次,它使用最先进的文本识别技术来提取所有的文本框。最后,CluSTi使用具有最优参数的水平和垂直聚类技术将文本框组织成正确的行和列。Z Zhang提出的分割、嵌入和合并(SEM)是一个准确的表结构识别器。M Namysl提出了一种通用的、模块化的表提取方法。

 

E Koci 提出了一种新的方法来识别电子表格中的表格,并在确定每个单元格的布局角色后构建布局区域。他们使用图形模型表示这些区域之间的空间相互关系。在此基础上,他们提出了删除和填充算法(RAC),这是一种基于一组精心选择的标准的表识别算法。

 

SA Siddiqui利用可变形卷积网络的潜力,提出了一种独特的方法来分析文档图片中的表格模式。P Riba提出了一种基于图的识别文档图片中的表格结构的技术。该方法也使用位置、上下文和内容类型,而不是原始内容(可识别的文本),因此它只是一种结构性感知技术,不依赖于语言或文本阅读的质量。E Koci使用基于遗传的技术进行图划分,以识别与电子表中的表格匹配的图的部分。

 

SA Siddiqui将结构识别问题描述为语义分割问题。为了分割行和列,作者采用了完全卷积网络。假设表结构的一致性的情况下,该方法引入了预测拼接方法,降低了表格结构识别的复杂性。作者从ImageNet导入预先训练的模型,并使用FCN编码器和解码器的结构模型。当给定图像时,模型创建与原始输入图像大小相同的特征。

 

SA Khan提出了一个鲁棒的基于深度学习的解决方案,用于从文档图片中已识别的表格中提取行和列。表格图片经过预处理,然后使用门控递归单元(GRU)和具有softmax激活的全连接层发送到双向递归神经网络。SF Rashid提供了一种新的基于学习的方法来识别不同文档图片中的表格内容。SR Qasim提出了一种基于图网络的表识别架构,作为典型神经网络的替代方案。S Raja提出了一种识别表格结构的方法,该方法结合了单元格检测和交互模块来定位单元格,并根据行和列预测它们与其他检测到的单元格的关系。此外,增加了结构限制的损失功能的单元格识别作为额外的差异组件。Y Deng 测试了现有的端到端表识别的问题,他还强调了在这一领域需要一个更大的数据集。

 

Y Zou的另一项研究呼吁开发一种利用全卷积网络的基于图像的表格结构识别技术。所示的工作将表格的行、列和单元格划分。所有表格组件的估计边界都通过连接组件分析进行了增强。根据行和列分隔符的位置,然后为每个单元格分配行和列号。此外,还利用特殊的算法优化单元格边界。

 

为了识别表中的行和列,KA Hashmi [118]提出了一种表结构识别的引导技术。根据本研究,通过使用锚点优化方法,可以更好地实现行和列的定位。在他们提出的工作中,使用掩模R-CNN和优化的锚点来检测行和列的边界。

 

另一项分割表格结构的努力是由W Xue撰写的ReS2TIM论文,它提出了从表格中对句法结构的重建。回归每个单元格的坐标是这个模型的主要目标。最初使用该新技术构建了一个可以识别表格中每个单元格的邻居的网络。本研究给出了一个基于距离的加权系统,这将有助于网络克服与训练相关的类不平衡问题。

 

C Tensmeyer提出了SPLERGE(Split and Merge),另一种使用扩展卷积的方法。他们的策略需要使用两种不同的深度学习模型,第一个模型建立了表的网格状布局,第二个模型决定了是否可能在许多行或列上进行进一步的单元格跨度。

 

Nassar为表格结构提供了一个新的识别模型。在两个重要方面增强了PubTabNet端到端深度学习模型中最新的encoder-dual-decoder。首先,作者提供了一种全新的表格单元目标检测解码器。这使得它们可以轻松地访问编程pdf中的表格单元格的内容,而不必训练任何专有的OCR解码器。作者称,这种体系结构的改进使表格内容的提取更加精确,并使它们能够使用非英语表。第二,基于transformer的解码器取代了LSTM解码器。

 

S Raja提出了一种新的基于目标检测的深度模型,它被定制用于快速优化并捕获表格内单元格的自然对齐。即使使用精确的单元格检测,密集的表格识别也可能仍然存在问题,因为多行/列跨越单元格使得捕获远程行/列关系变得困难。因此,作者也寻求通过确定一个独特的直线的基于图的公式来增强结构识别。作者从语义的角度强调了表格中空单元格的相关性。作者建议修改一个很受欢迎的评估标准,以考虑到这些单元格。为了促进这个问题的新观点,然后提供一个中等大的进行了人类认知注释后的评估数据集。

X Shen提出了两个模块,分别称为行聚合(RA)和列聚合(CA)。首先,作者应用了特征切片和平铺,对行和列进行粗略的预测,并解决高容错性的问题。其次,计算信道的attention map,进一步获得行和列信息。为了完成行分割和列分割,作者利用RA和CA构建了一个语义分割网络,称为行和列聚合网络(RCANet)。

 

C Ma提出了一种识别表格的结构并从各种不同的文档图片中检测其边界的新方法。作者建议使用CornerNet作为一种新的区域候选网络,为fasterR-CNN生成更高质量的候选表格,这大大提高了更快的R-CNN对表格识别的定位精度。该方法只利用最小的ResNet-18骨干网络。此外,作者提出了一种全新的split-and-merge方法来识别表格结构。该方法利用一种新的spatial CNN分离线预测模块将每个检测表格划分为一个单元网格,然后使用一个GridCNN单元合并模块来恢复生成单元格。它们的表格结构识别器可以准确地识别具有显著空白区域的表格和几何变形(甚至是弯曲的)表格,因为spatial CNN模块可以有效地向整个表图片传输上下文信息。B Xiao假设一个复杂的表格结构可以用一个图来表示,其中顶点和边代表单个单元格以及它们之间的连接。然后,作者设计了一个conditional attention网络,并将表格结构识别问题描述为一个单元格关联分类问题(CATT-Net)。

 

Jain建议训练一个深度网络来识别表格图片中包含的各种字符对之间的空间关系,以破译表格的结构。作者提供了一个名为TSR-DSAW的端到端pipeline:TSR,通过深度空间的字符联系,它以像HTML这样的结构化格式生成表格图片的数字表示。该技术首先利用文本检测网络,如CRAFT,来识别输入表图片中的每个字符。接下来,使用动态规划,创建字符配对。这些字符配对在每个单独的图像中加下划线,然后交给DenseNet-121分类器,该分类器被训练来识别同行、同列、同单元格或无单元格等空间相关性。最后,作者将后处理应用于分类器的输出,以生成HTML表格结构。

 

H Li将这个问题表述为一个单元格关系提取的挑战,并提供了T2,一种前沿的两阶段方法,成功地从数字保存的文本中提取表格结构。T2提供了一个广泛的概念,即基本连接,准确地代表了单元格之间的直接关系。为了找到复杂的表格结构,它还构建了一个对齐图,并使用了一个消息传递网络。

实际场景应用中的表格结构识别,不仅要同时完成表格检测和结构识别,还要对每个单元格的文本进行识别和信息抽取,其流程比以上的研究领域都更为复杂。

 

 

 

 

 

 

参考文献:

Gao L C, Li Y B, Du L, Zhang X P, Zhu Z Y, Lu N, Jin L W, Huang Y S, Tang Z . 2022.A survey on table recognition technology. Journal of Image and Graphics, 27(6): 1898-1917.

M Kasem , A Abdallah, A Berendeyev,E Elkady , M Abdalla, M Mahmouda, M Hamada, D Nurseitovd, I Taj-Eddin.Deep learning for table detection and structure recognition: A survey.arXiv:2211.08469v1 [cs.CV] 15 Nov 2022

S A Siddiqui , M I Malik,S Agne , A Dengel and S Ahmed. DeCNT: Deep Deformable CNN for Table Detection. in IEEE Access, vol.6, pp.74151-74161, [DOI: 10.1109/ACCESS.2018.2880211]

T Shehzadi, K A Hashmi, D Stricker, M Liwicki , and M Z Afzal.Towards End-to-End Semi-Supervised Table Detection with Deformable Transformer.arXiv:2305.02769v2 [cs.CV] 7 May 2023

架构与思维:秒杀和竞拍的业务架构,永不过时的话题 - Hello-Brand - 博客园

mikel阅读(368)

来源: 架构与思维:秒杀和竞拍的业务架构,永不过时的话题 – Hello-Brand – 博客园

1 互联网架构越来越复杂?

为啥感觉互联网架构越来越复杂了,早期我们的系统,可能也就那么少部分人使用,大都是一些后台管理系统。
所以不用考虑很多东西,比如:

  • 流量少,无需考虑并发问题
  • 数据少,不用考虑什么索引优化、分库分表
  • 访问不集中,不用考虑缓存、过载保护
  • 如果数据不重要,不用考虑安全策略,甚至不用考虑容灾备份
  • 可重复提交,所以不用关系幂等性
  • 允许短暂宕机和定期关停维护,所以不用考虑多活架构

但是随着互联网的普及和用户的激增,为了应对流量增量带来的各种问题,我们的架构体系衍生出很多强大的技术方案。

2 什么是秒杀/竞拍业务

秒杀业务也是随着互联网电商的发展而不断普及的,我们来看看普通业务和秒杀业务的区别

2.1 普通的业务

  1. 微信的个人信息:个人的注册信息,公众号、视频号的基础信息,微信好友列表,微信群列表。这种是 1:1 的,一般也不会被别人看到。
  2. 微信朋友圈:你盆友圈公开的内容是可以被多个好友看到的,你也可以对应看到你多个好友的盆友圈。这种是 1:n 的,多读的一种场景。

2.2 秒杀/竞拍业务

只有少量的数据,却会在集中的时间段被一批人看到和抢购,集中式的高频读写。
业内也称为 群蜂请求 ,你可以想象下你捅了马蜂窝的场景。哈哈哈

典型秒杀/竞拍业务案例:

  1. 春运前的火车票开售那一刻,可能瞬间有千万级请求涌入
  2. 将来某个遥遥领先开售,可能是一秒售罄

这些业务场景有如下技术难点:

  1. 瞬时流量特别大,你的接入层、应用层、数据层等能否扛得住
  2. 大量流量涌入 对一个数据进行操作,怎么保证数据原子增减、顺序公平性,怎么保证数据不超卖
  3. 如何 保证数据安全,如防攻击、防刷数、保持幂等
  4. 如果使用 并发控制,如何保证不产生死锁

所以,一个优秀的秒杀业务架构,在现在的互联网业务中,是一个永不过时的话题

3 如何优化

这边只针对几个对秒杀业务有效改进的点做展开,什么集群动态扩容、流量控制、弹性伸缩、智能限流啊,可以参考我的这篇文章《千万级流量冲击下,如何保证极致性能》。

3.1 清除无效请求

尽量在前面就把一些无效请求给清理掉,所以这些操作Web前端 或者 App Client端做就行了,越前端越好,尽量不要伤害到服务端,比如:

  • 未登录拦截
  • 重复提交拦截(未响应则按钮置灰,直至响应或者5S超时才恢复,幂等保证)
  • 频繁提交拦截(单用户一分钟不超过100次,避免AI刷机)
  • 验证码拦截(避免AI刷数据、黑客攻击等)
  • 参与条件拦截(可提前加载名单):如用户等级不够、注册未满3个月、用户进入黑名单等

image

3.2 服务端+缓存层做高效原子操作

公共数据做缓存
缓存是提升系统性能的重要手段。通过缓存热点数据,缓存还可以提高数据的访问速度,见很少对数据库的访问速度,提升用户体验。Redis单机每秒10w没什么问题,再加上多集群多副本模式。

原子操作保证秒杀的计数
在Redis中,高效地进行原子计数通常使用INCRINCRBYDECRDECRBY等命令。这些命令都是原子操作,意味着在执行时不会被其他Redis命令打断,从而保证了计数的准确性和一致性。

# 计算已售卖1000台库里南
> INCRBY cullinan_counter 1000

# 获取当前售卖数量
> GET cullinan_counter
> 1000

# 超过1000,返回秒杀失败

队列保证请求有序进入
使用Redis的 Stream 队列功能。Stream 实际上是一个 key,你可以使用 XADD 命令向其中添加消息。

XADD mystream * field1 value1 field2 value2

这里 mystream 是 Stream 的名称,* 表示让 Redis 自动生成一个唯一的消息 ID。field1 value1 和 field2 value2 是消息的内容,你可以根据需要添加任意数量的字段。
如果你只有1000台库里南供抢购,那么第1001就不要进入队列了。

扩展阅读
缓存可以扩展阅读作者的这个系列的文章:★ Redis24篇集合

image

3.3 数据层做终兜底

经过上面的保证之后,到数据层的量就很少了,大概率就是你定额的商品数量同等的数量。
比如1000,数据库绝对的扛得住的。
唯一可以做的就是检查数量是否符合预期,这个可以创建约束或者触发器来实现。

image

3.4 全球式业务,单元化处理

有些人可能会说,我的商品全球售卖,那我的缓存中心、数据中心放哪里,如果放中国,那跨地域跨机房访问,在0.1微妙都能决定我是不是买得到,欧洲的客户铁定抢不到库里南了。
现在的做法一般是单元化隔离,比如:

image

A/B中心都有这样的缓存或者数据结构,配置中心统一下发配置。然后在各自的单元里面玩耍,互不干预。 秒杀业务千万不要想着跨地域+跨机房,用户存在不公平性。

4 写在最后

  1. 无效请求拦截,尽量在前端完成,避免走入后端,造成服务端压力
  2. 缓存支持高性能检索、原子计算和有序队列
  3. 数据层做存储兜底
  4. 分治原理:单元化隔离,避免集中处理

C# 开发技巧 轻松监控方法执行耗时 - 小码编匠 - 博客园

mikel阅读(356)

来源: C# 开发技巧 轻松监控方法执行耗时 – 小码编匠 – 博客园

MethodTimer.Fody 是一个功能强大的库,可以用于测量 .NET 应用程序中的方法的执行时间。允许你在不修改代码的情况下,自动地测量和记录方法的执行时间。

这个工具是基于.NET的 weaving 技术,通过修改IL(Intermediate Language,中间语言)代码来插入计时逻辑,从而在方法调用前后记录时间戳,进而计算出方法的执行时间。

它使用 Fody 插件框架可以无缝集成到项目中,所以向代码中添加性能测量功能变得非常容易。

使用方法

1、安装NuGet包

在Visual Studio中,打开NuGet包管理器,搜索并安装MethodTimer.Fody或者使用命令方式

PM> Install-Package Fody
PM> Install-Package MethodTimer.Fody

具体操作如下图所示:

2、使用 Time 特性

复制代码
using MethodTimer;

namespace DemoConsole
{
    internal class Program
    {
        /// <summary>
        /// 程序入口
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // 调用示例方法
            new Program().DoSomething();

            Console.WriteLine("测试方法执行结束!!!");

            Console.ReadKey();
        }

        /// <summary>
        /// 示例方法
        /// </summary>
        [Time]
        public void DoSomething()
        {
            Console.WriteLine("测试方法执行时间!!!");
        }
    }
}
复制代码

Fody是一个.NET的weaving框架,需要确保项目已经启用了Fody,并且在项目属性的”Fody”标签页中添加了MethodTimer模块。

3、执行效果

启动运行程序,可以在输出窗口查看方法的执行耗时,具体如下图所示:

4、其他说明

Time 特性不仅可以加在方法上还可以直接添加到 Class 上,具体如下代码所示:

复制代码
using MethodTimer;

namespace ConsoleApp3
{
    [Time]
    internal class Program
    {
        /// <summary>
        /// 程序入口
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // 调用示例方法
            new Program().DoSomething();

            new Program().ToDoSomething();

            Console.WriteLine("方法执行结束!!!");

            Console.ReadKey();
        }

        /// <summary>
        /// 示例方法1
        /// </summary>
      
        public void DoSomething()
        {
            Console.WriteLine("001——测试执行时间方法!!!");
           
        }
        /// <summary>
        /// 示例方法2
        /// </summary>

        public void ToDoSomething()
        {
            Console.WriteLine("002——测试执行时间方法!!!");

        }
    }
}
复制代码

运行程序后,可以输出类中每个方法的执行时间。

实际上,在代码中添加了 Time 特性以后,Fody 会自动生成下面的代码

复制代码
 public class MyClass
 {
        [Time]
        public void DoSomething()
        {
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            // 原始方法体
            System.Threading.Thread.Sleep(1000); // 模拟工作

            stopwatch.Stop();

            // 输出或记录执行时间
            Console.WriteLine($"执行时间:{stopwatch.Elapsed.TotalMilliseconds} ms");
        }
 }
复制代码

5、拦截记录

如果想手动处理日志记录,可以定义一个静态类来拦截日志记录,方法的示例,具体如下代码所示

复制代码
public static class MethodTimeLogger
{
    public static void Log(MethodBase methodBase, TimeSpan elapsed, string message)
    {
        //Do some logging here
    }
}
复制代码

生成后的代码

复制代码
public class MyClass
{
    public void MyMethod()
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            Console.WriteLine("Hello");
        }
        finally
        {
            stopwatch.Stop();
            MethodTimeLogger.Log(methodof(MyClass.MyMethod), stopwatch.Elapsed);
        }
    }
}
复制代码

MethodTimer.Fody是一个非常有用的工具,尤其在性能调优阶段,可以帮助你快速识别出哪些方法是性能瓶颈,从而针对性地进行优化。

主要特点

1、非侵入式

MethodTimer.Fody不需要在源代码中添加额外的计时代码,只需要在项目中添加相应的NuGet包,并在项目属性中做一些配置,就可以自动地为方法添加计时功能。

2、灵活的配置

你可以选择性地对某些方法进行计时,或者排除不想被计时的方法。这通常通过方法的特性或者类的命名空间来进行配置。

3、输出结果多样化

MethodTimer.Fody可以将计时结果输出到不同的地方,如控制台、日志文件或者通过事件追踪(ETW)等方式,这取决于你的配置。

4、性能影响小

尽管MethodTimer.Fody在方法中插入了计时逻辑,但它被设计得尽可能地对性能影响最小,通过精心优化的IL代码插入策略来实现这一点。

总结

MethodTimer.Fody 是一个强大的工具,提供了简便的方式来监控 C# 方法的执行时间,特别适用于需要快速诊断性能问题的场合。

通过其灵活的配置和非侵入性的特性,它可以无缝地融入现有的开发流程中,帮助我们团队提高应用的性能和响应速度。

这个工具特别适合在开发和测试阶段快速识别性能瓶颈,而无需在代码中显式地添加计时代码,可以保持源代码的整齐性和可维护性。

开源地址

https://github.com/Fody/MethodTimer

Asp .Net Core 系列:基于 T4 模板生成代码 - Code技术分享 - 博客园

mikel阅读(423)

来源: Asp .Net Core 系列:基于 T4 模板生成代码 – Code技术分享 – 博客园

T4模板,即Text Template Transformation Toolkit,是微软官方在Visual Studio中引入的一种代码生成引擎。自Visual Studio 2008开始,T4模板就被广泛应用于生成各种类型的文本文件,包括网页、资源文件以及各种编程语言的源代码等。

T4模板是一种由文本块和控制逻辑组成的混合模板,它可以根据预设的规则和输入数据生成目标文本文件。

官网:https://learn.microsoft.com/zh-cn/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2022

组成部分

T4模板主要由以下几部分组成:

  1. 指令块:向文本模板化引擎提供关于如何生成转换代码和输出文件的一般指令。常见的指令包括<#@ template #><#@ parameter #><#@ assembly #><#@ import #><#@ include #><#@ output #>等。
    • 模板指令<#@ template #>):定义模板的基本属性,如使用的编程语言、是否开启调试模式等。
    • 参数指令<#@ parameter #>):声明模板代码中从外部上下文传入的值初始化的属性。
    • 程序集指令<#@ assembly #>):引用外部程序集,以便在模板中使用其中的类型和方法。
    • 导入指令<#@ import #>):允许在模板中引用其他命名空间中的类型,类似于C#中的using指令或Visual Basic中的Imports指令。
    • 包含指令<#@ include #>):在模板中包含另一个文件的内容,通常用于共享常用的代码片段或模板设置。
    • 输出指令<#@ output #>):定义输出文件的扩展名和编码方式。
  2. 文本块:直接复制到输出文件的内容,不会进行任何处理或转换。
  3. 代码语句块(Statement Block)

    代码语句块通过<#Statement#>的形式表示,中间是一段通过相应编程语言编写的程序调用,我们可以通过代码语句快控制文本转化的流程。在上面的代码中,我们通过代码语句块实现对一个数组进行遍历,输出重复的Console.WriteLine("Hello {0},Welcome to T4 World!","<#= p.Name #>");语句。

  4. 表达式块(Expression Block)

    表达式块以<#=Expression#>的形式表示,通过它之际上动态的解析的字符串表达内嵌到输出的文本中。比如在上面的foreach循环中,每次迭代输出的人名就是通过表达式块的形式定义的(<#= p.Name #>

  5. 类特性块(Class Feature Block)

    如果文本转化需要一些比较复杂的逻辑,我们需要写在一个单独的辅助方法中,甚至是定义一些单独的类,我们就是将它们定义在类特性块中。类特性块的表现形式为<#+ FeatureCode #>

分类

  1. 设计时模板(文本模版)

    在 Visual Studio 中执行设计时 T4 文本模板,以便定义应用程序的部分源代码和其他资源。通常,您可以使用读取单个输入文件或数据库中的数据的多个模板,并生成一些 .cs、.vb 或其他源文件。每个模板都生成一个文件。 在 Visual Studio 或 MSBuild 内执行它们。若要创建设计时模板,请向您的项目中添加“文本模板”文件。 另外,您还可以添加纯文本文件并将其“自定义工具”属性设置为“TextTemplatingFileGenerator”。

  2. 运行时模板(预处理模板)

    可在应用程序中执行运行时 T4 文本模板(“预处理过的”模板)以便生成文本字符串(通常作为其输出的一部分)。若要创建运行时模板,请向您的项目中添加“已预处理的文本模板”文件。另外,您还可以添加纯文本文件并将其“自定义工具”属性设置为“TextTemplatingFilePreprocessor”。

Visual Studio 中使用T4模板

1.创建T4模板文件

  1. 新建文件:在Visual Studio中,你可以通过右键点击项目,选择“添加” -> “新建项…”,然后在搜索框中输入“T4”或“Text Template”来找到T4模板文件模板(通常称为“文本模板”)。选择它并命名你的模板文件(例如:MyTemplate.tt)。

image

  1. 编辑模板:双击新创建的.tt文件以在Visual Studio中打开它。此时,你可以看到模板的初始内容,包括一些基本的指令和控制块。

2. 编写T4模板

在T4模板中,你可以使用C#或VB.NET代码(取决于你的项目设置)来编写控制逻辑,并使用特定的语法来定义输出文本的格式。

  • 指令块:如前所述,使用指令块来定义模板的行为和引入必要的资源。
  • 控制块:使用<# ... #>来包围代码块,这些代码块在模板转换时执行。
  • 表达式块:使用<#= ... #>来输出表达式的值到生成的文本中。
  • 类特征块:使用<#+ ... #>来定义辅助方法、属性或类,这些方法可以在模板的其他部分中被调用。
<#@ template debug="false" hostspecific="false" language="C#" #>  
<#@ output extension=".cs" #>
using System;  
  
namespace MyNamespace  
{  
    public class MyClass  
    {  
        public string MyProperty { get; set; }  
  
        public void MyMethod()  
        {  
            Console.WriteLine("Hello from T4 Template!");  
        }  
    }  
}

3. 转换模板

  • 自动转换:在Visual Studio中,通常当你保存T4模板文件时,Visual Studio会自动执行模板转换并生成输出文件。
  • 手动转换:你也可以通过右键点击模板文件并选择“运行自定义工具”来手动触发模板的转换。

中心控制Manager

上面T4模板的简单内容。可以生成模板,但是只能保存在t4模板的目录下方,无法进行更多操作。假如是项目集,还需要手动赋值粘贴很麻烦,基于Manage类进行块控制和保存文件到指定位置

<#@ assembly name="System.Core"#>
<#@ assembly name="EnvDTE"#>
<#@ import namespace="System.Collections.Generic"#>
<#@ import namespace="System.IO"#>
<#@ import namespace="System.Text"#>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating"#>
<#@ output extension=".cs" #>
<#+
class Manager
{
    public struct Block {
        public int Start, Length;
		public String Name,OutputPath;
    }

    public List<Block> blocks = new List<Block>();
    public Block currentBlock;
    public Block footerBlock = new Block();
    public Block headerBlock = new Block();
    public ITextTemplatingEngineHost host;
    public ManagementStrategy strategy;
    public StringBuilder template;
    public Manager(ITextTemplatingEngineHost host, StringBuilder template, bool commonHeader) {
        this.host = host;
        this.template = template;
        strategy = ManagementStrategy.Create(host);
    }
    public void StartBlock(String name,String outputPath) {
        currentBlock = new Block { Name = name, Start = template.Length ,OutputPath=outputPath};
    }

    public void StartFooter() {
        footerBlock.Start = template.Length;
    }

    public void EndFooter() {
        footerBlock.Length = template.Length - footerBlock.Start;
    }

    public void StartHeader() {
        headerBlock.Start = template.Length;
    }

    public void EndHeader() {
        headerBlock.Length = template.Length - headerBlock.Start;
    }    

    public void EndBlock() {
        currentBlock.Length = template.Length - currentBlock.Start;
        blocks.Add(currentBlock);
    }
    public void Process(bool split) {
        String header = template.ToString(headerBlock.Start, headerBlock.Length);
        String footer = template.ToString(footerBlock.Start, footerBlock.Length);
        blocks.Reverse();
        foreach(Block block in blocks) {
            String fileName = Path.Combine(block.OutputPath, block.Name);
            if (split) {
                String content = header + template.ToString(block.Start, block.Length) + footer;
                strategy.CreateFile(fileName, content);
                template.Remove(block.Start, block.Length);
            } else {
                strategy.DeleteFile(fileName);
            }
        }
    }
}
class ManagementStrategy
{
    internal static ManagementStrategy Create(ITextTemplatingEngineHost host) {
        return (host is IServiceProvider) ? new VSManagementStrategy(host) : new ManagementStrategy(host);
    }

    internal ManagementStrategy(ITextTemplatingEngineHost host) { }

    internal virtual void CreateFile(String fileName, String content) {
        File.WriteAllText(fileName, content);
    }

    internal virtual void DeleteFile(String fileName) {
        if (File.Exists(fileName))
            File.Delete(fileName);
    }
}

class VSManagementStrategy : ManagementStrategy
{
    private EnvDTE.ProjectItem templateProjectItem;

    internal VSManagementStrategy(ITextTemplatingEngineHost host) : base(host) {
        IServiceProvider hostServiceProvider = (IServiceProvider)host;
        if (hostServiceProvider == null)
            throw new ArgumentNullException("Could not obtain hostServiceProvider");

        EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
        if (dte == null)
            throw new ArgumentNullException("Could not obtain DTE from host");

        templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
    }
    internal override void CreateFile(String fileName, String content) {
        base.CreateFile(fileName, content);
        //((EventHandler)delegate { templateProjectItem.ProjectItems.AddFromFile(fileName); }).BeginInvoke(null, null, null, null);
    }
    internal override void DeleteFile(String fileName) {
        ((EventHandler)delegate { FindAndDeleteFile(fileName); }).BeginInvoke(null, null, null, null);
    }
    private void FindAndDeleteFile(String fileName) {
        foreach(EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems) {
            if (projectItem.get_FileNames(0) == fileName) {
                projectItem.Delete();
                return;
            }
        }
    }
}#>

每一个文件就要进行一次block的开关,即manager.StartBlock(文件名)manager.EndBlock(),在文件都结束后,执行manager.Process(true),进行文件的写操作。

注意:Manager类实现了文件块的开关和保存位置的设定。
这里需要设置template指令 :hostspecific=“true”

如果提示错误:T4 模板 错误 当前上下文中不存在名称“Host” ,请按照设置hostspecific=“true”

根据 MySQL 数据库生成实体

MySQLHelper.tt

<#@ assembly name="C:\Users\xxxx\.nuget\packages\mysql.data\9.0.0\lib\net48\MySql.Data.dll" #>
<#@ assembly name="System.Core.dll" #>
<#@ assembly name="System.Data.dll" #>
<#@ assembly name="System.Xml.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="MySql.Data.MySqlClient" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>

<#+  
    public class EntityHelper
    {
        public static List<Entity> GetEntities(string connectionString, List<string> databases)
        {
            var list = new List<Entity>();
            var conn = new MySqlConnection(connectionString);
            try
            {
                conn.Open();
                var dbs = string.Join("','", databases.ToArray());
                var cmd = string.Format(@"SELECT `information_schema`.`COLUMNS`.`TABLE_SCHEMA`
                                                    ,`information_schema`.`COLUMNS`.`TABLE_NAME`
                                                    ,`information_schema`.`COLUMNS`.`COLUMN_NAME`
                                                    ,`information_schema`.`COLUMNS`.`DATA_TYPE`
                                                    ,`information_schema`.`COLUMNS`.`COLUMN_COMMENT`
                                                FROM `information_schema`.`COLUMNS`
                                                WHERE `information_schema`.`COLUMNS`.`TABLE_SCHEMA` IN ('{0}') ", dbs);
                using (var reader = MySqlHelper.ExecuteReader(conn, cmd))
                {
                    while (reader.Read())
                    {
                        var db = reader["TABLE_SCHEMA"].ToString();
                        var table = reader["TABLE_NAME"].ToString();
                        var column = reader["COLUMN_NAME"].ToString();
                        var type = reader["DATA_TYPE"].ToString();
                        var comment = reader["COLUMN_COMMENT"].ToString();
                        var entity = list.FirstOrDefault(x => x.EntityName == table);
                        if (entity == null)
                        {
                            entity = new Entity(table);
                            entity.Fields.Add(new Field
                            {
                                Name = column,
                                Type = GetCLRType(type),
                                Comment = comment
                            });

                            list.Add(entity);
                        }
                        else
                        {
                            entity.Fields.Add(new Field
                            {
                                Name = column,
                                Type = GetCLRType(type),
                                Comment = comment
                            });
                        }
                    }
                }
            }
            finally
            {
                conn.Close();
            }

            return list;
        }

        public static string GetCLRType(string dbType)
        {
            switch (dbType)
            {
                case "tinyint":
                case "smallint":
                case "mediumint":
                case "int":
                case "integer":
                    return "int";
                case "double":
                    return "double";
                case "float":
                    return "float";
                case "decimal":
                    return "decimal";
                case "numeric":
                case "real":
                    return "decimal";
                case "bit":
                    return "bool";
                case "date":
                case "time":
                case "year":
                case "datetime":
                case "timestamp":
                    return "DateTime";
                case "tinyblob":
                case "blob":
                case "mediumblob":
                case "longblog":
                case "binary":
                case "varbinary":
                    return "byte[]";
                case "char":
                case "varchar":
                case "tinytext":
                case "text":
                case "mediumtext":
                case "longtext":
                    return "string";
                case "point":
                case "linestring":
                case "polygon":
                case "geometry":
                case "multipoint":
                case "multilinestring":
                case "multipolygon":
                case "geometrycollection":
                case "enum":
                case "set":
                default:
                    return dbType;
            }
        }
    }

    public class Entity
    {
        public Entity()
        {
            this.Fields = new List<Field>();
        }

        public Entity(string name)
            : this()
        {
            this.EntityName = name;
        }

        public string EntityName { get; set; }
        public List<Field> Fields { get; set; }
        public string PascalEntityName
        {
            get
            {
                return CommonConver.ToPascalCase(this.EntityName);
            }
        }
        public string CamelEntityName
        {
            get
            {
                return CommonConver.ToCamelCase(this.EntityName);
            }
        }
    }

    public class Field
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string Comment { get; set; }
    }
    public class CommonConver
    {
        public static string ToPascalCase(string tableName)
        {
            string upperTableName = tableName.Substring(0, 1).ToUpper() + tableName.Substring(1, tableName.Length - 1);
            return upperTableName;
        }
        public static string ToCamelCase(string tableName)
        {
            string lowerTableName = tableName.Substring(0, 1).ToLower() + tableName.Substring(1, tableName.Length - 1);
            return lowerTableName;
        }
    }

    class config
    {

        public static readonly string ConnectionString = "Database=test;Data Source=127.0.0.1;User Id=root;Password=123456;pooling=false;CharSet=utf8;port=3306";
        public static readonly string ModelNameSpace = "App.Entities";
    }
#>

AutoCreateModel.tt

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Data.Common.dll" #>
<#@ assembly name="System.Core.dll" #>
<#@ assembly name="System.Data.dll" #>
<#@ assembly name="System.Xml.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ include file="$(ProjectDir)Manage.tt"  #>
<#@ include file="$(ProjectDir)MySqlHelper.tt"  #>
<#@ output extension=".cs" #>
<# var manager = new Manager(Host, GenerationEnvironment, true); #>
<# 
    
      var OutputPath1 ="D:\\test"; //设置文件存储逇位置
      var entities =EntityHelper.GetEntities(config.ConnectionString,new List<string> { "test"});
      foreach(Entity entity in entities)
     {
	 manager.StartBlock(entity.EntityName+".cs",OutputPath1);
#>
using System;

namespace <#=config.ModelNameSpace#>
{
    /// <summary>
    /// <#= entity.EntityName #> Entity Model
    /// </summary>   

    public class <#= entity.EntityName #>
    {
<#
        for(int i = 0; i < entity.Fields.Count; i++)
        {
            if(i ==0)
            {
#>      
        /// <summary>
        /// <#= entity.Fields[i].Comment #>
        /// </summary>
        public <#= entity.Fields[i].Type #> <#= entity.Fields[i].Name #> { get; set; }
<#
            }
            else
            {
#>   
        /// <summary>
        /// <#= entity.Fields[i].Comment #>
        /// </summary>

        public <#= entity.Fields[i].Type #> <#= entity.Fields[i].Name #> { get; set; }
<#            
            }
        }
#>
    }
}
<#       
        manager.EndBlock();
    }
    manager.Process(true);
#>

介绍几个常用的$(variableName) 变量:

  • $(SolutionDir):当前项目所在解决方案目录
  • $(ProjectDir):当前项目所在目录
  • $(TargetPath):当前项目编译输出文件绝对路径
  • $(TargetDir):当前项目编译输出目录,即web项目的Bin目录,控制台、类库项目bin目录下的Debug或release目录(取决于当前的编译模式)

举个例子:比如我们在D盘根目录建立了一个控制台项目TestConsole,解决方案目录为D:\LzrabbitRabbit,项目目录为
D:\LzrabbitRabbit\TestConsole,那么此时在Debug编译模式下

  • $(SolutionDir)的值为D:\LzrabbitRabbit
  • $(ProjectDir)的值为D:\LzrabbitRabbit\TestConsole
  • $(TargetPath)值为D:\LzrabbitRabbit\TestConsole\bin\Debug\TestConsole.exe
  • $(TargetDir)值为D:\LzrabbitRabbit\TestConsole\bin\Debug\

Windows Server2012 R2 无法安装.NET Framework 3.5的解决方法 - __小白菜 - 博客园

mikel阅读(656)

来源: Windows Server2012 R2 无法安装.NET Framework 3.5的解决方法 – __小白菜 – 博客园

Windows server 2012R2,自带的是.NET Framework 4.5,如果想装SQL server2008或者SQL server2012就需要安装 .ENT Framework 3.5或者2.0的版本,建议安装  .NET3.5 版本,我本人亲测过,成功了!

安装不成功错误分析:

如果直接装SQL server2008或者2012,就会报:无法安装一下功能 .NET Framework 3.5。

如果找一个.NET Framework 3.5的来安装,系统会报安装了一个或者多个角色服务或功能失败,找不到原文件等错误。

按照提示从控制面板-程序-启动或关闭Windows功能里看看

这和Win7,win10 ,xp操作不一样,但是原理是一样;

 

 

 

我们看到系统默认安装了.NET Framework 4.5于是隐隐有种不祥的预感,但我们还是要硬着头皮勾选3.5

显示需要指定备用路径,但我没有指定

到这里就是一个失败的安装;

解决方法:

 

从网上参考了很多:https://blog.csdn.net/sunny_lv/article/details/73603360

这篇文章里说了很多方法,大家可以尝试。

从网上找了安装盘路径下的 C:\sources\sxs简包放入指定位置后,输入备用源路径也没起作用,只能乖乖下载整个镜像文件

(WindowsServer2012R2镜像文件迅雷链接:ed2k://|file|cn_windows_server_2012_r2_vl_with_update_x64_dvd_4051059.iso|4683122688|BD0B95997679F83A4EE2D062865D8E64|/ )

下载的镜像文件里有sxs这个文件

于是灵光乍现(投机取巧),让我们来试一试这个简包,于是单独复制sxs文件到服务器的C:/下。

填写备用源路径为C:/sxs

 

其实回想一下,备用源路径只要能指向到正确的安装盘下的sxs文件即可。之前下载的安装简包可能不是对应Windows servers 2012R2版本里切取出来的,所以使用本文方法的同学一定要注意选取对应版本的简包(我用的简包链接在文中),然后指定备用源路径即可。

为什么反射慢? - 北冥有鱼要继续奋斗 - 博客园

mikel阅读(395)

来源: 为什么反射慢? – 北冥有鱼要继续奋斗 – 博客园

反射机制就是通过字节码文件对象获取成员变量、成员方法和构造方法,然后进一步获取它们的具体信息,如名字、修饰符、类型等。

反射机制的性能较低有很多原因,这里详细总结以下4点原因:

(1)JIT优化受限:
JIT 编译器的优化是基于静态分析和预测的。反射是一种在运行时动态解析类型信息的机制,在编译时无法确定反射调用的具体方法,因此编译器无法对这些代码进行静态分析,从而无法进行一些JIT优化,比如:

内联优化受限:JIT 编译器通常会对频繁调用的方法进行内联优化,将方法调用替换为直接的代码。但是,由于反射调用的方法在运行时才能确定,因此 JIT 编译器无法进行有效的内联优化。

无法进行即时编译:因为反射调用的方法在运行时才能确定,因此在解释执行阶段,我们无法确定反射调用的方法会被执行多少次,会不会成为热点代码,也就无法对其进行即时编译优化。

(2)反射中频繁的自动拆装箱操作会导致应用性能下降:
在反射中,当你调用一个方法时,由于在编译时不知道具体要调用的方法参数类型,因此需要用最通用的引用类型来处理所有的参数,即Object。例如,通过Method对象调用方法时,使用的invoke方法签名大致如下:

public Object invoke(Object obj, Object... args)

对于基本数据类型的参数,它们必须被装箱成对应的包装类(如IntegerDouble等),以便它们可以作为对象被传递。在方法实际执行时,如果方法的参数是基本类型,JVM需要基本类型的值,而不是它们的包装类对象。因此,JVM会自动进行拆箱。例如,如果你通过反射调用的方法期望得到一个int类型的参数,但你传入的是Integer,在调用过程中JVM会自动将Integer对象拆箱为int类型。装箱和拆箱操作涉及到额外的对象创建(装箱时)和对象值的提取(拆箱时),在高性能要求的场景下,过度的装箱和拆箱可能会导致性能瓶颈。此外,由于装箱操作导致创建了许多短生命周期的对象,这些对象在成为垃圾后,需要通过垃圾回收过程来回收内存资源,当有大量对象需要回收时,GC会占用更多的CPU资源,可能导致应用性能暂时下降。

(3)遍历操作
反射在调用方法时会从方法数组中遍历查找,这对普通的方法调用来说是不需要的。

(4)方法访问检查
每次使用反射调用方法时,JVM都要检查是否允许访问该方法,例如是否为私有方法等。这些访问检查对普通的方法调用来说是不需要的,因为这些检查都是在编译时完成的。

C# 使用模式匹配的好处,因为好用所以推荐~ - 万雅虎 - 博客园

mikel阅读(327)

来源: C# 使用模式匹配的好处,因为好用所以推荐~ – 万雅虎 – 博客园

  1. 类型检查和转换:当你需要检查对象是否为特定类型,并且希望在同一时间内将其转换为那个类型时,模式匹配提供了一种更简洁的方式来完成这一任务,避免了使用传统的as和is操作符后还需要进行额外的null检查。
  2. 复杂条件逻辑:在处理复杂的条件逻辑时,特别是涉及到多个条件和类型的情况下,使用模式匹配可以使代码更加清晰易读。通过模式匹配,可以将复杂的if-else链或switch语句简化,使逻辑更直观。
  3. 解构复合类型:当你需要从复合类型(如元组、自定义类等)中提取值时,模式匹配允许你直接在条件检查中进行解构,这样可以避免编写额外的解构代码,使得代码更加简洁。
  4. 范围检查:对于需要进行范围检查的场景,如检查一个数是否落在某个区间内,使用C# 9.0引入的关系模式可以极大简化代码,使得范围检查逻辑一目了然。
  5. 逻辑组合:在需要对多个条件进行逻辑组合的情况下,如需要检查一个值是否满足多个条件之一或全部条件,使用逻辑模式可以直接在模式匹配表达式中使用and、or和not运算符,避免了复杂的逻辑嵌套。
  6. 数据验证:模式匹配可以用于数据验证场景,特别是当验证逻辑涉及到类型检查、值范围检查或特定属性值检查时。通过模式匹配,可以在单个表达式中完成所有这些检查,使得验证逻辑更加紧凑和易于维护。
  7. 多态行为:在处理需要根据对象类型执行不同操作的多态行为时,模式匹配提供了一种更灵活的方式来替代传统的虚方法或接口实现。这使得在不修改原有类层次结构的情况下,能够更容易地扩展或修改行为。
  8. 替代访问者模式:在实现访问者设计模式时,模式匹配可以作为一种更简洁的替代方案,特别是在处理复杂的对象结构时。通过模式匹配,可以直接在一个地方处理所有类型的情况,而不需要为每种类型创建单独的访问者方法。

模式匹配的这些用途展示了它在简化代码、提高可读性和灵活处理不同类型和条件的强大能力。随着C#语言的发展,模式匹配的功能和应用场景将会进一步扩展和深化。

下面我们看下一些经典的模式匹配编码风格:

is断言 变量str已被安全地转换为string类型

object obj = "Hello, World!";
if (obj is string str) {
    Console.WriteLine(str);
}

is对可空类型的断言

public record Person(int Id, string? Name, bool? IsActived);
var person = new Person(1, "vipwan", null);
if (person?.IsActived is true)
{
    Console.WriteLine($"Id {person.Id} 已激活");
}

switch 允许使用多种模式,包括类型模式、常量模式和var模式 ,无需我们提前做转换以节省编码量

switch (obj) {
    case 0:
        Console.WriteLine("Zero");
        break;
    case var value:
        Console.WriteLine($"Value: {value}");
        break;
}

switch 中使用弃元_代替变量

public static string CronEveryNHours(this int n) => n switch
{
	(>= 1 and < 24) => $"0 0/{n} * * *",
	_ => throw new ArgumentException("n must be between 1 and 24", nameof(n))
};

C# 8.0引入了属性模式,允许基于对象的属性进行模式匹配

public record Person(string Name,int Age);
var person = new Person("vipwan", 30);
//通俗易懂:如果person不为null,且name==vipwan 并且age>=18的时候
if (person is { Name: "vipwan", Age: >= 18 }) {
    Console.WriteLine("vipwan is an adult.");
}

C# 9.0引入的逻辑模式,它允许使用逻辑运算符andornot来组合模式。

if (number is > 0 and < 10 or 100) {
    Console.WriteLine("Number is between 0 and 10 or equals 100.");
}

元组模式允许你对元组的元素进行模式匹配,这在处理元组返回值或多值情况时非常有用

var numbers = (1, "one", 18);
if (numbers is (1, string name, int age)) {
    Console.WriteLine($"The name of 1 is {name}, age {age}!");
}

列表模式允许对数组、列表等集合进行模式匹配,可以匹配集合的长度、元素等属性。这对于处理集合数据时进行模式匹配提供了极大的便利。

int[] numbers = { 1, 2, 3 };
if (numbers is [1, 2, 3]) {
    Console.WriteLine("The array contains the numbers 1, 2, and 3 in that order.");
}

切片模式允许你匹配集合的一部分,而不是整个集合。这在你只关心集合的某个特定部分时特别有用。

int[] numbers = { 0, 1, 2, 3, 4 };
if (numbers is [0, .., 4]) {
    Console.WriteLine("The array starts with 0 and ends with 4.");
}

这里只是介绍了部分好用常见的模式匹配,随着C#语言的逐代增强,可能会有更多的新特性和改进被引入。