有线表格识别技术文档.md 11 KB

有线表格识别技术文档

本文档详细说明 UNET 有线表格识别模块的技术实现细节,适用于开发人员进行二次开发和问题排查。

概述

有线表格识别模块位于 models/adapters/wired_table/,提供基于深度学习的表格线检测、网格恢复和文本填充功能。与 VLM 方法相比,UNET 方法更适合处理规则的有线表格,具有更高的精度和更快的处理速度。

模块架构

wired_table/ ├── init.py # 模块初始化 ├── debug_utils.py # 调试工具(可视化输出) ├── ocr_formatter.py # OCR 格式转换 ├── skew_detection.py # 倾斜检测与矫正 ⭐ ├── grid_recovery.py # 网格恢复(表格线 → 单元格)⭐ ├── text_filling.py # 文本填充(OCR → 单元格)⭐⭐⭐ ├── html_generator.py # HTML 生成 └── visualization.py # 可视化工具

主入口:models/adapters/mineru_wired_table.py - MinerUWiredTableRecognizer


核心流程

完整处理流程

graph TB
    A[输入:表格图片 + OCR框] --> B[1. OCR预处理<br/>ocr_formatter.py]
    B --> C[2. UNET线检测<br/>MinerU模型]
    C --> D{启用倾斜矫正?}
    
    D -->|是| E[3. 倾斜检测<br/>skew_detection.py]
    D -->|否| G
    
    E --> F[4. 图片与坐标矫正<br/>cv2.warpAffine]
    F --> G[5. 网格恢复<br/>grid_recovery.py]
    
    G --> H[6. 文本填充<br/>text_filling.py]
    H --> I[7. HTML生成<br/>html_generator.py]
    
    I --> J[8. 坐标逆转换<br/>回到原图坐标系]
    J --> K{识别成功?}
    
    K -->|是| L[返回HTML + 坐标]
    K -->|否| M[Fallback到VLM]
    
    style E fill:#e1f5ff
    style F fill:#e1f5ff
    style G fill:#fff4e1
    style H fill:#fff4e1

六、单元格 OCR:文本填充机制(text_filling.py)⭐⭐⭐

这是整个表格识别中最复杂的部分。银行流水等场景的 OCR 质量直接影响最终结果,本节详细说明其设计思路和实现细节。

6.1 为什么需要二次 OCR?

一次 OCR(整页识别)存在天然局限:

问题 表现 原因
检测遗漏 单元格内无文字 表格线干扰、文字过小、字号不统一
识别不完整 只识别到"支行"而非完整户名 文字偏单元格底部,上半部分被忽略
水印干扰 "有限公司"被误识别为金额 斜向水印与正常文字重叠在同一单元格
低分碎片 识别结果置信度低 背景噪声、对比度不足

二次 OCR 在单元格裁剪图上重新识别,预处理空间更大,精度更高。

6.2 核心设计理念

