手记

从扫描PDF中提取文字的窍门:处理复杂布局的最佳方法

扫描的PDF是提取信息时常见的难题。与原生数字版的PDF不同,扫描的文档通常将文本作为图像呈现,这使得直接复制粘贴变得困难。这篇文章将详细介绍两种有效的方法,尤其是面对复杂布局和表格时。我们将介绍如何利用视觉语言模型(VLMs)的优势,并结合文档分割和OCR技术实现准确的文本提取。

PDF 扫描的难题。

扫描的PDF没有底层文本信息。这意味着简单的复制粘贴无法实现。实际上,你是在处理文本图片而不是真正的文本信息。复杂的布局、表格和不同大小的文字进一步加大了处理难度。我们需要能识别视觉布局并将图像准确转换为有意义、结构化文本的解决方案。

方法一:使用视觉-语言模型(VLMs)

如果你的页面数量不多,并且想要得到最好的结果,使用视觉语言模型(VLM)会是最好的选择。这些强大的模型能够准确地分析图像、理解和处理文本与表格。

我们将用谷歌的Gemini 1.5 Pro来做这件事,因为它在图像理解上特别出色,还能很好地提取文本。

代码实现部分:
</TRANSLATION>

     import PIL.Image  
    import os  
    import google.generativeai as genai  
    from pdf2image import convert_from_path  

    #  YOUR_API_KEY 替换为你的 API 密钥  
    GOOGLE_API_KEY = "YOUR_API_KEY"  
    genai.configure(api_key=GOOGLE_API_KEY)  
    pdf_path = "test.pdf" # 请将此路径更改为你的 PDF 路径  
    pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]  
    # 创建输出目录(如果它不存在)  
    output_dir = "GeminiResult"  
    os.makedirs(output_dir, exist_ok=True)  
    # 选择一个 Gemini 模型。  
    model = genai.GenerativeModel(model_name="gemini-1.5-pro")  
    prompt = """  
        提取图像中的所有文本内容和表格数据,严格遵循图像中的阅读顺序处理内容,不得重新排列或重新排序块或表格。  

1. **阅读顺序:** 根据图像中的阅读顺序处理内容,不得重新排列或重新排序块或表格。  

2. **文本块:** 提取图像中的独立文本块,并将每个块表示为一个独立实体,使用双换行符 ("\\n\\n") 分隔。  

3. **表格:** 识别图像中的任何表格。对于每个表格,以结构化的逗号分隔格式(.csv)输出。每行表格应放置在新行上,用逗号分隔列值。  
            - 包括(如有)表头行。  
            - 确保每一行的所有列都用逗号分隔。  

4. **输出格式:**  
            - 按照页面上的阅读顺序输出文本块和表格。当阅读页面时遇到表格,则在输出中的相应位置输出为 CSV 格式。  

5. 如果没有文本或表格,则返回空字符串。  
        如果表格仅含一行,则返回该行内容,用逗号分隔。  
         """  
    try:  
        # 将 PDF 的所有页面转换为 PIL 图像对象  
        images = convert_from_path(pdf_path)  

        if not images:  
            raise FileNotFoundError(f"无法将 PDF 转换为图像对象")  
        for i, img in enumerate(images):  
            page_number = i + 1  
            output_file_path = os.path.join(output_dir, f"{pdf_name}_{page_number}.txt")  

            try:  
               response = model.generate_content([prompt, img], generation_config={"max_output_tokens": 4096})  
               response.resolve()  
               with open(output_file_path, "w", encoding="utf-8") as f:  
                  f.write(response.text)  
               print(f"处理了第 {page_number} 页并保存到 {output_file_path}")  

            except Exception as page_err:  
               print(f"在处理第 {page_number} 页时遇到错误: {page_err}")  
               with open(output_file_path, "w", encoding="utf-8") as f:  
                  f.write(f"错误: 在处理第 {page_number} 页时发生错误:{page_err}")  
    except FileNotFoundError as e:  
        print(f"错误: 无法找到文件: {e}")  
    except Exception as e:  
        print(f"错误: 处理过程中发生错误: {e}")

它是怎么工作的:

  1. 设置步骤: 首先,我们导入所需的库并配置 Google Gemini API 的设置。
  2. PDF 转换: 然后,我们使用 pdf2image 将 PDF 的每一页转换成图像。
  3. 提示工程: 接着,提示引导模型按正确的顺序提取文本内容,将文本块分离并结构化表格。
  4. 图像处理: 接着,Gemini 模型分析图像并根据提示提取文本内容。
  5. 输出步骤: 最后,提取的文本和表格会被保存为每个 PDF 页面的独立文本文件。

优点:

  • 高准确性: VLMs 在处理少量页面时非常准确,能够很好地理解上下文和结构信息。
  • 简单性: 实现起来非常简单,特别是如果你熟悉 API 调用。
  • 表格处理: Gemini 可以提取结构化的 CSV 格式表格。

缺点:,

  • API费用: 例如Gemini这样的VLMs有一个相关的成本。
  • 扩展性: 处理非常大的PDF文件可能会非常慢并且成本很高。
要试试看!

这个链接指向了一个比特币发现基金的网站: 查看有关比特币发现基金的详情:https://www.energyfunders.com/bitcoindiscoveryfund

输出:

方法2:文档切分+OCR(光学字符识别)以实现更好的扩展性

对于更复杂的PDF文件,结合文档拆分和OCR技术是一种有效的策略。这种方法可以识别页面上的不同文本区域,然后对每个区域进行OCR处理。

我们将使用 YOLO 进行文档切分,使用 DocTr 进行 OCR。

Yolo模型链接:https://huggingface.co/DILHTWD/documentlayoutsegmentation_YOLOv8_ondoclaynet

代码实现:

from ultralytics import YOLO  
import fitz  
import os  
import pathlib  
from PIL import Image, ImageEnhance  
import numpy as np  
from doctr.io import DocumentFile  
from doctr.models import ocr_predictor  

# 要处理的示例PDF文件列表  
pdf_list = ['test.pdf']  
# 加载文档分割模型  
docseg_model = YOLO('yolov8x-doclaynet-epoch64-imgsz640-initiallr1e-4-finallr1e-5.pt')  
# 初始化一个字典来存储结果  
mydict = {}  

def enhance_image(img):  
    """应用图像增强以提高质量."""  
    # 增强清晰度  
    enhancer = ImageEnhance.Sharpness(img)  
    img = enhancer.enhance(1.5)  

    # 增强对比度  
    enhancer = ImageEnhance.Contrast(img)  
    img = enhancer.enhance(1.2)  

    # 增强颜色  
    enhancer = ImageEnhance.Color(img)  
    img = enhancer.enhance(1.1)  

    return img  

def process_pdf_page(pdf_path, page_num, docseg_model, output_dir):  
    """处理PDF中的单页,采用最高质量设置."""  

    pdf_doc = fitz.open(pdf_path)  
    page = pdf_doc[page_num]  

    # 提高分辨率矩阵以获得最高质量  
    zoom = 4  # 用于增加分辨率的放大因子  
    matrix = fitz.Matrix(zoom, zoom)  

    # 使用高质量渲染选项  
    pix = page.get_pixmap(  
        matrix=matrix,  
        alpha=False,  # 禁用alpha通道以获得更清晰的图像  
        colorspace=fitz.csRGB  # 强制使用RGB颜色空间  
    )  

    img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)  

    # 应用图像增强  
    img = enhance_image(img)  

    # 以高质量设置进行缩放  
    if zoom != 1:  
        original_size = (int(page.rect.width), int(page.rect.height))  
        img = img.resize(original_size, Image.Resampling.LANCZOS)  
    # 为页面图像生成一个临时文件名  
    temp_img_filename = os.path.join(output_dir, f"temp_page_{page_num}.png")  

    # 以最高质量设置保存图像  
    img.save(  
        temp_img_filename,  
        "PNG",  
        quality=100,  
        optimize=False,  
        dpi=(300, 300)  # 设置高DPI  
    )  
    # 对图像运行模型  
    results = docseg_model(source=temp_img_filename, save=True, show_labels=True, show_conf=True, boxes=True)  
    # 提取结果  
    page_width = page.rect.width  
    one_third_width = page_width / 3  

    all_coords = []  

    for entry in results:  
        thepath = pathlib.Path(entry.path)  
        thecoords = entry.boxes.xyxy.numpy()  
        all_coords.extend(thecoords)  

    # 将坐标分为两组,然后按y1排序每组  
    left_group = []  
    right_group = []  
    for bbox in all_coords:  
        x1 = bbox[0]  
        if x1 < one_third_width:  
            left_group.append(bbox)  
        else:  
            right_group.append(bbox)  

    left_group = sorted(left_group, key=lambda bbox: bbox[1])  
    right_group = sorted(right_group, key=lambda bbox: bbox[1])  

    sorted_coords = left_group + right_group  

    mydict[f"{pdf_path} Page {page_num}"] = sorted_coords  
    # 清理临时图像  
    os.remove(temp_img_filename)  
    pdf_doc.close()  

    # 处理列表中的每个PDF  
    for pdf_path in pdf_list:  
        try:  
            pdf_doc = fitz.open(pdf_path)  
            num_pages = pdf_doc.page_count  
            pdf_doc.close()  
            output_dir = os.path.splitext(pdf_path)[0] + "_output"  
            os.makedirs(output_dir, exist_ok=True)  
            for page_num in range(num_pages):  
                process_pdf_page(pdf_path, page_num, docseg_model, output_dir)  
        except Exception as e:  
            print(f"处理 {pdf_path} 时出错: {e}")  
    # 如果不存在,创建'tmp'目录  
    tmp_dir = 'tmp'  
    os.makedirs(tmp_dir, exist_ok=True)  
    # 遍历结果并保存裁剪的图像,采用最高质量  
    for key, coords in mydict.items():  
        pdf_name, page_info = key.split(" Page ")  
        page_number = int(page_info)  
        pdf_doc = fitz.open(pdf_name)  
        page = pdf_doc[page_number]  

        zoom = 4  
        matrix = fitz.Matrix(zoom,zoom)  
        for i, bbox in enumerate(coords):  
            # 适当缩放边界框坐标  
            xmin, ymin, xmax, ymax = map(lambda x: x , bbox)  

            # 使用边界框创建一个矩形  
            rect = fitz.Rect(xmin, ymin, xmax, ymax)  

            # 使用最大分辨率矩阵裁剪  
            cropped_pix = page.get_pixmap(  
                clip=rect,  
                matrix=matrix,  
                alpha=False,  
                colorspace=fitz.csRGB  
            )  

            cropped_img = Image.frombytes("RGB", [cropped_pix.width, cropped_pix.height], cropped_pix.samples)  
            cropped_img = enhance_image(cropped_img)  

            output_filename = os.path.join(tmp_dir, f"{os.path.splitext(os.path.basename(pdf_name))[0]}_page{page_number}_{i}.png")  

            # 保存裁剪的图像  
            cropped_img.save(output_filename, "PNG", quality=100, optimize=False, dpi=(300, 300))  
        pdf_doc.close()  

    def extract_text_from_image(image_path, model):  
        """使用DocTr从单个图像中提取文本."""  
        doc = DocumentFile.from_images(image_path)  
        result = model(doc)  
        text_content = ""  
        for page in result.pages:  
            for block in page.blocks:  
                for line in block.lines:  
                    for word in line.words:  
                        text_content += word.value + " "  
                text_content += "\n"  
        return text_content.strip()  

    def process_cropped_images(tmp_dir, pdf_list):  
        """遍历裁剪的图像,使用DocTr提取文本并将其存储在文本文件中."""  

        doctr_model = ocr_predictor(pretrained=True)  

        for pdf_path in pdf_list:  
            pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]  
            output_txt_path = f"{pdf_name}_extracted_text.txt"  

            with open(output_txt_path, 'w', encoding='utf-8') as outfile:  

                pdf_doc = fitz.open(pdf_path)  
                num_pages = pdf_doc.page_count  
                pdf_doc.close()  
                for page_num in range(num_pages):  

                    outfile.write(f"Page: {page_num}\n")  

                    # 按块顺序对裁剪的图像文件名进行排序  
                    cropped_images_for_page = sorted([  
                        f for f in os.listdir(tmp_dir)  
                        if f.startswith(f"{pdf_name}_page{page_num}_") and f.endswith(".png")  
                    ], key=lambda f: int(f.split("_")[-1].split(".")[0]))  

                    for i, image_filename in enumerate(cropped_images_for_page):  
                        image_path = os.path.join(tmp_dir, image_filename)  
                        text = extract_text_from_image(image_path, doctr_model)  
                        outfile.write(f"块 {i}: {text}\n")  
            print(f"从{pdf_name}提取的文本已保存到{output_txt_path}")  

    # 示例用法:  
    tmp_dir = 'tmp'  # 确保你的tmp目录存在  
    pdf_list = ['test.pdf']  # 你的PDF列表  
    process_cropped_images(tmp_dir, pdf_list)

它是怎么工作的:

  1. 设置: 导入库并加载用于文档分割的YOLO模型和用于OCR的DocTr模型。
  2. 图像预处理: 使用fitz(PyMuPDF)加载PDF,提高分辨率并增强图像,从而提升OCR的识别质量。保存临时图像。
  3. 文档分割: YOLO模型识别并定位页面上的文本块及其他元素。
  4. 裁剪: 根据边界框裁剪每个部分,应用图像增强技术,并将裁剪后的图像保存到临时目录中。
  5. OCR应用: 使用DocTr模型从每个裁剪部分提取文本内容。
  6. 输出: 将提取的文本按每页的节/块编号保存到文本文件中,每个部分都有对应的编号。

优点:

  • 可扩展性: 可高效处理大量页面的大量页面。
  • 在复杂布局中准确性: 文档分割技术通过将OCR聚焦于特定区域上来提高准确性。
  • 免费/开源: 使用的模型是开源的,降低了成本。
  • 灵活性和定制化: 提供根据准确性和速度需求在不同OCR模型间切换的功能。此外,用户可以集成专门用于提取复杂表格数据的VLM(视觉语言模型),同时使用传统OCR处理普通文本,从而实现最佳效果并提供定制解决方案。

不足:

  • 更复杂的实现: 需要使用更多的库和步骤来完成。
  • 准确性权衡: 此方法可能无法达到视觉语言模型那样的完美精度,特别是在处理复杂布局或不常见字体的情况下。可能会有文字或表格结构的细微差异被忽略或误解。
咱们试试吧!

https://www.energyfunders.com/bitcoindiscoveryfund

输出:

  • 使用其他OCR模型进行测试以找到最适合的模型。
  • 建议: DocTrPaddleOCR 或者甚至可以使用VLM处理表格,OCR处理文本框。
结论,

从复杂的扫描PDF中提取文本可能会很棘手,但并非不可能。视觉语言模型在识别较小文件时更为精确,而将文档分割与OCR技术结合使用则更适合处理较大的文件。选择最佳方法将取决于您的需求、文档页数以及可用资源。希望这次深入探讨能帮助您更好地利用扫描的PDF。

联系我 : https://www.linkedin.com/in/yash-bhaskar/
更多精彩文章 : https://medium.com/@yash9439

0人推荐
随时随地看视频
慕课网APP