从具有不同布局的 PDF 文件中提取文本信息 - 机器学习

oli*_*rbj 11 nlp machine-learning image-processing computer-vision neural-network

我目前正在尝试创建的 ML 项目需要帮助。

我从很多不同的供应商那里收到了很多发票——所有发票都有自己独特的布局。我需要从发票中提取3 个关键元素。这3 个元素都位于所有发票的表/行项目中。

3个要素是:

  • 1 : 关税编号(数字)
  • 2 : 数量(总是一个数字)
  • 3 : 总行数(货币价值)

请参考下面的屏幕截图,我在样本发票上标记了这些字段。

在此处输入图片说明

我使用基于正则表达式的模板方法开始了这个项目。然而,这根本无法扩展,我最终得到了大量不同的规则。

我希望机器学习可以帮助我 - 或者混合解决方案?

共同点

在我的所有发票中,尽管布局不同,但每个行项目将始终包含一个关税编号。此关税编号始终为 8 位数字,并且始终采用以下格式之一:

  • xxxxxxxxx
  • xxx.xxx
  • xx.xx.xx.xx

(其中“x”是 0 - 9 之间的数字)。

此外,正如您在发票上看到的,每行都有一个单价和一个总金额。我需要的金额始终是每行最高的。

输出

对于像上面那样的每张发票,我需要每一行的输出。例如,这可能是这样的:

{
    "line":"0",
    "tariff":"85444290",
    "quantity":"3",
    "amount":"258.93"
},
{
    "line":"1",
    "tariff":"85444290",
    "quantity":"4",
    "amount":"548.32"
},
{
    "line":"2",
    "tariff":"76109090",
    "quantity":"5",
    "amount":"412.30"
}
Run Code Online (Sandbox Code Playgroud)

然后去哪儿?

我不确定我要做什么属于机器学习,如果是,属于哪个类别。是计算机视觉吗?自然语言处理?命名实体识别?

我最初的想法是:

  1. 将发票转换为文本。(发票都是可文本化的 PDF,所以我可以使用类似的东西pdftotext来获取确切的文本值)
  2. 创建自定义命名实体quantitytariff以及amount
  3. 导出找到的实体。

但是,我觉得我可能会错过一些东西。

任何人都可以在正确的方向上帮助我吗?

编辑:

请参阅下面的更多示例,了解发票表部分的外观:

样本发票 #2 在此处输入图片说明

样本发票 #3 在此处输入图片说明

编辑2:

请参阅下面的三个示例图像,没有边框/边界框:

图 1: 示例 1 不带 bbox

图 2: 示例 2 不带 bbox

图 3: 示例 3 不带 bbox

nat*_*ncy 6

这是使用 OpenCV 的尝试,其想法是:

  1. 获取二值图像。我们加载图像,放大使用 imutils.resize以帮助获得更好的 OCR 结果(参见Tesseract 提高质量),转换为灰度,然后Otsu 阈值 获得二值图像(1 通道)。

  2. 删除表格网格线。我们创建一个水平和垂直内核,然后执行形态学操作将相邻的文本轮廓组合成一个单一的轮廓。这个想法是将 ROI 行作为一个整体提取到 OCR。

  3. 提取行 ROI。我们找到轮廓然后使用从上到下排序imutils.contours.sort_contours。这确保我们以正确的顺序遍历每一行。从这里我们遍历轮廓,使用 Numpy 切片提取行 ROI,使用Pytesseract提取 OCR ,然后解析数据。


这是每个步骤的可视化:

输入图像

在此处输入图片说明

二进制图像

在此处输入图片说明

变形关闭

在此处输入图片说明

遍历每一行的可视化

在此处输入图片说明

提取的行 ROI

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明

输出发票数据结果:

{'line': '0', 'tariff': '85444290', 'quantity': '3', 'amount': '258.93'}
{'line': '1', 'tariff': '85444290', 'quantity': '4', 'amount': '548.32'}
{'line': '2', 'tariff': '76109090', 'quantity': '5', 'amount': '412.30'}
Run Code Online (Sandbox Code Playgroud)

不幸的是,在尝试第二个和第三个图像时,我得到了不同的结果。这种方法在其他图像上不会产生很好的效果,因为发票的布局都不同。但是,这种方法表明,假设您有固定的发票布局,可以使用传统的图像处理技术来提取发票信息。

代码