"先匹配,后反刍"

  1. 一验fill_text_by_center_point):将整页 OCR 结果按中心点 + 重叠比例匹配到单元格
  2. 触发判定_should_second_pass_cell):判断哪些格需要二次 OCR
  3. 二验second_pass_ocr_fill):对判定需重处理的格,裁剪后再做 OCR

    graph TB
    subgraph 一验:整页OCR匹配
        A[整页OCR结果<br/>ocr_boxes] --> B[遍历每个单元格]
        B --> C{中心点+重叠比例<br/>匹配到OCR框?}
        C -->|是| D[斜框过滤<br/>_is_bbox_slanted >10°?]
        D -->|否,水平| E[纵向完整性检测<br/>_is_ocr_vertically_incomplete?]
        D -->|是,斜框| D2[丢弃该框<br/>水印豁免]
        E -->|不完整| E2[清空text/score<br/>→ 加入need_reocr_indices]
        E -->|完整| F[嵌套框处理<br/>_resolve_cell_matched_boxes]
        F --> G[拼接文本 + 计算置信度]
        C -->|否| H[text=""<br/>score=0.0]
    end
        
    subgraph 触发判定:_should_second_pass_cell
        I{是否需要二次OCR?} --> J[force_all]
        I --> K[spanning/cross_cell]
        I --> L[low_first_pass_score <0.9]
        I --> M[tall_cell_low_score]
        I --> N{bank_statement<br/>空格特殊逻辑}
        N -->|表头空格| N1[header_row_empty → 触发]
        N -->|表体空格| N2[_column_empty_ratio<br/>matched_boxes_list判空]
        N2 -->|列大部分有框| N3[body_row_empty_column_mostly_filled → 触发]
        N2 -->|列大部分无框| N4[跳过,列本身就空]
        I --> O{任何reason命中?}
    end
        
    subgraph 二验:单元格裁剪OCR
        P[裁剪cell图像] --> Q[Pass1: 格级预处理<br/>去水印 + upscale + OCR]
        Q --> R{Pass1分数达标?}
        R -->|是| S[采纳Pass1结果]
        R -->|否| T[Pass2: enhance_retry<br/>更强预处理 + OCR]
        T --> U[择优:pick_better]
        U --> V[更新texts]
    end
        
    G --> I
    H --> I
    D2 --> F
    E2 --> I
    O -->|是| P
    O -->|否| W[保持一验结果]
        
    style D fill:#ffe1e1
    style E fill:#ffe1e1
    style F fill:#e1f5ff
    style N fill:#e1f5ff
    style N2 fill:#e1f5ff
    style Q fill:#fff4e1
    style T fill:#fff4e1
    

6.3 一验匹配策略

中心点 + 重叠比例

# 第一步:中心点筛选
if not (cell_x1 <= cx <= cell_x2 and cell_y1 <= cy <= cell_y2):
    continue  # 中心点必须在单元格内

# 第二步:重叠比例检查
overlap_ratio = calculate_overlap_ratio(ocr_bbox, cell_bbox)
if overlap_ratio >= 0.5:  # OCR框至少50%在单元格内
    matched.append(...)

相比纯 IOU 更宽松,能匹配到更多 OCR 框;比纯中心点更准确,能过滤跨单元格的 OCR 框。

