ソースを参照

feat: 更新多个工具的 README 文档,添加详细文档链接

- 在 DotsOCR、MinerU、PaddleOCR 和 PP-StructureV3 的 README 中新增详细文档链接,提供技术文档、环境配置和参数说明。
- 删除不再需要的文档文件,优化项目结构。
- 更新 OCR 合并工具的相关文档,提升用户体验和可读性。
zhch158_admin 1 週間 前
コミット
9733ac0b36

+ 2 - 0
ocr_tools/dots.ocr_vl_tool/README.md

@@ -2,6 +2,8 @@
 
 基于 DotsOCR 的批量文档处理工具,支持 PDF 和图片文件的批量处理。
 
+> 📚 **详细文档**:更多技术文档、环境配置等,请查看 [docs/dotsocr/](../../docs/dotsocr/)
+
 ## 功能特性
 
 - ✅ 统一输入接口:支持 PDF 文件、图片文件、图片目录、文件列表(.txt)、CSV 文件

+ 2 - 0
ocr_tools/mineru_vl_tool/README.md

@@ -2,6 +2,8 @@
 
 基于 MinerU demo.py 框架的批量文档处理工具,支持 PDF 和图片文件的批量处理。
 
+> 📚 **详细文档**:更多技术文档、环境配置、处理流程等,请查看 [docs/mineru/](../../docs/mineru/)
+
 ## 功能特性
 
 - ✅ 统一输入接口:支持 PDF 文件、图片文件、图片目录、文件列表(.txt)、CSV 文件

+ 0 - 264
ocr_tools/ocr_merger/DP算法多路径探索说明.md

@@ -1,264 +0,0 @@
-# DP算法多路径探索说明:为什么不是只从最优解往下?
-
-## ❓ 问题
-
-**上一行只会从最优解往下进行吗?上一行的其他解会往下计算吗?**
-
-## ✅ 答案
-
-**不是!算法会从上一行的所有有效解(经过剪枝后最多30个)往下计算,而不是只从最优解往下。**
-
----
-
-## 📝 代码证据
-
-### 关键代码片段
-
-```python
-# 第604行:获取上一行所有有效位置
-valid_prev_indices = [j for j in range(n_paddle) if dp[i-1][j] > -np.inf]
-
-# 第619-621行:剪枝优化(但仍然是多个状态)
-if len(valid_prev_indices) > 30:
-    valid_prev_indices.sort(key=lambda j: dp[i-1][j], reverse=True)
-    valid_prev_indices = valid_prev_indices[:30]  # 保留前30个,不是只保留1个!
-
-# 第637行:遍历所有有效的上一行状态
-for prev_j in valid_prev_indices:  # 遍历多个状态,不是只有一个
-    prev_score = dp[i-1][prev_j]
-    # ... 从每个prev_j往下计算
-```
-
----
-
-## 🎯 原因分析
-
-### 原因1:动态规划的核心思想
-
-**DP算法的本质是:考虑所有可能的状态转移,找到全局最优解。**
-
-```
-全局最优 ≠ 局部最优的累积
-
-示例:
-HTML行0 → OCR组[0,1,2]  得分: 0.98  ← 局部最优
-HTML行0 → OCR组[0,1]    得分: 0.95  ← 次优解
-
-如果只从最优解(0.98)往下:
-HTML行1 → OCR组[3,4]    得分: 0.98 + 0.90 = 1.88
-
-如果也从次优解(0.95)往下:
-HTML行1 → OCR组[2,3,4]  得分: 0.95 + 0.95 = 1.90  ← 更优!
-
-结论:次优解可能通向全局最优!
-```
-
-### 原因2:局部最优 ≠ 全局最优
-
-**当前行的最优解可能不是全局最优路径的起点。**
-
-#### 示例场景
-
-```
-DP状态矩阵(简化):
-     OCR组索引 j →
-HTML  0    1    2    3    4    5
-行↓
-0    [0.98] [0.95] [0.92] -    -    -
-      ↑     ↑     ↑
-    最优   次优  第三
-
-1    -     -     [1.88] [1.90] [1.85] -
-                      ↑
-                   全局最优!
-                   来自次优解(0.95)
-```
-
-**说明**:
-- HTML行0的最优解是 `dp[0][0] = 0.98`
-- 但如果只从 `dp[0][0]` 往下,得到 `dp[1][3] = 1.88`
-- 而从次优解 `dp[0][1] = 0.95` 往下,得到 `dp[1][4] = 1.90`(更优!)
-
-### 原因3:多路径探索的必要性
-
-**保留多个候选路径,避免过早剪枝导致错过最优解。**
-
-#### 场景:OCR检测不准确
-
-```
-HTML行0的匹配情况:
-┌─────────────────────────────────────────────────────────────┐
-│ 匹配1: OCR组[0,1,2]  相似度: 0.98  ← 看起来最优              │
-│        但OCR组2可能包含了一些噪声                           │
-│                                                             │
-│ 匹配2: OCR组[0,1]    相似度: 0.95  ← 次优,但更干净         │
-│        没有噪声,可能后续匹配更准确                          │
-└─────────────────────────────────────────────────────────────┘
-
-如果只保留匹配1:
-→ HTML行1可能因为OCR组2的噪声影响,匹配不佳
-→ 全局得分: 0.98 + 0.85 = 1.83
-
-如果也保留匹配2:
-→ HTML行1从OCR组2开始,匹配更准确
-→ 全局得分: 0.95 + 0.92 = 1.87  ← 更优!
-```
-
----
-
-## 📊 可视化说明
-
-### 场景1:只从最优解往下(错误做法)
-
-```
-DP状态转移(错误):
-     OCR组索引 j →
-HTML  0    1    2    3    4    5
-行↓
-0    [0.98] [0.95] [0.92] -    -    -
-      │
-      └─→ 只从最优解往下
-          │
-1    -    -    -    [1.88] -    -
-                      ↑
-                    唯一路径
-                    可能错过全局最优
-```
-
-### 场景2:从多个解往下(正确做法)
-
-```
-DP状态转移(正确):
-     OCR组索引 j →
-HTML  0    1    2    3    4    5
-行↓
-0    [0.98] [0.95] [0.92] -    -    -
-      │     │     │
-      └─────┴─────┴─→ 从多个解往下探索
-          │     │     │
-1    -    -    [1.88] [1.90] [1.85] -
-                      ↑     ↑
-                    路径1  路径2(更优!)
-                    
-最终选择: dp[1][4] = 1.90(来自dp[0][1])
-```
-
----
-
-## 🔍 剪枝策略的平衡
-
-### 为什么保留30个而不是全部?
-
-**原因:平衡准确性和性能**
-
-```python
-# 如果保留所有状态
-valid_prev_indices = [0, 1, 2, ..., 47]  # 48个状态
-计算量: 48 × 15 × 4 = 2,880 次匹配/行
-总计算量: 2,880 × 7 = 20,160 次
-
-# 如果只保留最优解
-valid_prev_indices = [0]  # 1个状态
-计算量: 1 × 15 × 4 = 60 次匹配/行
-总计算量: 60 × 7 = 420 次
-问题: 可能错过全局最优解 ❌
-
-# 如果保留前30个(当前策略)
-valid_prev_indices = [0, 1, 2, ..., 29]  # 30个状态
-计算量: 30 × 15 × 4 = 1,800 次匹配/行
-总计算量: 1,800 × 7 = 12,600 次
-平衡: 既保证准确性,又控制计算量 ✅
-```
-
-### 剪枝的合理性
-
-**保留前30个得分最高的状态,通常已经包含了所有有希望通向全局最优的路径。**
-
-```
-得分分布示例:
-dp[0][0] = 0.98  ← 第1名
-dp[0][1] = 0.95  ← 第2名
-dp[0][2] = 0.92  ← 第3名
-...
-dp[0][29] = 0.65 ← 第30名
-dp[0][30] = 0.20 ← 第31名(被剪枝)
-
-分析:
-- 前30个状态得分都在0.65以上,有希望通向全局最优
-- 第31个状态得分只有0.20,即使后续匹配完美,也很难超过前30个
-- 剪枝是安全的 ✅
-```
-
----
-
-## 💡 实际案例
-
-### 案例:银行流水表匹配
-
-```
-HTML行0(表头信息行)的匹配情况:
-
-匹配1: OCR组[0,1,2]  得分: 0.98
-       文本: "部门department:01372403999柜员searchteller:ebf0000打印日期..."
-       但OCR组2可能包含了一些后续行的内容
-
-匹配2: OCR组[0,1]    得分: 0.95
-       文本: "部门department:01372403999柜员searchteller:ebf0000"
-       更精确,没有混入后续内容
-
-匹配3: OCR组[0,1,2,3] 得分: 0.92
-       文本: "部门department:...查询起日..."
-       包含了太多内容,可能影响后续匹配
-```
-
-**如果只从匹配1往下**:
-```
-HTML行1 → OCR组[3,4]  得分: 0.98 + 0.85 = 1.83
-(因为OCR组2已经消耗了一些内容,导致HTML行1匹配不佳)
-```
-
-**如果也从匹配2往下**:
-```
-HTML行1 → OCR组[2,3,4] 得分: 0.95 + 0.92 = 1.87  ← 更优!
-(从OCR组2开始,HTML行1能匹配到更完整的内容)
-```
-
-**结论**:保留多个候选路径,最终找到全局最优解!
-
----
-
-## 📋 总结
-
-### 核心要点
-
-1. **不是只从最优解往下**
-   - 算法会从上一行的所有有效解(最多30个)往下计算
-   - 这是动态规划的标准做法
-
-2. **原因**
-   - 局部最优 ≠ 全局最优
-   - 次优解可能通向全局最优
-   - 多路径探索保证找到全局最优解
-
-3. **剪枝策略**
-   - 保留前30个得分最高的状态
-   - 平衡准确性和性能
-   - 通常已经包含了所有有希望的路径
-
-4. **实际效果**
-   - 在保证准确性的同时,控制计算量
-   - 避免错过全局最优解
-   - 提高算法的鲁棒性
-
----
-
-## 🎓 算法设计启示
-
-这个设计体现了**动态规划算法的精髓**:
-
-1. **状态空间探索**:考虑所有可能的状态转移
-2. **剪枝优化**:在保证准确性的前提下,减少计算量
-3. **全局最优**:通过多路径探索,找到全局最优解
-
-**这就是为什么这个算法能够准确匹配HTML行与OCR组的原因!**
-

+ 0 - 430
ocr_tools/ocr_merger/Tablecells匹配-动态规划.md

