识别扫描版本的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:会把形态学结果、画过线的可视化图另存,便于调参。
⸻
核心检测函数逐行讲解
-
calculate_line_properties(…)
• 给一条线段(x1,y1,x2,y2)计算:长度、角度(°,用 atan2)、中心 y 值。
• 角度用于“是否近似水平”的过滤;中心 y 用于比较两条线的“上下距离”(行间距)。 -
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 渲染 → 像素宽高。
- 图像预处理
• 把 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)
- 霍夫直线检测(宽松找线)
• minLineLength = pix.width * 0.3
• HoughLinesP(…, threshold=20, maxLineGap=40)
目标是尽量不漏:把潜在的横线都召回。
4. 结构化验证(严格筛)
• 先按角度过滤:abs(angle) < 5° → 候选线
• 对每根候选线,找“并行邻居”:
• 中心 y 距离 < 60 像素
• 夹角差 < 2°
• 只要一根线有足够多并行邻居(≥2 条),就判定这一页的该区域“有表格结构”。
5. 调试输出
• 保存形态学图(黑白)
• 把候选线画成绿色,通过验证的画红/蓝
• 打印“找到/没找到”的提示
为什么这样能识别“表格”
常见扫描表格的横向分隔线是一组近似等间距、近似水平的线段。单根横线可能是页眉横线、页脚细线或装订阴影;但多条近似平行且在条带里聚集,很大概率是表格的行分隔线。
⸻
-
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 起)。
• 返回所有跨页对。 -
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 改良版。
识别扫描PDF跨页表格方法
1107

被折叠的 条评论
为什么被折叠?