嵌套框处理(_resolve_cell_matched_boxes

一个单元格内可能出现大框套小框的情况:

场景 处理
大框有字、小框在内 丢弃小框碎片,保留大框
大框无字、小框在内 丢弃小框,整格 score=0 → 触发二次 OCR

斜框过滤(_is_bbox_slanted

银行流水的水印通常为 30-45° 斜向,与正常水平文字(0-2°)角度差异明显:

@staticmethod
def _is_bbox_slanted(original_box, *, angle_threshold=10.0):
    """基于原始多边形角度检测。上边与水平面夹角 > angle_threshold → 斜框。"""
    poly = box.get("poly") or box.get("original_bbox") or box.get("bbox") or []
    # 计算上边 dx, dy → atan2 → 角度
    angle_deg = abs(math.degrees(math.atan2(dy, dx)))
    return angle_deg > angle_threshold

为什么放在 _resolve_cell_matched_boxes 中? 斜框丢弃与嵌套框丢弃的语义一致——都是"某个匹配 OCR 框不应被保留"。

纵向完整性检测(_is_ocr_vertically_incomplete

银行流水单元格文字纵向居中。若一次 OCR 只匹配到局部字(如"支行"),需要触发二次 OCR。

两种判定互补

条件 原理 示例
y_center 偏移 合并 bbox 的 y_center 偏离 cell y_center 超过 cell_h * 1/3 OCR 框完全跑到单元格底部
空白不对称 上下空白占比差 > 0.3(文字贴顶部或底部) "支行" 36px 高偏在 86px 高单元格的底部
# 条件一:y_center 偏移
deviation = abs(merged_yc - cell_yc)
if deviation > cell_h * y_deviation_ratio:  # 默认 1/3
    return True

# 条件二:空白不对称
top_gap = merged_y1 - cell_y1
bottom_gap = cell_y2 - merged_y2
asymmetry = abs(top_gap/cell_h - bottom_gap/cell_h)
return asymmetry > margin_asymmetry_threshold  # 默认 0.3

为什么不单纯用 y_center 偏移? "支行"偏底部但 y_center 偏差不大(20.5px < 28.6px),单靠 y_center 条件无法检测。空白不对称(上方 53% vs 下方 5% → 0.48)才能正确触发。

6.4 bank_statement 空单元格特殊逻辑

银行流水的表格结构具有规律性,可以对空单元格做更智能的判断。

问题

如果一列的多数格本身都是空的(如"附言"列),那么其中一个空格就可能是真的空,不应触发二次 OCR。反之,如果一列大部分有文字,某个空格大概率是 OCR 遗漏。

实现:基于 matched_boxes_list 的列空判断

关键设计决策:用 matched_boxes_list(是否命中 OCR 框)而非 texts(是否识别出文字)判空。

matched_boxes_list[i] texts[i] 含义
[{text: '', bbox: [...]}] "" OCR 检到了框但未识别 → 列不空
[] "" 真的空 → 列空
[{text: '广东农信', ...}] "广东农信" 正常

因为 OCR 可能检测出框(说明有内容)但识别失败(text 为空)。用 texts 判空会把"有框无字"的格误判为该列为空 → 跳过二次 OCR → 本该识别的内容永远丢失。

@staticmethod
def _column_empty_ratio(merged_cells, matched_boxes_list, col, header_row):
    col_cells = [
        matched_boxes_list[j]  # 基于已匹配的 OCR 框判空
        for j, c in enumerate(merged_cells)
        if int(c.get("col")) == col and int(c.get("row")) > header_row
    ]
    return sum(1 for boxes in col_cells if not boxes) / len(col_cells)

触发规则

场景 条件 触发原因
表头行空格 row == header_row header_row_empty
表体行空格 + 列大部分有 OCR 框 col_empty_ratio < 0.5 body_row_empty_column_mostly_filled
表体行空格 + 列大部分无 OCR 框 col_empty_ratio >= 0.5 跳过,列本来就空

6.5 二次 OCR 预处理

graph TB
    A[raw_crop 单元格裁剪] --> B{Pass1: light 模式}
    B --> C[格级去水印<br/>WatermarkProcessor cell]
    C --> D[可选: denoise / contrast]
    D --> E[upscale<br/>light.upscale_min_side]
    E --> F[det分行 + whole兜底 OCR]
    
    F --> G{分数达标?}
    G -->|是| H[采纳Pass1]
    G -->|否| I{Pass2: enhance_retry<br/>是否为启用?}
    I -->|否| H
    I -->|是| J[enhance 模式预处理<br/>可选更激进去水印+对比度+upscale]
    J --> K[再次 OCR]
    K --> L[择优: pick_better_ocr_result]
    L --> H
    
    style C fill:#fff4e1
    style E fill:#e1f5ff
    style J fill:#fff4e1

6.6 配置示例

table_recognition_wired:
  second_pass_ocr:
    reocr_mode: "bank_statement"   # 启用水印过滤、列空判断等启发式
    min_hit_score: 0.9
    suspicious_short_min_chars: 4
    cell_preprocess:               # Pass1 预处理
      watermark:
        enabled: true
        method: threshold
        threshold: 155
      upscale_min_side: 96         # Pass1 放大最短边
      contrast:
        enabled: false
    enhance_retry:                 # Pass2 预处理
      enabled: true
      upscale_min_side: 128        # Pass2 独立放大尺寸
      contrast:
        enabled: true
        method: clahe
        clip_limit: 1.0
        tile_grid_size: 4