@@ -1,430 +0,0 @@
-基于您提供的 table_cell_matcher.py 代码,`_match_html_rows_to_paddle_groups` 方法实现了一个**全局动态规划 (Global Dynamic Programming)** 算法。
-
-这个算法的核心目标是解决两个序列(HTML 表格行序列 vs PaddleOCR 文本行序列)的**非线性对齐**问题。
-
-以下是该算法的详细原理解析:
-
-### 1. 核心思想:为什么用动态规划?
-
-传统的“贪婪算法”是逐行匹配,如果第 1 行匹配错了,第 2 行就会基于错误的位置继续找,导致“一步错,步步错”。
-
-**动态规划 (DP)** 的思想是:**不急着做决定**。它会计算所有可能的匹配路径的得分,最后回溯找出一条**总分最高**的路径。即使中间某一行匹配分值较低,只要它能让整体结构最合理,DP 就会选择它。
-
-### 2. 算法状态定义
-
-在代码中,`dp` 矩阵定义如下:
-
-*   **维度**:`dp[n_html][n_paddle]`
-*   **含义**:`dp[i][j]` 表示 **“HTML 的前 `i` 行”** 成功匹配到了 **“Paddle 的前 `j` 组”** 时,所能获得的**最大累计得分**。
-*   **值**:
-    *   `-inf` (负无穷):表示此状态不可达(例如 HTML 第 5 行不可能匹配到 Paddle 第 1 组,因为顺序不对)。
-    *   `float`:表示当前的累计相似度分数。
-
-### 3. 算法执行流程
-
-#### 第一步:预计算 (Pre-computation)
-为了性能,代码预先计算了 Paddle 组的合并文本。
-*   `merged_cache[(j, k)]`:存储从 Paddle 第 `j` 组开始,合并 `k` 个组后的文本。
-*   这避免了在多重循环中反复进行字符串拼接。
-
-#### 第二步:初始化 (Initialization)
-处理 HTML 的第 0 行 (`i=0`)。
-*   尝试将 HTML 第 0 行匹配 Paddle 的第 `0` 到 `SEARCH_WINDOW` 组。
-*   允许合并 `1` 到 `MAX_MERGE` (4) 个 Paddle 组。
-*   **得分计算**:`相似度 - 跳过惩罚`。
-*   如果得分有效,填入 `dp[0][end_j]`。
-
-#### 第三步:状态转移 (State Transition) - 核心循环
-这是算法最复杂的部分,遍历每一行 HTML (`i` 从 1 到 N)。对于当前行,有两种选择:
-
-**选择 A:跳过当前 HTML 行 (The "Skip" Strategy)**
-*   **场景**:HTML 有这一行,但 OCR 漏识别了,或者 OCR 顺序错乱导致当前位置找不到对应的 OCR 组。
-*   **逻辑**:直接继承上一行的最佳状态,但扣除 `SKIP_HTML_PENALTY`。
-*   **代码**:
-    ```python
-    score_skip = dp[i-1][prev_j] - SKIP_HTML_PENALTY
-    if score_skip > dp[i][prev_j]:
-        dp[i][prev_j] = score_skip
-        path[(i, prev_j)] = (prev_j, prev_j + 1) # 标记未消耗新组
-    ```
-*   **作用**:防止“链条断裂”。即使这一行匹配失败,状态也能传递给下一行。
-
-**选择 B:匹配 Paddle 组 (The "Match" Strategy)**
-*   **场景**:正常匹配。
-*   **逻辑**:
-    1.  找到上一行所有有效的结束位置 `prev_j`。
-    2.  **Gap (跳过)**:允许跳过中间的一些 Paddle 组(可能是噪音或页眉),即 `start_j = prev_j + 1 + gap`。
-    3.  **Merge (合并)**:尝试将 `start_j` 开始的 `1` 到 `4` 个 Paddle 组视为一行。
-    4.  **计算得分**:
-        $$Score = Similarity(HTML\_Text, Paddle\_Text) - Gap\_Penalty - Length\_Penalty$$
-    5.  **转移方程**:
-        $$dp[i][end\_j] = \max(dp[i][end\_j], \ dp[i-1][prev\_j] + Score)$$
-*   **代码**:
-    ```python
-    total_score = prev_score + current_score
-    if total_score > dp[i][end_j]:
-        dp[i][end_j] = total_score
-        path[(i, end_j)] = (prev_j, start_j) # 记录路径
-    ```
-
-#### 第四步:回溯 (Backtracking)
-当填满 `dp` 表后,我们需要找出最优路径:
-1.  **找终点**:在最后一行 HTML (`n_html-1`) 中找到得分最高的 `j`。如果最后一行没匹配上,就往前找倒数第二行,以此类推。
-2.  **倒推**:利用 `path` 字典,从终点一步步回到起点。
-    *   `path[(i, j)]` 存储了 `(prev_j, start_j)`。
-    *   如果 `start_j <= j`:说明 HTML 行 `i` 对应了 Paddle 组 `[start_j, ..., j]`。
-    *   如果 `start_j > j`:说明 HTML 行 `i` 被跳过了(对应空列表)。
-
-#### 第五步:孤儿挽救 (Orphan Rescue)
-DP 找的是“主干路径”,可能会漏掉一些没能进入最优路径的 Paddle 组(Orphans)。
-*   **逻辑**:遍历所有未使用的 Paddle 组,看它在物理位置上(Y坐标)离哪个已匹配的组最近(上方或下方)。
-*   **归属**:将其强行合并到最近的 HTML 行中。这解决了 OCR 过度切分导致的小碎片丢失问题。
-
-### 4. 关键参数解析
-
-*   **`MAX_MERGE = 4`**:
-    *   允许一行 HTML 对应最多 4 行 OCR 文本。解决 OCR 将长表格行切分成多行的问题。
-*   **`SEARCH_WINDOW = 15`**:
-    *   允许 OCR 结果相对于 HTML 出现较大的位移或中间插入了大量噪音(如印章干扰)。
-*   **`SKIP_HTML_PENALTY = 0.3`**:
-    *   这是您之前遇到问题的关键修复。允许算法以 0.3 分的代价放弃一行 HTML,从而保全后续的匹配。
-*   **`SKIP_PADDLE_PENALTY = 0.1`**:
-    *   允许跳过 OCR 中的噪音行,但跳过越多扣分越多,倾向于连续匹配。
-
-### 5. 总结
-
-这个算法将表格行匹配问题转化为了一个**寻找最大权值路径**的图论问题。
-
-*   **节点**:`(HTML行索引, Paddle组索引)`
-*   **边**:匹配操作(得分)或跳过操作(罚分)。
-*   **目标**:找到从第 0 行到第 N 行的总分最高路线。
-
-这种方法极其鲁棒,能够同时处理:OCR 分行、OCR 漏行、OCR 噪音、HTML 空行以及轻微的顺序错乱问题。
-
----
-
-# 行级动态规划 (Row-Level DP) 
-
-1.  **贪婪算法 (Greedy)**:它是一个接一个匹配的。如果第一个单元格错误地“吞掉”了本属于第二个单元格的 Box(或者像之前的案例,短日期匹配到了长 ID),后面的单元格就无路可走了。
-2.  **逻辑耦合 (Coupling)**:匹配流程控制(循环、指针移动)与 相似度评分逻辑(各种防御、加分、扣分)深度纠缠在一起,导致代码臃肿且难以维护。
-
-### 🚀 更好的思路:行级动态规划 (Row-Level DP)
-
-既然你已经在行与行的匹配中使用了 DP,**在行内的单元格匹配中,DP 同样是终极解决方案。**
-
-**核心思想**:
-不要问“当前单元格最匹配哪个 Box”,而要问“**如何将这一行的 M 个 Box 分配给 N 个单元格,使得整体匹配得分最高**”。
-
-这样,如果“短日期”匹配“长ID”会导致“长ID”对应的单元格没东西匹配(总分变低),DP 就会自动放弃这个错误匹配,选择全局最优解。
-
-### 🛠️ 重构方案
-
-我们将代码拆分为两个清晰的部分:
-1.  **纯粹的评分函数 (`_compute_match_score`)**:只负责计算“文本 A”和“文本 B”的匹配度,包含所有的防御逻辑(长度、类型、子序列等)。
-2.  **DP 匹配器 (`_match_cells_in_row_dp`)**:只负责路径规划,决定哪个 Box 归哪个 Cell。
-
-#### 1. 提取评分逻辑 (解耦与瘦身)
-
-把所有复杂的 `if/else` 防御逻辑移到这里。
-
-```python
-    def _compute_match_score(self, cell_text: str, box_text: str) -> float:
-        """
-        纯粹的评分函数:计算单元格文本与候选 Box 文本的匹配得分
-        包含所有防御逻辑、加权逻辑
-        """
-        # 1. 预处理
-        cell_norm = self.text_matcher.normalize_text(cell_text)
-        box_norm = self.text_matcher.normalize_text(box_text)
-        
-        if not cell_norm or not box_norm:
-            return 0.0
-            
-        # --- ⚡️ 快速防御 (Fast Fail) ---
-        len_cell = len(cell_norm)
-        len_box = len(box_norm)
-        
-        # 长度差异过大直接 0 分 (除非是包含关系且特征明显)
-        if len_box > len_cell * 3 + 5:
-            # 除非 cell 很长 (长ID),否则不许短 cell 匹配超长 box
-            if len_cell < 5: 
-                return 0.0
-
-        # --- 🔍 核心相似度计算 ---
-        cell_proc = self._preprocess_text_for_matching(cell_text)
-        box_proc = self._preprocess_text_for_matching(box_text)
-        
-        # A. Token Sort (解决乱序)
-        score_sort = fuzz.token_sort_ratio(cell_proc, box_proc)
-        
-        # B. Partial (解决截断/包含)
-        score_partial = fuzz.partial_ratio(cell_norm, box_norm)
-        
-        # C. Subsequence (解决噪音插入)
-        score_subseq = 0.0
-        if len_cell > 5:
-            score_subseq = self._calculate_subsequence_score(cell_norm, box_norm)
-
-        # --- 🛡️ 深度防御逻辑 (Deep Defense) ---
-        
-        # 1. 短文本防御 (防止 "1" 匹配 "1000")
-        if score_partial > 80:
-            import re
-            has_content = lambda t: bool(re.search(r'[a-zA-Z0-9\u4e00-\u9fa5]', t))
-            
-            # 纯符号防御
-            if not has_content(cell_norm) and has_content(box_norm):
-                if len_box > len_cell + 2: score_partial = 0.0
-            
-            # 微小碎片防御
-            elif len_cell <= 2 and len_box > 8:
-                score_partial = 0.0
-                
-            # 覆盖率防御 (防止 "2024" 匹配 "ID2024...")
-            else:
-                coverage = len_cell / len_box
-                if coverage < 0.3 and score_sort < 45:
-                    score_partial = 0.0
-
-        # 2. 子序列防御 (防止短数字匹配长ID)
-        if score_subseq > 80:
-            if len_box > len_cell * 1.5:
-                import re
-                # 如果是数字类型且较短
-                if re.match(r'^[\d\-\:\.\s]+$', cell_norm) and len_cell < 12:
-                    score_subseq = 0.0
-
-        # --- 📊 综合评分 ---
-        final_score = max(score_sort, score_partial, score_subseq)
-        
-        # 精确匹配奖励
-        if cell_norm == box_norm:
-            final_score = 100.0
-        # 包含关系奖励
-        elif cell_norm in box_norm:
-            final_score = min(100, final_score + 5)
-            
-        return final_score
-```
-
-#### 2. 实现行内 DP (解决贪婪问题)
-
-用 DP 替代 `_match_cell_sequential`。
-
-```python
-    def _match_cells_in_row_dp(self, html_cells: List, row_boxes: List[Dict]) -> List[Dict]:
-        """
-        使用动态规划进行行内单元格匹配
-        目标:找到一种分配方案,使得整行的匹配总分最高
-        """
-        n_cells = len(html_cells)
-        n_boxes = len(row_boxes)
-        
-        # dp[i][j] 表示:前 i 个单元格 消耗了 前 j 个 boxes 的最大得分
-        dp = np.full((n_cells + 1, n_boxes + 1), -np.inf)
-        dp[0][0] = 0
-        
-        # path[i][j] = (prev_j, matched_info) 用于回溯
-        path = {}
-        
-        # 允许合并的最大 box 数量
-        MAX_MERGE = 5 
-        
-        for i in range(1, n_cells + 1):
-            cell = html_cells[i-1]
-            cell_text = cell.get_text(strip=True)
-            
-            # 优化:如果单元格为空,直接继承状态,不消耗 box
-            if not cell_text:
-                for j in range(n_boxes + 1):
-                    if dp[i-1][j] > -np.inf:
-                        dp[i][j] = dp[i-1][j]
-                        path[(i, j)] = (j, None) # None 表示空匹配
-                continue
-
-            # 遍历当前 box 指针 j
-            for j in range(n_boxes + 1):
-                # 1. 策略:跳过当前单元格 (Cell Missing)
-                # 继承 dp[i-1][j]
-                if dp[i-1][j] > dp[i][j]:
-                    dp[i][j] = dp[i-1][j]
-                    path[(i, j)] = (j, None)
-
-                # 2. 策略:匹配 k 个 boxes (从 j-k 到 j)
-                # 我们尝试用 boxes[prev_j : j] 来匹配 cell[i]
-                # prev_j 是上一个单元格结束的位置
-                
-                # 限制搜索范围,提高性能
-                # 假设中间跳过的噪音 box 不超过 3 个
-                search_start = max(0, j - MAX_MERGE - 3)
-                
-                for prev_j in range(j - 1, search_start - 1, -1):
-                    if dp[i-1][prev_j] == -np.inf:
-                        continue
-                        
-                    # 取出中间的 boxes (prev_j 到 j)
-                    # 注意:这里我们允许中间有一些 box 被跳过(视为噪音),
-                    # 但为了简化 DP,我们通常假设连续取用,或者只取末尾的 k 个
-                    
-                    # 简化版:尝试合并 boxes[prev_j:j] 中的所有内容
-                    candidate_boxes = row_boxes[prev_j:j]
-                    
-                    # 组合文本
-                    merged_text = "".join([b['text'] for b in candidate_boxes])
-                    # 或者用空格连接,视情况而定
-                    merged_text_spaced = " ".join([b['text'] for b in candidate_boxes])
-                    
-                    # 计算得分
-                    score = self._compute_match_score(cell_text, merged_text_spaced)
-                    
-                    if score > 60: # 只有及格的匹配才考虑
-                        new_score = dp[i-1][prev_j] + score
-                        if new_score > dp[i][j]:
-                            dp[i][j] = new_score
-                            path[(i, j)] = (prev_j, {
-                                'text': merged_text_spaced,
-                                'boxes': candidate_boxes,
-                                'score': score
-                            })
-
-        # --- 回溯找最优解 ---
-        # 找到最后一行得分最高的 j
-        best_j = np.argmax(dp[n_cells])
-        if dp[n_cells][best_j] == -np.inf:
-            return [] # 匹配完全失败
-            
-        results = []
-        curr_i, curr_j = n_cells, best_j
-        
-        while curr_i > 0:
-            prev_j, match_info = path.get((curr_i, curr_j), (curr_j, None))
-            
-            if match_info:
-                # 构造结果
-                cell_idx = curr_i - 1
-                # 这里需要把结果存下来,最后反转
-                results.append({
-                    'cell_idx': cell_idx,
-                    'match_info': match_info
-                })
-            
-            curr_i -= 1
-            curr_j = prev_j
-            
-        return results[::-1]
-```
-
-### 💡 为什么这个方案更好?
-
-1.  **全局最优**:
-    *   **场景**:Cell 1=`2024`, Cell 2=`ID2024...`。
-    *   **贪婪**:Cell 1 看到 `ID2024...`,部分匹配 100分,抢走。Cell 2 没得吃,0分。总分 100。
-    *   **DP**:
-        *   方案 A (Cell 1 抢走): Cell 1 得 100,Cell 2 得 0。总分 100。
-        *   方案 B (Cell 1 跳过): Cell 1 匹配后面的短日期 (100分),Cell 2 匹配长ID (100分)。总分 200。
-    *   **结果**:DP 自动选择方案 B。
-
-2.  **代码清晰**:
-    *   `_compute_match_score` 只有评分逻辑,没有循环和指针。
-    *   `_match_cells_in_row_dp` 只有路径规划,没有复杂的字符串处理。
-
-3.  **天然处理合并**:
-    *   DP 的内层循环 `candidate_boxes = row_boxes[prev_j:j]` 天然支持将多个 Box 合并给一个 Cell,不需要单独写 `merged_bboxes` 逻辑。
-
-# 全局动态规划与行级动态规划详解
-
-结合 `ocr_verify/merger/table_cell_matcher.py` 代码与提供的流水分析数据,为您讲解这两种动态规划在表格匹配中的应用。
-
-## 核心背景
-
-表格匹配的任务是将 **HTML 表格结构**(来自 PP-Structure)与 **OCR 文字框**(来自 PaddleOCR)进行对齐。由于 OCR 结果可能存在漏检、误检、多行被识别为单行或单行被切分为多行的情况,简单的顺序匹配容易出错。因此,代码采用了**两级动态规划(DP)**策略。
-
----
-
-## 1. 全局动态规划 (Global DP):行级匹配
-
-**目标**:将 HTML 的 `<tr>` 行与 OCR 的“行组”(Grouped Boxes)进行对齐。
-
-### 代码位置
-`TableCellMatcher._match_html_rows_to_paddle_groups` (Line 548)
-
-### 算法原理
-*   **状态定义**:`dp[i][j]` 表示 HTML 前 `i` 行与 OCR 前 `j` 个行组匹配的最大得分。
-*   **核心挑战**:HTML 行数与 OCR 行组数往往不一致(例如 OCR 将一行文字拆成了两行,或者漏掉了某一行)。
-*   **转移方程**:
-    1.  **匹配 (Match)**:HTML 第 `i` 行与 OCR 第 `j` 组(或合并 `j` 到 `j+k` 组)匹配。
-        *   `score = similarity(html_text, ocr_text) - penalty`
-    2.  **跳过 HTML (Skip HTML)**:HTML 第 `i` 行在 OCR 中没找到对应(可能是 OCR 漏检)。
-        *   `dp[i][j] = dp[i-1][j] - SKIP_HTML_PENALTY`
-    3.  **跳过 OCR (Skip Paddle)**:OCR 第 `j` 组是噪音,不匹配任何 HTML 行(通过 `gap` 参数实现)。
-
-### 实例演示
-**数据来源**:`A用户_单元格扫描流水_page_007.json`
-
-假设 HTML 结构如下(简化):
-*   **HTML Row 0**: `1000107... 2024-07-21...` (交易记录1)
-*   **HTML Row 1**: `1000107... 2024-07-21...` (交易记录2)
-
-OCR 分组结果(按 Y 坐标聚类):
-*   **OCR Group 0**: `1000107... 2024-07-21...` (对应 HTML Row 0)
-*   **OCR Group 1**: `1000107... 2024-07-21...` (对应 HTML Row 1)
-*   **OCR Group 2**: `(噪音/水印)`
-
-**DP 过程**:
-1.  `dp[0][0]` 计算 HTML Row 0 与 OCR Group 0 的相似度,得分高。
-2.  `dp[1][1]` 基于 `dp[0][0]`,计算 HTML Row 1 与 OCR Group 1 的相似度,得分高。
-3.  如果 OCR Group 2 是噪音,算法会发现 HTML Row 2 与 OCR Group 2 匹配度极低,可能会选择跳过 OCR Group 2,或者如果 HTML 结束了,OCR Group 2 就成为“未匹配组”。
-
----
-
-## 2. 行级动态规划 (Row-level DP):单元格匹配
-
-**目标**:在已对齐的某一行内,将 HTML 的 `<td>` 单元格与该行的 OCR Boxes 进行对齐。
-
-### 代码位置
-`TableCellMatcher._match_cells_in_row_dp` (Line 171)
-
-### 算法原理
-*   **状态定义**:`dp[i][j]` 表示该行前 `i` 个 HTML 单元格消耗了前 `j` 个 OCR Boxes 的最大得分。
-*   **核心挑战**:一个单元格可能对应多个 OCR Box(例如长文本被切断),或者某个单元格内容 OCR 漏检。
-*   **转移方程**:
-    1.  **匹配 (Match)**:HTML 单元格 `i` 匹配 OCR Boxes `prev_j` 到 `j` 的合并内容。
-        *   `score = similarity(cell_text, merged_box_text)`
-        *   这里允许合并最多 `MAX_MERGE` (5) 个 Box。
-    2.  **单元格缺失 (Cell Missing)**:当前 HTML 单元格为空或 OCR 没识别到。
-        *   `dp[i][j] = dp[i-1][j]` (继承上一个状态,相当于当前单元格不消耗 Box)
-
-### 实例演示
-**数据来源**:`A用户_单元格扫描流水_page_007.json` (HTML Row 0)
-
-**HTML 单元格**:
-1.  Cell 0: `100010710124072100059119291975743003` (长数字)
-2.  Cell 1: `2024-07-2107:50:38` (时间)
-3.  Cell 2: `扫二维码付款`
-
-**OCR Boxes (按 X 排序)**:
-1.  Box 0: `100010710124072100059119291975743003`
-2.  Box 1: `2024-07-21`
-3.  Box 2: `07:50:38` (假设时间被切分为两个 Box)
-4.  Box 3: `扫二维码付款`
-
-**DP 过程**:
-1.  **i=1 (Cell 0)**:
-    *   尝试匹配 Box 0: 相似度 100%,得分高。
-    *   `dp[1][1]` 更新为高分。
-2.  **i=2 (Cell 1)**:
-    *   尝试匹配 Box 1: `2024-07-21` vs `2024-07-2107:50:38` -> 相似度一般。
-    *   尝试匹配 Box 1+2: `2024-07-21 07:50:38` vs `2024-07-2107:50:38` -> 相似度极高。
-    *   算法选择合并 Box 1 和 Box 2 赋给 Cell 1。
-    *   `dp[2][3]` 更新为 `dp[1][1] + score`。
-3.  **i=3 (Cell 2)**:
-    *   尝试匹配 Box 3: 相似度 100%。
-    *   `dp[3][4]` 更新。
-
-最终回溯路径:Cell 0 <- [Box 0], Cell 1 <- [Box 1, Box 2], Cell 2 <- [Box 3]。
-
-## 总结
-
-*   **全局 DP** 解决了“行对齐”问题,处理行层面的漏检和错位。
-*   **行级 DP** 解决了“单元格对齐”问题,处理单元格内的碎片化识别和内容匹配。
-*   两者结合,实现了从整页到细节的高精度表格还原。

ファイルの差分が大きいため隠しています
+ 0 - 4
ocr_tools/ocr_merger/坐标系变换.md


+ 0 - 345
ocr_tools/ocr_merger/表格行匹配算法可视化图示.md

