# 有线表格识别技术文档 本文档详细说明 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` --- ## 核心流程 ### 完整处理流程 ```mermaid graph TB A[输入:表格图片 + OCR框] --> B[1. OCR预处理
ocr_formatter.py] B --> C[2. UNET线检测
MinerU模型] C --> D{启用倾斜矫正?} D -->|是| E[3. 倾斜检测
skew_detection.py] D -->|否| G E --> F[4. 图片与坐标矫正
cv2.warpAffine] F --> G[5. 网格恢复
grid_recovery.py] G --> H[6. 文本填充
text_filling.py] H --> I[7. HTML生成
html_generator.py] I --> J[8. 坐标逆转换
回到原图坐标系] 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 ```mermaid graph TB subgraph 一验:整页OCR匹配 A[整页OCR结果
ocr_boxes] --> B[遍历每个单元格] B --> C{中心点+重叠比例
匹配到OCR框?} C -->|是| D[斜框过滤
_is_bbox_slanted >10°?] D -->|否,水平| E[纵向完整性检测
_is_ocr_vertically_incomplete?] D -->|是,斜框| D2[丢弃该框
水印豁免] E -->|不完整| E2[清空text/score
→ 加入need_reocr_indices] E -->|完整| F[嵌套框处理
_resolve_cell_matched_boxes] F --> G[拼接文本 + 计算置信度] C -->|否| H[text=""
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
空格特殊逻辑} N -->|表头空格| N1[header_row_empty → 触发] N -->|表体空格| N2[_column_empty_ratio
matched_boxes_list判空] N2 -->|列大部分有框| N3[body_row_empty_column_mostly_filled → 触发] N2 -->|列大部分无框| N4[跳过,列本身就空] I --> O{任何reason命中?} end subgraph 二验:单元格裁剪OCR P[裁剪cell图像] --> Q[Pass1: 格级预处理
去水印 + upscale + OCR] Q --> R{Pass1分数达标?} R -->|是| S[采纳Pass1结果] R -->|否| T[Pass2: enhance_retry
更强预处理 + 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 一验匹配策略 #### 中心点 + 重叠比例 ```python # 第一步:中心点筛选 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°)角度差异明显: ```python @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 高单元格的底部 | ```python # 条件一: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 → 本该识别的内容永远丢失。 ```python @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 预处理 ```mermaid graph TB A[raw_crop 单元格裁剪] --> B{Pass1: light 模式} B --> C[格级去水印
WatermarkProcessor cell] C --> D[可选: denoise / contrast] D --> E[upscale
light.upscale_min_side] E --> F[det分行 + whole兜底 OCR] F --> G{分数达标?} G -->|是| H[采纳Pass1] G -->|否| I{Pass2: enhance_retry
是否为启用?} I -->|否| H I -->|是| J[enhance 模式预处理
可选更激进去水印+对比度+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 配置示例 ```yaml 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 ```