# 有线表格识别技术文档
本文档详细说明 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
```