@@ -1,345 +0,0 @@
-# 表格行匹配算法可视化图示
-
-## 📐 数据结构可视化
-
-### 输入数据
-
-```
-HTML表格行(7行):
-┌─────────────────────────────────────────────────────────┐
-│ 行0: 部门Department: 01372403999 柜员Search Teller...  │
-├─────────────────────────────────────────────────────────┤
-│ 行1: 账号/卡号Account/Card No: 6222621010026732125...  │
-├─────────────────────────────────────────────────────────┤
-│ 行2: 查询起日Query Starting Date: 2023-08-12...       │
-├─────────────────────────────────────────────────────────┤
-│ 行3: 查询时间Query Time: 2024年08月12日 11:21:23...    │
-├─────────────────────────────────────────────────────────┤
-│ 行4: 证件种类 ID Type: 第二代居民身份证...              │
-├─────────────────────────────────────────────────────────┤
-│ 行5: Serial Num | Trans Date | Trans Time | ... (表头) │
-├─────────────────────────────────────────────────────────┤
-│ 行6: 1 | 2023-08-12 | 13:04:52 | 网上银行卡转入...     │
-└─────────────────────────────────────────────────────────┘
-
-OCR文本分组(48组):
-┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
-│ 组0 │ 组1 │ 组2 │ 组3 │ 组4 │ 组5 │ ... │ 组46│ 组47│
-├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
-│部门 │账号 │查询 │查询 │证件 │序号 │ ... │司   │9646 │
-│信息 │信息 │起日 │时间 │信息 │表头 │     │2023 │42s  │
-└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
-```
-
----
-
-## 🔄 DP状态转移可视化
-
-### 阶段1:初始化第一行(i=0)
-
-```
-HTML行0的匹配搜索空间:
-┌─────────────────────────────────────────────────────────────┐
-│ OCR组索引:  0    1    2    3    4    5  ...  18            │
-│            ┌────┐                                          │
-│            │组0 │  ← 单独匹配                               │
-│            └────┘                                          │
-│            ┌────┬────┐                                      │
-│            │组0 │组1 │  ← 合并2组                           │
-│            └────┴────┘                                      │
-│            ┌────┬────┬────┐                                │
-│            │组0 │组1 │组2 │  ← 合并3组 (最优: 得分0.98)    │
-│            └────┴────┴────┘                                │
-│            ┌────┬────┬────┬────┐                          │
-│            │组0 │组1 │组2 │组3 │  ← 合并4组                │
-│            └────┴────┴────┴────┘                          │
-└─────────────────────────────────────────────────────────────┘
-
-DP状态矩阵(初始化后):
-     OCR组索引 j →
-HTML  0    1    2    3    4    5  ...  47
-行↓
-0    [0.98] -    -    -    -    -  ...  -
-      ↑
-   最优匹配: HTML行0 → OCR组[0,1,2]
-```
-
----
-
-### 阶段2:处理第二行(i=1)
-
-```
-从 dp[0][2] = 0.98 出发,搜索HTML行1的匹配:
-
-搜索窗口(SEARCH_WINDOW=15):
-┌─────────────────────────────────────────────────────────────┐
-│ 上一行结束位置: 组2                                         │
-│                                                             │
-│ 搜索范围: 组3 到 组17 (最多向前15个组)                     │
-│                                                             │
-│ 组索引:  2  [3]  [4]  [5]  [6]  ...  [17]                  │
-│         ↑    │    │    │    │         │                    │
-│      起点    └────┴────┴────┴─────────┘                    │
-│             搜索窗口                                        │
-└─────────────────────────────────────────────────────────────┘
-
-匹配尝试:
-┌─────────────────────────────────────────────────────────────┐
-│ gap=0, start_j=3, count=1 → end_j=3                        │
-│   匹配: 组3单独                                             │
-│   相似度: 0.85, 得分: 0.98 + 0.85 = 1.83                   │
-│                                                             │
-│ gap=0, start_j=3, count=2 → end_j=4                        │
-│   匹配: 组3+组4                                             │
-│   相似度: 0.92, 得分: 0.98 + 0.92 = 1.90 ✅ (最优)         │
-│                                                             │
-│ gap=1, start_j=4, count=1 → end_j=4                        │
-│   匹配: 组4单独                                             │
-│   相似度: 0.20, 得分: 0.98 + 0.20 = 1.18                   │
-└─────────────────────────────────────────────────────────────┘
-
-DP状态矩阵更新:
-     OCR组索引 j →
-HTML  0    1    2    3    4    5  ...  47
-行↓
-0    [0.98] -    -    -    -    -  ...  -
-1    -     -    -    1.83 [1.90] -  ...  -
-                      ↑     ↑
-                   候选1  最优匹配
-```
-
----
-
-### 阶段3:完整DP过程(简化版)
-
-```
-DP状态矩阵(完整过程):
-     OCR组索引 j →
-HTML  0    1    2    3    4    5    6    7  ...  40  41  ...  47
-行↓
-0    [0.98] -    -    -    -    -    -    -  ...  -   -  ...  -
-      │
-      └─→ 匹配组[0,1,2]
-      
-1    -     -    -    1.83 [1.90] -    -    -  ...  -   -  ...  -
-                      │     │
-                      └─────┴─→ 匹配组[3,4]
-                      
-2    -     -    -    -    -    [2.85] -    -  ...  -   -  ...  -
-                              │
-                              └─→ 匹配组[5,6]
-                              
-3    -     -    -    -    -    -    [3.80] -  ...  -   -  ...  -
-                                   │
-                                   └─→ 匹配组[7,8,9]
-                                   
-4    -     -    -    -    -    -    -    [4.75] ...  -   -  ...  -
-                                        │
-                                        └─→ 匹配组[10,11]
-                                        
-5    -     -    -    -    -    -    -    -  ...  [5.20] -  ...  -
-                                                      │
-                                                      └─→ 匹配组[12..39]
-                                                      
-6    -     -    -    -    -    -    -    -  ...  -   -  ...  [5.50]
-                                                                 │
-                                                                 └─→ 匹配组[40..47] ✅
-```
-
----
-
-## 🔍 回溯路径可视化
-
-### 从最优终点回溯
-
-```
-最优终点: dp[6][47] = 5.50
-
-回溯过程:
-┌─────────────────────────────────────────────────────────────┐
-│ 步骤1: (i=6, j=47)                                         │
-│   path[(6, 47)] = (prev_j=45, start_j=46)                  │
-│   → HTML行6匹配OCR组[46, 47]                               │
-│   → 移动到 (i=5, j=45)                                     │
-│                                                             │
-│ 步骤2: (i=5, j=45)                                         │
-│   path[(5, 45)] = (prev_j=39, start_j=40)                  │
-│   → HTML行5匹配OCR组[40..45]                               │
-│   → 移动到 (i=4, j=39)                                     │
-│                                                             │
-│ 步骤3: (i=4, j=39)                                         │
-│   path[(4, 39)] = (prev_j=37, start_j=38)                  │
-│   → HTML行4匹配OCR组[38, 39]                               │
-│   → 移动到 (i=3, j=37)                                     │
-│                                                             │
-│ ... 继续回溯 ...                                            │
-│                                                             │
-│ 步骤7: (i=0, j=2)                                          │
-│   path[(0, 2)] = (prev_j=-1, start_j=0)                   │
-│   → HTML行0匹配OCR组[0, 1, 2]                              │
-│   → 回溯结束                                               │
-└─────────────────────────────────────────────────────────────┘
-
-最终匹配结果:
-HTML行0 ──────→ OCR组[0, 1, 2]
-HTML行1 ──────→ OCR组[3, 4]
-HTML行2 ──────→ OCR组[5, 6]
-HTML行3 ──────→ OCR组[7, 8, 9]
-HTML行4 ──────→ OCR组[10, 11]
-HTML行5 ──────→ OCR组[12, 13, ..., 39]
-HTML行6 ──────→ OCR组[40, 41, ..., 47]
-```
-
----
-
-## 🎯 关键概念图示
-
-### 1. 合并文本缓存(merged_cache)
-
-```
-merged_cache[(start_j, count)] = 从start_j开始合并count个组的文本
-
-示例: start_j=0, count=3
-┌─────┬─────┬─────┐
-│ 组0 │ 组1 │ 组2 │  → 合并文本: "部门department:...账号/卡号...查询起日..."
-└─────┴─────┴─────┘
-   ↑     ↑     ↑
-   └─────┴─────┘
-    count=3
-```
-
-### 2. Gap搜索机制
-
-```
-上一行结束位置: prev_j = 2
-
-搜索窗口:
-┌─────────────────────────────────────────────────────────────┐
-│ prev_j=2                                                     │
-│   │                                                          │
-│   │  gap=0: start_j=3  (紧接上一行)                         │
-│   │  gap=1: start_j=4  (跳过1个组)                          │
-│   │  gap=2: start_j=5  (跳过2个组)                          │
-│   │  ...                                                     │
-│   │  gap=14: start_j=16 (最多跳过14个组)                    │
-│   │                                                          │
-│   └──────────────────────────────────────────────────────────┘
-│              SEARCH_WINDOW = 15
-└─────────────────────────────────────────────────────────────┘
-```
-
-### 3. 跳过HTML行的处理
-
-```
-场景: HTML行i无法匹配任何OCR组
-
-处理方式:
-┌─────────────────────────────────────────────────────────────┐
-│ HTML行i-1 → OCR组j (得分: 2.5)                              │
-│   │                                                          │
-│   ├─→ HTML行i匹配OCR组j+1 (正常匹配)                        │
-│   │                                                          │
-│   └─→ HTML行i跳过 (继承状态,得分: 2.5 - 0.3 = 2.2)         │
-│       → HTML行i+1可以从OCR组j开始匹配                        │
-└─────────────────────────────────────────────────────────────┘
-```
-
----
-
-## 📊 性能优化可视化
-
-### 剪枝优化
-
-```
-剪枝前: 上一行有50个有效状态
-┌─────────────────────────────────────────────────────────────┐
-│ valid_prev_indices = [0, 1, 2, ..., 49]  (50个状态)        │
-│                                                             │
-│ 需要计算: 50 × 15 × 4 = 3000 次匹配                         │
-└─────────────────────────────────────────────────────────────┘
-
-剪枝后: 只保留得分最高的30个
-┌─────────────────────────────────────────────────────────────┐
-│ valid_prev_indices = [2, 5, 8, ..., 47]  (30个状态)         │
-│                                                             │
-│ 需要计算: 30 × 15 × 4 = 1800 次匹配  (减少40%)              │
-└─────────────────────────────────────────────────────────────┘
-```
-
-### 长度预筛选
-
-```
-HTML文本长度: 100字符
-OCR文本长度: 10字符
-
-长度比例: 10 / 100 = 0.1 < 0.2 (阈值)
-
-┌─────────────────────────────────────────────────────────────┐
-│ ❌ 直接跳过,不计算相似度                                    │
-│   节省计算时间                                               │
-└─────────────────────────────────────────────────────────────┘
-```
-
----
-
-## 🔄 完整流程图
-
-```
-开始
- │
- ├─→ 提取HTML行文本 (7行)
- │
- ├─→ 提取OCR组文本 (48组)
- │
- ├─→ 检查行数是否相等?
- │   ├─→ 是 → 验证对角线匹配
- │   │        ├─→ 匹配良好 → 1:1映射 → 结束
- │   │        └─→ 匹配不佳 → 继续DP
- │   └─→ 否 → 继续DP
- │
- ├─→ 预计算合并文本缓存 (merged_cache)
- │
- ├─→ 初始化第一行 (i=0)
- │   └─→ 搜索前15+4个组,找到最佳匹配
- │
- ├─→ DP状态转移 (i=1..6)
- │   ├─→ 获取上一行有效状态
- │   ├─→ 剪枝(保留前30个)
- │   ├─→ 允许跳过HTML行
- │   └─→ 正常匹配(搜索窗口15,最多合并4组)
- │
- ├─→ 回溯找最优路径
- │   └─→ 从最优终点回溯,构建映射关系
- │
- ├─→ 后处理
- │   ├─→ 处理未匹配的OCR组(Orphans)
- │   └─→ 按Y坐标排序
- │
- └─→ 返回映射结果
-```
-
----
-
-## 💡 实际案例数据流
-
-### 用户提供的实际数据
-
-```
-HTML行数: 7
-OCR组数: 48
-
-HTML行0文本: '部门department:01372403999柜员searchteller:ebf0000...'
-OCR组0文本: '部门department:01372403999柜员searchteller:ebf0000...'
-→ 相似度: 0.95+ ✅
-
-HTML行5文本: 'serialnum序号transdate交易日期transtime交易时间...'
-OCR组5-39文本: 'serialtransdatetranstimetradingtypedcflg...'
-→ 相似度: 0.85+ ✅
-
-HTML行6文本: '12023-08-1213:04:52网上银行卡转入贷cr33.52...'
-OCR组40-47文本: '2023-08-1213:04:5233.52554.5210*****69...'
-→ 相似度: 0.80+ ✅
-```
-
-这个算法通过动态规划,成功地将7个HTML行与48个OCR组进行了智能匹配!
-

+ 0 - 375
ocr_tools/ocr_merger/表格行匹配算法详解.md

