Browse Source

feat(debug_overlap_removal): add debugging script for overlapping box removal in layout detection

- Introduced a new script to trace the execution of the overlapping box removal algorithm, enhancing debugging capabilities.
- Implemented detailed logging to track the decision-making process during the removal of overlapping boxes, particularly for 'table_body' elements.
- Added utility functions for calculating IoU and overlap ratios, improving the analysis of bounding box interactions.
zhch158_admin 10 hours ago
parent
commit
cb9843f8de
1 changed files with 690 additions and 0 deletions
  1. 690 0
      ocr_tools/universal_doc_parser/tests/debug_overlap_removal.py

+ 690 - 0
ocr_tools/universal_doc_parser/tests/debug_overlap_removal.py

@@ -0,0 +1,690 @@
+#!/usr/bin/env python3
+"""
+调试脚本:追踪 table_body 重叠处理算法的执行过程
+
+此脚本模拟 _remove_overlapping_boxes 方法的执行,添加详细的日志输出
+用于调试 table_body 被错误移除的问题。
+"""
+import sys
+from pathlib import Path
+from typing import Dict, Any, List, Set
+from collections import defaultdict
+
+# 添加项目路径
+project_root = Path(__file__).parent.parent.parent.parent
+sys.path.insert(0, str(project_root))
+
+# 直接实现 CoordinateUtils 的核心方法,避免依赖问题
+class CoordinateUtils:
+    @staticmethod
+    def calculate_iou(bbox1, bbox2):
+        """计算两个 bbox 的 IoU(交并比)"""
+        x_left = max(bbox1[0], bbox2[0])
+        y_top = max(bbox1[1], bbox2[1])
+        x_right = min(bbox1[2], bbox2[2])
+        y_bottom = min(bbox1[3], bbox2[3])
+        
+        if x_right < x_left or y_bottom < y_top:
+            return 0.0
+        
+        intersection_area = (x_right - x_left) * (y_bottom - y_top)
+        bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
+        bbox2_area = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
+        
+        if bbox1_area == 0 or bbox2_area == 0:
+            return 0.0
+        
+        return intersection_area / float(bbox1_area + bbox2_area - intersection_area)
+    
+    @staticmethod
+    def calculate_overlap_ratio(bbox1, bbox2):
+        """计算重叠面积占小框面积的比例"""
+        x_left = max(bbox1[0], bbox2[0])
+        y_top = max(bbox1[1], bbox2[1])
+        x_right = min(bbox1[2], bbox2[2])
+        y_bottom = min(bbox1[3], bbox2[3])
+        
+        if x_right < x_left or y_bottom < y_top:
+            return 0.0
+        
+        intersection_area = (x_right - x_left) * (y_bottom - y_top)
+        area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
+        area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
+        min_area = min(area1, area2)
+        
+        if min_area == 0:
+            return 0.0
+        
+        return intersection_area / min_area
+    
+    @staticmethod
+    def calculate_overlap_in_bbox1_ratio(bbox1, bbox2):
+        """计算 bbox1 被 bbox2 覆盖的面积比例"""
+        x_left = max(bbox1[0], bbox2[0])
+        y_top = max(bbox1[1], bbox2[1])
+        x_right = min(bbox1[2], bbox2[2])
+        y_bottom = min(bbox1[3], bbox2[3])
+        
+        if x_right < x_left or y_bottom < y_top:
+            return 0.0
+        
+        intersection_area = (x_right - x_left) * (y_bottom - y_top)
+        bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
+        
+        if bbox1_area == 0:
+            return 0.0
+        
+        return intersection_area / bbox1_area
+
+
+def debug_remove_overlapping_boxes(
+    layout_results: List[Dict[str, Any]],
+    coordinate_utils: Any,
+    iou_threshold: float = 0.8,
+    overlap_ratio_threshold: float = 0.8,
+    table_body_area_threshold: float = 0.5,
+    table_body_confidence_threshold: float = 0.1,
+    table_body_overlap_threshold: float = 0.8
+) -> List[Dict[str, Any]]:
+    """
+    调试版本的重叠框移除函数
+    
+    与原函数逻辑相同,但添加了详细的日志输出
+    """
+    if not layout_results or len(layout_results) <= 1:
+        return layout_results
+    
+    # 复制列表避免修改原数据
+    results = [item.copy() for item in layout_results]
+    need_remove = set()
+    
+    # 记录每个元素的移除历史
+    removal_history = defaultdict(list)  # idx -> list of removal records
+    
+    def _is_bbox_inside(inner_bbox: List[float], outer_bbox: List[float]) -> bool:
+        """检查 inner_bbox 是否完全包含在 outer_bbox 内"""
+        if len(inner_bbox) < 4 or len(outer_bbox) < 4:
+            return False
+        return (inner_bbox[0] >= outer_bbox[0] and 
+                inner_bbox[1] >= outer_bbox[1] and 
+                inner_bbox[2] <= outer_bbox[2] and 
+                inner_bbox[3] <= outer_bbox[3])
+    
+    def _format_element(idx: int, element: Dict[str, Any]) -> str:
+        """格式化元素信息用于日志"""
+        bbox = element.get('bbox', [])
+        category = element.get('category', '')
+        score = element.get('confidence', element.get('score', 0))
+        return f"[{idx}] {category}({score:.3f}) bbox={bbox}"
+    
+    print("\n" + "="*80)
+    print("开始处理重叠框")
+    print("="*80)
+    print(f"\n原始元素数量: {len(results)}")
+    print("\n原始元素列表:")
+    for i, elem in enumerate(results):
+        print(f"  {_format_element(i, elem)}")
+    
+    comparison_count = 0
+    
+    for i in range(len(results)):
+        if i in need_remove:
+            print(f"\n[跳过] 元素 {i} 已被标记为移除,跳过")
+            continue
+            
+        for j in range(i + 1, len(results)):
+            if j in need_remove:
+                continue
+            
+            comparison_count += 1
+            bbox1 = results[i].get('bbox', [0, 0, 0, 0])
+            bbox2 = results[j].get('bbox', [0, 0, 0, 0])
+            category1 = results[i].get('category', '')
+            category2 = results[j].get('category', '')
+            
+            if len(bbox1) < 4 or len(bbox2) < 4:
+                continue
+            
+            score1 = results[i].get('confidence', results[i].get('score', 0))
+            score2 = results[j].get('confidence', results[j].get('score', 0))
+            
+            print(f"\n{'='*80}")
+            print(f"比较 {comparison_count}: 元素 {i} vs 元素 {j}")
+            print(f"  元素 {i}: {_format_element(i, results[i])}")
+            print(f"  元素 {j}: {_format_element(j, results[j])}")
+            
+            # 计算 IoU
+            iou = coordinate_utils.calculate_iou(bbox1, bbox2)
+            print(f"  IoU = {iou:.4f}")
+            
+            if iou > iou_threshold:
+                print(f"  → IoU > {iou_threshold} (高度重叠)")
+                
+                # abandon 类别优先级最低,应该被其他类别移除
+                is_abandon = (category1 == 'abandon' or category2 == 'abandon')
+                if is_abandon:
+                    print(f"  → 检测到 abandon 类别")
+                    # 如果一个是 abandon,另一个不是,保留非 abandon 的
+                    if category1 == 'abandon':
+                        print(f"  → 决策: 移除元素 {i} (abandon), 保留元素 {j}")
+                        need_remove.add(i)
+                        removal_history[i].append({
+                            'reason': f'与元素 {j} ({category2}) 高度重叠,abandon 优先级最低',
+                            'compared_with': j,
+                            'iou': iou,
+                            'branch': 'high_iou_abandon'
+                        })
+                        break
+                    else:  # category2 == 'abandon'
+                        print(f"  → 决策: 移除元素 {j} (abandon), 保留元素 {i}")
+                        need_remove.add(j)
+                        removal_history[j].append({
+                            'reason': f'与元素 {i} ({category1}) 高度重叠,abandon 优先级最低',
+                            'compared_with': i,
+                            'iou': iou,
+                            'branch': 'high_iou_abandon'
+                        })
+                        continue
+                
+                # 对于 table_body 与其他类型的重叠,使用特殊处理
+                is_table_body_vs_other_high_iou = (
+                    (category1 == 'table_body' and category2 in ['text', 'image_body']) or
+                    (category2 == 'table_body' and category1 in ['text', 'image_body'])
+                )
+                
+                # 对于 table_body 与 table_body 的重叠,优先保留置信度高的
+                is_table_body_vs_table_body = (category1 == 'table_body' and category2 == 'table_body')
+                
+                if is_table_body_vs_other_high_iou:
+                    print(f"  → table_body vs 其他类型 (高度重叠)")
+                    # 确定哪个是 table_body (t1),哪个是其他类型 (o1)
+                    if category1 == 'table_body':
+                        t1_score, o1_score = score1, score2
+                        t1_idx, o1_idx = i, j
+                        t1_cat, o1_cat = category1, category2
+                    else:  # category2 == 'table_body'
+                        t1_score, o1_score = score2, score1
+                        t1_idx, o1_idx = j, i
+                        t1_cat, o1_cat = category2, category1
+                    
+                    print(f"    table_body: 元素 {t1_idx} (置信度={t1_score:.3f})")
+                    print(f"    其他类型: 元素 {o1_idx} (置信度={o1_score:.3f})")
+                    print(f"    阈值: table_body_confidence_threshold={table_body_confidence_threshold}")
+                    
+                    # 如果 t1 置信度 > o1 置信度 + 阈值,保留 t1
+                    if t1_score > o1_score + table_body_confidence_threshold:
+                        print(f"    → 决策: t1 置信度足够高,保留 table_body (元素 {t1_idx}), 移除 {o1_cat} (元素 {o1_idx})")
+                        need_remove.add(o1_idx)
+                        removal_history[o1_idx].append({
+                            'reason': f'table_body (元素 {t1_idx}) 置信度 ({t1_score:.3f}) > {o1_cat} (元素 {o1_idx}) 置信度 ({o1_score:.3f}) + 阈值 ({table_body_confidence_threshold})',
+                            'compared_with': t1_idx,
+                            'iou': iou,
+                            'branch': 'high_iou_table_body_vs_other'
+                        })
+                        if o1_idx == i:
+                            break
+                        continue
+                    # 如果 o1 置信度 > t1 置信度 + 阈值,且差值足够大(> 0.2),才保留 o1
+                    elif o1_score > t1_score + max(table_body_confidence_threshold, 0.2):
+                        print(f"    → 决策: o1 置信度明显更高,保留 {o1_cat} (元素 {o1_idx}), 移除 table_body (元素 {t1_idx})")
+                        need_remove.add(t1_idx)
+                        removal_history[t1_idx].append({
+                            'reason': f'{o1_cat} (元素 {o1_idx}) 置信度 ({o1_score:.3f}) > table_body (元素 {t1_idx}) 置信度 ({t1_score:.3f}) + 阈值 ({max(table_body_confidence_threshold, 0.2)})',
+                            'compared_with': o1_idx,
+                            'iou': iou,
+                            'branch': 'high_iou_table_body_vs_other'
+                        })
+                        if t1_idx == i:
+                            break
+                        continue
+                    # 否则置信度接近,优先保留 table_body(即使置信度稍低)
+                    else:
+                        print(f"    → 决策: 置信度接近,优先保留 table_body (元素 {t1_idx}), 移除 {o1_cat} (元素 {o1_idx})")
+                        need_remove.add(o1_idx)
+                        removal_history[o1_idx].append({
+                            'reason': f'置信度接近,优先保留 table_body',
+                            'compared_with': t1_idx,
+                            'iou': iou,
+                            'branch': 'high_iou_table_body_vs_other'
+                        })
+                        if o1_idx == i:
+                            break
+                        continue
+                elif is_table_body_vs_table_body:
+                    print(f"  → table_body vs table_body (高度重叠)")
+                    # table_body 与 table_body 的重叠,保留置信度高的
+                    if score1 >= score2:
+                        print(f"    → 决策: 保留元素 {i} (置信度={score1:.3f}), 移除元素 {j} (置信度={score2:.3f})")
+                        need_remove.add(j)
+                        removal_history[j].append({
+                            'reason': f'与元素 {i} 重叠,置信度更低 ({score2:.3f} < {score1:.3f})',
+                            'compared_with': i,
+                            'iou': iou,
+                            'branch': 'high_iou_table_body_vs_table_body'
+                        })
+                    else:
+                        print(f"    → 决策: 保留元素 {j} (置信度={score2:.3f}), 移除元素 {i} (置信度={score1:.3f})")
+                        need_remove.add(i)
+                        removal_history[i].append({
+                            'reason': f'与元素 {j} 重叠,置信度更低 ({score1:.3f} < {score2:.3f})',
+                            'compared_with': j,
+                            'iou': iou,
+                            'branch': 'high_iou_table_body_vs_table_body'
+                        })
+                        break  # i 被移除,跳出内层循环
+                else:
+                    print(f"  → 非 table_body 与其他类型的重叠")
+                    # 非 table_body 与其他类型的重叠,保留置信度高的
+                    if score1 >= score2:
+                        print(f"    → 决策: 保留元素 {i} (置信度={score1:.3f}), 移除元素 {j} (置信度={score2:.3f})")
+                        need_remove.add(j)
+                        removal_history[j].append({
+                            'reason': f'与元素 {i} 重叠,置信度更低 ({score2:.3f} < {score1:.3f})',
+                            'compared_with': i,
+                            'iou': iou,
+                            'branch': 'high_iou_other'
+                        })
+                    else:
+                        print(f"    → 决策: 保留元素 {j} (置信度={score2:.3f}), 移除元素 {i} (置信度={score1:.3f})")
+                        need_remove.add(i)
+                        removal_history[i].append({
+                            'reason': f'与元素 {j} 重叠,置信度更低 ({score1:.3f} < {score2:.3f})',
+                            'compared_with': j,
+                            'iou': iou,
+                            'branch': 'high_iou_other'
+                        })
+                        break  # i 被移除,跳出内层循环
+            else:
+                # 检查包含关系
+                area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
+                area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
+                
+                print(f"  → IoU <= {iou_threshold}, 检查包含关系")
+                print(f"    元素 {i} 面积: {area1:.1f}")
+                print(f"    元素 {j} 面积: {area2:.1f}")
+                
+                # 判断哪个框更小
+                if area1 <= area2:
+                    small_bbox, large_bbox = bbox1, bbox2
+                    small_idx, large_idx = i, j
+                    small_score, large_score = score1, score2
+                    small_category, large_category = category1, category2
+                    small_area, large_area = area1, area2
+                else:
+                    small_bbox, large_bbox = bbox2, bbox1
+                    small_idx, large_idx = j, i
+                    small_score, large_score = score2, score1
+                    small_category, large_category = category2, category1
+                    small_area, large_area = area2, area1
+                
+                print(f"    小框: 元素 {small_idx} ({small_category}, 面积={small_area:.1f}, 置信度={small_score:.3f})")
+                print(f"    大框: 元素 {large_idx} ({large_category}, 面积={large_area:.1f}, 置信度={large_score:.3f})")
+                
+                # 检查小框是否被大框包含
+                is_inside = _is_bbox_inside(small_bbox, large_bbox)
+                overlap_ratio = coordinate_utils.calculate_overlap_ratio(small_bbox, large_bbox)
+                print(f"    是否包含: {is_inside}")
+                print(f"    重叠比例: {overlap_ratio:.4f}")
+                
+                # abandon 类别优先级最低,应该被其他类别移除
+                is_abandon = (small_category == 'abandon' or large_category == 'abandon')
+                if (is_inside or overlap_ratio > overlap_ratio_threshold) and is_abandon:
+                    print(f"  → 检测到 abandon 类别 (包含关系)")
+                    # 如果一个是 abandon,另一个不是,保留非 abandon 的
+                    if small_category == 'abandon':
+                        print(f"    → 决策: 移除元素 {small_idx} (abandon), 保留元素 {large_idx}")
+                        need_remove.add(small_idx)
+                        removal_history[small_idx].append({
+                            'reason': f'被元素 {large_idx} ({large_category}) 包含,abandon 优先级最低',
+                            'compared_with': large_idx,
+                            'overlap_ratio': overlap_ratio,
+                            'is_inside': is_inside,
+                            'branch': 'containment_abandon'
+                        })
+                        if small_idx == i:
+                            break
+                        continue
+                    else:  # large_category == 'abandon'
+                        print(f"    → 决策: 移除元素 {large_idx} (abandon), 保留元素 {small_idx}")
+                        need_remove.add(large_idx)
+                        removal_history[large_idx].append({
+                            'reason': f'包含元素 {small_idx} ({small_category}),abandon 优先级最低',
+                            'compared_with': small_idx,
+                            'overlap_ratio': overlap_ratio,
+                            'is_inside': is_inside,
+                            'branch': 'containment_abandon'
+                        })
+                        if large_idx == i:
+                            break
+                        continue
+                
+                # 判断是否是 table_body 与其他类型的重叠
+                is_table_body_vs_other = (
+                    (small_category == 'table_body' and large_category in ['text', 'image_body']) or
+                    (large_category == 'table_body' and small_category in ['text', 'image_body'])
+                )
+                
+                if is_inside or overlap_ratio > overlap_ratio_threshold:
+                    print(f"  → 包含或高度重叠 (overlap_ratio={overlap_ratio:.4f} > {overlap_ratio_threshold})")
+                    
+                    # 对于 table_body 与其他类型的重叠,特殊处理
+                    if is_table_body_vs_other:
+                        print(f"  → table_body vs 其他类型 (包含关系)")
+                        # 确定哪个是 table_body (t1),哪个是其他类型 (o1)
+                        if small_category == 'table_body':
+                            t1_bbox, o1_bbox = small_bbox, large_bbox
+                            t1_idx, o1_idx = small_idx, large_idx
+                            t1_score, o1_score = small_score, large_score
+                            t1_area, o1_area = small_area, large_area
+                            # 这是情况B: o1 包含 t1 (o1 是大框,t1 是小框)
+                            is_t1_inside_o1 = True
+                        else:  # large_category == 'table_body'
+                            t1_bbox, o1_bbox = large_bbox, small_bbox
+                            t1_idx, o1_idx = large_idx, small_idx
+                            t1_score, o1_score = large_score, small_score
+                            t1_area, o1_area = large_area, small_area
+                            # 这是情况A: t1 包含 o1 (t1 是大框,o1 是小框)
+                            is_t1_inside_o1 = False
+                        
+                        print(f"    table_body: 元素 {t1_idx} (面积={t1_area:.1f}, 置信度={t1_score:.3f})")
+                        print(f"    其他类型: 元素 {o1_idx} (面积={o1_area:.1f}, 置信度={o1_score:.3f})")
+                        print(f"    t1 被 o1 包含: {is_t1_inside_o1}")
+                        
+                        # 情况A: t1 包含 o1 (t1 是大框,o1 是小框)
+                        if is_inside and not is_t1_inside_o1:
+                            print(f"    情况A: table_body (大框) 包含 其他类型 (小框)")
+                            if (t1_area > o1_area * table_body_area_threshold and 
+                                t1_score > o1_score + table_body_confidence_threshold):
+                                print(f"      → 决策: 保留 table_body (元素 {t1_idx}), 移除 {large_category} (元素 {o1_idx})")
+                                need_remove.add(o1_idx)
+                                removal_history[o1_idx].append({
+                                    'reason': f'table_body 包含且满足面积和置信度条件',
+                                    'compared_with': t1_idx,
+                                    'overlap_ratio': overlap_ratio,
+                                    'is_inside': is_inside,
+                                    'branch': 'containment_table_body_vs_other_case_a'
+                                })
+                                if o1_idx == i:
+                                    break
+                                continue
+                        
+                        # 情况B: o1 包含 t1 (o1 是大框,t1 是小框)
+                        elif is_inside and is_t1_inside_o1:
+                            print(f"    情况B: 其他类型 (大框) 包含 table_body (小框)")
+                            if (o1_area > t1_area * table_body_area_threshold and 
+                                t1_score > o1_score + table_body_confidence_threshold):
+                                print(f"      → 决策: 保护高置信度 table_body,保留 table_body (元素 {t1_idx}), 移除 {large_category} (元素 {o1_idx})")
+                                need_remove.add(o1_idx)
+                                removal_history[o1_idx].append({
+                                    'reason': f'table_body 被包含但置信度足够高',
+                                    'compared_with': t1_idx,
+                                    'overlap_ratio': overlap_ratio,
+                                    'is_inside': is_inside,
+                                    'branch': 'containment_table_body_vs_other_case_b'
+                                })
+                                if o1_idx == i:
+                                    break
+                                continue
+                            elif t1_score > o1_score + table_body_confidence_threshold:
+                                print(f"      → 决策: table_body 置信度足够高,保留 table_body (元素 {t1_idx}), 移除 {large_category} (元素 {o1_idx})")
+                                need_remove.add(o1_idx)
+                                removal_history[o1_idx].append({
+                                    'reason': f'table_body 置信度足够高 ({t1_score:.3f} > {o1_score:.3f} + {table_body_confidence_threshold})',
+                                    'compared_with': t1_idx,
+                                    'overlap_ratio': overlap_ratio,
+                                    'is_inside': is_inside,
+                                    'branch': 'containment_table_body_vs_other_case_b'
+                                })
+                                if o1_idx == i:
+                                    break
+                                continue
+                        
+                        # 情况C: 高度重叠(非包含关系),使用 t1 被覆盖的比例
+                        elif not is_inside and overlap_ratio > overlap_ratio_threshold:
+                            print(f"    情况C: 高度重叠(非包含关系)")
+                            # 计算 t1 被 o1 覆盖的比例(交集 / t1 的面积)
+                            try:
+                                t1_coverage = coordinate_utils.calculate_overlap_in_bbox1_ratio(t1_bbox, o1_bbox)
+                            except AttributeError:
+                                # 备用实现:如果方法不存在,使用交集 / t1 面积
+                                x_left = max(t1_bbox[0], o1_bbox[0])
+                                y_top = max(t1_bbox[1], o1_bbox[1])
+                                x_right = min(t1_bbox[2], o1_bbox[2])
+                                y_bottom = min(t1_bbox[3], o1_bbox[3])
+                                if x_right > x_left and y_bottom > y_top:
+                                    intersection = (x_right - x_left) * (y_bottom - y_top)
+                                    t1_coverage = intersection / t1_area if t1_area > 0 else 0.0
+                                else:
+                                    t1_coverage = 0.0
+                            
+                            print(f"      t1 被覆盖比例: {t1_coverage:.4f}")
+                            if (t1_coverage > table_body_overlap_threshold and 
+                                t1_score > o1_score + table_body_confidence_threshold):
+                                print(f"      → 决策: t1 被高度覆盖且置信度足够高,保留 table_body (元素 {t1_idx}), 移除 {large_category} (元素 {o1_idx})")
+                                need_remove.add(o1_idx)
+                                removal_history[o1_idx].append({
+                                    'reason': f'table_body 被高度覆盖 ({t1_coverage:.4f} > {table_body_overlap_threshold}) 且置信度足够高',
+                                    'compared_with': t1_idx,
+                                    'overlap_ratio': overlap_ratio,
+                                    't1_coverage': t1_coverage,
+                                    'branch': 'containment_table_body_vs_other_case_c'
+                                })
+                                if o1_idx == i:
+                                    break
+                                continue
+                    
+                    # 对于 table_body 与 table_body 的重叠,优先保留置信度高的
+                    if small_category == 'table_body' and large_category == 'table_body':
+                        print(f"  → table_body vs table_body (包含关系)")
+                        # 如果大框置信度明显低于小框(差值 > 0.3),保留小框(高置信度的)
+                        if large_score < small_score - 0.3:
+                            print(f"    → 决策: 大框置信度明显低于小框,保留小框 (元素 {small_idx}), 移除大框 (元素 {large_idx})")
+                            need_remove.add(large_idx)
+                            removal_history[large_idx].append({
+                                'reason': f'大框置信度明显低于小框 ({large_score:.3f} < {small_score:.3f} - 0.3)',
+                                'compared_with': small_idx,
+                                'overlap_ratio': overlap_ratio,
+                                'is_inside': is_inside,
+                                'branch': 'containment_table_body_vs_table_body'
+                            })
+                            if large_idx == i:
+                                break
+                            continue
+                        # 如果小框置信度明显低于大框(差值 > 0.3),保留大框(高置信度的)
+                        elif small_score < large_score - 0.3:
+                            print(f"    → 决策: 小框置信度明显低于大框,保留大框 (元素 {large_idx}), 移除小框 (元素 {small_idx})")
+                            need_remove.add(small_idx)
+                            removal_history[small_idx].append({
+                                'reason': f'小框置信度明显低于大框 ({small_score:.3f} < {large_score:.3f} - 0.3)',
+                                'compared_with': large_idx,
+                                'overlap_ratio': overlap_ratio,
+                                'is_inside': is_inside,
+                                'branch': 'containment_table_body_vs_table_body'
+                            })
+                            if small_idx == i:
+                                break
+                            continue
+                        # 如果置信度接近(差值 <= 0.3),保留置信度高的
+                        else:
+                            if small_score >= large_score:
+                                print(f"    → 决策: 置信度接近,保留小框 (元素 {small_idx}, 置信度={small_score:.3f}), 移除大框 (元素 {large_idx})")
+                                need_remove.add(large_idx)
+                                removal_history[large_idx].append({
+                                    'reason': f'置信度接近,保留高置信度小框',
+                                    'compared_with': small_idx,
+                                    'overlap_ratio': overlap_ratio,
+                                    'is_inside': is_inside,
+                                    'branch': 'containment_table_body_vs_table_body'
+                                })
+                                if large_idx == i:
+                                    break
+                            else:
+                                print(f"    → 决策: 置信度接近,保留大框 (元素 {large_idx}, 置信度={large_score:.3f}), 移除小框 (元素 {small_idx})")
+                                need_remove.add(small_idx)
+                                removal_history[small_idx].append({
+                                    'reason': f'置信度接近,保留高置信度大框',
+                                    'compared_with': large_idx,
+                                    'overlap_ratio': overlap_ratio,
+                                    'is_inside': is_inside,
+                                    'branch': 'containment_table_body_vs_table_body'
+                                })
+                                if small_idx == i:
+                                    break
+                            continue
+                    
+                    # 非表格类别,使用原有逻辑:保留大框并扩展边界
+                    print(f"  → 非表格类别,保留大框 (元素 {large_idx}), 移除小框 (元素 {small_idx})")
+                    results[large_idx]['bbox'] = [
+                        min(small_bbox[0], large_bbox[0]),
+                        min(small_bbox[1], large_bbox[1]),
+                        max(small_bbox[2], large_bbox[2]),
+                        max(small_bbox[3], large_bbox[3])
+                    ]
+                    need_remove.add(small_idx)
+                    removal_history[small_idx].append({
+                        'reason': f'非表格类别,保留大框',
+                        'compared_with': large_idx,
+                        'overlap_ratio': overlap_ratio,
+                        'is_inside': is_inside,
+                        'branch': 'containment_other'
+                    })
+                    if small_idx == i:
+                        break  # i 被移除,跳出内层循环
+    
+    # 返回去重后的结果
+    final_results = [results[i] for i in range(len(results)) if i not in need_remove]
+    
+    print(f"\n{'='*80}")
+    print("处理完成")
+    print("="*80)
+    print(f"\n移除的元素数量: {len(need_remove)}")
+    print(f"最终元素数量: {len(final_results)}")
+    
+    print(f"\n{'='*80}")
+    print("移除历史")
+    print("="*80)
+    for idx in sorted(need_remove):
+        print(f"\n元素 {idx}: {_format_element(idx, results[idx])}")
+        for record in removal_history[idx]:
+            print(f"  原因: {record['reason']}")
+            print(f"  分支: {record['branch']}")
+    
+    print(f"\n{'='*80}")
+    print("最终结果")
+    print("="*80)
+    for i, elem in enumerate(final_results):
+        print(f"  {_format_element(i, elem)}")
+    
+    # 统计 table_body 的保留情况
+    original_table_bodies = [i for i, elem in enumerate(results) if elem.get('category') == 'table_body']
+    final_table_bodies = [elem for elem in final_results if elem.get('category') == 'table_body']
+    removed_table_bodies = [i for i in original_table_bodies if i in need_remove]
+    
+    print(f"\n{'='*80}")
+    print("table_body 统计")
+    print("="*80)
+    print(f"原始 table_body 数量: {len(original_table_bodies)}")
+    print(f"原始 table_body 索引: {original_table_bodies}")
+    print(f"被移除的 table_body 数量: {len(removed_table_bodies)}")
+    print(f"被移除的 table_body 索引: {removed_table_bodies}")
+    print(f"最终保留的 table_body 数量: {len(final_table_bodies)}")
+    
+    if removed_table_bodies:
+        print(f"\n被移除的 table_body 详情:")
+        for idx in removed_table_bodies:
+            print(f"\n  {_format_element(idx, results[idx])}")
+            for record in removal_history[idx]:
+                print(f"    原因: {record['reason']}")
+                print(f"    分支: {record['branch']}")
+    
+    return final_results
+
+
+def main():
+    """主函数:示例用法"""
+    # 示例数据(可以根据实际数据替换)
+    # 这里使用计划中提到的数据:4个table_body(置信度:0.883, 0.584, 0.381, 0.304)和1个abandon(置信度0.313)
+    # 注意:这里使用示例bbox,实际使用时需要替换为真实数据
+    
+    # 测试场景1:table_body 之间高度重叠
+    print("="*80)
+    print("测试场景1:table_body 之间高度重叠")
+    print("="*80)
+    layout_results = [
+        {'category': 'table_body', 'confidence': 0.883, 'bbox': [100, 100, 500, 400]},
+        {'category': 'table_body', 'confidence': 0.584, 'bbox': [120, 120, 480, 380]},
+        {'category': 'table_body', 'confidence': 0.381, 'bbox': [110, 110, 490, 390]},
+        {'category': 'table_body', 'confidence': 0.304, 'bbox': [130, 130, 470, 370]},
+        {'category': 'abandon', 'confidence': 0.313, 'bbox': [105, 105, 495, 395]},
+    ]
+    
+    coordinate_utils = CoordinateUtils()
+    
+    result = debug_remove_overlapping_boxes(
+        layout_results,
+        coordinate_utils,
+        iou_threshold=0.8,
+        overlap_ratio_threshold=0.8,
+        table_body_area_threshold=0.5,
+        table_body_confidence_threshold=0.1,
+        table_body_overlap_threshold=0.8
+    )
+    
+    # 测试场景2:table_body 与其他类型(text)重叠,table_body 置信度较低
+    print("\n\n" + "="*80)
+    print("测试场景2:table_body 与其他类型(text)重叠,table_body 置信度较低")
+    print("="*80)
+    layout_results2 = [
+        {'category': 'table_body', 'confidence': 0.5, 'bbox': [100, 100, 500, 400]},
+        {'category': 'text', 'confidence': 0.7, 'bbox': [110, 110, 490, 390]},  # 高度重叠
+    ]
+    
+    result2 = debug_remove_overlapping_boxes(
+        layout_results2,
+        coordinate_utils,
+        iou_threshold=0.8,
+        overlap_ratio_threshold=0.8,
+        table_body_area_threshold=0.5,
+        table_body_confidence_threshold=0.1,
+        table_body_overlap_threshold=0.8
+    )
+    
+    # 测试场景3:abandon 与其他类型低重叠
+    print("\n\n" + "="*80)
+    print("测试场景3:abandon 与其他类型低重叠(应该被移除)")
+    print("="*80)
+    layout_results3 = [
+        {'category': 'text', 'confidence': 0.8, 'bbox': [100, 100, 500, 400]},
+        {'category': 'abandon', 'confidence': 0.3, 'bbox': [200, 200, 400, 300]},  # 低重叠
+    ]
+    
+    result3 = debug_remove_overlapping_boxes(
+        layout_results3,
+        coordinate_utils,
+        iou_threshold=0.8,
+        overlap_ratio_threshold=0.8,
+        table_body_area_threshold=0.5,
+        table_body_confidence_threshold=0.1,
+        table_body_overlap_threshold=0.8
+    )
+    
+    coordinate_utils = CoordinateUtils()
+    
+    print("使用示例数据进行调试...")
+    print("注意:如果使用实际数据,请修改 layout_results 变量")
+    
+    result = debug_remove_overlapping_boxes(
+        layout_results,
+        coordinate_utils,
+        iou_threshold=0.8,
+        overlap_ratio_threshold=0.8,
+        table_body_area_threshold=0.5,
+        table_body_confidence_threshold=0.1,
+        table_body_overlap_threshold=0.8
+    )
+    
+    print(f"\n调试完成!")
+
+
+if __name__ == '__main__':
+    main()