识别扫描版本的PDF中是不是存在跨页面的表格

识别扫描PDF跨页表格方法

识别扫描版本的PDF中是不是存在跨页面的表格,如果存在跨页面的表格,需要转化成两张图片一起发送给视觉模型,进行识别。

import fitz  # PyMuPDF
import numpy as np
import os
import cv2
import math


# ==============================================================================
# --- 集中式参数配置中心 (V5 - Final Corrected Version) ---
# ==============================================================================
class Config:
    # --- 基础设置 ---
    DPI = 200
    SCAN_HEIGHT = 150
    # ** 核心修正:根据您的建议,移除安全边距,从页面绝对边缘开始扫描 **
    HEADER_FOOTER_MARGIN_POINTS = 0

    # --- 图像预处理与形态学参数 ---
    # 保持一个较为通用的内核宽度
    MORPH_KERNEL_WIDTH = 40

    # --- 霍夫变换参数 (大幅放宽,确保“宽进”) ---
    # ** 核心修正:大幅降低投票阈值,确保能检测到第一页底部的真实线条 **
    HOUGH_THRESHOLD = 20
    # 恢复较大的线间隙,以连接扫描件中断裂的真实线条
    MAX_LINE_GAP = 40
    # 稍微降低最小线长要求,以应对页边距
    MIN_LINE_LENGTH_RATIO = 0.3

    # --- 智能过滤参数 (保持“严出”) ---
    # 第一层:单条线属性
    MAX_SKEW_ANGLE_DEGREES = 5.0

    # 第二层:结构化上下文分析
    MIN_PARALLEL_LINES_FOR_TABLE = 2
    PARALLEL_LINE_MAX_DISTANCE_PX = 60
    PARALLEL_LINE_MAX_ANGLE_DIFF_DEGREES = 2.0

    # --- 调试设置 ---
    DEBUG_MODE = True
    DEBUG_DIR = "debug_images"


# ==============================================================================
# --- 核心算法实现 (V5 - Final Corrected Version) ---
# ==============================================================================

def calculate_line_properties(x1, y1, x2, y2):
    """计算直线的长度、角度和中心Y坐标"""
    length = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
    angle_rad = math.atan2(y2 - y1, x2 - x1)
    angle_deg = math.degrees(angle_rad)
    center_y = (y1 + y2) / 2
    return length, angle_deg, center_y