@@ -1,375 +0,0 @@
-# 表格行匹配算法详解:`_match_html_rows_to_paddle_groups`
-
-## 📋 算法概述
-
-这是一个**动态规划(DP)算法**,用于将HTML表格行与OCR检测到的文本分组进行智能匹配。
-
-### 问题场景
-- **输入1**: HTML表格行(7行)
-  - 第0行:表头信息(部门、柜员、打印日期等)
-  - 第1-5行:账户信息行(账号、查询日期、证件信息等)
-  - 第6行:表头(Serial Num, Trans Date等)
-  - 第7行:数据行(实际交易记录)
-
-- **输入2**: OCR文本分组(48组)
-  - 每组包含多个OCR检测框,按Y坐标分组
-  - 组0-4:表头信息
-  - 组5-47:表头和数据行混合
-
-### 核心挑战
-1. **1对多匹配**:一个HTML行可能对应多个OCR组(合并单元格、OCR分割错误)
-2. **跳过处理**:某些HTML行可能没有对应的OCR组(OCR漏检)
-3. **顺序不一致**:HTML行顺序与OCR组顺序可能不完全一致
-
----
-
-## 🎯 算法流程
-
-### 阶段1:预处理与验证
-
-#### 1.1 提取文本
-```python
-# HTML行文本(7行)
-html_row_texts = [
-    '部门department:01372403999柜员searchteller:ebf0000...',  # 行0
-    '账号/卡号account/cardno:6222621010026732125...',         # 行1
-    '查询起日querystartingdate:2023-08-12...',                 # 行2
-    '查询时间querytime:2024年08月12日...',                     # 行3
-    '证件种类idtype:第二代居民身份证...',                       # 行4
-    'serialnum序号transdate交易日期...',                       # 行5(表头)
-    '12023-08-1213:04:52网上银行卡转入...'                    # 行6(数据行)
-]
-
-# OCR组文本(48组)
-group_texts = [
-    '部门department:01372403999...',  # 组0
-    '账号/卡号account/cardno:...',    # 组1
-    '查询起日querystartingdate:...',  # 组2
-    ...
-    '964642s0110305'                  # 组47
-]
-```
-
-#### 1.2 快速路径:行数相等时的验证
-```python
-if len(html_rows) == len(grouped_boxes):  # 7 == 48? False
-    # 不执行此分支
-```
-
-**实际场景**:7行 ≠ 48组,需要DP算法
-
----
-
-### 阶段2:预计算合并文本缓存
-
-#### 2.1 构建 `merged_cache`
-为了支持**一个HTML行匹配多个OCR组**,预计算所有可能的组合:
-
-```python
-MAX_MERGE = 4  # 最多合并4个OCR组
-merged_cache = {
-    (0, 1): '部门department:01372403999...',           # 组0单独
-    (0, 2): '部门department:...账号/卡号account...',    # 组0+组1
-    (0, 3): '部门department:...查询起日...',            # 组0+组1+组2
-    (0, 4): '部门department:...查询时间...',            # 组0+组1+组2+组3
-    (1, 1): '账号/卡号account/cardno:...',              # 组1单独
-    (1, 2): '账号/卡号...查询起日...',                  # 组1+组2
-    ...
-    (47, 1): '964642s0110305'                           # 组47单独
-}
-```
-
-**作用**:避免在DP过程中重复拼接文本,提升性能
-
----
-
-### 阶段3:动态规划初始化
-
-#### 3.1 DP状态定义
-```python
-# dp[i][j] = 将HTML前i行匹配到OCR前j组的最大得分
-dp = np.full((7, 48), -np.inf)  # 初始化为负无穷
-
-# path[(i, j)] = (prev_j, start_j)
-# prev_j: 上一行结束的OCR组索引
-# start_j: 当前行开始的OCR组索引
-path = {}
-```
-
-#### 3.2 初始化第一行(i=0)
-
-**目标**:找到HTML第0行与OCR组的最佳匹配
-
-```python
-SEARCH_WINDOW = 15  # 只在前15个组中搜索
-MAX_MERGE = 4       # 最多合并4个组
-
-# 遍历所有可能的匹配
-for end_j in range(min(48, 15 + 4)):  # end_j: 0..18
-    for count in range(1, 5):          # count: 1..4
-        start_j = end_j - count + 1    # 计算起始组索引
-        
-        # 例如:end_j=3, count=2 → start_j=2
-        # 表示:HTML行0 匹配 OCR组2+组3
-        
-        current_text = merged_cache.get((start_j, count), "")
-        similarity = calculate_similarity(html_row_texts[0], current_text)
-        
-        penalty = start_j * 0.1  # 跳过组的惩罚
-        score = similarity - penalty
-        
-        if score > 0.1:  # 只有得分足够高才记录
-            dp[0][end_j] = max(dp[0][end_j], score)
-            path[(0, end_j)] = (-1, start_j)  # -1表示这是第一行
-```
-
-**示例计算过程**:
-
-| end_j | count | start_j | 匹配的OCR组 | 相似度 | 惩罚 | 得分 | 是否记录 |
-|-------|-------|---------|-------------|--------|------|------|----------|
-| 0 | 1 | 0 | 组0 | 0.95 | 0.0 | 0.95 | ✅ |
-| 1 | 1 | 1 | 组1 | 0.15 | 0.1 | 0.05 | ❌ |
-| 1 | 2 | 0 | 组0+组1 | 0.98 | 0.0 | 0.98 | ✅ |
-| 2 | 3 | 0 | 组0+组1+组2 | 0.92 | 0.0 | 0.92 | ✅ |
-| ... | ... | ... | ... | ... | ... | ... | ... |
-
-**结果**:假设 `dp[0][2] = 0.98` 是最优,表示HTML行0匹配OCR组0+组1+组2
-
----
-
-### 阶段4:DP状态转移(核心)
-
-#### 4.1 处理第i行(i=1..6)
-
-**状态转移方程**:
-```
-dp[i][end_j] = max {
-    dp[i-1][prev_j] + match_score(i, start_j..end_j) - gap_penalty
-}
-```
-
-**具体步骤**:
-
-##### Step 1: 获取上一行的有效状态
-```python
-# 找到所有 dp[i-1][j] > -inf 的位置
-valid_prev_indices = [j for j in range(48) if dp[i-1][j] > -np.inf]
-
-# 例如处理第1行时:
-# valid_prev_indices = [2]  # 因为dp[0][2] = 0.98
-```
-
-##### Step 2: 剪枝优化
-```python
-if len(valid_prev_indices) > 30:
-    # 只保留得分最高的前30个
-    valid_prev_indices.sort(key=lambda j: dp[i-1][j], reverse=True)
-    valid_prev_indices = valid_prev_indices[:30]
-```
-
-##### Step 3: 允许跳过HTML行
-```python
-# 如果当前HTML行没有匹配,可以跳过(继承上一行状态)
-for prev_j in valid_prev_indices:
-    score_skip = dp[i-1][prev_j] - 0.3  # SKIP_HTML_PENALTY
-    if score_skip > dp[i][prev_j]:
-        dp[i][prev_j] = score_skip
-        path[(i, prev_j)] = (prev_j, prev_j + 1)  # 空范围,表示跳过
-```
-
-##### Step 4: 正常匹配逻辑
-```python
-for prev_j in valid_prev_indices:  # 例如:prev_j = 2
-    prev_score = dp[i-1][prev_j]    # 例如:0.98
-    
-    max_gap = min(15, 48 - prev_j - 1)  # 最多向前搜索15个组
-    
-    for gap in range(max_gap):  # gap: 0..14
-        start_j = prev_j + 1 + gap  # 例如:gap=0 → start_j=3
-        
-        for count in range(1, 5):  # count: 1..4
-            end_j = start_j + count - 1  # 例如:count=2 → end_j=4
-            
-            # 获取合并文本
-            current_text = merged_cache.get((start_j, count), "")
-            
-            # 长度预筛选(避免明显不匹配的情况)
-            if len(html_text) > 10 and len(current_text) < len(html_text) * 0.2:
-                continue  # 跳过
-            
-            # 计算相似度
-            similarity = calculate_similarity(html_text, current_text)
-            
-            # 计算惩罚
-            gap_penalty = gap * 0.1  # 跳过组的惩罚
-            len_penalty = ...         # 长度不匹配的惩罚
-            
-            current_score = similarity - gap_penalty - len_penalty
-            
-            if current_score > 0.1:  # 只有正收益才转移
-                total_score = prev_score + current_score
-                
-                if total_score > dp[i][end_j]:
-                    dp[i][end_j] = total_score
-                    path[(i, end_j)] = (prev_j, start_j)
-```
-
-**示例:处理HTML行1(账号信息行)**
-
-假设 `dp[0][2] = 0.98`(上一行匹配到组2)
-
-| gap | start_j | count | end_j | 匹配的OCR组 | 相似度 | 总得分 | 是否更新 |
-|-----|---------|-------|-------|-------------|--------|--------|----------|
-| 0 | 3 | 1 | 3 | 组3 | 0.85 | 0.98+0.85=1.83 | ✅ |
-| 0 | 3 | 2 | 4 | 组3+组4 | 0.92 | 0.98+0.92=1.90 | ✅ |
-| 1 | 4 | 1 | 4 | 组4 | 0.20 | 0.98+0.20=1.18 | ✅ |
-| ... | ... | ... | ... | ... | ... | ... | ... |
-
-最终:`dp[1][4] = 1.90`,`path[(1, 4)] = (2, 3)`
-
----
-
-### 阶段5:回溯找最优路径
-
-#### 5.1 找到最优终点
-```python
-best_end_j = -1
-max_score = -np.inf
-
-# 从最后一行往前找
-for i in range(6, -1, -1):  # i: 6, 5, 4, ..., 0
-    for j in range(48):
-        if dp[i][j] > max_score:
-            max_score = dp[i][j]
-            best_end_j = j
-            best_last_row = i
-
-# 例如:dp[6][47] = 5.2 是最大值
-# → best_end_j = 47, best_last_row = 6
-```
-
-#### 5.2 回溯构建映射
-```python
-mapping = {}
-curr_i = 6  # best_last_row
-curr_j = 47  # best_end_j
-
-while curr_i >= 0:
-    if (curr_i, curr_j) in path:
-        prev_j, start_j = path[(6, 47)]  # 例如:(45, 46)
-        
-        # 如果 start_j <= curr_j,说明消耗了OCR组
-        if start_j <= curr_j:  # 46 <= 47? True
-            indices = list(range(start_j, curr_j + 1))  # [46, 47]
-            mapping[6] = [46, 47]  # HTML行6匹配OCR组46+47
-        
-        curr_j = prev_j  # 45
-        curr_i -= 1      # 5
-    else:
-        break
-
-# 继续回溯...
-# mapping[5] = [40, 41, 42, 43, 44, 45]
-# mapping[4] = [38, 39]
-# ...
-# mapping[0] = [0, 1, 2]
-```
-
-**最终映射结果**:
-```python
-mapping = {
-    0: [0, 1, 2],           # HTML行0 → OCR组0+1+2
-    1: [3, 4],               # HTML行1 → OCR组3+4
-    2: [5, 6],               # HTML行2 → OCR组5+6
-    3: [7, 8, 9],            # HTML行3 → OCR组7+8+9
-    4: [10, 11],             # HTML行4 → OCR组10+11
-    5: [12, 13, ..., 39],   # HTML行5 → OCR组12..39(表头)
-    6: [40, 41, ..., 47]    # HTML行6 → OCR组40..47(数据行)
-}
-```
-
----
-
-### 阶段6:后处理
-
-#### 6.1 处理未匹配的OCR组(Orphans)
-```python
-used_groups = {0, 1, 2, ..., 47}  # 已使用的组
-unused_groups = []  # 如果没有未使用的组,跳过
-
-# 如果有未使用的组,找到最近的已匹配组,合并过去
-```
-
-#### 6.2 按Y坐标排序
-```python
-for row_idx in mapping:
-    if mapping[row_idx]:
-        # 按OCR组的Y坐标排序,确保顺序正确
-        mapping[row_idx].sort(key=lambda idx: grouped_boxes[idx]['y_center'])
-```
-
----
-
-## 📊 算法复杂度
-
-- **时间复杂度**:O(n_html × n_paddle × SEARCH_WINDOW × MAX_MERGE)
-  - 本例:7 × 48 × 15 × 4 ≈ 20,160 次计算
-- **空间复杂度**:O(n_html × n_paddle)
-  - 本例:7 × 48 = 336 个DP状态
-
----
-
-## 🎨 可视化示例
-
-### DP状态矩阵(简化版)
-
-```
-        OCR组索引 →
-HTML  0  1  2  3  4  5  6  7  ...  47
-行↓
-0    [0.98] -  -  -  -  -  -  ...  -
-1    -  -  -  [1.90] -  -  -  ...  -
-2    -  -  -  -  -  [2.85] -  ...  -
-3    -  -  -  -  -  -  [3.80] ...  -
-4    -  -  -  -  -  -  -  [4.75] ...  -
-5    -  -  -  -  -  -  -  -  ...  [5.20]
-6    -  -  -  -  -  -  -  -  ...  [5.50] ← 最优终点
-```
-
-### 匹配路径(箭头表示)
-
-```
-HTML行0 → OCR组[0,1,2]
-    ↓
-HTML行1 → OCR组[3,4]
-    ↓
-HTML行2 → OCR组[5,6]
-    ↓
-...
-    ↓
-HTML行6 → OCR组[40..47]
-```
-
----
-
-## 🔑 关键优化点
-
-1. **预计算合并文本**:避免重复拼接,提升性能
-2. **剪枝优化**:只保留前30个最优状态,防止组合爆炸
-3. **允许跳过HTML行**:提高鲁棒性,处理OCR漏检
-4. **长度预筛选**:快速排除明显不匹配的情况
-5. **多组合并**:支持一个HTML行匹配多个OCR组(MAX_MERGE=4)
-
----
-
-## 📝 总结
-
-这个算法通过**动态规划**实现了:
-- ✅ 智能匹配HTML行与OCR组
-- ✅ 支持1对多匹配(合并单元格)
-- ✅ 处理OCR漏检(跳过HTML行)
-- ✅ 处理顺序不一致(通过gap搜索)
-- ✅ 高效计算(预计算+剪枝)
-
-最终得到每个HTML行对应的OCR组索引列表,用于后续的单元格匹配。
-

+ 2 - 0
ocr_tools/paddle_vl_tool/README.md

@@ -2,6 +2,8 @@
 
 基于 PaddleOCR-VL 的批量文档处理工具,支持 PDF 和图片文件的批量处理。
 
+> 📚 **详细文档**:更多技术文档、环境配置、参数说明等,请查看 [docs/paddlex/](../../docs/paddlex/)
+
 ## 功能特性
 
 - ✅ 统一输入接口:支持 PDF 文件、图片文件、图片目录、文件列表(.txt)、CSV 文件

+ 2 - 0
ocr_tools/ppstructure_tool/README.md

@@ -2,6 +2,8 @@
 
 基于 PP-StructureV3 的批量文档处理工具,支持 PDF 和图片文件的批量处理。
 
+> 📚 **详细文档**:更多技术文档、环境配置、参数说明等,请查看 [docs/paddlex/](../../docs/paddlex/)
+
 本工具提供两种处理方式:
 - **本地处理** (`main.py`):直接调用本地 PaddleX pipeline 进行处理
 - **API 客户端** (`api_client.py`):通过 HTTP API 调用远程服务进行处理

+ 2 - 0
ocr_tools/pytorch_models/README.md

@@ -1,5 +1,7 @@
 # Unified PyTorch Models
 
+> 📚 **详细文档**:ONNX 转换、推理算法等详细技术文档,请查看 [docs/ocr_tools/pytorch_models/](../../docs/ocr_tools/pytorch_models/)
+
 统一的 PyTorch 模型推理接口,支持布局检测、OCR、文档方向分类等功能。
 
 **位置**: `ocr_platform/ocr_tools/pytorch_models/`

+ 0 - 324
ocr_tools/pytorch_models/onnx转换、推理算法.md

