表格OCR任务,隶属于Document Analysis and Recognition领域,相关顶会有ICDAR。本文主要记录在Github上开源的个人项目Hyper-Table-OCR的整个开发历程,如标题所说,Hyper-Table-OCR的创新点主要在为有表格线的表格OCR识别与重建提供了流水线,我们将全过程分为:
- 预处理,包括四点透视变换(可选)与表格角度调整(可选);
- 表格检测(可选),可应对单图片多表格情形;
- 提取表格单元格,记作TableCell;根据TableCell的坐标信息重建表格结构;
- 对全图进行OCR,记录OCR返回的文字区域与文字内容,记作OCRBlock;
- 以IOU为依据,将步骤3中得到的TableCell与步骤4中得到的OCRBlock进行匹配。没有成功匹配的OCRBlock中的文字内容视为表格外内容;
整个流水线思路基本来自于鹅厂TEG的一篇技术博客,文中声称这项技术应用在了腾讯文档中,不过鄙人在当前版本的腾讯文档中没有找到这项功能。既然是人家的线上项目,开源怕是走不通了,好在文章内容还算详细,Hyper-Table-OCR完成了文中的关键功能,除了:
- 同时处理行、列方向上的合并单元格;博客中提到的遍历方法我左思右想都没有头绪,于是自己瞎凑了一个可以处理行方向上合并单元格的解决方案。
由表格框线推导单元格坐标就不太容易了。因为现实中存在很多单元格合并的情况,一个单元格可能跨了若干行和若干列。对此我们的思路是列举所有的单元格候选,每个单元格表示为(起始行,结束行,起始列,结束列),然后对所有单元格按面积从小到大排序。接着遍历排序好的候选单元格,去判断其上下左右的框线是否都真实存在,若存在,则此单元格就在原图存在。注意到,每当确立一个单元格存在,所有与其共享起始行和起始列的其他单元格则不可能再存在,因为我们不考虑单元格中套着单元格的情况。所以虽然单元格候选集很大,但我们可以利用这一性质在遍历过程中进行剪枝,所以会很高效。
- 只关注OCR内容,没有关注单元格的字体、字号大小、对齐方式等细节,这部分内容主要是因为工作量原因没有实现。
作为前端废物,让好兄弟帮忙撸了一个好看的前端模板作为门面,最后的效果还像模像样的。总的来说,Hyper-Table-OCR完成度尚可,总体效果可参考GIF图与演示视频。完成这个项目给我的最大感受就是:鹅厂的线上业务用到的技术栈竟在我身边!
预处理
边缘检测 & 透视变换
在完成基本预处理之后(灰度化、高斯模糊),首先需要对图像进行边缘检测,以完成后续的透视变换操作;Hyper-Table-OCR提供了Canny算子与HED(Holistically-Nested Edge Detection)两种边缘检测方式,后者作为DL-based的方法耗时更多,但在极端情况下更加稳定。
在边缘检测结果中使用cv2.approxPolyDP
方法逼近四边形,并用四边形的四个点使用cv2.warpPerspective
进行透视变换,透视变换过程可参考下图示,其目的是为了得到横平竖直的表格线。
角度调整
Hyper-Table-OCR中的角度调整在Document Retrieval里又叫作文本倾斜校正(text skew correction/deskewing text)。讲道理,绝大部分表格OCR的应用场景是不需要调整文本角度的,因为透视变换已经保证了校正纸平面的同时校正了表格相对摄像机的角度,除非是下面这种表格在纸上本身就是倾斜的情况。
一个很trivial的方法如这篇知乎文章所述,cv2.minAreaRect
可以求得最小旋转矩形的角度。
另一个方法是通过这个Issue的解释,对于水平排版的文字,行与行之间的像素值方差应该最大,对[-angles, angles]区间内的角度尝试求一次方差并取其中最大的即可。代码段在web/__init__.py#L246
。
表格检测
表格检测适用于单幅图片中出现多个表格的情景,通过检测结果将每个表格按照相同的后续步骤处理即可。
表格检测与目标检测任务没有显著差别,这里选用中了CVPR2020 Workshop的CascadeTabNet,在当前MMDetection版本下CascadeTabNet已经无法正常工作,所以顺便给原仓库提了个PR适配一下新版本的mmdetecion。
提取表格单元格 & 重建表格
准确提取表格单元格是项目的最大难点,鹅厂博客中提到可以用UNet对图片做pixel-level的语义分割,像素分为3类:horizontal、vertical和unclassified;考虑到表格线的横竖像素的确蕴含很强的语义信息,语义分割的实际效果也的确比传统的腐蚀膨胀好上不少,因此项目中有关传统方法的部分就没做过多调试。
得到pixel-level的语义信息后,通过对全图求八联通区域并求最小包围矩形,就能得到一堆相对准确的CellBox啦!对应代码段在boardered/extractor.py#L159
。
通过CellBox的坐标信息,大致可以构建出表格的整体结构,对应代码段在table/__init__.py#L193
:
- CellBox上边缘中点y坐标近似的CellBox归为在同一行,这样一次遍历可以得到表格行数。
- 通过每一行的CellBox数量,可以判断出哪些行中含有合并单元格,把这些行称为merged_rows,把完整行称为complete_rows(这些行的CellBox数量少于Cellbox最多的行的Cellbox数量)
- 对含有合并单元格的行再做一次遍历,与complete_rows中任意一行一一对比左边缘x坐标,可以得出所有合并单元格的col_range(以complete_row为基准,横跨单元格的范围);
- 为了增强鲁棒性(求最小包围矩形时会出现overlap很大的CellBox),在最后舍弃那些与附近CellBox重叠较大的单元格。
上面的流程最大的缺点是:没有办法同时处理行、列两个方向上的合并单元格,而鹅厂博客里写的那个方法乍一看有点道理,实际上难以写出满意的逻辑,所以留了个坑在这个Issue里,有更好想法的可以在这里跟我交流。
OCR
OCR部分我就不班门弄斧了,相关的资料数不胜数。多亏了百度的PaddleOCR
,花10分钟搭起来的OCR流水线也可以跟大厂云上的OCR API扳扳手腕。项目中内置了Paddle与PaddleLite两个Paddle官方的模型,根据推理时间与计算资源合理选用即可。
稍微需要注意的是页面左上角的“禁用OCR定位模块”的选项,绝大部分OCR会在识别前用detection的方法定位字符区域,并逐一识别各个字符区域中的文字内容;禁用OCR定位模块意味着OCR的recognition直接利用其上个阶段中TableCell的位置信息,理论上可以起到speed up的效果。但实际上开启后识别与推理速度上都难以令人满意,主要因为:
- 对比发现TableCell中文字区域较小,向recognition模块提交整个TableCell区域加大了reg_net的推理时间开销;
- 由于reg_net与det_net一般是联合训练,提交整个TableCell区域还拉低了识别准确率,得不偿失。
- 使得识别表格标题的功能失效,因为表格标题本身不属于任何的TableCell。
匹配
逆向匹配TableCell与OCRBlock,不属于任何TableCell的OCRBlock被划分为表格的标题,代码段在table/__init__.py#L169
结语 & 展望
至此,稍微涉及算法的部分就结束了,剩下的无非是如何处理推理端与Web端之间的关系、前后端的一些简单交互,总体来说没有大问题,有兴趣的自行参考代码段。
对项目进行了一些粗略的性能评估,一张720p、包含数十个TableCell的单表格图片约耗时200ms;如果真的要做成鹅厂那样的线上项目,不伤筋动骨的前提下,我想到的至少还有以下部分能够改进:
- 步骤3与步骤4可以完全并行,为了避免抢占GPU,在GPU数量大于等于2个的时候可以用多个GPU异步进行3、4;如果参考经典的推理时间占比,大约可以节约20%的时间。
- 同理,OCR的recognition环节也可以拿detection结果来做map-reduce。
- 全部用tensorrt/onnx的手段部署。
上面这些坑就留着自己有了一些并行编程经验后再来慢慢填吧。
5 comments
大佬,太强了,学习了!一键三连了!
模型训练用的是什么显卡呀
所有模型使用的均是预训练权重,推理时最好使用8GB显存以上的GPU,否则可能需要注释掉一些模型。
很高兴看到你更新啦,我看目前的算法都是基于GPU的,能不能基于CPU做呢?
本项目没有特定依赖CPU的部分,你可以在所有推理框架中均指定
cpu
为计算设备,基本不会影响推理结果。