Jelajahi Sumber

feat(grid_recovery): add implicit dividers for large cells in grid recovery

- Introduced a new static method to add implicit dividers from large cells, enhancing the grid recovery process by ensuring complete table boundaries and preventing mismerging of large boxes.
- Updated the `recover_grid_structure` method to utilize the new implicit divider functionality, improving the accuracy of row and column divider detection.
- Enhanced handling of boundary cells and large frames, allowing for better grid structure recovery in OCR tasks.
zhch158_admin 2 hari lalu
induk
melakukan
3272c0f6ed

+ 207 - 2
ocr_tools/universal_doc_parser/models/adapters/wired_table/grid_recovery.py

@@ -565,6 +565,203 @@ class GridRecovery:
         return grid_lines
     
     @staticmethod
+    def _add_implicit_dividers_from_large_cells(
+        bboxes: List[List[float]],
+        row_dividers: List[float],
+        col_dividers: List[float],
+        min_gap_ratio: float = 0.1,
+        min_span_ratio: float = 2.5,
+        tolerance: float = 5.0
+    ) -> tuple[List[float], List[float]]:
+        """
+        从大框边界补充隐式分割线
+        
+        1. 优先处理边界单元格(表格的最顶部、最底部、最左侧、最右侧):
+           无条件添加边界单元格的边界到dividers中,确保表格边界完整。
+           这解决了由于find_grid_lines的min_support限制导致的边界缺失问题。
+        
+        2. 检测跨多列/多行的大框,如果其边界与已有分割线距离超过阈值,
+           则添加隐式分割线,避免大框被错误归并到已有行/列。
+        
+        Args:
+            bboxes: 所有单元格 bbox 列表
+            row_dividers: 已有行分割线(已排序)
+            col_dividers: 已有列分割线(已排序)
+            min_gap_ratio: 最小间距比例(相对于平均行高/列宽)
+            min_span_ratio: 最小跨列/行比例(相对于平均宽度/高度)
+            tolerance: 坐标容差(像素)
+            
+        Returns:
+            (补充后的行分割线, 补充后的列分割线),已去重并排序
+        """
+        if not bboxes:
+            return row_dividers, col_dividers
+        
+        # 计算平均宽度和高度
+        widths = [b[2] - b[0] for b in bboxes]
+        heights = [b[3] - b[1] for b in bboxes]
+        avg_width = sum(widths) / len(widths) if widths else 0
+        avg_height = sum(heights) / len(heights) if heights else 0
+        
+        if avg_width == 0 or avg_height == 0:
+            return row_dividers, col_dividers
+        
+        new_row_dividers = list(row_dividers)
+        new_col_dividers = list(col_dividers)
+        
+        # 计算表格的整体边界(用于判断大框是否跨大部分列/行)
+        if bboxes:
+            all_lefts = [b[0] for b in bboxes]
+            all_rights = [b[2] for b in bboxes]
+            all_tops = [b[1] for b in bboxes]
+            all_bottoms = [b[3] for b in bboxes]
+            table_left = min(all_lefts)
+            table_right = max(all_rights)
+            table_top = min(all_tops)
+            table_bottom = max(all_bottoms)
+            table_width = table_right - table_left
+            table_height = table_bottom - table_top
+        else:
+            table_width = table_height = 0
+        
+        # 检测大框并添加隐式分割线
+        # 边界单元格的容差(允许小误差)
+        boundary_tolerance = 2.0
+        
+        for bbox in bboxes:
+            b_left, b_top, b_right, b_bottom = bbox[0], bbox[1], bbox[2], bbox[3]
+            b_width = b_right - b_left
+            b_height = b_bottom - b_top
+            
+            # 判断是否是边界单元格(表格的最顶部、最底部、最左侧、最右侧)
+            is_top_boundary = abs(b_top - table_top) < boundary_tolerance
+            is_bottom_boundary = abs(b_bottom - table_bottom) < boundary_tolerance
+            is_left_boundary = abs(b_left - table_left) < boundary_tolerance
+            is_right_boundary = abs(b_right - table_right) < boundary_tolerance
+            
+            # 处理边界单元格的行边界
+            if is_top_boundary or is_bottom_boundary:
+                if row_dividers:
+                    if is_top_boundary:
+                        closest_top = min(row_dividers, key=lambda y: abs(y - b_top))
+                        if abs(closest_top - b_top) > tolerance:
+                            new_row_dividers.append(b_top)
+                    if is_bottom_boundary:
+                        closest_bottom = min(row_dividers, key=lambda y: abs(y - b_bottom))
+                        if abs(closest_bottom - b_bottom) > tolerance:
+                            new_row_dividers.append(b_bottom)
+                else:
+                    # 如果没有已有分割线,直接添加
+                    if is_top_boundary:
+                        new_row_dividers.append(b_top)
+                    if is_bottom_boundary:
+                        new_row_dividers.append(b_bottom)
+            
+            # 处理边界单元格的列边界
+            if is_left_boundary or is_right_boundary:
+                if col_dividers:
+                    if is_left_boundary:
+                        closest_left = min(col_dividers, key=lambda x: abs(x - b_left))
+                        if abs(closest_left - b_left) > tolerance:
+                            new_col_dividers.append(b_left)
+                    if is_right_boundary:
+                        closest_right = min(col_dividers, key=lambda x: abs(x - b_right))
+                        if abs(closest_right - b_right) > tolerance:
+                            new_col_dividers.append(b_right)
+                else:
+                    # 如果没有已有分割线,直接添加
+                    if is_left_boundary:
+                        new_col_dividers.append(b_left)
+                    if is_right_boundary:
+                        new_col_dividers.append(b_right)
+            
+            # # 判断是否是大框(跨多列或跨多行)
+            # is_large_col = b_width >= avg_width * min_span_ratio
+            # is_large_row = b_height >= avg_height * min_span_ratio
+            
+            # # 判断是否跨大部分表格宽度/高度(用于更激进的添加分割线)
+            # spans_most_cols = table_width > 0 and b_width >= table_width * 0.8
+            # spans_most_rows = table_height > 0 and b_height >= table_height * 0.8
+            
+            # # 处理行分割线(检查大框的顶部和底部)
+            # if is_large_row or spans_most_rows:
+            #     min_gap = avg_height * min_gap_ratio
+                
+            #     # 检查顶部边界
+            #     if row_dividers:
+            #         closest_top = min(row_dividers, key=lambda y: abs(y - b_top))
+            #         gap_top = abs(closest_top - b_top)
+            #         # 对于跨大部分列的大框,如果顶部明显在已有分割线之下,即使距离小也添加
+            #         # 判断标准:b_top > closest_top(在下方)且距离 > 0.5 像素
+            #         is_below_existing = b_top > closest_top and gap_top > 0.5
+            #         # 对于跨大部分行的大框,只要距离 > tolerance 就添加
+            #         # 对于普通大框,需要同时满足 > tolerance 和 > min_gap
+            #         should_add_top = (spans_most_cols and is_below_existing) or \
+            #                         (spans_most_rows and gap_top > tolerance) or \
+            #                         (gap_top > tolerance and gap_top > min_gap)
+            #         if should_add_top:
+            #             new_row_dividers.append(b_top)
+            #     else:
+            #         # 如果没有已有分割线,直接添加
+            #         new_row_dividers.append(b_top)
+                
+            #     # 检查底部边界
+            #     if row_dividers:
+            #         closest_bottom = min(row_dividers, key=lambda y: abs(y - b_bottom))
+            #         gap_bottom = abs(closest_bottom - b_bottom)
+            #         # 对于跨大部分列的大框,如果底部明显在已有分割线之上,即使距离小也添加
+            #         # 判断标准:b_bottom < closest_bottom(在上方)且距离 > 0.5 像素
+            #         is_above_existing = b_bottom < closest_bottom and gap_bottom > 0.5
+            #         # 对于跨大部分行的大框,只要距离 > tolerance 就添加
+            #         # 对于普通大框,需要同时满足 > tolerance 和 > min_gap
+            #         should_add_bottom = (spans_most_cols and is_above_existing) or \
+            #                            (spans_most_rows and gap_bottom > tolerance) or \
+            #                            (gap_bottom > tolerance and gap_bottom > min_gap)
+            #         if should_add_bottom:
+            #             new_row_dividers.append(b_bottom)
+            #     else:
+            #         # 如果没有已有分割线,直接添加
+            #         new_row_dividers.append(b_bottom)
+            
+            # # 处理列分割线(检查大框的左侧和右侧)
+            # if is_large_col or spans_most_cols:
+            #     min_gap = avg_width * min_gap_ratio
+                
+            #     # 检查左侧边界
+            #     if col_dividers:
+            #         closest_left = min(col_dividers, key=lambda x: abs(x - b_left))
+            #         gap_left = abs(closest_left - b_left)
+            #         # 对于跨大部分列的大框,只要距离 > tolerance 就添加
+            #         # 对于普通大框,需要同时满足 > tolerance 和 > min_gap
+            #         should_add_left = (spans_most_cols and gap_left > tolerance) or \
+            #                          (gap_left > tolerance and gap_left > min_gap)
+            #         if should_add_left:
+            #             new_col_dividers.append(b_left)
+            #     else:
+            #         # 如果没有已有分割线,直接添加
+            #         new_col_dividers.append(b_left)
+                
+            #     # 检查右侧边界
+            #     if col_dividers:
+            #         closest_right = min(col_dividers, key=lambda x: abs(x - b_right))
+            #         gap_right = abs(closest_right - b_right)
+            #         # 对于跨大部分列的大框,只要距离 > tolerance 就添加
+            #         # 对于普通大框,需要同时满足 > tolerance 和 > min_gap
+            #         should_add_right = (spans_most_cols and gap_right > tolerance) or \
+            #                           (gap_right > tolerance and gap_right > min_gap)
+            #         if should_add_right:
+            #             new_col_dividers.append(b_right)
+            #     else:
+            #         # 如果没有已有分割线,直接添加
+            #         new_col_dividers.append(b_right)
+        
+        # 去重并排序
+        new_row_dividers = sorted(set(new_row_dividers))
+        new_col_dividers = sorted(set(new_col_dividers))
+        
+        return new_row_dividers, new_col_dividers
+    
+    @staticmethod
     def recover_grid_structure(bboxes: List[List[float]]) -> List[Dict]:
         """
         从散乱的单元格 bbox 恢复表格的行列结构 (row, col, rowspan, colspan)
@@ -586,14 +783,22 @@ class GridRecovery:
             y_coords.append(b[1])
             y_coords.append(b[3])
         
-        row_dividers = GridRecovery.find_grid_lines(y_coords, tolerance=5, min_support=2)
+        row_dividers_raw = GridRecovery.find_grid_lines(y_coords, tolerance=5, min_support=2)
         
         # 2. 识别列分割线 (X轴)
         x_coords = []
         for b in bboxes:
             x_coords.append(b[0])
             x_coords.append(b[2])
-        col_dividers = GridRecovery.find_grid_lines(x_coords, tolerance=5, min_support=2)
+        col_dividers_raw = GridRecovery.find_grid_lines(x_coords, tolerance=5, min_support=2)
+        
+        # 2.5. 从大框边界补充隐式分割线
+        row_dividers, col_dividers = GridRecovery._add_implicit_dividers_from_large_cells(
+            bboxes, row_dividers_raw, col_dividers_raw,
+            min_gap_ratio=0.1,  # 最小间距比例(相对于平均行高/列宽)
+            min_span_ratio=2.5,  # 跨2.5倍平均宽度/高度以上才考虑
+            tolerance=5.0
+        )
         
         # 3. 构建网格结构
         structured_cells = []