@@ -1,324 +0,0 @@
-```bash
-pip install onnx2pytorch
-paddlex \
-    --paddle2onnx \
-    --paddle_model_dir /Users/zhch158/.paddlex/official_models/RT-DETR-H_layout_17cls \
-    --onnx_model_dir ./unified_pytorch_models/Layout
-
-paddlex \
-    --paddle2onnx \
-    --paddle_model_dir /Users/zhch158/.paddlex/official_models/PP-LCNet_x1_0_doc_ori \
-    --onnx_model_dir ./unified_pytorch_models/Layout
-```
-
----
-
-# onnx方向检测算法说明
-
-**mineru/model/ori_cls/paddle_ori_cls.py**
-
-## 方法概览
-
-这个方法将输入图像预处理为适合方向分类模型的格式,包含三个主要步骤:**缩放**、**裁剪**和**归一化**。
-```
-
----
-
-## 步骤 1: 图像缩放
-
-```python
-scale = 256 / min(h, w)
-h_resize = round(h * scale)
-w_resize = round(w * scale)
-img = cv2.resize(input_img, (w_resize, h_resize), interpolation=1)
-```
-
-**`256` 的含义:**
-
-- 这是**目标最短边长度**
-- 确保图像有足够的分辨率进行后续处理
-- 例如:如果原图是 100×200,缩放后变为 256×512
-
-**`interpolation=1`:**
-
-- 对应 `cv2.INTER_LINEAR`,双线性插值方法
-
----
-
-## 步骤 2: 中心裁剪为正方形
-
-```python
-cw, ch = 224, 224
-x1 = max(0, (w - cw) // 2)
-y1 = max(0, (h - ch) // 2)
-x2 = min(w, x1 + cw)
-y2 = min(h, y1 + ch)
-```
-
-**`224×224` 的含义:**
-
-- 这是模型的**标准输入尺寸**
-- 大多数图像分类模型(如 ResNet、VGG)使用此尺寸
-- 从图像中心裁剪出 224×224 的区域
-
-**裁剪逻辑:**
-
-- `(w - cw) // 2`:计算水平方向的起始位置(居中)
-- `(h - ch) // 2`:计算垂直方向的起始位置(居中)
-
----
-
-## 步骤 3: 归一化
-
-```python
-std = [0.229, 0.224, 0.225]
-scale = 0.00392156862745098  # 1/255
-mean = [0.485, 0.456, 0.406]
-alpha = [scale / std[i] for i in range(len(std))]
-beta = [-mean[i] / std[i] for i in range(len(std))]
-```
-
-**这些"魔法数字"的含义:**
-
-### `scale = 0.00392156862745098`
-
-- 等于 $\frac{1}{255}$
-- 将像素值从 $[0, 255]$ 范围归一化到 $[0, 1]$
-
-### `mean = [0.485, 0.456, 0.406]` 和 `std = [0.229, 0.224, 0.225]`
-
-- 这是 **ImageNet 数据集的统计值**(RGB 三个通道)
-- 用于标准化图像,使其分布与训练数据一致
-- 归一化公式:
-
-$$
-\text{normalized\_pixel} = \frac{\frac{\text{pixel}}{255} - \text{mean}}{\text{std}}
-$$
-
-**代码中的优化实现:**
-
-```python
-alpha = [scale / std[i] for i in range(len(std))]  # [1/255/std[0], 1/255/std[1], 1/255/std[2]]
-beta = [-mean[i] / std[i] for i in range(len(std))]
-```
-
-- 预计算系数以提高效率
-- 每个通道: `pixel * alpha[c] + beta[c]`
-
----
-
-## 潜在问题与改进建议
-
-### ⚠️ 问题 1: 重复定义变量
-
-```python
-# 类中已定义
-self.std = [0.229, 0.224, 0.225]
-self.mean = [0.485, 0.456, 0.406]
-self.cw, self.ch = 224, 224
-
-# 但方法中又重新定义了局部变量
-std = [0.229, 0.224, 0.225]
-cw, ch = 224, 224
-```
-
-**建议:**使用类属性以提高代码可维护性:
-
-````python
-def preprocess(self, input_img):
-    # 放大图片,使其最短边长为256
-    h, w = input_img.shape[:2]
-    scale = self.less_length / min(h, w)  # 使用 self.less_length (256)
-    h_resize = round(h * scale)
-    w_resize = round(w * scale)
-    img = cv2.resize(input_img, (w_resize, h_resize), interpolation=1)
-  
-    # 调整为224*224的正方形
-    h, w = img.shape[:2]
-    x1 = max(0, (w - self.cw) // 2)  # 使用 self.cw
-    y1 = max(0, (h - self.ch) // 2)  # 使用 self.ch
-    x2 = min(w, x1 + self.cw)
-    y2 = min(h, y1 + self.ch)
-  
-    if w < self.cw or h < self.ch:
-        raise ValueError(
-            f"Input image ({w}, {h}) smaller than target ({self.cw}, {self.ch})."
-        )
-    img = img[y1:y2, x1:x2, ...]
-  
-    # 正则化
-    split_im = list(cv2.split(img))
-    alpha = [self.scale / self.std[i] for i in range(len(self.std))]  # 使用 self.std
-    beta = [-self.mean[i] / self.std[i] for i in range(len(self.std))]  # 使用 self.mean
-  
-    for c in range(img.shape[2]):
-        split_im[c] = split_im[c].astype(np.float32)
-        split_im[c] *= alpha[c]
-        split_im[c] += beta[c]
-  
-    img = cv2.merge(split_im)
-    img = img.transpose((2, 0, 1))  # CHW 格式
-    imgs = [img]
-    x = np.stack(imgs, axis=0).astype(dtype=np.float32, copy=False)
-    return x
-````
-
-### ⚠️ 问题 2: 不必要的列表包装
-
-```python
-imgs = [img]
-x = np.stack(imgs, axis=0)
-```
-
-可以简化为:
-
-```python
-x = np.expand_dims(img, axis=0).astype(dtype=np.float32, copy=False)
-```
-
----
-
-## 总结
-
-| 数值         | 含义             | 作用                    |
-| ------------ | ---------------- | ----------------------- |
-| `256`      | 缩放后最短边长度 | 保证图像有足够分辨率    |
-| `224`      | 模型输入尺寸     | 标准分类模型输入        |
-| `1/255`    | 像素归一化因子   | 将 [0,255] 映射到 [0,1] |
-| `mean/std` | ImageNet 统计值  | 标准化图像分布          |
-
-这些数值都是计算机视觉领域的**标准配置**,源于在 ImageNet 数据集上训练的经典模型。
-
----
-
-# 什么是正则化(Normalization)?
-
-在这个代码中的"正则化"实际上是**标准化(Standardization)**,是深度学习中的一种**数据预处理技术**。
-
----
-
-## 1️⃣ 核心概念
-
-### 数学公式
-
-```python
-normalized_value = (pixel_value / 255 - mean) / std
-```
-
-对应代码中的实现:
-
-```python
-alpha = [scale / std[i] for i in range(len(std))]  # scale = 1/255
-beta = [-mean[i] / std[i] for i in range(len(std))]
-
-# 对每个像素: pixel * alpha + beta
-split_im[c] *= alpha[c]  # pixel/255/std
-split_im[c] += beta[c]   # -mean/std
-```
-
----
-
-## 2️⃣ 为什么需要标准化?
-
-### **原因 1: 统一数据分布**
-
-```python
-# 原始像素值范围: [0, 255]
-# 不同图像的分布差异很大
-
-# 标准化后: 均值≈0, 标准差≈1
-# 使所有图像具有相似的统计特性
-```
-
-### **原因 2: 加速模型训练**
-
-- 梯度下降更稳定
-- 学习率更容易设置
-- 避免某些特征主导训练
-
-### **原因 3: 匹配训练数据分布**
-
-```python
-mean = [0.485, 0.456, 0.406]  # ImageNet 数据集的均值
-std = [0.229, 0.224, 0.225]   # ImageNet 数据集的标准差
-```
-
-这些值是从 ImageNet 百万张图像计算得出的,使用相同的标准化确保推理时的输入分布与训练时一致。
-
----
-
-## 3️⃣ 代码逐步解析
-
-### **步骤 1: 分离通道**
-
-```python
-split_im = list(cv2.split(img))
-# 将 BGR 图像分离为 [B, G, R] 三个通道
-```
-
-### **步骤 2: 计算标准化系数**
-
-```python
-alpha = [scale / std[i] for i in range(len(std))]
-# alpha = [1/255/0.229, 1/255/0.224, 1/255/0.225]
-# alpha ≈ [0.01709, 0.01752, 0.01741]
-
-beta = [-mean[i] / std[i] for i in range(len(std))]
-# beta = [-0.485/0.229, -0.456/0.224, -0.406/0.225]
-# beta ≈ [-2.118, -2.036, -1.804]
-```
-
-### **步骤 3: 应用标准化**
-
-```python
-for c in range(img.shape[2]):  # 对每个通道 (B, G, R)
-    split_im[c] = split_im[c].astype(np.float32)
-    split_im[c] *= alpha[c]  # (pixel / 255) / std
-    split_im[c] += beta[c]   # - mean / std
-```
-
-### **步骤 4: 合并通道**
-
-```python
-img = cv2.merge(split_im)
-```
-
----
-
-## 4️⃣ 实际效果对比
-
-```python
-# 示例:蓝色通道的一个像素值为 128
-
-# 原始值
-pixel = 128
-
-# 标准化后
-normalized = (128 / 255 - 0.485) / 0.229
-           = (0.502 - 0.485) / 0.229
-           ≈ 0.074
-
-# 代码中的计算
-result = 128 * alpha[0] + beta[0]
-       = 128 * 0.01709 - 2.118
-       ≈ 0.069  # 与上面结果一致(有浮点误差)
-```
-
----
-
-## 5️⃣ 术语澄清
-
-| 中文术语         | 英文术语        | 代码中的用法                 |
-| ---------------- | --------------- | ---------------------------- |
-| **归一化** | Normalization   | 将数据缩放到 [0,1] 或 [-1,1] |
-| **标准化** | Standardization | 使数据均值=0,标准差=1        |
-| **正则化** | Regularization  | 防止过拟合的技术(L1/L2)      |
-
-**代码注释有误:**
-
-```python
-# 正则化  ← 这里应该写"标准化"
-split_im = list(cv2.split(img))
-```
-
----

+ 0 - 278
ocr_tools/universal_doc_parser/Layout后处理-文本转表格.md

@@ -1,278 +0,0 @@
-# Layout后处理:大面积文本块转表格
-
-## 📋 功能说明
-
-当Layout检测将大面积的表格区域误识别为文本框时,可以通过后处理自动将其转换为表格类型。
-
-## 🎯 使用场景
-
-### 典型问题
-
-从 `page_022.json` 可以看到:
-- 一个很大的文本框(bbox: [226, 288, 1495, 1685])
-- 包含了多个表格的内容(账龄分析、主要单位往来、存货等)
-- 但被识别为 `type: "text"`,而不是 `type: "table"`
-
-### 解决方案
-
-通过后处理规则,自动将满足条件的大文本块转换为表格:
-- ✅ 面积占比超过阈值(默认25%)
-- ✅ 宽度和高度都超过一定比例(避免细长条)
-- ✅ 页面中没有其他表格(避免误判)
-
-## ⚙️ 配置选项
-
-### 配置文件示例
-
-```yaml
-# layout后处理配置
-layout:
-  # 将大面积文本块转换为表格(后处理)
-  convert_large_text_to_table: true  # 是否启用
-  min_text_area_ratio: 0.25           # 最小面积占比(25%)
-  min_text_width_ratio: 0.4          # 最小宽度占比(40%)
-  min_text_height_ratio: 0.3         # 最小高度占比(30%)
-```
-
-### 参数说明
-
-| 参数 | 类型 | 默认值 | 说明 |
-|-----|------|--------|------|
-| `convert_large_text_to_table` | bool | `true` | 是否启用文本转表格功能 |
-| `min_text_area_ratio` | float | `0.25` | 最小面积占比(0-1),文本块面积需占页面25%以上 |
-| `min_text_width_ratio` | float | `0.4` | 最小宽度占比(0-1),文本块宽度需占页面40%以上 |
-| `min_text_height_ratio` | float | `0.3` | 最小高度占比(0-1),文本块高度需占页面30%以上 |
-
-## 🔍 判断规则
-
-### 1. 面积占比判断
-
-```python
-area_ratio = text_block_area / page_area
-if area_ratio >= min_text_area_ratio:  # 默认 >= 0.25
-    # 满足面积条件
-```
-
-**示例**:
-- 页面尺寸:2338 × 1654 = 3,867,052 像素²
-- 文本块:1207 × 1397 = 1,686,179 像素²
-- 面积占比:1,686,179 / 3,867,052 = **43.6%** ✅ 满足条件
-
-### 2. 尺寸比例判断
-
-```python
-width_ratio = text_block_width / page_width
-height_ratio = text_block_height / page_height
-
-if (width_ratio >= min_text_width_ratio and 
-    height_ratio >= min_text_height_ratio):
-    # 满足尺寸条件
-```
-
-**示例**:
-- 页面宽度:1654px,文本块宽度:1269px → 宽度占比:**76.7%** ✅
-- 页面高度:2338px,文本块高度:1397px → 高度占比:**59.8%** ✅
-
-### 3. 表格冲突检查
-
-```python
-has_table = any(
-    item.get('category', '').lower() in ['table', 'table_body']
-    for item in layout_results
-)
-
-if has_table:
-    # 页面已有表格,不进行转换(避免误判)
-    return layout_results
-```
-
-**逻辑**:
-- 如果页面中已经有表格元素,说明Layout检测正常工作
-- 此时不进行转换,避免将普通文本误判为表格
-
-## 📊 转换效果
-
-### 转换前
-
-```json
-{
-  "bbox": [226, 288, 1495, 1685],
-  "category": "text",
-  "text": "1、账龄分析\n账龄 期末余额\n1年以内 178,051,695.35\n..."
-}
-```
-
-### 转换后
-
-```json
-{
-  "bbox": [226, 288, 1495, 1685],
-  "category": "table",
-  "original_category": "text",  // 保留原始类别
-  "text": "1、账龄分析\n账龄 期末余额\n1年以内 178,051,695.35\n..."
-}
-```
-
-## 🎯 调优建议
-
-### 场景1:财务报表(多表格)
-
-```yaml
-layout:
-  convert_large_text_to_table: true
-  min_text_area_ratio: 0.25      # 25%面积
-  min_text_width_ratio: 0.4      # 40%宽度
-  min_text_height_ratio: 0.3     # 30%高度
-```
-
-### 场景2:密集表格(小表格多)
-
-```yaml
-layout:
-  convert_large_text_to_table: true
-  min_text_area_ratio: 0.15      # 降低到15%(更敏感)
-  min_text_width_ratio: 0.3      # 降低到30%
-  min_text_height_ratio: 0.2     # 降低到20%
-```
-
-### 场景3:宽松检测(避免误判)
-
-```yaml
-layout:
-  convert_large_text_to_table: true
-  min_text_area_ratio: 0.35      # 提高到35%(更严格)
-  min_text_width_ratio: 0.5      # 提高到50%
-  min_text_height_ratio: 0.4     # 提高到40%
-```
-
-### 场景4:关闭功能
-
-```yaml
-layout:
-  convert_large_text_to_table: false  # 完全关闭
-```
-
-## ⚠️ 注意事项
-
-### 1. **误判风险**
-
-- **风险**:可能将大段普通文本误判为表格
-- **缓解**:通过面积和尺寸比例阈值控制
-- **建议**:根据实际文档类型调整阈值
-
-### 2. **已有表格检查**
-
-- **逻辑**:如果页面已有表格,不进行转换
-- **原因**:说明Layout检测正常工作,不需要后处理
-- **例外**:如果Layout检测完全失败,可能无法检测到表格
-
-### 3. **转换时机**
-
-- **位置**:在Layout检测之后,元素分类之前
-- **顺序**:
-  1. Layout检测
-  2. 重叠框去重
-  3. **文本转表格(后处理)** ← 这里
-  4. 元素分类
-  5. 元素处理
-
-### 4. **保留原始信息**
-
-- 转换后的元素会保留 `original_category` 字段
-- 便于调试和追溯转换历史
-
-## 🔧 代码实现
-
-### 核心函数
-
-```python
-@staticmethod
-def convert_large_text_to_table(
-    layout_results: List[Dict[str, Any]],
-    image_shape: Tuple[int, int],
-    min_area_ratio: float = 0.25,
-    min_width_ratio: float = 0.4,
-    min_height_ratio: float = 0.3
-) -> List[Dict[str, Any]]:
-    """
-    将大面积的文本块转换为表格
-    
-    判断规则:
-    1. 面积占比:占页面面积超过 min_area_ratio(默认25%)
-    2. 尺寸比例:宽度和高度都超过一定比例(避免细长条)
-    3. 不与其他表格重叠:如果已有表格,不转换
-    """
-    # 实现逻辑...
-```
-
-### 调用位置
-
-```python
-# 在 pipeline_manager_v2.py 中
-# 2.6 将大面积文本块转换为表格(后处理)
-if layout_results:
-    convert_large_text = self.config.get('layout', {}).get('convert_large_text_to_table', True)
-    if convert_large_text:
-        image_shape = (detection_image.shape[0], detection_image.shape[1])
-        layout_results = LayoutUtils.convert_large_text_to_table(
-            layout_results,
-            image_shape,
-            min_area_ratio=self.config.get('layout', {}).get('min_text_area_ratio', 0.25),
-            min_width_ratio=self.config.get('layout', {}).get('min_text_width_ratio', 0.4),
-            min_height_ratio=self.config.get('layout', {}).get('min_text_height_ratio', 0.3)
-        )
-```
-
-## 📈 效果评估
-
-### 转换前的问题
-
-- ❌ 大表格被识别为文本
-- ❌ 无法使用VLM进行表格结构识别
-- ❌ 表格内容无法正确提取
-
-### 转换后的改进
-
-- ✅ 大表格被正确识别为表格类型
-- ✅ 可以使用VLM进行表格结构识别
-- ✅ 表格内容可以正确提取和处理
-
-## 🎯 最佳实践
-
-1. **默认启用**:对于财务报表等包含大表格的文档,建议启用
-2. **阈值调优**:根据实际文档类型调整阈值
-3. **监控日志**:查看转换日志,确认转换效果
-4. **验证结果**:检查转换后的表格是否正确处理
-
-## 📝 日志输出
-
-转换时会输出详细日志:
-
-```
-🔄 Converted large text block to table: area=43.6%, size=76.7%×59.8%, bbox=[226, 288, 1495, 1685]
-✅ Converted 1 large text block(s) to table(s)
-```
-
-## 🔄 与其他功能的关系
-
-### 与VLM表格识别
-
-- 转换后的表格会进入VLM处理流程
-- VLM会识别表格结构(HTML格式)
-- OCR会提取单元格文本和坐标
-
-### 与跨页表格合并
-
-- 转换后的表格可以参与跨页合并
-- 需要确保转换后的表格类型正确
-
-### 与元素分类
-
-- 转换在元素分类之前进行
-- 转换后的表格会被正确分类到 `TABLE_BODY_CATEGORIES`
-
-## 📚 相关文档
-
-- [模型统一框架.md](./模型统一框架.md) - 整体架构说明
-- [OCR识别差异分析与改进方案.md](./OCR识别差异分析与改进方案.md) - OCR优化说明
-

+ 0 - 260
ocr_tools/universal_doc_parser/OCR识别差异分析与改进方案.md

@@ -1,260 +0,0 @@
-# OCR识别差异分析与改进方案
-
-## 📊 问题描述
-
-对比两个OCR识别结果:
-- **PPStructureV3**:准确率高,文本识别完整
-- **统一OCR框架(MinerU封装)**:很多文本未识别,部分文本块检测错误
-
-### 典型错误示例
-
-从 `bank_statement_yusys_v2/2023年度报告母公司_page_003.json` 中发现的错误:
-
-| 正确文本 | 识别结果 | 匹配分数 | 问题 |
-|---------|---------|---------|------|
-| 合同资产 | 货资产 | 80.0% | 检测框不完整 |
-| 其他应付款 | 税款 | 66.67% | 检测框错误 |
-| 长期待摊费用 | 效用 | 66.67% | 检测框错误 |
-| 其他非流动资产 | 非其非产动产 | 61.54% | 检测框合并错误 |
-| 资本公积 | 资公存股 | 66.67% | 检测框合并错误 |
-
-## 🔍 根本原因分析
-
-### 1. **检测参数差异**
-
-| 参数 | PPStructureV3 | MinerU默认 | 影响 |
-|-----|--------------|-----------|------|
-| `box_thresh` | **0.6** | **0.3** | 阈值过低导致检测到大量噪声框 |
-| `unclip_ratio` | **1.5** | **1.8** | 扩展比例过高,框不够精确 |
-| `thresh` | 0.3 | 0.3 | 相同 |
-
-**问题**:
-- `box_thresh=0.3` 太低,会检测到很多低置信度的噪声框
-- 这些噪声框可能被错误地合并到正确的文本框中
-- 导致最终识别结果不准确
-
-### 2. **检测框合并策略**
-
-| 特性 | PPStructureV3 | MinerU | 影响 |
-|-----|--------------|--------|------|
-| 框合并 | 无(或更保守) | `enable_merge_det_boxes=True` | 可能错误合并相邻文本块 |
-
-**问题**:
-- `enable_merge_det_boxes=True` 会合并相邻的检测框
-- 对于表格等密集文本,可能将不同单元格的文本错误合并
-- 例如:"其他非流动资产" 被合并成 "非其非产动产"
-
-### 3. **识别置信度过滤**
-
-| 参数 | PPStructureV3 | MinerU | 影响 |
-|-----|--------------|--------|------|
-| `score_thresh` | **0.0** | **0.5** | 丢弃低置信度结果 |
-
-**问题**:
-- `drop_score=0.5` 会丢弃置信度 < 0.5 的识别结果
-- 某些正确但置信度较低的文本可能被丢弃
-- PPStructureV3 使用 `score_thresh=0.0`,保留所有结果
-
-### 4. **文本行方向识别**
-
-| 特性 | PPStructureV3 | MinerU | 影响 |
-|-----|--------------|--------|------|
-| 文本行方向识别 | ✅ `use_textline_orientation: True` | ❌ 无 | 倾斜文本识别错误 |
-
-**问题**:
-- PPStructureV3 有专门的文本行方向识别模块
-- 可以处理倾斜的文本行,提高识别准确率
-- MinerU 缺少此功能
-
-### 5. **模型版本差异**
-
-| 组件 | PPStructureV3 | MinerU | 影响 |
-|-----|--------------|--------|------|
-| 检测模型 | PP-OCRv5_server_det | ch/ch_lite | 可能使用较旧版本 |
-| 识别模型 | PP-OCRv5_server_rec | ch/ch_lite | 可能使用较旧版本 |
-
-**问题**:
-- PP-OCRv5_server 是较新的模型,准确率更高
-- MinerU 可能使用较旧版本的模型
-
-## 💡 改进方案
-
-### 方案1:调整OCR参数(推荐,快速改进)
-
-修改 `MinerUOCRRecognizer` 的初始化参数,使其更接近 PPStructureV3:
-
-```python
-# 在 mineru_adapter.py 中修改
-self.ocr_model = self.atom_model_manager.get_atom_model(
-    atom_model_name=AtomicModel.OCR,
-    det_db_box_thresh=0.6,  # 从 0.3 提高到 0.6
-    lang=self.config.get('language', 'ch'),
-    det_db_unclip_ratio=1.5,  # 从 1.8 降低到 1.5
-    enable_merge_det_boxes=False,  # 从 True 改为 False(表格场景)
-)
-```
-
-**优点**:
-- ✅ 快速实施,无需修改核心代码
-- ✅ 可以显著提高检测准确率
-- ✅ 减少错误合并
-
-**缺点**:
-- ⚠️ 可能漏检一些低置信度的文本(但通常这些是噪声)
-- ⚠️ 对于非表格场景,可能需要 `enable_merge_det_boxes=True`
-
-### 方案2:降低识别置信度阈值
-
-修改 `drop_score` 参数,保留更多识别结果:
-
-```python
-# 需要修改 PytorchPaddleOCR 的初始化
-kwargs['drop_score'] = 0.3  # 从默认 0.5 降低到 0.3
-```
-
-**优点**:
-- ✅ 保留更多识别结果
-- ✅ 减少误丢弃
-
-**缺点**:
-- ⚠️ 可能引入更多噪声结果
-- ⚠️ 需要修改 MinerU 核心代码
-
-### 方案3:根据场景动态调整参数(最佳方案)
-
-根据文档类型(表格/文本)和PDF类型(扫描件/数字PDF)动态调整参数:
-
-```python
-def get_ocr_config(pdf_type: str, has_tables: bool) -> Dict[str, Any]:
-    """根据场景返回OCR配置"""
-    if has_tables:
-        # 表格场景:更严格的检测,不合并框
-        return {
-            'det_db_box_thresh': 0.6,
-            'det_db_unclip_ratio': 1.5,
-            'enable_merge_det_boxes': False,
-            'drop_score': 0.3,
-        }
-    elif pdf_type == 'txt':
-        # 数字PDF:可以合并框,提高检测阈值
-        return {
-            'det_db_box_thresh': 0.5,
-            'det_db_unclip_ratio': 1.6,
-            'enable_merge_det_boxes': True,
-            'drop_score': 0.3,
-        }
-    else:
-        # 扫描件:平衡检测和合并
-        return {
-            'det_db_box_thresh': 0.4,
-            'det_db_unclip_ratio': 1.6,
-            'enable_merge_det_boxes': True,
-            'drop_score': 0.3,
-        }
-```
-
-**优点**:
-- ✅ 针对不同场景优化
-- ✅ 兼顾准确率和召回率
-
-**缺点**:
-- ⚠️ 实现复杂度较高
-- ⚠️ 需要场景判断逻辑
-
-### 方案4:集成文本行方向识别(长期改进)
-
-参考 PPStructureV3,添加文本行方向识别模块:
-
-1. 在 OCR 识别前,先进行文本行方向识别
-2. 根据识别结果旋转文本行
-3. 再进行 OCR 识别
-
-**优点**:
-- ✅ 显著提高倾斜文本识别准确率
-- ✅ 与 PPStructureV3 能力对齐
-
-**缺点**:
-- ⚠️ 需要额外的模型和计算资源
-- ⚠️ 实现复杂度高
-
-## 🎯 推荐实施步骤
-
-### 阶段1:快速改进(立即实施)
-
-1. **调整检测参数**:
-   - `det_db_box_thresh`: 0.3 → **0.6**
-   - `det_db_unclip_ratio`: 1.8 → **1.5**
-   - `enable_merge_det_boxes`: True → **False**(表格场景)
-
-2. **降低识别阈值**:
-   - `drop_score`: 0.5 → **0.3**
-
-### 阶段2:场景优化(短期)
-
-3. **实现动态参数调整**:
-   - 根据文档类型和PDF类型选择参数
-   - 表格场景:严格检测,不合并
-   - 文本场景:平衡检测和合并
-
-### 阶段3:能力增强(长期)
-
-4. **集成文本行方向识别**:
-   - 添加文本行方向识别模块
-   - 提高倾斜文本识别准确率
-
-## 📝 配置建议
-
-### 表格场景(当前问题场景)
-
-```yaml
-ocr:
-  det_db_box_thresh: 0.6      # 提高检测阈值
-  det_db_unclip_ratio: 1.5    # 降低扩展比例
-  enable_merge_det_boxes: false  # 不合并框
-  drop_score: 0.3             # 降低识别阈值
-```
-
-### 文本场景
-
-```yaml
-ocr:
-  det_db_box_thresh: 0.4      # 中等检测阈值
-  det_db_unclip_ratio: 1.6    # 中等扩展比例
-  enable_merge_det_boxes: true   # 允许合并
-  drop_score: 0.3             # 降低识别阈值
-```
-
-## 🔧 代码修改位置
-
-1. **`models/adapters/mineru_adapter.py`**:
-   - `MinerUOCRRecognizer.__init__()` - 修改初始化参数
-   - 添加场景判断逻辑
-
-2. **`core/pipeline_manager_v2.py`**:
-   - `_process_single_page()` - 检测是否有表格
-   - 传递场景信息给 OCR 识别器
-
-3. **配置文件**:
-   - 添加 OCR 参数配置选项
-
-## 📈 预期效果
-
-实施阶段1改进后:
-- ✅ 检测准确率提升:减少噪声框和错误合并
-- ✅ 识别完整度提升:保留更多低置信度但正确的结果
-- ✅ 表格识别准确率:预计从 60-70% 提升到 85-90%
-
-## ⚠️ 注意事项
-
-1. **参数调优**:
-   - 不同文档可能需要不同的参数
-   - 建议通过测试集验证最优参数
-
-2. **性能影响**:
-   - 提高 `box_thresh` 可能略微降低召回率
-   - 需要平衡准确率和召回率
-
-3. **向后兼容**:
-   - 保持默认参数不变,通过配置覆盖
-   - 确保现有功能不受影响
-

+ 0 - 4
ocr_tools/universal_doc_parser/VLM服务地址.md

@@ -1,4 +0,0 @@
-mineru_vllm 10.192.72.11:20006
-paddleocr_vllm 10.192.72.11:20016
-ppstructure_v3 10.192.72.11:20026
-dots_vllm 10.192.72.11:8101

+ 0 - 288
ocr_tools/universal_doc_parser/unclip_ratio参数说明.md

@@ -1,288 +0,0 @@
-# `unclip_ratio` 参数说明
-
-## 📋 概述
-
-`unclip_ratio` 是 OCR 文本检测中的一个重要参数,用于控制检测框的扩展比例。
-
-## 🎯 作用原理
-
-### 1. **Vatti Clipping 算法**
-
-`unclip_ratio` 使用 **Vatti Clipping 算法**对检测到的文字区域进行扩张(unclip)。
-
-### 2. **工作原理**
-
-```
-原始检测框(可能过紧)
-┌─────────┐
-│  文本   │  ← 检测模型输出的框可能太紧,裁剪了文字边缘
-└─────────┘
-
-应用 unclip_ratio = 1.5 后
-┌───────────────┐
-│               │
-│    文本       │  ← 扩展后的框,包含完整的文字区域
-│               │
-└───────────────┘
-```
-
-### 3. **数学公式**
-
-```python
-# 计算扩展后的框尺寸
-width = x2 - x1
-height = y2 - y1
-
-new_width = width * unclip_ratio
-new_height = height * unclip_ratio
-
-# 保持中心点不变,扩展框
-center_x = x1 + width / 2
-center_y = y1 + height / 2
-
-new_x1 = center_x - new_width / 2
-new_y1 = center_y - new_height / 2
-new_x2 = center_x + new_width / 2
-new_y2 = center_y + new_height / 2
-```
-
-## 📊 参数影响
-
-### 不同 `unclip_ratio` 值的效果
-
-| unclip_ratio | 扩展程度 | 适用场景 | 优缺点 |
-|-------------|---------|---------|--------|
-| **1.0** | 不扩展 | 检测框已经很精确 | ✅ 精确度高<br>❌ 可能裁剪文字边缘 |
-| **1.3** | 轻微扩展 | 高质量扫描件 | ✅ 精确度较高<br>✅ 包含完整文字 |
-| **1.5** | 中等扩展 | **推荐值**(PPStructureV3默认) | ✅ 平衡精确度和完整性<br>✅ 适合大多数场景 |
-| **1.8** | 较大扩展 | MinerU默认,质量较差的扫描件 | ✅ 确保包含完整文字<br>❌ 可能包含过多背景 |
-| **2.0+** | 大幅扩展 | 极低质量图像 | ✅ 最大程度包含文字<br>❌ 精确度低,可能包含相邻文字 |
-
-## 🔍 实际效果对比
-
-### 示例1:正常文本
-
-```
-原始检测框(unclip_ratio=1.0)
-┌────────┐
-│ 货币资金 │  ← 可能裁剪了"货"字的左边
-└────────┘
-
-unclip_ratio=1.5(推荐)
-┌───────────┐
-│  货币资金  │  ← 包含完整文字
-└───────────┘
-
-unclip_ratio=1.8(过大)
-┌─────────────┐
-│   货币资金   │  ← 包含过多空白,可能影响识别
-└─────────────┘
-```
-
-### 示例2:密集文本(表格)
-
-```
-unclip_ratio=1.5(推荐)
-┌─────┐ ┌─────┐
-│ 资产 │ │ 负债 │  ← 框精确,不重叠
-└─────┘ └─────┘
-
-unclip_ratio=1.8(过大)
-┌──────┐┌──────┐
-│ 资产  ││ 负债 │  ← 框重叠,可能合并相邻单元格
-└──────┘└──────┘
-```
-
-## ⚙️ 参数调优建议
-
-### 1. **根据文档质量调整**
-
-```yaml
-# 高质量扫描件(清晰、无倾斜)
-ocr:
-  unclip_ratio: 1.3  # 较小扩展,保持精确
-
-# 中等质量扫描件(推荐)
-ocr:
-  unclip_ratio: 1.5  # 平衡精确度和完整性
-
-# 低质量扫描件(模糊、倾斜)
-ocr:
-  unclip_ratio: 1.8  # 较大扩展,确保包含完整文字
-```
-
-### 2. **根据文档类型调整**
-
-```yaml
-# 表格场景(密集文本)
-ocr:
-  unclip_ratio: 1.5  # 较小扩展,避免框重叠
-  enable_merge_det_boxes: false  # 不合并框
-
-# 普通文本场景
-ocr:
-  unclip_ratio: 1.6  # 中等扩展
-  enable_merge_det_boxes: true  # 可以合并框
-
-# 稀疏文本场景
-ocr:
-  unclip_ratio: 1.8  # 较大扩展,确保包含完整文字
-```
-
-### 3. **与 `box_thresh` 配合使用**
-
-```yaml
-# 严格检测 + 精确扩展
-ocr:
-  det_threshold: 0.6      # 高阈值,减少噪声框
-  unclip_ratio: 1.5       # 精确扩展
-
-# 宽松检测 + 较大扩展
-ocr:
-  det_threshold: 0.3      # 低阈值,检测更多框
-  unclip_ratio: 1.8       # 较大扩展,确保包含文字
-```
-
-## 📈 性能影响
-
-### 识别准确率
-
-| unclip_ratio | 文字完整性 | 精确度 | 相邻文字干扰 | 综合评分 |
-|-------------|----------|--------|------------|---------|
-| 1.3 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
-| **1.5** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | **⭐⭐⭐⭐⭐** |
-| 1.8 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
-
-### 处理速度
-
-- `unclip_ratio` 对处理速度影响很小(< 1%)
-- 主要影响识别准确率,而非速度
-
-## 🎯 推荐配置
-
-### PPStructureV3 配置(推荐)
-
-```yaml
-ocr:
-  det_threshold: 0.6
-  unclip_ratio: 1.5      # ✅ 推荐值
-  enable_merge_det_boxes: false
-```
-
-### MinerU 默认配置
-
-```yaml
-ocr:
-  det_threshold: 0.3
-  unclip_ratio: 1.8      # ⚠️ 可能过大,建议改为1.5
-  enable_merge_det_boxes: true
-```
-
-### 优化后的配置
-
-```yaml
-ocr:
-  det_threshold: 0.6     # 提高检测阈值
-  unclip_ratio: 1.5      # 降低扩展比例(从1.8改为1.5)
-  enable_merge_det_boxes: false  # 表格场景不合并
-```
-
-## 🔧 代码实现
-
-### PaddleOCR 中的使用
-
-```python
-# 在文本检测后处理中使用
-def unclip_boxes(boxes, unclip_ratio):
-    """
-    扩展检测框
-    
-    Args:
-        boxes: 检测框列表 [(x1, y1, x2, y2), ...]
-        unclip_ratio: 扩展比例(如 1.5)
-    
-    Returns:
-        扩展后的检测框列表
-    """
-    expanded_boxes = []
-    for box in boxes:
-        x1, y1, x2, y2 = box
-        width = x2 - x1
-        height = y2 - y1
-        
-        # 计算扩展后的尺寸
-        new_width = width * unclip_ratio
-        new_height = height * unclip_ratio
-        
-        # 保持中心点不变
-        center_x = x1 + width / 2
-        center_y = y1 + height / 2
-        
-        # 计算新的坐标
-        new_x1 = center_x - new_width / 2
-        new_y1 = center_y - new_height / 2
-        new_x2 = center_x + new_width / 2
-        new_y2 = center_y + new_height / 2
-        
-        expanded_boxes.append([new_x1, new_y1, new_x2, new_y2])
-    
-    return expanded_boxes
-```
-
-## ⚠️ 常见问题
-
-### Q1: `unclip_ratio` 越大越好吗?
-
-**A:** 不是。过大的 `unclip_ratio` 会导致:
-- 检测框包含过多背景
-- 相邻文字框重叠
-- 识别准确率下降
-
-**建议**:从 1.5 开始,根据实际效果调整。
-
-### Q2: 为什么 PPStructureV3 使用 1.5,而 MinerU 使用 1.8?
-
-**A:** 
-- **PPStructureV3 (1.5)**:更注重精确度,适合高质量文档
-- **MinerU (1.8)**:更注重完整性,适合低质量扫描件
-
-**建议**:对于表格等密集文本,使用 1.5 更合适。
-
-### Q3: 如何判断 `unclip_ratio` 是否合适?
-
-**A:** 检查识别结果:
-- **框太紧**:文字被裁剪 → 增大 `unclip_ratio`
-- **框太松**:包含相邻文字 → 减小 `unclip_ratio`
-- **框重叠**:相邻单元格合并 → 减小 `unclip_ratio` 或关闭 `enable_merge_det_boxes`
-
-### Q4: `unclip_ratio` 和 `box_thresh` 的关系?
-
-**A:** 
-- `box_thresh`:控制哪些检测框被保留(过滤低置信度框)
-- `unclip_ratio`:控制检测框的扩展程度(调整框的大小)
-
-两者独立,但可以配合使用:
-- 高 `box_thresh` + 低 `unclip_ratio`:严格检测,精确扩展
-- 低 `box_thresh` + 高 `unclip_ratio`:宽松检测,较大扩展
-
-## 📚 相关文档
-
-- [OCR识别差异分析与改进方案.md](./OCR识别差异分析与改进方案.md) - 参数优化说明
-- [模型统一框架.md](./模型统一框架.md) - OCR配置说明
-
-## 🎯 总结
-
-| 特性 | 说明 |
-|-----|------|
-| **作用** | 扩展文本检测框,确保包含完整文字 |
-| **算法** | Vatti Clipping 算法 |
-| **推荐值** | **1.5**(PPStructureV3默认) |
-| **范围** | 1.0 - 2.0(通常 1.3 - 1.8) |
-| **影响** | 主要影响识别准确率,对速度影响很小 |
-| **调优** | 根据文档质量和类型调整 |
-
-**最佳实践**:
-- ✅ 表格场景:`unclip_ratio=1.5`,`enable_merge_det_boxes=false`
-- ✅ 普通文本:`unclip_ratio=1.5-1.6`,`enable_merge_det_boxes=true`
-- ✅ 低质量扫描件:`unclip_ratio=1.8`,`enable_merge_det_boxes=true`
-

+ 0 - 184
ocr_tools/universal_doc_parser/utils/README_OUTPUT_FORMAT.md

@@ -1,184 +0,0 @@
-# MinerU 输出格式说明
-
-## 概述
-
-`MinerUOutputFormatter` 严格遵循 MinerU 的输出格式,与 `mineru_vllm_results_cell_bbox` 目录下的格式完全一致。
-
-## 输出格式
-
-### 1. JSON 格式
-
-每页一个 JSON 文件,包含元素列表:
-
-```json
-[
-  {
-    "type": "header",
-    "text": "页眉内容",
-    "bbox": [160, 126, 590, 161],
-    "page_idx": 0
-  },
-  {
-    "type": "text",
-    "text": "正文内容...",
-    "bbox": [158, 226, 1463, 322],
-    "page_idx": 0
-  },
-  {
-    "type": "table",
-    "img_path": "images/xxx.jpg",
-    "table_caption": ["表格标题"],
-    "table_footnote": [],
-    "table_body": "<table>...</table>",
-    "bbox": [251, 264, 1404, 2111],
-    "page_idx": 0,
-    "table_cells": [...],
-    "image_rotation_angle": 270.0,
-    "skew_angle": -0.26
-  }
-]
-```
-
-### 2. 表格单元格格式 (table_cells)
-
-```json
-{
-  "type": "table_cell",
-  "text": "单元格内容",
-  "matched_text": "OCR匹配的文本",
-  "bbox": [273, 1653, 302, 2106],
-  "row": 2,
-  "col": 1,
-  "score": 100.0,
-  "paddle_bbox_indices": [11, 12]
-}
-```
-
-### 3. 表格 HTML 格式 (table_body)
-
-HTML 表格带有 `data-bbox` 属性:
-
-```html
-<table>
-  <tr>
-    <td data-bbox="[232,273,685,302]" data-paddle-indices="[11, 12]" data-score="100.0000">流动资产:</td>
-    <td></td>
-    <td></td>
-  </tr>
-</table>
-```
-
-### 4. Markdown 格式
-
-带 bbox 注释的 Markdown:
-
-```markdown
-<!-- bbox: [160, 126, 590, 161] -->
-<!-- 页眉: 广东荣德会计师事务所有限公司 -->
-
-<!-- bbox: [158, 226, 1463, 322] -->
-在按照审计准则执行审计工作的过程中...
-
-<!-- bbox: [251, 264, 1404, 2111] -->
-**资产负债表**
-
-<table>...</table>
-```
-
-## 使用方法
-
-### 方法1: 直接使用格式化器
-
-```python
-from utils.mineru_output_formatter import MinerUOutputFormatter
-
-# 创建格式化器
-formatter = MinerUOutputFormatter(
-    output_dir="./output",
-    save_images=True
-)
-
-# 格式化并保存
-output_paths = formatter.format_and_save(
-    results=pipeline_results,
-    doc_name="my_document"
-)
-
-print(output_paths)
-# {
-#     'json': ['./output/my_document_page_001.json', ...],
-#     'markdown': ['./output/my_document_page_001.md', ...],
-#     'images': ['./output/images/xxx.png', ...]
-# }
-```
-
-### 方法2: 使用便捷函数
-
-```python
-from utils.mineru_output_formatter import save_mineru_format
-
-output_paths = save_mineru_format(
-    results=pipeline_results,
-    output_dir="./output",
-    doc_name="my_document",
-    save_images=True
-)
-```
-
-### 方法3: 仅转换格式(不保存)
-
-```python
-from utils.mineru_output_formatter import MinerUFormatConverter
-
-# 转换为 MinerU content_list 格式
-content_list = MinerUFormatConverter.convert_pipeline_result_to_mineru(
-    pipeline_results
-)
-```
-
-## 与 MinerU 原生输出的对比
-
-| 特性 | MinerU 原生 | MinerUOutputFormatter |
-|------|------------|----------------------|
-| JSON 格式 | ✅ | ✅ 完全一致 |
-| Markdown | ✅ | ✅ 完全一致 |
-| bbox 注释 | ✅ | ✅ 完全一致 |
-| table_body HTML | ✅ | ✅ 完全一致 |
-| table_cells | ✅ | ✅ 完全一致 |
-| data-bbox 属性 | ✅ | ✅ 完全一致 |
-| image_rotation_angle | ✅ | ✅ 完全一致 |
-| skew_angle | ✅ | ✅ 完全一致 |
-
-## 坐标系统
-
-### 输出坐标
-
-- **bbox**: `[x1, y1, x2, y2]` 格式,基于**原始未旋转图片**的像素坐标
-- **table_cells.bbox**: 同上,基于原始图片坐标系
-
-### 旋转信息
-
-- **image_rotation_angle**: 图片旋转角度(0, 90, 180, 270)
-- **skew_angle**: 倾斜校正角度
-
-## 目录结构
-
-```
-output/
-├── document_page_001.json
-├── document_page_001.md
-├── document_page_002.json
-├── document_page_002.md
-└── images/
-    ├── table_0_251_264_xxx.jpg
-    ├── image_0_133_1582_xxx.jpg
-    └── ...
-```
-
-## 注意事项
-
-1. **坐标系**: 所有坐标都是基于原始未旋转图片的坐标系
-2. **图片**: 保存的是原始未旋转的图片
-3. **表格 HTML**: 包含 `data-bbox` 属性,可用于前端渲染和坐标定位
-4. **单元格匹配**: `paddle_bbox_indices` 记录了匹配的 OCR 框索引
-

+ 0 - 521
ocr_tools/universal_doc_parser/模型统一框架.md

@@ -1,521 +0,0 @@
-# 金融文档处理统一框架
-
-参考 MinerU 实现的模型统一框架,针对金融场景设计。
-
-## 支持场景
-
-| 场景类型 | 特点 | 表格形式 |
-|---------|------|---------|
-| **银行交易流水** | 单栏列表形式,无合并单元格 | 有线表格 / 无线表格 |
-| **财务报表** | 多栏列表形式,有合并单元格,表头复杂 | 有线表格 / 无线表格 |
-
-## 模型选择
-
-| 模型类型 | 推荐模型 | 说明 |
-|---------|---------|------|
-| **版式检测** | Docling Layout / DocLayout-YOLO | HuggingFace 或 MinerU 模型 |
-| **文字识别** | PaddleOCR (PyTorch) | 效果好,支持角度校正 |
-| **表格结构识别** | MinerU VLM / PaddleOCR-VL | VLM 返回 HTML 结构 |
-| **公式识别** | MinerU VLM | 返回 LaTeX |
-| **方向识别** | PP-LCNet | 沿用 MinerU 实现 |
-| **单元格坐标匹配** | TableCellMatcher | OCR 检测框与 VLM 结构匹配 |
-
----
-
-## 处理流程
-
-```mermaid
-graph TB
-    A[输入 PDF/图片] --> B{PDF 分类}
-    
-    B -->|扫描件/图片| C1[页面方向识别<br/>PP-LCNet]
-    B -->|数字原生PDF| D
-    C1 --> D[Layout 检测<br/>去重叠框]
-    
-    D --> E[整页 OCR<br/>获取所有 text spans]
-    E --> F[Span-Block 匹配<br/>SpanMatcher]
-    F --> G{元素分类}
-    
-    G --> H[文本类]
-    G --> I[表格类]
-    G --> J[图片类]
-    G --> K[公式类]
-    G --> L[丢弃类]
-    
-    subgraph 文本处理
-        H --> H1{有匹配的spans?}
-        H1 -->|是| H2[合并 spans 文本]
-        H1 -->|否| H3{PDF类型?}
-        H3 -->|数字PDF| H4[PDF 字符提取]
-        H4 --> H5{成功?}
-        H5 -->|否| H6[裁剪区域 OCR]
-        H5 -->|是| H7[文本结果]
-        H3 -->|扫描件| H6
-        H6 --> H7
-        H2 --> H7
-    end
-    
-    subgraph 表格处理
-        I --> I1[OCR 检测<br/>获取文本框坐标]
-        I --> I2[VLM 结构识别<br/>返回 HTML]
-        I1 --> I3[坐标匹配<br/>TableCellMatcher]
-        I2 --> I3
-        I3 --> I4[带坐标的表格]
-    end
-    
-    subgraph 图片处理
-        J --> J1[裁剪保存]
-    end
-    
-    subgraph 公式处理
-        K --> K1[VLM 识别<br/>返回 LaTeX]
-    end
-    
-    subgraph 丢弃元素
-        L --> L1{有匹配的spans?}
-        L1 -->|是| L2[合并 spans 文本]
-        L1 -->|否| L3[裁剪区域 OCR]
-        L2 --> L4[保留备用]
-        L3 --> L4
-    end
-    
-    H7 --> M[合并所有结果]
-    I4 --> M
-    J1 --> M
-    K1 --> M
-    L4 --> M
-    
-    M --> N[按阅读顺序排序]
-    N --> O[坐标转换回原图]
-    O --> P[合并跨页表格]
-    P --> Q[金额数字标准化]
-    Q --> R[多格式输出]
-```
-
-### 关键改进:整页 OCR + Span 匹配
-
-参考 MinerU 的处理方式,新流程采用 **整页 OCR → Span-Block 匹配** 策略:
-
-1. **整页 OCR**:先对整个页面进行 OCR,获取所有 text spans(包含坐标和文本)
-2. **Span 去重**:移除高 IoU 重叠的 spans,保留置信度高的
-3. **Span-Block 匹配**:将 OCR spans 按重叠比例匹配到对应的 layout blocks
-4. **文本合并**:将匹配到同一 block 的 spans 按阅读顺序合并
-
-**优势**:
-- ✅ 避免裁剪小图 OCR 失败的问题
-- ✅ OCR 可以利用更多上下文信息
-- ✅ 坐标更精确(整页坐标系)
-- ✅ 与 MinerU 处理方式一致
-
-### 元素分类说明
-
-| 元素类别 | 包含类型 | 处理方式 |
-|---------|---------|---------|
-| **文本类** | text, title, header, footer, ref_text, table_caption, image_caption 等 | 优先使用匹配的 spans,否则 PDF 提取或裁剪 OCR |
-| **表格类** | table, table_body | OCR 坐标 + VLM 结构 |
-| **图片类** | image, image_body, figure | 裁剪保存 |
-| **公式类** | interline_equation, equation | VLM 识别 |
-| **丢弃类** | abandon, discarded | 优先使用匹配的 spans,否则裁剪 OCR |
-
----
-
-## 方向识别策略
-
-| 处理阶段 | 方向识别 | 建议 |
-|---------|---------|------|
-| **页面级** | PP-LCNet | 可配置,扫描件开启,数字PDF关闭 |
-| **表格区域** | - | 可选,VLM 有一定容忍度,OCR 自带角度校正 |
-| **文本区域** | - | 不需要,OCR 自带校正 |
-
----
-
-## Layout 后处理
-
-### 大面积文本块转表格
-
-当Layout检测将大面积的表格区域误识别为文本框时,可以通过后处理自动转换:
-
-**判断规则**:
-- 面积占比:占页面面积超过阈值(默认25%)
-- 尺寸比例:宽度和高度都超过一定比例(避免细长条)
-- 表格冲突:如果页面已有表格,不进行转换(避免误判)
-
-**配置示例**:
-```yaml
-layout:
-  convert_large_text_to_table: true  # 是否启用
-  min_text_area_ratio: 0.25          # 最小面积占比(25%)
-  min_text_width_ratio: 0.4          # 最小宽度占比(40%)
-  min_text_height_ratio: 0.3         # 最小高度占比(30%)
-```
-
-详细说明请参考:`Layout后处理-文本转表格.md`
-
----
-
-## OCR 使用策略
-
-| PDF 类型 | 文字块处理 | 表格处理 |
-|---------|-----------|---------|
-| **扫描件/图片** | 整页 OCR → Span 匹配 | OCR 检测(坐标) + VLM(结构) |
-| **数字原生 PDF** | 整页 OCR → Span 匹配 / PDF 字符提取 | OCR 检测(坐标) + VLM(结构) |
-
-**关键点**:
-- **整页 OCR 优先**:先对整页进行 OCR,再将结果匹配到 layout blocks
-- 数字原生 PDF 在 spans 匹配失败时,会尝试 PDF 字符提取
-- **表格处理无论 PDF 类型都需要 OCR 检测**,用于获取单元格内文本的精确坐标
-- VLM 只返回表格结构(HTML),不返回单元格坐标,需要与 OCR 检测结果匹配
-- 当前实现仅使用 VLM(MinerU VLM 或 PaddleOCR-VL)进行表格结构识别
-
-### OCR 参数配置
-
-**默认参数(已优化,参考PPStructureV3)**:
-- `det_threshold`: 0.6(检测框置信度阈值,提高可减少噪声框)
-- `unclip_ratio`: 1.5(检测框扩展比例,降低可提高框精确度)
-- `enable_merge_det_boxes`: False(是否合并检测框,表格场景建议False)
-
-**配置示例**:
-```yaml
-ocr:
-  det_threshold: 0.6              # 检测阈值(0.3-0.7,越高越严格)
-  unclip_ratio: 1.5               # 扩展比例(1.3-1.8,越低越精确)
-  enable_merge_det_boxes: false   # 合并框(表格场景建议false)
-  language: ch                     # 语言(ch/ch_lite/en等)
-```
-
-**参数调优建议**:
-- **表格密集场景**:`det_threshold=0.6`, `enable_merge_det_boxes=false`
-- **文本稀疏场景**:`det_threshold=0.4`, `enable_merge_det_boxes=true`
-- **扫描件质量差**:`det_threshold=0.3`, `unclip_ratio=1.8`
-
-详细分析请参考:`OCR识别差异分析与改进方案.md`
-
----
-
-## 目录结构
-
-```
-universal_doc_parser/
-├── config/                              # 配置文件
-│   ├── bank_statement_yusys_v2.yaml    # 银行流水配置(Docling + PaddleOCR-VL)
-│   ├── bank_statement_mineru_v2.yaml   # 银行流水配置(MinerU layout + MinerU VLM)
-│   ├── bank_statement_mineru_vl.yaml   # 银行流水配置(MinerU VLM)
-│   └── bank_statement_paddle_vl.yaml   # 银行流水配置(PaddleOCR-VL)
-│
-├── core/                                # 核心处理模块
-│   ├── pipeline_manager_v2.py          # 主流水线管理器 ⭐
-│   ├── element_processors.py           # 元素处理器(文本、表格、图片等)
-│   ├── coordinate_utils.py             # 坐标转换工具
-│   ├── layout_utils.py                 # 布局处理工具(排序、去重、SpanMatcher)⭐
-│   ├── pdf_utils.py                    # PDF 处理工具
-│   ├── config_manager.py               # 配置管理
-│   └── model_factory.py                # 模型工厂
-│
-├── models/                              # 模型适配器
-│   └── adapters/
-│       ├── base.py                     # 适配器基类
-│       ├── mineru_adapter.py           # MinerU 适配器
-│       ├── paddle_vl_adapter.py        # PaddleOCR-VL 适配器
-│       ├── paddle_layout_detector.py   # PaddleX RT-DETR 布局检测器
-│       └── docling_layout_adapter.py   # Docling 布局检测器 ⭐
-│
-├── utils/                               # 输出工具模块
-│   ├── output_formatter_v2.py          # 统一输出格式化器 ⭐
-│   ├── json_formatters.py              # JSON 格式化(middle.json, page.json)
-│   ├── markdown_generator.py           # Markdown 生成器
-│   ├── html_generator.py               # HTML 生成器
-│   ├── visualization_utils.py          # 可视化工具(layout/OCR 图片)
-│   └── normalize_financial_numbers.py  # 金额数字标准化
-│
-├── main_v2.py                           # 命令行入口 ⭐
-└── 模型统一框架.md                       # 本文档
-```
-
----
-
-## 使用方法
-
-### 命令行
-
-```bash
-# 处理单个 PDF 文件
-python main_v2.py -i document.pdf -c config/bank_statement_yusys_v2.yaml
-
-# 处理图片目录
-python main_v2.py -i ./images/ -c config/bank_statement_yusys_v2.yaml
-
-# 开启 debug 模式(输出可视化图片)
-python main_v2.py -i doc.pdf -c config/bank_statement_yusys_v2.yaml --debug
-
-# 指定输出目录
-python main_v2.py -i doc.pdf -c config/bank_statement_yusys_v2.yaml -o ./my_output/
-```
-
-### Python API
-
-```python
-from core.pipeline_manager_v2 import EnhancedDocPipeline
-from utils import OutputFormatterV2
-
-# 初始化流水线
-with EnhancedDocPipeline("config/bank_statement_yusys_v2.yaml") as pipeline:
-    # 处理文档
-    results = pipeline.process_document("document.pdf")
-    
-    # 保存结果
-    formatter = OutputFormatterV2("./output")
-    output_paths = formatter.save_results(results, {
-        'save_json': True,
-        'save_markdown': True,
-        'save_html': True,
-        'save_layout_image': True,  # debug
-        'save_ocr_image': True,     # debug
-        'normalize_numbers': True,   # 金额标准化
-    })
-```
-
----
-
-## 输出文件说明
-
-| 输出文件 | 说明 |
-|---------|------|
-| `{doc}_middle.json` | MinerU 标准格式 JSON |
-| `{doc}_page_001.json` | 每页独立 JSON(包含单元格坐标) |
-| `{doc}.md` | 完整文档 Markdown |
-| `{doc}_page_001.md` | 每页独立 Markdown(带坐标注释) |
-| `tables/*.html` | 表格 HTML 文件(带 data-bbox 坐标) |
-| `images/` | 提取的图片元素 |
-| `{doc}_page_001_layout.png` | Layout 可视化图片(debug 模式) |
-| `{doc}_page_001_ocr.png` | OCR 可视化图片(debug 模式) |
-| `*_original.*` | 标准化前的原始文件(如有修改) |
-
----
-
-## 配置说明
-
-配置文件采用 YAML 格式,主要配置项:
-
-```yaml
-# 场景名称
-scene_name: "bank_statement"
-
-# 输入配置
-input:
-  supported_formats: [".pdf", ".png", ".jpg", ".jpeg"]
-  dpi: 200  # PDF 转图片的 DPI
-
-# 预处理(方向识别)
-preprocessor:
-  module: "mineru"
-  orientation_classifier:
-    enabled: true  # 扫描件自动开启
-
-# 版式检测
-layout_detection:
-  module: "docling"  # 可选: "mineru", "paddle", "docling"
-  model_name: "docling-layout-old"
-  model_dir: "ds4sd/docling-layout-old"  # HuggingFace 模型仓库
-  device: "cpu"
-  conf: 0.3
-
-# VL 识别(表格、公式)
-vl_recognition:
-  module: "paddle"  # 可选: "mineru", "paddle"
-  backend: "http-client"
-  server_url: "http://xxx:8110"
-  table_recognition:
-    return_cells_coordinate: true
-
-# OCR 识别
-ocr_recognition:
-  module: "mineru"
-  language: "ch"
-
-# 输出配置
-output:
-  create_subdir: false        # 是否创建子目录
-  save_json: true
-  save_markdown: true
-  save_html: true
-  save_layout_image: false    # debug 模式开启
-  save_ocr_image: false       # debug 模式开启
-  normalize_numbers: true     # 金额数字标准化
-```
-
----
-
-## 支持的布局检测器
-
-### 1. Docling Layout (推荐)
-
-基于 HuggingFace transformers 的 RT-DETR 模型。
-
-```yaml
-layout_detection:
-  module: "docling"
-  model_name: "docling-layout-old"
-  model_dir: "ds4sd/docling-layout-old"
-```
-
-支持的模型:
-- `ds4sd/docling-layout-old`
-- `ds4sd/docling-layout-heron`
-- `ds4sd/docling-layout-egret-medium`
-- `ds4sd/docling-layout-egret-large`
-
-### 2. PaddleX RT-DETR (ONNX)
-
-基于 ONNX Runtime 的 PaddleX 布局检测器。
-
-```yaml
-layout_detection:
-  module: "paddle"
-  model_name: "RT-DETR-H_layout_17cls"
-  model_dir: "/path/to/RT-DETR-H_layout_17cls.onnx"
-```
-
-### 3. MinerU DocLayout-YOLO
-
-MinerU 内置的布局检测模型。
-
-```yaml
-layout_detection:
-  module: "mineru"
-  model_name: "layout"
-```
-
----
-
-## 类别映射
-
-所有布局检测器的输出都会统一映射到 MinerU/EnhancedDocPipeline 类别体系:
-
-| 类别分类 | 包含类别 |
-|---------|---------|
-| **文本类** (TEXT) | text, title, header, footer, page_number, ref_text, page_footnote, aside_text, ocr_text |
-| **表格类** (TABLE) | table, table_body, table_caption, table_footnote |
-| **图片类** (IMAGE) | image, image_body, figure, image_caption, image_footnote |
-| **公式类** (EQUATION) | interline_equation, inline_equation, equation |
-| **代码类** (CODE) | code, code_body, code_caption, algorithm |
-| **丢弃类** (DISCARD) | abandon, discarded |
-
----
-
-## 核心组件
-
-### 1. EnhancedDocPipeline (`pipeline_manager_v2.py`)
-
-主流水线管理器,实现完整处理流程:
-- PDF 分类(扫描件/数字原生)
-- 页面方向识别
-- Layout 检测与去重
-- **整页 OCR + Span-Block 匹配** ⭐
-- 元素分类处理
-- 阅读顺序排序
-- 坐标转换
-
-### 2. SpanMatcher (`layout_utils.py`)
-
-OCR Span 与 Layout Block 匹配器,参考 MinerU 实现:
-- `match_spans_to_blocks()` - 将 spans 匹配到对应的 blocks
-- `merge_spans_to_text()` - 将多个 spans 合并为文本
-- `remove_duplicate_spans()` - 去除重复 spans
-- `poly_to_bbox()` - 多边形坐标转 bbox
-
-### 3. ElementProcessors (`element_processors.py`)
-
-元素处理器,处理不同类型的元素:
-- `process_text_element()` - 文本处理(支持 pre_matched_spans)
-- `process_table_element()` - 表格处理(VLM + OCR 坐标匹配)
-- `process_image_element()` - 图片处理
-- `process_equation_element()` - 公式处理
-- `process_code_element()` - 代码处理
-- `process_discard_element()` - 丢弃元素处理(支持 pre_matched_spans)
-
-### 3. OutputFormatterV2 (`output_formatter_v2.py`)
-
-统一输出格式化器:
-- MinerU 标准 middle.json 格式
-- 每页独立 JSON(含单元格坐标)
-- Markdown 输出(完整版 + 按页)
-- 表格 HTML(带 data-bbox 属性)
-- 可视化图片(Layout/OCR)
-- 金额数字标准化
-
-### 4. TableCellMatcher (来自 `merger`)
-
-表格单元格坐标匹配器:
-- 使用动态规划进行行内单元格匹配
-- 将 OCR 检测框与 VLM 表格结构匹配
-- 输出带坐标的增强 HTML
-
----
-
-## 依赖说明
-
-### MinerU 组件
-- `mineru.utils.pdf_image_tools` - PDF 图像处理
-- `mineru.utils.pdf_text_tool` - PDF 文本提取
-- `mineru.utils.boxbase` - 边界框计算
-- `mineru.model.ocr` - OCR 模型
-- `mineru.model.ori_cls` - 方向分类模型
-
-### Merger 组件(来自 ocr_verify)
-- `merger.table_cell_matcher.TableCellMatcher` - 单元格坐标匹配
-- `merger.text_matcher.TextMatcher` - 文本匹配
-
-### 其他依赖
-- transformers - Docling 模型加载
-- huggingface_hub - 模型下载
-- onnxruntime - ONNX 模型推理
-- Pillow - 图像处理
-- NumPy - 数值计算
-- BeautifulSoup4 - HTML 解析
-- PyYAML - 配置文件解析
-- loguru - 日志
-
----
-
-## 安装 merger 模块
-
-```bash
-# 1. 进入 ocr_verify 目录
-cd /Users/zhch158/workspace/repository.git/ocr_verify
-
-# 2. 安装 merger 模块(可编辑模式)
-pip uninstall -y merger && pip install -e .
-
-# 3. 验证安装
-python3 -c "from merger.table_cell_matcher import TableCellMatcher; print('✅ 安装成功')"
-```
-
-完成后,在任何 Python 文件中都可以直接导入:
-
-```python
-from merger.table_cell_matcher import TableCellMatcher
-from merger.text_matcher import TextMatcher
-from merger.bbox_extractor import BBoxExtractor
-```
-
----
-
-## 项目结构
-
-```
-/Users/zhch158/workspace/repository.git/
-├── ocr_verify/                          # 源项目
-│   ├── setup.py                         # 安装配置
-│   └── merger/                          # 表格匹配模块
-│       ├── table_cell_matcher.py        # 表格单元格匹配
-│       ├── text_matcher.py              # 文本匹配
-│       └── bbox_extractor.py            # 边界框提取
-│
-└── MinerU/                              # 目标项目
-    └── zhch/
-        └── universal_doc_parser/        # 金融文档处理框架
-            ├── core/                    # 核心处理模块
-            ├── models/adapters/         # 模型适配器
-            ├── utils/                   # 输出工具
-            └── config/                  # 配置文件
-```

+ 0 - 283
ocr_tools/universal_doc_parser/流式处理模式说明.md

@@ -1,283 +0,0 @@
-# 流式处理模式说明
-
-## 📋 概述
-
-流式处理模式是对原有批量处理模式的优化,主要解决大文档处理时的内存占用问题。
-
-### 原有模式(批量处理)
-
-- **处理方式**:将所有页面读入内存,处理完成后统一保存
-- **内存占用**:高(所有页面的图像、OCR结果、表格数据都在内存中)
-- **适用场景**:小到中等文档(< 50页)
-
-### 流式处理模式
-
-- **处理方式**:按页处理,处理完一页立即保存并释放内存
-- **内存占用**:低(只保留当前页面的数据)
-- **适用场景**:大文档(> 50页)或内存受限环境
-
-## 🎯 核心优势
-
-### 1. **内存优化**
-
-```
-批量模式内存占用:
-- 100页文档 × 每页约50MB = 5GB内存
-
-流式模式内存占用:
-- 当前页约50MB + 元数据约10MB = 60MB内存
-```
-
-### 2. **容错性提升**
-
-- 批量模式:处理到第99页出错,前98页数据丢失
-- 流式模式:处理到第99页出错,前98页已保存,可继续处理
-
-### 3. **实时反馈**
-
-- 可以实时查看已处理页面的结果
-- 不需要等待所有页面处理完成
-
-## 🔧 使用方法
-
-### 命令行使用
-
-```bash
-# 使用流式处理模式
-python main_v2.py -i large_doc.pdf -c config.yaml --streaming
-
-# 批量处理模式(默认)
-python main_v2.py -i small_doc.pdf -c config.yaml
-```
-
-### 代码使用
-
-```python
-from core.pipeline_manager_v2_streaming import StreamingDocPipeline
-
-# 初始化流式处理流水线
-pipeline = StreamingDocPipeline(config_path, output_dir)
-
-# 处理文档
-results = pipeline.process_document_streaming(
-    document_path="large_doc.pdf",
-    page_range="1-100",
-    output_config={
-        'save_json': True,
-        'save_markdown': True,
-        'save_page_json': True,
-        'normalize_numbers': True,
-        'merge_cross_page_tables': True,
-    }
-)
-```
-
-## 📊 处理流程对比
-
-### 批量处理模式
-
-```
-1. 加载所有页面到内存
-2. 处理第1页 → 存储到 results['pages'][0]
-3. 处理第2页 → 存储到 results['pages'][1]
-4. ...
-5. 处理第N页 → 存储到 results['pages'][N-1]
-6. 统一保存所有结果
-7. 生成完整Markdown
-```
-
-### 流式处理模式
-
-```
-1. 初始化Markdown文件(流式写入)
-2. 处理第1页
-   → 立即保存 page_001.json
-   → 立即保存图片元素
-   → 写入Markdown(单页内容)
-   → 释放内存
-3. 处理第2页
-   → 立即保存 page_002.json
-   → ...
-4. ...
-5. 处理第N页
-   → 立即保存 page_NNN.json
-   → ...
-6. 关闭Markdown文件
-7. 从已保存的JSON文件加载
-8. 跨页表格合并
-9. 重新生成完整Markdown(包含合并后的表格)
-10. 生成middle.json
-```
-
-## 📁 输出文件结构
-
-### 批量模式输出
-
-```
-output/
-├── doc_name_middle.json          # 完整middle.json
-├── doc_name.md                   # 完整Markdown
-├── doc_name_page_001.json       # 第1页JSON
-├── doc_name_page_002.json       # 第2页JSON
-└── images/                       # 图片元素
-```
-
-### 流式模式输出
-
-```
-output/
-├── doc_name_middle.json          # 完整middle.json(最后生成)
-├── doc_name.md                   # 完整Markdown(包含合并表格)
-├── doc_name_page_001.json       # 第1页JSON(立即保存)
-├── doc_name_page_002.json       # 第2页JSON(立即保存)
-├── images/                       # 图片元素(立即保存)
-└── _temp_pages/                  # 临时JSON文件(最后清理)
-    ├── page_001.json
-    └── page_002.json
-```
-
-## ⚙️ 配置选项
-
-### output_config 参数
-
-| 参数 | 类型 | 默认值 | 说明 |
-|-----|------|--------|------|
-| `save_json` | bool | True | 是否生成middle.json |
-| `save_markdown` | bool | True | 是否生成Markdown |
-| `save_page_json` | bool | True | 是否保存每页JSON |
-| `save_images` | bool | True | 是否保存图片元素 |
-| `save_layout_image` | bool | False | 是否保存layout可视化图片 |
-| `save_ocr_image` | bool | False | 是否保存OCR可视化图片 |
-| `normalize_numbers` | bool | True | 是否标准化金额数字 |
-| `merge_cross_page_tables` | bool | True | 是否合并跨页表格 |
-| `cleanup_temp_files` | bool | True | 是否清理临时文件 |
-
-## 🔍 性能对比
-
-### 内存占用
-
-| 文档页数 | 批量模式 | 流式模式 | 节省 |
-|---------|---------|---------|------|
-| 10页 | ~500MB | ~60MB | 88% |
-| 50页 | ~2.5GB | ~60MB | 97.6% |
-| 100页 | ~5GB | ~60MB | 98.8% |
-| 500页 | ~25GB | ~60MB | 99.76% |
-
-### 处理时间
-
-- **批量模式**:略快(无需重复加载JSON)
-- **流式模式**:略慢(需要保存和重新加载JSON)
-
-**差异**:通常 < 5%,对于大文档可以忽略
-
-## ⚠️ 注意事项
-
-### 1. 跨页表格合并
-
-- 流式模式需要重新加载所有页面JSON才能合并跨页表格
-- 这会导致额外的I/O开销,但内存占用仍然很低
-
-### 2. 临时文件
-
-- 流式模式会在 `_temp_pages/` 目录创建临时JSON文件
-- 处理完成后会自动清理(如果 `cleanup_temp_files=True`)
-
-### 3. Markdown生成
-
-- 流式模式会先生成一个临时Markdown(边处理边写入)
-- 跨页表格合并后,会重新生成完整的Markdown
-
-### 4. 错误恢复
-
-- 如果处理中断,已保存的页面JSON可以用于恢复
-- 可以指定 `page_range` 参数继续处理剩余页面
-
-## 🎯 使用建议
-
-### 使用流式模式
-
-- ✅ 文档页数 > 50页
-- ✅ 内存受限环境(< 8GB RAM)
-- ✅ 需要实时查看处理结果
-- ✅ 需要容错性(处理中断后可恢复)
-
-### 使用批量模式
-
-- ✅ 文档页数 < 50页
-- ✅ 内存充足(> 16GB RAM)
-- ✅ 需要最快处理速度
-- ✅ 不需要实时查看结果
-
-## 📝 示例
-
-### 处理大文档(100页)
-
-```bash
-# 使用流式模式,节省内存
-python main_v2.py \
-  -i large_report.pdf \
-  -c config/bank_statement_mineru_v2.yaml \
-  --streaming \
-  --output_dir ./output/large_report_streaming
-```
-
-### 处理指定页面范围
-
-```bash
-# 流式模式 + 页面范围
-python main_v2.py \
-  -i large_report.pdf \
-  -c config.yaml \
-  --streaming \
-  -p 1-50  # 只处理前50页
-```
-
-### 继续处理剩余页面
-
-```bash
-# 如果之前处理到第50页中断,可以继续处理
-python main_v2.py \
-  -i large_report.pdf \
-  -c config.yaml \
-  --streaming \
-  -p 51-  # 从第51页到最后
-```
-
-## 🔄 迁移指南
-
-### 从批量模式迁移到流式模式
-
-1. **添加 `--streaming` 参数**:
-   ```bash
-   # 之前
-   python main_v2.py -i doc.pdf -c config.yaml
-   
-   # 之后
-   python main_v2.py -i doc.pdf -c config.yaml --streaming
-   ```
-
-2. **代码修改**:
-   ```python
-   # 之前
-   from core.pipeline_manager_v2 import EnhancedDocPipeline
-   pipeline = EnhancedDocPipeline(config_path)
-   results = pipeline.process_document(document_path)
-   
-   # 之后
-   from core.pipeline_manager_v2_streaming import StreamingDocPipeline
-   pipeline = StreamingDocPipeline(config_path, output_dir)
-   results = pipeline.process_document_streaming(
-       document_path,
-       output_config=output_config
-   )
-   ```
-
-3. **输出格式保持一致**:
-   - 流式模式和批量模式的输出格式完全相同
-   - 可以直接替换使用,无需修改后续处理代码
-
-## 📚 相关文档
-
-- [模型统一框架.md](./模型统一框架.md) - 整体架构说明
-- [OCR识别差异分析与改进方案.md](./OCR识别差异分析与改进方案.md) - OCR优化说明
-

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません