Răsfoiți Sursa

refactor(grid_recovery): simplify implicit divider logic and enhance boundary handling

- Revised the grid recovery process to focus on completing outer boundary lines and merging close lines based on a specified tolerance.
- Removed complex large frame detection logic, streamlining the method for better performance and clarity.
- Improved handling of boundary cells to ensure accurate detection of row and column dividers, enhancing overall grid structure recovery in OCR tasks.
zhch158_admin 2 zile în urmă
părinte
comite
897d3ef01c

+ 58 - 170
ocr_tools/universal_doc_parser/models/adapters/wired_table/grid_recovery.py

@@ -574,14 +574,20 @@ class GridRecovery:
         tolerance: float = 5.0
     ) -> tuple[List[float], List[float]]:
         """
-        从大框边界补充隐式分割线
-        
-        1. 优先处理边界单元格(表格的最顶部、最底部、最左侧、最右侧):
-           无条件添加边界单元格的边界到dividers中,确保表格边界完整。
-           这解决了由于find_grid_lines的min_support限制导致的边界缺失问题。
-        
-        2. 检测跨多列/多行的大框,如果其边界与已有分割线距离超过阈值,
-           则添加隐式分割线,避免大框被错误归并到已有行/列。
+        补充分割线(简化版)
+
+        前提:连通域/矢量重构阶段已保证网格结构正确。
+        因此这里不再做“大框推断/出现次数(min_support)推断”,只做两件事:
+
+        1) **补全最外边界线**:确保 top/bottom/left/right 边界线存在
+        2) **近线合并**:若两条线距离 <= tolerance,则合并为一条(取均值)
+
+        说明:
+        - 对于 Y 方向(行):强制使用 min(all_tops) / max(all_bottoms) 作为外边界
+          (可解决你提到的 1511.0 这种只出现一次但是真实边界没被保留的问题)
+        - 对于 X 方向(列):为避免极少数离群 bbox 把 left/right 拉偏导致“多出空白列”,
+          默认使用 5%/95% 分位数作为列外边界;若 min/max 与分位数足够接近(<= tolerance),
+          则退化为 min/max。
         
         Args:
             bboxes: 所有单元格 bbox 列表
@@ -596,169 +602,51 @@ class GridRecovery:
         """
         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)
+
+        def _merge_close_lines(lines: List[float], tol: float) -> List[float]:
+            """按容差合并近似重复网格线(替代 set(),避免浮点近似导致多出窄列/窄行)"""
+            if not lines:
+                return []
+            lines_sorted = sorted(float(x) for x in lines)
+            merged: List[float] = []
+            cluster: List[float] = [lines_sorted[0]]
+            for x in lines_sorted[1:]:
+                if abs(x - cluster[-1]) <= tol:
+                    cluster.append(x)
                 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))
-        
+                    merged.append(sum(cluster) / len(cluster))
+                    cluster = [x]
+            merged.append(sum(cluster) / len(cluster))
+            return merged
+        
+        # 统计 bbox 边界
+        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]
+
+        hard_top = float(min(all_tops))
+        hard_bottom = float(max(all_bottoms))
+
+        # 列边界:默认用分位数抵抗离群点(避免多出空白列)
+        lefts_sorted = sorted(float(x) for x in all_lefts)
+        rights_sorted = sorted(float(x) for x in all_rights)
+        q05 = int(round((len(lefts_sorted) - 1) * 0.05))
+        q95 = int(round((len(rights_sorted) - 1) * 0.95))
+        q05 = max(0, min(q05, len(lefts_sorted) - 1))
+        q95 = max(0, min(q95, len(rights_sorted) - 1))
+        robust_left = lefts_sorted[q05]
+        robust_right = rights_sorted[q95]
+        hard_left = float(min(all_lefts))
+        hard_right = float(max(all_rights))
+        # 如果 min/max 与分位数很接近(<= tolerance),说明不存在明显离群点,则使用 min/max
+        col_left = hard_left if abs(hard_left - robust_left) <= tolerance else robust_left
+        col_right = hard_right if abs(hard_right - robust_right) <= tolerance else robust_right
+
+        # 仅做:补边界 + 合并近线
+        new_row_dividers = _merge_close_lines(list(row_dividers) + [hard_top, hard_bottom], tol=tolerance)
+        new_col_dividers = _merge_close_lines(list(col_dividers) + [col_left, col_right], tol=tolerance)
+
         return new_row_dividers, new_col_dividers
     
     @staticmethod