【问题标题】:What is the best way to extract text contained within a table in a pdf using python?使用python提取pdf中表格中包含的文本的最佳方法是什么?
【发布时间】:2019-12-04 05:09:23
【问题描述】:

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

我在 python 中使用了几个不同的 pdf 阅读器,包括 tabula-py 和 pdfminer,但它们都没有完全达到我想要做的。 Tabula 可以很好地阅读所有文本,但是它会拉动所有内容,因为它明确地水平放置,不包括某些文本被包裹在一个盒子中的事实。例如,如果您打开我附上的示例 SBC,其中显示“总免赔额是多少?” Tabula 将在“什么是整体 $500/Individual 或...”中阅读,跳过“免赔额”一词实际上是第一句的一部分这一事实。 (请注意,我使用的文件是 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

【问题讨论】:

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


    【解决方案1】:

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

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

    策略如下:

    1. 增加图像伽玛使网格变暗
    2. 去除颜色并应用 Otsu 阈值
    3. 在图像中找到长的垂直线和水平线,并使用 erodedilate 函数从中创建蒙版
    4. 使用findContours 函数查找掩码中的单元格块。
    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 像素

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

    结果:

    【讨论】:

      【解决方案2】:

      我认为完成您需要的最佳方法是找到并隔离文件中的单元格,然后将 OCR 应用于每个单独的单元格。

      在 SO 中有很多解决方案,我从 this answer 获得了代码,并使用了一些参数来获得下面的输出(还不完美,但你可以自己稍微调整一下) .

      import os
      import cv2
      import imutils
      
      # This only works if there's only one table on a page
      # Important parameters:
      #  - morph_size
      #  - min_text_height_limit
      #  - max_text_height_limit
      #  - cell_threshold
      #  - min_columns
      
      
      def pre_process_image(img, save_in_file, morph_size=(23, 23)):
      
          # get rid of the color
          pre = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
          # Otsu threshold
          pre = cv2.threshold(pre, 250, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
          # dilate the text to make it solid spot
          cpy = pre.copy()
          struct = cv2.getStructuringElement(cv2.MORPH_RECT, morph_size)
          cpy = cv2.dilate(~cpy, struct, anchor=(-1, -1), iterations=1)
          pre = ~cpy
      
          if save_in_file is not None:
              cv2.imwrite(save_in_file, pre)
          return pre
      
      
      def find_text_boxes(pre, min_text_height_limit=20, max_text_height_limit=120):
          # Looking for the text spots contours
          contours, _ = cv2.findContours(pre, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
      
          # Getting the texts bounding boxes based on the text size assumptions
          boxes = []
          for contour in contours:
              box = cv2.boundingRect(contour)
              h = box[3]
      
              if min_text_height_limit < h < max_text_height_limit:
                  boxes.append(box)
      
          return boxes
      
      
      def find_table_in_boxes(boxes, cell_threshold=100, min_columns=3):
          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_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__":
          in_file = os.path.join(".", "test.jpg")
          pre_file = os.path.join(".", "pre.png")
          out_file = os.path.join(".", "out.png")
      
          img = cv2.imread(os.path.join(in_file))
      
          pre_processed = pre_process_image(img, pre_file)
          text_boxes = find_text_boxes(pre_processed)
          cells = find_table_in_boxes(text_boxes)
          hor_lines, ver_lines = build_lines(cells)
      
          # Visualize the result
          vis = img.copy()
      
          # for box in text_boxes:
          #     (x, y, w, h) = box
          #     cv2.rectangle(vis, (x, y), (x + w - 2, y + h - 2), (0, 255, 0), 1)
      
          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(out_file, vis)
      

      【讨论】:

      • 美丽。这是我试图去的方向,应该会很好。我会玩弄代码并对其进行调整,然后返回我的结果。
      猜你喜欢
      • 2020-01-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-05
      • 1970-01-01
      • 2019-02-13
      • 2010-09-09
      • 1970-01-01
      相关资源
      最近更新 更多