def has_table_features_at_edge_by_clipping(page, region):
    """
    [终极修正版] 修正扫描区域,并使用平衡的“检测-验证”模型。
    """
    # 1. 修正后的安全边距裁剪
    page_rect = page.rect
    # margin_pixels 现在为0,但保留变量以便理解
    margin_pixels = int(Config.HEADER_FOOTER_MARGIN_POINTS * (Config.DPI / 72.0))

    if region == 'bottom':
        # 从页面绝对底部向上扫描
        y0 = page_rect.height - Config.SCAN_HEIGHT
        y1 = page_rect.height
        clip_rect = fitz.Rect(0, y0, page_rect.width, y1)
    else:  # region == 'top'
        # 从页面绝对顶部向下扫描
        y0 = 0
        y1 = Config.SCAN_HEIGHT
        clip_rect = fitz.Rect(0, y0, page_rect.width, y1)

    clip_rect.normalize()
    if not page_rect.contains(clip_rect) or clip_rect.is_empty: return False
    pix = page.get_pixmap(dpi=Config.DPI, clip=clip_rect)
    if pix.width == 0 or pix.height == 0: return False

    # 2. 图像预处理与形态学增强
    img_cv = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, pix.n)
    img_for_drawing = cv2.cvtColor(img_cv, cv2.COLOR_GRAY2BGR) if pix.n == 1 else img_cv.copy()
    img_gray = cv2.cvtColor(img_for_drawing, cv2.COLOR_BGR2GRAY)
    img_binary = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)
    horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (Config.MORPH_KERNEL_WIDTH, 1))
    detected_lines_img = cv2.morphologyEx(img_binary, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)

    # 3. 霍夫变换检测直线 (使用宽松参数)
    min_line_length = pix.width * Config.MIN_LINE_LENGTH_RATIO
    lines = cv2.HoughLinesP(detected_lines_img, 1, np.pi / 180, threshold=Config.HOUGH_THRESHOLD,
                            minLineLength=min_line_length, maxLineGap=Config.MAX_LINE_GAP)

    # 4. 结构化上下文验证 (保持严格)
    # ... (此部分验证逻辑无需修改)
    candidate_lines = []
    if lines is not None:
        for line in lines:
            x1, y1, x2, y2 = line[0]
            length, angle, center_y = calculate_line_properties(x1, y1, x2, y2)
            if abs(angle) < Config.MAX_SKEW_ANGLE_DEGREES:
                candidate_lines.append({'length': length, 'angle': angle, 'center_y': center_y, 'coords': line[0]})

    found_table_structure = False
    validated_lines_info = None
    if candidate_lines:
        for i, current_line in enumerate(candidate_lines):
            parallel_neighbors_count = 1
            neighbors = []
            for j, other_line in enumerate(candidate_lines):
                if i == j: continue
                y_distance = abs(current_line['center_y'] - other_line['center_y'])
                angle_diff = abs(current_line['angle'] - other_line['angle'])
                if (
                        0 < y_distance < Config.PARALLEL_LINE_MAX_DISTANCE_PX and angle_diff < Config.PARALLEL_LINE_MAX_ANGLE_DIFF_DEGREES):
                    parallel_neighbors_count += 1
                    neighbors.append(other_line)

            if parallel_neighbors_count >= Config.MIN_PARALLEL_LINES_FOR_TABLE:
                found_table_structure = True
                validated_lines_info = {'main': current_line, 'neighbors': neighbors}
                if Config.DEBUG_MODE:
                    print(
                        f"  -> [成功] Page {page.number + 1}/{region}: 发现表格结构 (中心线在 y={current_line['center_y']:.0f}, 拥有 {len(neighbors)} 个邻居)")
                break

    # 5. 调试输出... (无需修改)
    if Config.DEBUG_MODE:
        # ... (调试代码保持原样)
        debug_filename_base = os.path.join(Config.DEBUG_DIR, f"page_{page.number + 1}_{region}")
        cv2.imwrite(f"{debug_filename_base}_3_morph_horizontal.png", detected_lines_img)

        if candidate_lines:
            for line in candidate_lines:
                x1, y1, x2, y2 = line['coords']
                cv2.line(img_for_drawing, (x1, y1), (x2, y2), (0, 255, 0), 1)

        if found_table_structure:
            main_line = validated_lines_info['main']
            x1, y1, x2, y2 = main_line['coords']
            cv2.line(img_for_drawing, (x1, y1), (x2, y2), (0, 0, 255), 2)
            for neighbor in validated_lines_info['neighbors']:
                x1, y1, x2, y2 = neighbor['coords']
                cv2.line(img_for_drawing, (x1, y1), (x2, y2), (255, 0, 0), 2)

        cv2.imwrite(f"{debug_filename_base}_4_final_result.png", img_for_drawing)

        if not candidate_lines:
            if Config.DEBUG_MODE: print(f"[信息] Page {page.number + 1}/{region}: 未找到任何候选线条。")
        elif not found_table_structure:
            if Config.DEBUG_MODE: print(f"[信息] Page {page.number + 1}/{region}: 所有候选线均未通过结构化上下文验证。")

    return found_table_structure


# --- 主执行函数等 (无需修改) ---
def find_split_tables_in_scanned_pdf(pdf_path):
    # ... (此函数无需修改)
    split_tables_pages = []
    try:
        doc = fitz.open(pdf_path)
        if doc.page_count < 2: return []
        for i in range(doc.page_count - 1):
            current_page = doc.load_page(i)
            next_page = doc.load_page(i + 1)

            print(f"\n--- 正在分析 Page {i + 1} 底部 和 Page {i + 2} 顶部 ---")
            has_table_bottom = has_table_features_at_edge_by_clipping(current_page, 'bottom')
            has_table_top = has_table_features_at_edge_by_clipping(next_page, 'top')

            if has_table_bottom and has_table_top:
                split_tables_pages.append([i + 1, i + 2])
                print(f"*** 检测到跨页表格: 从第 {i + 1} 页到第 {i + 2} 页 ***")

    except Exception as e:
        print(f"[警告] 图像分析过程失败: {e}")
    return split_tables_pages


def run_main_analysis(pdf_file_path):
    # ... (此函数无需修改)
    if not os.path.exists(pdf_file_path):
        print(f"错误:文件 '{pdf_file_path}' 不存在。")
        return

    print(f"开始分析文件: '{os.path.basename(pdf_file_path)}'")

    if Config.DEBUG_MODE:
        if not os.path.exists(Config.DEBUG_DIR):
            os.makedirs(Config.DEBUG_DIR)
        print(f"调试模式已开启。中间过程图片将保存在 '{Config.DEBUG_DIR}' 目录下。")

    print("\n--- 切换到图像分析模式 ---")
    split_info = find_split_tables_in_scanned_pdf(pdf_file_path)

    print("\n" + "=" * 40)
    print("          最终分析报告")
    print("=" * 40)
    if not split_info:
        print(f"在文件中没有检测到跨页的表格。")
    else:
        print(f"在文件中检测到以下跨页表格:")
        for pages in split_info:
            print(f"  -> 表格从第 {pages[0]} 页跨到了第 {pages[1]} 页。")
    print("=" * 40)


if __name__ == "__main__":
    pdf_file_path = "wanzheng-15-17.pdf"
    run_main_analysis(pdf_file_path)

这段代码的目标(一句话)

在扫描/图像型 PDF 中,判断每一页底部和下一页顶部窄条区域里是否出现“像表格的横线结构”,如果两处都检测到,就判定该表格是跨页(从第 i 页延续到第 i+1 页)。

总体流程图(逻辑)
1. 遍历 doc 的相邻两页(i 与 i+1)
2. 在第 i 页的 bottom 区域做“表格线”检测
3. 在第 i+1 页的 top 区域做“表格线”检测
4. 若两者都为真 → 记录为“跨页表格 i→i+1”
5. 输出报告;若开了 DEBUG,保存中间图像(形态学结果、画线可视化等)

参数区 Config(为什么这样配)
• DPI = 200
渲染 PDF 为位图时的分辨率;DPI 越高,像素越多,线更清晰,但运算更慢。200 是一个在“精度/速度”之间折中且对线条足够友好的值。
• SCAN_HEIGHT = 150(单位:PDF 点,1 inch=72 pt)
只在页面边缘的条带区域内扫描。以 200 DPI 渲染时,可得到约 150 * 200/72 ≈ 416 像素的扫描高度,既能覆盖页眉/页脚,也不至于扫太多干扰。
• HEADER_FOOTER_MARGIN_POINTS = 0
明确从绝对边缘开始扫(之前版本可能留了安全边距导致漏检)。
• 形态学 & 霍夫参数
• MORPH_KERNEL_WIDTH = 40:水平方向开运算(开 = 先腐蚀后膨胀)用的矩形核,宽一些能把横线凸显出来、干扰点去掉。
• HOUGH_THRESHOLD = 20、MAX_LINE_GAP = 40、MIN_LINE_LENGTH_RATIO = 0.3:
放宽霍夫直线的门槛(“宽进”),允许断裂线被连起来,并要求线段长度 ≥ 宽度的 30%。
• 智能过滤(“严出”):
• MAX_SKEW_ANGLE_DEGREES = 5.0:只要近似水平的线;
• “并行线组”判定:要求至少 MIN_PARALLEL_LINES_FOR_TABLE = 2 条、且行间距 < 60 像素、夹角差 < 2°。
这一步是关键:不是有一根横线就算表格,必须有多条近似平行的横线,更像表格的“行网格”。
• DEBUG_MODE 与 DEBUG_DIR:会把形态学结果、画过线的可视化图另存,便于调参。

核心检测函数逐行讲解

  1. calculate_line_properties(…)
    • 给一条线段(x1,y1,x2,y2)计算:长度、角度(°,用 atan2)、中心 y 值。
    • 角度用于“是否近似水平”的过滤;中心 y 用于比较两条线的“上下距离”(行间距)。

  2. has_table_features_at_edge_by_clipping(page, region)

作用:在某页的“顶部/底部”条带区域,判断是否存在表格样式的横线结构。

关键步骤:
1. 裁剪并渲染
• 用 fitz.Rect 定出 top/bottom 的条带区域(单位是 pt),然后

pix = page.get_pixmap(dpi=Config.DPI, clip=clip_rect)

得到该条带的位图(像素空间)。
这里的单位转换由 PyMuPDF 内部完成:pt(PDF 坐标)→ 按 DPI 渲染 → 像素宽高。

  1. 图像预处理
    • 把 pix.samples 转为 H×W×C 的 numpy 数组;
    • 统一为 BGR(或灰度)后转灰度:
img_gray = cv2.cvtColor(img_for_drawing, cv2.COLOR_BGR2GRAY)

自适应阈值 + 取反(线通常比背景暗,取反后成为白色,利于形态学处理):

img_binary = cv2.adaptiveThreshold(..., cv2.THRESH_BINARY_INV, ...)

水平方向开运算((40,1) 的核)突出横线,去掉小噪点:

