python-3.x - 使用 python 提取 pdf 表格中包含的文本的最佳方法是什么?

标签 python-3.x pdf ocr pdf-reader

我正在构建一个程序来从 pdf 中提取文本,将其放入结构化格式,然后将其发送到数据库。我有大约 1,400 个单独的 pdf,它们都遵循类似的格式,但文档总结的措辞和计划设计中的细微差别使它变得棘手。

我在 python 中玩过几个不同的 pdf 阅读器,包括 tabula-py 和 pdfminer,但没有一个能完全理解我想做的事情。 Tabula 可以很好地阅读所有文本,但是它会拉出所有内容,因为它明确地水平放置,不包括一些文本被包裹在一个盒子里的事实。例如,如果您打开我附上的样本 SBC,上面写着“总免赔额是多少?” Tabula 将读到“总计 500 美元/个人或……”,而忽略了“免赔额”一词实际上是第一句的一部分这一事实。 (请注意,我正在处理的文件是 pdf,但我附上了 jpeg,因为我不知道如何附上 pdf。)

import tabula

df = tabula.read_pdf(*filepath*, pandas_options={'header': None))

print(df.iloc[0][0])
print(df)

最后,我真的很想能够解析出每个框中的文本,以便我可以更好地识别哪些值属于免赔额、自付费用限额、共付额/共同保险等。我想可能某种 OCR 可以让我识别 PDF 的哪些部分包含在蓝色矩形中,然后从那里拉出字符串,但我真的不知道从哪里开始。 Sample SBC

最佳答案

@jpnadas 在这种情况下,您从我在 this post 的答案中复制的代码不是很合适,因为它解决了表格没有周围网格的情况。该算法寻找重复的文本 block ,并尝试以启发式方式找到类似于表格的模式。

但在这种特殊情况下,表格确实有网格,通过利用这一优势,我们可以获得更准确的结果。

策略如下:

  1. 增加图像 Gamma 值使网格更暗
  2. 去除颜色并应用 Otsu 阈值
  3. 在图像中找到长的垂直和水平线,并使用 erodedilate 函数从中创建一个掩码
  4. 使用 findContours 函数在掩码中找到单元 block 。
  5. 查找表格对象

    5.1 其余的可以和post about finding a table without the grid中的一样: 启发式查找表结构

    5.2 替代方法可以使用 findContours 函数返回的 hierarchy。这种方法更加准确和 允许在单个图像上查找多个表格。

  6. 有了细胞坐标,很容易从原始图像中提取特定的细胞图像:

    cell_image = image[cell_y:cell_y + cell_h, cell_x:cell_x + cell_w]

  7. 对每个 cell_image 应用 OCR。

但是!当您无法读取 PDF 的内容时,我认为 OpenCV 方法是最后的手段:例如,当 PDF 中包含光栅图像时。

如果它是基于矢量的 PDF 并且其内容是可读的,那么在内容中找到表格并从中读取文本而不是进行繁重的“OCR 提升”会更有意义。

为了更准确的识别表格,引用代码如下:

import os
import imutils
import numpy as np
import argparse
import cv2


def gamma_correction(image, gamma = 1.0):
    look_up_table = np.empty((1,256), np.uint8)

    for i in range(256):
        look_up_table[0,i] = np.clip(pow(i / 255.0, gamma) * 255.0, 0, 255)

    result = cv2.LUT(image, look_up_table)

    return result


def pre_process_image(image):
    # Let's get rid of color first

    # Applying gamma to make the table lines darker
    gamma = gamma_correction(image, 2)

    # Getting rid of color
    gray = cv2.cvtColor(gamma, cv2.COLOR_BGR2GRAY)

    # Then apply Otsu threshold to reveal important areas
    ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

    # inverting the thresholded image
    return ~thresh


def get_horizontal_lines_mask(image, horizontal_size=100):

    horizontal = image.copy()
    horizontal_structure = cv2.getStructuringElement(cv2.MORPH_RECT, (horizontal_size, 1))
    horizontal = cv2.erode(horizontal, horizontal_structure, anchor=(-1, -1), iterations=1)
    horizontal = cv2.dilate(horizontal, horizontal_structure, anchor=(-1, -1), iterations=1)

    return horizontal


def get_vertical_lines_mask(image, vertical_size=100):
    vertical = image.copy()
    vertical_structure = cv2.getStructuringElement(cv2.MORPH_RECT, (1, vertical_size))
    vertical = cv2.erode(vertical, vertical_structure, anchor=(-1, -1), iterations=1)
    vertical = cv2.dilate(vertical, vertical_structure, anchor=(-1, -1), iterations=1)

    return vertical


def make_lines_mask(preprocessed, min_horizontal_line_size=100, min_vertical_line_size=100):

    hor = get_horizontal_lines_mask(preprocessed, min_horizontal_line_size)
    ver = get_vertical_lines_mask(preprocessed, min_vertical_line_size)

    mask = np.zeros((preprocessed.shape[0], preprocessed.shape[1], 1), dtype=np.uint8)
    mask = cv2.bitwise_or(mask, hor)
    mask = cv2.bitwise_or(mask, ver)

    return ~mask


def find_cell_boxes(mask):
    # Looking for the text spots contours
    # OpenCV 3
    # img, contours, hierarchy = cv2.findContours(pre, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    # OpenCV 4
    contours = cv2.findContours(mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    contours = imutils.grab_contours(contours)
    contours = sorted(contours, key=cv2.contourArea, reverse=True)

    image_width = mask.shape[1]

    # Getting the texts bounding boxes based on the text size assumptions
    boxes = []
    for contour in contours:
        box = cv2.boundingRect(contour)
        w = box[2]

        # Excluding the page box shape but adding smaller boxes
        if w < 0.95 * image_width:
            boxes.append(box)

    return boxes


def find_table_in_boxes(boxes, cell_threshold=10, min_columns=2):
    rows = {}
    cols = {}

    # Clustering the bounding boxes by their positions
    for box in boxes:
        (x, y, w, h) = box
        col_key = x // cell_threshold
        row_key = y // cell_threshold
        cols[row_key] = [box] if col_key not in cols else cols[col_key] + [box]
        rows[row_key] = [box] if row_key not in rows else rows[row_key] + [box]

    # Filtering out the clusters having less than 2 cols
    table_cells = list(filter(lambda r: len(r) >= min_columns, rows.values()))
    # Sorting the row cells by x coord
    table_cells = [list(sorted(tb)) for tb in table_cells]
    # Sorting rows by the y coord
    table_cells = list(sorted(table_cells, key=lambda r: r[0][1]))

    return table_cells


def build_vertical_lines(table_cells):
    if table_cells is None or len(table_cells) <= 0:
        return [], []

    max_last_col_width_row = max(table_cells, key=lambda b: b[-1][2])
    max_x = max_last_col_width_row[-1][0] + max_last_col_width_row[-1][2]

    max_last_row_height_box = max(table_cells[-1], key=lambda b: b[3])
    max_y = max_last_row_height_box[1] + max_last_row_height_box[3]

    hor_lines = []
    ver_lines = []

    for box in table_cells:
        x = box[0][0]
        y = box[0][1]
        hor_lines.append((x, y, max_x, y))

    for box in table_cells[0]:
        x = box[0]
        y = box[1]
        ver_lines.append((x, y, x, max_y))

    (x, y, w, h) = table_cells[0][-1]
    ver_lines.append((max_x, y, max_x, max_y))
    (x, y, w, h) = table_cells[0][0]
    hor_lines.append((x, max_y, max_x, max_y))

    return hor_lines, ver_lines


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("-i", "--image", required=True, help="path to images directory")
    args = vars(ap.parse_args())

    in_file = args["image"]
    filename_base = in_file.replace(os.path.splitext(in_file)[1], "")

    img = cv2.imread(in_file)

    pre_processed = pre_process_image(img)

    # Visualizing pre-processed image
    cv2.imwrite(filename_base + ".pre.png", pre_processed)

    lines_mask = make_lines_mask(pre_processed, min_horizontal_line_size=1800, min_vertical_line_size=500)

    # Visualizing table lines mask
    cv2.imwrite(filename_base + ".mask.png", lines_mask)

    cell_boxes = find_cell_boxes(lines_mask)

    cells = find_table_in_boxes(cell_boxes)

    # apply OCR to each cell rect here
    # the cells array contains cell coordinates in tuples (x, y, w, h)

    hor_lines, ver_lines = build_vertical_lines(cells)

    # Visualize the table lines
    vis = img.copy()

    for line in hor_lines:
        [x1, y1, x2, y2] = line
        cv2.line(vis, (x1, y1), (x2, y2), (0, 0, 255), 1)

    for line in ver_lines:
        [x1, y1, x2, y2] = line
        cv2.line(vis, (x1, y1), (x2, y2), (0, 0, 255), 1)

    cv2.imwrite(filename_base + ".result.png", vis)

一些参数是硬编码的:

  • 页面大小阈值 - 0.95

  • 最小水平线尺寸 - 1800 像素

  • 最小垂直线尺寸 - 500 像素

您可以将它们作为可配置参数提供或使它们与图像大小相关。

结果: Table detection by its grid

关于python-3.x - 使用 python 提取 pdf 表格中包含的文本的最佳方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57210148/

相关文章:

python - 在 pyinstaller 中包含 google bigquery python lib

python - Pygame 将 28x28 像素缩放到 420x420

ocr - 通过Tesseract OCR在七段显示器上进行文本检测

在 Java 中使用 TESSERACT API 时出现 java.lang.UnsatisfiedLinkError 异常

android - 创建针对 OCR 优化/适用的相机 Activity

python - 使用python读取和绘制VTK文件数据结构

python-3.x - 必须为 '#' 格式定义 uwsgi PY_SSIZE_T_CLEAN 宏

r - 在 R 中找出 PDF 的编码

pdf - 如何执行 ImageMagick 仅将多页 PDF 的第一页转换为 JPEG?

node.js - 如何向 puppeteer 生成的 PDF 添加水印?