import cv2
import numpy as np
import pytesseract
from imutils import contours
import imutils

pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"

# Load image, enlarge, convert to grayscale, Otsu's threshold
image = cv2.imread('1.png')
image = imutils.resize(image, width=1000)
height, width = image.shape[:2]
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Remove horizontal lines
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50,1))
detect_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
cnts = cv2.findContours(detect_horizontal, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    cv2.drawContours(thresh, [c], -1, 0, -1)

# Remove vertical lines
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,50))
detect_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
cnts = cv2.findContours(detect_vertical, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    cv2.drawContours(thresh, [c], -1, 0, -1)

# Morph close to combine adjacent contours into a single contour
invoice_data = []
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (85,5))
close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=3)

# Find contours, sort from top-to-bottom
# Iterate through contours, extract row ROI, OCR, and parse data
cnts = cv2.findContours(close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
(cnts, _) = contours.sort_contours(cnts, method="top-to-bottom")

row = 0
for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    ROI = image[y:y+h, 0:width]
    ROI = cv2.GaussianBlur(ROI, (3,3), 0)
    data = pytesseract.image_to_string(ROI, lang='eng', config='--psm 6')
    parsed = [word.lower() for word in data.split()] 
    if 'tariff' in parsed or 'number' in parsed:
        row_data = {}
        row_data['line'] = str(row)
        row_data['tariff'] = parsed[-1]
        row_data['quantity'] = parsed[2]
        row_data['amount'] = str(max(parsed[10], parsed[11]))
        row += 1

        print(row_data)
        invoice_data.append(row_data)

        # Visualize row extraction
        '''
        mask = np.zeros(image.shape, dtype=np.uint8)
        cv2.rectangle(mask, (0, y), (width, y + h), (255,255,255), -1)
        display_row = cv2.bitwise_and(image, mask)

        cv2.imshow('ROI', ROI)
        cv2.imshow('display_row', display_row)
        cv2.waitKey(1000)
        '''
print(invoice_data)
cv2.imshow('thresh', thresh)
cv2.imshow('close', close)
cv2.waitKey()
Run Code Online (Sandbox Code Playgroud)

  • 谢谢@nathancy!虽然这个答案并不是我所有已发布发票的通用解决方案,但我仍然认为这是我见过的最接近的解决方案 - 而且这只是使用 OpenCV。非常酷,你的代码示例教会了我很多!再次感谢您花时间发布此内容。 (3认同)

小智 5

我正在研究物流行业中的一个类似问题,当我说这些文档表有多种布局时,请相信我。下面提到了许多已经解决并正在改进这个问题的公司

  • 领导者:ABBYY、AntWorks、Kofax 和 WorkFusion
  • 主要竞争者:Automation Anywhere、Celaton、Datamatics、EdgeVerve、Extract Systems、Hyland、Hyland、Infrrd 和 Parascript
  • 有抱负的人:Ikarus、Rossum、Shipmnts(Alex)、Amazon(Textract)、Docsumo、Docparser、Aidock

我想把这个问题归为多模态学习,因为文本和图像模态在这个问题上都有很大的贡献。尽管 OCR 标记在属性值分类中起着至关重要的作用,但它们在页面上的位置、间距和字符间距离在检测表、行和列边界方面是非常重要的特征。当行跨页或某些列携带非空值时,问题变得更加有趣。

虽然学术界和会议使用术语智能文档处理,但通常用于提取单一字段和表格数据。在研究文献中,前者以属性值分类为人所知,后者以表提取或重复结构提取而闻名。

在我们处理这些半结构化文档的 3 年中,我觉得同时实现准确性和可扩展性是一个漫长而艰巨的旅程。提供可扩展性/“无模板”方法的解决方案确实已注释了数万甚至数百万的半结构化业务文档的语料库。虽然这种方法是一种可扩展的解决方案,但它与它接受过培训的文档一样好。如果您的文件来自以复杂布局而闻名的物流或保险行业,并且由于合规程序而需要超级准确,那么“基于模板”的解决方案将是您解决问题的灵丹妙药。它保证提供更高的准确性。

如果您需要现有研究的链接,请在下面的评论中提及,我很乐意与您分享。

另外,我建议在 pdf2text 或 pdfminer 上使用 pdfparser 1,因为前者在数字文件中以更好的性能提供字符级信息。

很乐意加入任何反馈,因为这是我在这里的第一个答案。