detected_lines_img = cv2.morphologyEx(img_binary, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
  1. 霍夫直线检测(宽松找线)

• minLineLength = pix.width * 0.3
• HoughLinesP(…, threshold=20, maxLineGap=40)

目标是尽量不漏:把潜在的横线都召回。
4. 结构化验证(严格筛)
• 先按角度过滤:abs(angle) < 5° → 候选线
• 对每根候选线,找“并行邻居”:
• 中心 y 距离 < 60 像素
• 夹角差 < 2°
• 只要一根线有足够多并行邻居(≥2 条),就判定这一页的该区域“有表格结构”。
5. 调试输出
• 保存形态学图(黑白)
• 把候选线画成绿色,通过验证的画红/蓝
• 打印“找到/没找到”的提示

为什么这样能识别“表格”
常见扫描表格的横向分隔线是一组近似等间距、近似水平的线段。单根横线可能是页眉横线、页脚细线或装订阴影;但多条近似平行且在条带里聚集,很大概率是表格的行分隔线。

  1. find_split_tables_in_scanned_pdf(pdf_path)
    • 顺序遍历 0…page_count-2:对每一对相邻页 (i, i+1)
    • 取第 i 页 bottom 条带做检测;
    • 取第 i+1 页 top 条带做检测;
    • 二者都真 → 记录 [i+1, i+2] 为一次跨页表格(页号按人类习惯从 1 起)。
    • 返回所有跨页对。

  2. run_main_analysis(pdf_file_path)
    • 检查文件、准备 DEBUG 目录
    • 调 find_split_tables_in_scanned_pdf
    • 汇总打印最终报告(没检测到/检测到了哪些跨页区间)

两个容易忽略的细节(建议立刻修正)

❶ RGBA → 灰度的通道数问题

有些 PDF 渲染出来是 4 通道(带 Alpha)。你这里对 pix.n == 4 时直接:

img_for_drawing = img_cv.copy()
img_gray = cv2.cvtColor(img_for_drawing, cv2.COLOR_BGR2GRAY)

会触发 OpenCV 报错(输入是 4 通道,转换却用 BGR→GRAY)。

更稳的写法(推荐其一):
• 方案 A:渲染时直接去 Alpha:

pix = page.get_pixmap(dpi=Config.DPI, clip=clip_rect, alpha=False)  # 这样就只有 1 或 3 通道
•	方案 B:按通道数分别处理:
if pix.n == 1:
    img_for_drawing = cv2.cvtColor(img_cv, cv2.COLOR_GRAY2BGR)
elif pix.n == 3:
    img_for_drawing = img_cv  # 已是RGB/BGR序,不影响后续
elif pix.n == 4:
    # 丢弃Alpha:RGBA -> BGR
    img_for_drawing = cv2.cvtColor(img_cv, cv2.COLOR_RGBA2BGR)
else:
    raise ValueError(f"Unsupported channel count: {pix.n}")
img_gray = cv2.cvtColor(img_for_drawing, cv2.COLOR_BGR2GRAY)

❷ 形态学核宽度与 DPI/版心宽度的尺度

MORPH_KERNEL_WIDTH = 40 在 200 DPI、A4 横向像素约 1650–1750 时通常够用,但如果:
• DPI 提高到 300,或
• 页面实际表格列距更大、横线更粗
可能需要随宽度自适应(更稳):

adaptive_kernel = max(20, int(pix.width * 0.025))  # 宽度的2.5%为核宽
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (adaptive_kernel, 1))

可能的误报与改进建议
1. 页眉/页脚装饰线造成误报
• 你的“并行线组”已能过滤掉很多噪声,但若文档的页脚本身就有 2–3 条装饰线,可能仍会误报。
改进:
• 再加一条规则:“通过线段的水平投影长度”(即线段覆盖的 X 方向跨度)要接近页面宽度的某个比例(如 ≥ 60%),或
• 检查“等间距性”:邻居中心 y 的差值是否近似一致(标准差较小)。
2. 没有线框的“无边框表格”
• 这种纯靠文本对齐的表格,图像形态学+霍夫直线不好识别。
改进路线:
• 追加一条“文本对齐度”的检测(如果是矢量文本 PDF,可配合 pdfplumber 的字符坐标判断列齐性),或
• 引入版面检测模型(layoutparser/DocLayNet/PubLayNet),但这就超出纯 OpenCV 方案。
3. 只看横线,没看竖线
• 本代码只针对“横向分隔线组”。如果你想进一步提高置信度,可以在相同区域再做一次纵向检测:
• 把核换成 (1, k),看是否也存在多条近似垂直的并行线;横竖都满足更像表格。

