chore: archive extract-table-cell-boxes proposal

Archived the extract-table-cell-boxes proposal which implemented:
- Table cell boxes extraction from PP-StructureV3 table_res_list
- Layered rendering for tables with cell borders
- CV-based table line detection (disabled)
- Scan artifact removal preprocessing
- PDF orientation detection for rotated documents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-11-30 14:22:29 +08:00
parent 6252be6c6f
commit 6806fff1d5
3 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
# Change: Extract Table Cell Boxes via Direct Model Invocation
## Why
PPStructureV3 (PaddleX 3.x) 的高層 API 在處理表格時,只輸出 HTML 格式的表格內容,**不返回每個 cell 的座標 (bbox)**。
### 問題分析
經過測試確認:
```python
# PPStructureV3 輸出 (parsing_res_list)
{
'block_label': 'table',
'block_content': '<html>...</html>', # 只有 HTML
'block_bbox': [84, 269, 1174, 1508], # 只有整個表格的 bbox
# ❌ 沒有 cell boxes
}
```
但底層模型 (SLANeXt) 實際上**有輸出 cell boxes**
```python
# 直接調用 SLANeXt 模型
from paddlex import create_model
table_model = create_model('SLANeXt_wired')
result = table_model.predict(table_img)
# result.json['res']['bbox'] → 29 個 cell 座標 (8點多邊形)
```
### 影響
缺少 cell boxes 導致:
- OCR Track 的 PDF 版面還原表格渲染不準確
- 無法精確定位每個 cell 的位置
- 表格內容可能重疊或錯位
## What Changes
### 方案:補充調用底層 SLANeXt 模型
`pp_structure_enhanced.py` 處理表格時,補充調用 PaddleX 底層模型獲取 cell boxes
```
┌─────────────────────────────────────────────────────────────┐
│ 修改後的流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ PPStructureV3.predict() │
│ │ │
│ ▼ │
│ parsing_res_list (HTML only) │
│ │ │
│ ▼ (對於 TABLE 類型) │
│ ┌─────────────────────────────────────┐ │
│ │ 補充調用底層模型 │ │
│ │ 1. 裁切表格區域 │ │
│ │ 2. 調用 SLANeXt 獲取 cell boxes │ │
│ │ 3. 轉換座標到全域座標 │ │
│ │ 4. 存入 element['cell_boxes'] │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 完整的表格元素 (HTML + cell_boxes) │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 模型選擇邏輯
根據表格類型選擇對應的 SLANeXt 模型:
| 表格類型 | 判斷方式 | 使用模型 |
|---------|---------|---------|
| 有線表格 (wired) | PP-LCNet 分類 | SLANeXt_wired |
| 無線表格 (wireless) | PP-LCNet 分類 | SLANeXt_wireless |
### Cell Boxes 格式
SLANeXt 輸出的 bbox 是 8 點多邊形格式:
```python
[x1, y1, x2, y2, x3, y3, x4, y4] # 四個角點座標
# 例如: [11, 4, 692, 5, 675, 57, 10, 56]
```
需要轉換為全域座標(加上表格偏移量)。
## Impact
### Affected Specs
- `ocr-processing` - 表格處理增強
### Affected Code
- `backend/app/services/pp_structure_enhanced.py`
- 添加底層模型緩存機制
- 修改 `_process_parsing_res_list` 中的 TABLE 處理邏輯
- 添加 cell boxes 提取和座標轉換
- `backend/app/services/pdf_generator_service.py`
- 利用 cell_boxes 改進表格渲染
### Quality Impact
| 項目 | 改進前 | 改進後 |
|------|--------|--------|
| Cell 座標 | ❌ 無 | ✅ 有 (8點多邊形) |
| 表格渲染 | 平均分配行列 | 精確定位 |
| 版面還原 | 內容可能重疊 | 準確對應 |
### Performance Impact
- 額外模型調用:每個表格需要額外調用一次 SLANeXt
- 緩存優化:模型實例可緩存,避免重複載入
- 預估開銷:每表格增加 ~0.5-1 秒
## Risks
1. **性能開銷**
- 風險:額外模型調用增加處理時間
- 緩解:緩存模型實例,僅在需要時調用
2. **模型不一致**
- 風險PPStructureV3 內部可能已使用不同參數的模型
- 緩解:使用相同的模型配置
3. **座標轉換錯誤**
- 風險bbox 座標系可能有差異
- 緩解:充分測試,確保座標正確轉換
## Not Included
- 完全繞過 PPStructureV3保留用於 Layout 分析)
- RT-DETR cell detection可作為後續增強
- 其他元素的增強處理

View File

@@ -0,0 +1,132 @@
# Spec: OCR Processing - Table Cell Boxes Extraction
## Overview
在 OCR Track 處理表格時,補充調用 PaddleX 底層 SLANeXt 模型,獲取每個 cell 的座標信息。
## Requirements
### 1. 模型管理
#### 1.1 模型緩存
```python
class PPStructureEnhanced:
def __init__(self, structure_engine):
self.structure_engine = structure_engine
# 底層模型緩存
self._table_cls_model = None
self._wired_table_model = None
self._wireless_table_model = None
```
#### 1.2 延遲載入
- 模型只在首次需要時載入
- 使用 `paddlex.create_model()` API
- 模型配置從 settings 讀取
### 2. Cell Boxes 提取流程
#### 2.1 處理條件
`mapped_type == ElementType.TABLE` 且有有效的 `block_bbox` 時觸發。
#### 2.2 處理步驟
```
1. 裁切表格圖片
- 從原始圖片中根據 block_bbox 裁切
- 確保邊界不超出圖片範圍
2. 判斷表格類型 (可選)
- 調用 PP-LCNet_x1_0_table_cls
- 獲取 wired/wireless 分類結果
- 或直接使用 PPStructureV3 內部的分類結果
3. 調用對應 SLANeXt 模型
- wired → SLANeXt_wired
- wireless → SLANeXt_wireless
4. 提取 cell boxes
- 從 result.json['res']['bbox'] 獲取
- 格式: [[x1,y1,x2,y2,x3,y3,x4,y4], ...]
5. 座標轉換
- 將相對座標轉為全域座標
- global_box = [box[i] + offset for each point]
- offset = (table_x, table_y) from block_bbox
6. 存入 element
- element['cell_boxes'] = processed_boxes
- element['cell_boxes_format'] = 'polygon_8'
```
### 3. 數據格式
#### 3.1 Cell Boxes 結構
```python
element = {
'element_id': 'pp3_0_3',
'type': ElementType.TABLE,
'bbox': [84, 269, 1174, 1508], # 表格整體 bbox
'content': '<html>...</html>', # HTML 內容
'cell_boxes': [ # 新增cell 座標
[95, 273, 776, 274, 759, 326, 94, 325], # cell 0 (全域座標)
[119, 296, 575, 295, 560, 399, 117, 401], # cell 1
# ...
],
'cell_boxes_format': 'polygon_8', # 座標格式說明
'table_type': 'wired', # 可選:表格類型
}
```
#### 3.2 座標格式
- `polygon_8`: 8 點多邊形 `[x1,y1,x2,y2,x3,y3,x4,y4]`
- 順序:左上 → 右上 → 右下 → 左下
### 4. 錯誤處理
#### 4.1 失敗情況
- 模型載入失敗
- 圖片裁切失敗
- 預測返回空結果
#### 4.2 處理方式
- 記錄警告日誌
- 繼續處理element 不包含 cell_boxes
- 不影響原有 HTML 提取流程
### 5. 配置項
```python
# config.py
class Settings:
# 是否啟用 cell boxes 提取
enable_table_cell_boxes_extraction: bool = True
# 表格結構識別模型 (已存在)
wired_table_model_name: str = "SLANeXt_wired"
wireless_table_model_name: str = "SLANeXt_wireless"
```
## Implementation Notes
### 模型共享
PPStructureV3 內部已載入了這些模型,但高層 API 不暴露。
直接使用 `paddlex.create_model()` 會重新載入模型。
考慮是否可以訪問 PPStructureV3 內部的模型實例(經測試:不可行)。
### 性能優化
- 模型實例緩存在 PPStructureEnhanced 中
- 避免每次處理表格都重新載入模型
- 考慮在內存緊張時釋放緩存
### 座標縮放
如果圖片在 Layout 分析前經過縮放ScalingInfo
cell boxes 座標也需要相應縮放回原始座標系。
## Test Cases
1. **有線表格**:確認 cell boxes 提取正確
2. **無線表格**:確認模型選擇和提取正確
3. **複雜表格**:跨行跨列的表格
4. **小表格**cell 數量少的簡單表格
5. **錯誤處理**:無效 bbox、模型失敗等情況

View File

@@ -0,0 +1,273 @@
# Tasks: Extract Table Cell Boxes
## 重要發現 (2025-11-28)
**PPStructureV3 (PaddleX 3.3.9) 確實提供 `table_res_list`**
之前的實現假設需要額外調用 SLANeXt 模型,但經過深入測試發現:
- `result.json['res']['table_res_list']` 包含所有表格的 `cell_box_list`
- 不需要額外的模型調用
- 已移除多餘的 SLANeXt 代碼
## Phase 1: 基礎設施 (已完成)
### Task 1.1: 配置項
- [x] ~~添加 `enable_table_cell_boxes_extraction` 配置~~ (已移除,不再需要)
- [x] 確認 PPStructureV3 提供 `table_res_list`
### Task 1.2: 模型緩存機制
- [x] ~~實現 SLANeXt 模型緩存~~ (已移除,不再需要)
- [x] 直接使用 PPStructureV3 內建的 `table_res_list`
## Phase 2: Cell Boxes 提取 (已完成)
### Task 2.1: 從 table_res_list 提取
- [x]`result.json['res']['table_res_list']` 獲取 `cell_box_list`
- [x] 通過 HTML 內容匹配表格
- [x] 驗證座標格式 (已是絕對座標)
### Task 2.2: Image-in-Table 處理
- [x]`layout_det_res` 獲取 image boxes
- [x] 檢測表格內的圖片
- [x] 裁切保存圖片
- [x] 嵌入到表格 HTML
## Phase 3: PDF 生成優化 (已完成)
### Task 3.1: ~~利用 Cell Boxes 推斷網格~~ (已棄用)
- [x] ~~修改 `draw_table_region` 使用 cell_boxes~~
- [x] ~~根據實際 cell 位置計算行高列寬~~
- [x] 測試渲染效果 → **發現問題HTML 結構與 cell_boxes 不匹配**
### Task 3.2: 方案 B - 分層渲染 (Layered Rendering) ✓ 已完成
**問題分析 (2025-11-30)**
- HTML 表格結構與 cell_boxes 不匹配,無法正確推斷網格
- 嘗試在 cell 內繪製文字失敗(超出邊框、匹配錯誤)
**解決方案**:分層渲染 - 分離表格邊框與文字繪製
- Layer 1: 使用 cell_boxes 繪製表格邊框
- Layer 2: 使用 raw OCR positions 繪製文字(獨立於表格結構)
- Layer 3: 繪製 embedded_images
**實作步驟 (2025-11-30)**
- [x] 修改 `GapFillingService._is_region_covered()` - 跳過 TABLE 元素覆蓋檢測
- [x] 簡化 `_draw_table_with_cell_boxes()` - 只繪製邊框 + 圖片
- [x] 修改 `regions_to_avoid` - 排除表格,讓文字穿透表格區域
- [x] 整合測試test_layered_rendering.py
### Task 3.3: 備選方案
- [x] 當 cell_boxes 不可用時,使用 ReportLab Table
- [x] 確保向後兼容
## Phase 4: 測試與驗證 (已完成)
### Task 4.1: 單元測試
- [x] 測試 cell_box_list 提取 (29 cells 成功)
- [x] 測試 Image-in-Table 處理 (1 image embedded)
- [x] 測試錯誤處理
### Task 4.2: 整合測試
- [x] 使用實際 PDF 測試 OCR Track (test_layered_rendering.py)
- [x] 驗證 PDF 版面還原效果
- [x] 分層渲染測試結果:
- 50 text elements (從 raw OCR 補充,原本只有 5 個)
- 31 cell_boxes (8 + 23)
- 1 embedded_image
- PDF 生成成功 (57,290 bytes)
## Phase 5: 清理 (已完成)
### Task 5.1: 移除舊代碼
- [x] 移除 SLANeXt 模型緩存代碼
- [x] 移除 `_get_slanet_model()`, `_get_table_classifier()`, `_extract_cell_boxes_with_slanet()`, `release_slanet_models()`
- [x] 移除 `enable_table_cell_boxes_extraction` 配置
- [x] 清理調試日誌
---
## 技術細節
### 關鍵代碼位置
| 文件 | 修改內容 |
|------|---------|
| `backend/app/core/config.py` | 移除 `enable_table_cell_boxes_extraction` |
| `backend/app/services/pp_structure_enhanced.py` | 使用 `table_res_list`, 添加 `_embed_images_in_table()` |
| `backend/app/services/pdf_generator_service.py` | 分層渲染:只繪製邊框,排除表格區域的文字過濾 |
| `backend/app/services/gap_filling_service.py` | `_is_region_covered()` 跳過 TABLE 元素 |
| `backend/tests/test_layered_rendering.py` | 分層渲染整合測試 |
### PPStructureV3 數據結構
```python
result.json = {
'res': {
'parsing_res_list': [...], # 解析結果
'layout_det_res': {...}, # Layout 檢測結果
'table_res_list': [ # 表格識別結果
{
'cell_box_list': [[x1,y1,x2,y2], ...], # ← 關鍵!
'pred_html': '<html>...',
'table_ocr_pred': {...}
}
],
'overall_ocr_res': {...}
}
}
```
### 測試結果
- Task ID: `442f9345-09ba-4a7d-949f-3bc88c2fa895`
- cell_boxes: 29 cells (source: table_res_list)
- embedded_images: 1 (img_in_table_935_838_1118_1031)
### 本地 vs 雲端差異
| 特性 | 本地 PaddleX 3.3.9 | 雲端 pp_demo |
|------|-------------------|--------------|
| `table_res_list` | ✓ 提供 | ✓ 提供 |
| `cell_box_list` | ✓ 29 cells | ✓ 27+8 cells |
| Layout 識別 | 1 個合併表格 | 2 個獨立表格 |
| Image-in-Table | 需自行處理 | 自動嵌入 HTML |
### 遺留問題
1. **Layout 識別合併表格**:本地 Layout 模型把多個表格合併成一個大表格
- 這導致 `table_res_list` 只有 1 個表格
- 雲端識別為 2 個獨立表格
- 可能需要調整 Layout 模型參數或後處理邏輯
---
## 分層渲染技術設計 (2025-11-30)
### 問題根因
ReportLab Table 需要規則矩形網格,但 PPStructureV3 的 cell_boxes 反映實際視覺位置,與 HTML 邏輯結構不匹配。嘗試在 cell 內繪製文字會導致:
- 文字超出邊框
- 匹配錯誤
- 部分文字遺失
### 解決方案:分層渲染
將表格渲染解耦為三個獨立層次:
```
┌─────────────────────────────────────────┐
│ Layer 3: Embedded Images │
│ (從 metadata['embedded_images'] 獲取) │
├─────────────────────────────────────────┤
│ Layer 2: Text at Raw OCR Positions │
│ (從 GapFillingService 補充的原始 OCR) │
├─────────────────────────────────────────┤
│ Layer 1: Table Cell Borders │
│ (從 metadata['cell_boxes'] 繪製) │
└─────────────────────────────────────────┘
```
### 實作細節
**1. GapFillingService 修改** (`_is_region_covered`):
```python
# 跳過 TABLE 元素覆蓋檢測,讓表格內文字通過
if skip_table_coverage and element.type == ElementType.TABLE:
continue
```
**2. PDF Generator 修改** (`regions_to_avoid`):
```python
# 排除表格,只避免與圖片重疊
regions_to_avoid = [img for img in images_metadata if img.get('type') != 'table']
```
**3. 簡化的 `_draw_table_with_cell_boxes`**:
```python
def _draw_table_with_cell_boxes(...):
"""只繪製邊框和圖片,不處理文字"""
# 1. 繪製每個 cell 的邊框
for box in cell_boxes:
pdf_canvas.rect(x, y, width, height, stroke=1, fill=0)
# 2. 繪製 embedded_images
for img in embedded_images:
self._draw_embedded_image(...)
```
### 優勢
1. **解耦**:邊框渲染與文字渲染完全獨立
2. **精確**:文字位置直接使用 OCR 結果,不需推斷
3. **穩定**:不受 cell_boxes 與 HTML 不匹配影響
4. **相容**visualization 中 overall_ocr_res.png 的效果可直接還原
### 測試結果
- Task ID: `84899366-f361-44f1-b989-5aba72419ca5`
- cell_boxes: 31 (8 + 23)
- 原始 text elements: 5
- 補充後 text elements: 50 (從 raw OCR 補充)
- PDF 大小: 57,290 bytes
---
## 混合渲染優化 (2025-11-30)
### 問題發現
分層渲染後仍有問題:
1. 表格歪斜cell_boxes 有 2-11 像素的座標偏差
2. Title 等元素樣式未應用OCR track 不套用樣式
### 解決方案:混合渲染 + 網格對齊
**1. Cell Boxes 網格對齊** (`_normalize_cell_boxes_to_grid`):
```python
def _normalize_cell_boxes_to_grid(self, cell_boxes, threshold=10.0):
"""
將相鄰座標聚合為統一值,消除 2-11 像素的偏差。
- 收集所有 X/Y 座標
- 聚類相近座標threshold 內)
- 使用平均值作為對齊後的座標
"""
```
**2. 元素類型樣式** (OCR track):
```python
# 在 draw_text_region 中加入元素類型檢查
element_type = region.get('element_type', 'text')
if element_type == 'title':
font_size = min(font_size * 1.3, 36) # 30% 放大
elif element_type == 'header':
font_size = min(font_size * 1.15, 24) # 15% 放大
elif element_type == 'caption':
font_size = max(font_size * 0.9, 6) # 10% 縮小
```
**3. 元素類型傳遞**:
```python
# convert_unified_document_to_ocr_data 中加入
text_region = {
'text': text_content,
'bbox': bbox_polygon,
'element_type': element.type.value # 新增
}
```
### 改進後效果
| 項目 | 改進前 | 改進後 |
|------|--------|--------|
| 表格邊框 | 歪斜 (2-11px 偏差) | 網格對齊 |
| Title 樣式 | 無 (與普通文字相同) | 36pt 放大字體 |
| 混合渲染 | 只用 raw OCR | PP-Structure + raw OCR |
### 測試結果 (2025-11-30)
- Task ID: `3a3f350f-2d81-4af4-8a18-021ea09ac433`
- Table 1: 8 cell_boxes → 網格對齊
- Table 2: 23 cell_boxes → 網格對齊 + 1 embedded image
- Title: Applied title style: size=36.0
- PDF 大小: 104,082 bytes