这段代码好用的点
• 算力轻:纯 OpenCV + PyMuPDF,无需部署大模型或检测网络;
• 可解释:每个阈值/核都能在 DEBUG 图里看到效果,便于“就地调参”;
• 鲁棒性思路正确:**宽进(霍夫宽松)+严出(并行线组验证)**是做版面启发式的常见套路。

小结
• 代码核心:**在页面边缘条带内,用形态学 + 霍夫检测“多条近似平行的横线”**来定义“表格特征”。
• find_split_tables_in_scanned_pdf 用“上一页底部 + 下一页顶部”同时为真的条件来判定跨页表格。
• 建议立刻应用的两点修正:去 Alpha 通道、形态学核宽度随宽度或 DPI 自适应。
• 若仍有误报,可加“横向投影长度阈值”“等间距指标”,若有漏报(无边框表),则考虑“文本对齐度”或版面检测模型作补充。

如果你愿意,我可以把上面两处修正和“横向投影长度判定/等间距性”都直接合进你的函数,给你一份可替换的 has_table_features_at_edge_by_clipping 改良版。

<think>嗯,用户想要从扫描版的PDF中提取表格数据,我得想想有什么方法或者工具可以实现这个。首先,扫描版的PDF其实就是图片,里面的文字不能直接复制,所以必须用OCR技术来识别文字和表格结构。我记得之前看过几个相关的工具,比如OCR-Table和OCR表格提取这两个开源项目,可能可以推荐给用户。另外,万知网站好像也提供这样的服务,可能更方便一些。 用户提到OCR技术可能遇到的问题,比如图像质量差、字体复杂或者对比度低,这些都会影响识别效果。所以在回答时,需要说明处理步骤,比如预处理PDF、使用工具的具体操作,以及后期检查的重要性。还有,要分步骤讲清楚,可能用户不太熟悉技术细节,所以语言要尽量简洁明了。 然后,根据引用内容,OCR-Table和OCR表格提取都是基于Python的开源工具,使用Tesseract OCR引擎,可能还需要配合其他工具比如Imagemagick。用户可能需要安装这些依赖,然后运行命令行脚本。不过对于非技术人员来说,可能命令行操作有点难度,这时候可以推荐万知的在线工具,上传文件就能处理,更简单。 还要注意提到如何处理扫描PDF的特殊情况,比如先转换成图片、调整分辨率,或者使用图像处理技术增强对比度,这些预处理步骤能提高OCR的准确性。另外,不同语言的识别问题,可能需要用户选择适合的OCR语言包,比如中文的。 最后,生成相关问题的时候,要覆盖用户可能关心的其他方面,比如开源工具推荐、提高准确率的方法、处理中文表格的注意事项,以及在线工具的优势。这样用户如果有进一步的问题,也能找到答案。</think>### 从扫描PDF中提取表格数据的OCR技术实现步骤 #### 1. **预处理PDF文件** - 将扫描PDF转换为高分辨率图像(如PNG或TIFF格式),可使用工具如`Imagemagick`或`pdftoppm`[^2] - 调整图像对比度和亮度,增强文字与背景的区分度,例如使用OpenCV的直方图均衡化[^3] #### 2. **选择OCR工具** - **开源方案**:使用`OCR-Table`或`OCR表格提取`项目(基于Tesseract OCR引擎) ```bash # 示例:使用OCR表格提取工具 git clone https://github.com/cseas/ocr-table.git python ocr_table.py -i input.pdf -o output.txt ``` - **在线工具**:万知(https://www.wanzhi.com/chatwithdoc)直接上传PDF自动处理[^4] #### 3. **表格结构识别** - 通过Tesseract的布局分析功能检测表格边界,结合Hough变换识别横纵线[^1] - 使用`pytesseract`库提取文本坐标信息: ```python import pytesseract from PIL import Image data = pytesseract.image_to_data(Image.open('page_1.png'), output_type=pytesseract.Output.DICT) ``` #### 4. **数据对齐与修正** - 根据坐标信息重建表格行列结构,处理合并单元格等复杂情况[^1] - 使用正则表达式匹配金额、日期等特定格式数据 #### 5. **输出与验证** - 生成CSV/TXT文件后,需人工核对关键数据(如发票号、金额) - 对识别错误区域可手动标注重新识别 $$ \text{OCR识别率公式:} \quad \eta = \frac{N_{\text{正确字符}}}{N_{\text{总字符}}} \times 100\% $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MonkeyKing.sun

对你有帮助的话,可以打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值