8 Achegas ff828e2d08 ... 9c6ed0277d

Autor SHA1 Mensaxe Data
  zhch158_admin 9c6ed0277d feat: 增加边界框线宽以优化可视化效果 hai 6 días
  zhch158_admin 9f90a3c17f feat: 更新表格单元格匹配逻辑以实现简单的1:1映射 hai 6 días
  zhch158_admin 363e45a366 feat: 更新默认配置文件路径以指向新的PaddleOCR_VL和PaddleOCR JSON文件 hai 6 días
  zhch158_admin 1ff40aab1d feat: 更新合并逻辑以提取PaddleOCR的旋转角度和原始图像尺寸,增强数据处理能力 hai 6 días
  zhch158_admin 89bffd5fb5 feat: 更新合并逻辑以提取PaddleOCR的旋转角度和原始图像尺寸,增强数据处理能力 hai 6 días
  zhch158_admin 07f07a9842 feat: 更新合并逻辑以提取旋转角度和原始图像尺寸,增强数据处理能力 hai 6 días
  zhch158_admin 37baebf479 feat: 更新数据处理器以支持旋转角度和原始图像尺寸,增强表格和文本处理逻辑 hai 6 días
  zhch158_admin d04f16fd9c feat: 更新提取函数以返回旋转角度和原始图像尺寸,添加坐标旋转和反向旋转功能 hai 6 días

+ 202 - 7
merger/bbox_extractor.py

@@ -2,7 +2,7 @@
 bbox 提取模块
 负责从 PaddleOCR 结果中提取文字框信息
 """
-from typing import List, Dict
+from typing import List, Dict, Tuple
 import numpy as np
 from pathlib import Path
 
@@ -11,7 +11,7 @@ class BBoxExtractor:
     """bbox 提取器"""
     
     @staticmethod
-    def extract_paddle_text_boxes(paddle_data: Dict) -> List[Dict]:
+    def extract_paddle_text_boxes(paddle_data: Dict) -> Tuple[List[Dict], float, Tuple[int, int]]:
         """
         提取 PaddleOCR 的文字框信息
         
@@ -19,12 +19,14 @@ class BBoxExtractor:
             paddle_data: PaddleOCR 输出的数据
         
         Returns:
-            文字框列表(坐标已转换为 angle=0 时的坐标)
+            文字框列表(保持旋转后的angle角度)和旋转角度
         """
         text_boxes = []
+        rotation_angle = 0.0
+        orig_image_size = (0,0)
         
         if 'overall_ocr_res' not in paddle_data:
-            return text_boxes
+            return text_boxes, rotation_angle, orig_image_size
         
         ocr_res = paddle_data['overall_ocr_res']
         rec_texts = ocr_res.get('rec_texts', [])
@@ -33,9 +35,52 @@ class BBoxExtractor:
         
         # 🎯 获取旋转角度
         rotation_angle = BBoxExtractor._get_rotation_angle(paddle_data)
+        if rotation_angle != 0:
+            orig_image_size = BBoxExtractor._get_original_image_size(paddle_data)
+            print(f"🔄 检测到旋转角度: {rotation_angle}°")
+            print(f"📐 原始图像尺寸: {orig_image_size[0]} x {orig_image_size[1]}")
+                
+        for i, (text, poly, score) in enumerate(zip(rec_texts, rec_polys, rec_scores)):
+            if text and text.strip():
+                # 计算 bbox (x_min, y_min, x_max, y_max)
+                bbox = BBoxExtractor._poly_to_bbox(poly)
+                
+                text_boxes.append({
+                    'text': text,
+                    'bbox': bbox,
+                    'poly': poly,
+                    'score': score,
+                    'paddle_bbox_index': i,
+                    'used': False
+                })
         
-        # 🎯 如果有旋转,需要获取原始图像尺寸
-        orig_image_size = None
+        return text_boxes, rotation_angle, orig_image_size
+    
+    @staticmethod
+    def extract_paddle_text_boxes_inverse_rotate(paddle_data: Dict) -> Tuple[List[Dict], float, Tuple[int, int]]:
+        """
+        提取 PaddleOCR 的文字框信息
+        
+        Args:
+            paddle_data: PaddleOCR 输出的数据
+        
+        Returns:
+            文字框列表(坐标已转换为 angle=0 时的坐标)
+        """
+        text_boxes = []
+        rotation_angle = 0.0
+        orig_image_size = (0,0)
+        
+        if 'overall_ocr_res' not in paddle_data:
+            return text_boxes, rotation_angle, orig_image_size
+        
+        ocr_res = paddle_data['overall_ocr_res']
+        rec_texts = ocr_res.get('rec_texts', [])
+        rec_polys = ocr_res.get('rec_polys', [])
+        rec_scores = ocr_res.get('rec_scores', [])
+        
+        # 🎯 获取旋转角度
+        rotation_angle = BBoxExtractor._get_rotation_angle(paddle_data)
         
         if rotation_angle != 0:
             orig_image_size = BBoxExtractor._get_original_image_size(paddle_data)
@@ -62,7 +107,7 @@ class BBoxExtractor:
                     'used': False
                 })
         
-        return text_boxes
+        return text_boxes, rotation_angle, orig_image_size
     
     @staticmethod
     def _get_rotation_angle(paddle_data: Dict) -> float:
@@ -135,6 +180,53 @@ class BBoxExtractor:
         return (2480, 3508)
     
     @staticmethod
+    def rotate_box_coordinates(bbox: List[float], 
+                             angle: float,
+                             orig_image_size: tuple) -> List[float]:
+        """
+        旋转 bbox 坐标(与图像旋转保持一致)
+        
+        参考 ocr_validator_utils.rotate_image_and_coordinates 的操作
+        
+        旋转逻辑:
+        - 0°: 不旋转
+        - 90°: 逆时针旋转 90°
+        - 180°: 旋转 180°
+        - 270°: 顺时针旋转 90°(或逆时针 270°)
+        
+        Args:
+            bbox: 原图像上的边界框 [x_min, y_min, x_max, y_max]
+            angle: 旋转角度(0, 90, 180, 270)
+            orig_image_size: 原始图像尺寸 (width, height)
+        """
+        poly = BBoxExtractor._bbox_to_poly(bbox)
+        rotated_poly = BBoxExtractor._rotate_coordinates(poly, angle, orig_image_size)
+        rotated_bbox = BBoxExtractor._poly_to_bbox(rotated_poly)
+        return rotated_bbox
+
+    @staticmethod
+    def inverse_rotate_box_coordinates(bbox: List[float], 
+                                    angle: float,
+                                    orig_image_size: tuple) -> List[float]:
+        """
+        反向旋转 bbox 坐标
+        
+        参考 ocr_validator_utils.rotate_image_and_coordinates 的逆操作
+        
+        PaddleOCR 在旋转后的图像上识别,坐标是旋转后的
+        我们需要将坐标转换回原始图像(未旋转)
+        
+        Args:
+            bbox: 旋转后图像上的边界框 [x_min, y_min, x_max, y_max]
+            angle: 旋转角度(度数,PaddleX 使用的角度)
+            orig_image_size: 原始图像尺寸 (width, height)
+        """
+        poly = BBoxExtractor._bbox_to_poly(bbox)
+        inverse_poly = BBoxExtractor._inverse_rotate_coordinates(poly, angle, orig_image_size)
+        inverse_bbox = BBoxExtractor._poly_to_bbox(inverse_poly)
+        return inverse_bbox
+
+    @staticmethod
     def _inverse_rotate_coordinates(poly: List[List[float]], 
                                     angle: float,
                                     orig_image_size: tuple) -> List[List[float]]:
@@ -207,6 +299,109 @@ class BBoxExtractor:
         return inverse_poly
     
     @staticmethod
+    def _rotate_coordinates(poly: List[List[float]], 
+                        angle: float,
+                        orig_image_size: tuple) -> List[List[float]]:
+        """
+        旋转多边形坐标(与图像旋转保持一致)
+        
+        参考 ocr_validator_utils.rotate_image_and_coordinates 的操作
+        
+        旋转逻辑:
+        - 0°: 不旋转
+        - 90°: 逆时针旋转 90°
+        - 180°: 旋转 180°
+        - 270°: 顺时针旋转 90°(或逆时针 270°)
+        
+        Args:
+            poly: 原图像上的多边形坐标 [[x', y'], ...]
+            angle: 旋转角度(0, 90, 180, 270)
+            orig_image_size: 原始图像尺寸 (width, height)
+        
+        Returns:
+            旋转后的多边形坐标 [[x, y], ...]
+        
+        Example:
+            >>> poly = [[100, 200], [150, 200], [150, 250], [100, 250]]
+            >>> rotated = rotate_coordinates(poly, 90, (1000, 800))
+            >>> print(rotated)
+            [[200, 900], [200, 850], [250, 850], [250, 900]]
+        """
+        if not poly or angle == 0:
+            return poly
+        
+        orig_width, orig_height = orig_image_size
+        rotated_poly = []
+        
+        for point in poly:
+            x, y = point[0], point[1]
+            
+            if angle == 90:
+                # 逆时针旋转 90°
+                # 新坐标系: 宽度=原高度, 高度=原宽度
+                # x_new = y_old
+                # y_new = 原宽度 - x_old
+                new_x = y
+                new_y = orig_width - x
+                
+            elif angle == 180:
+                # 旋转 180°
+                # 新坐标系: 宽度=原宽度, 高度=原高度
+                # x_new = 原宽度 - x_old
+                # y_new = 原高度 - y_old
+                new_x = orig_width - x
+                new_y = orig_height - y
+                
+            elif angle == 270:
+                # 顺时针旋转 90°(或逆时针 270°)
+                # 新坐标系: 宽度=原高度, 高度=原宽度
+                # x_new = 原高度 - y_old
+                # y_new = x_old
+                new_x = orig_height - y
+                new_y = x
+                
+            else:
+                # 不支持的角度,保持原坐标
+                new_x, new_y = x, y
+            
+            rotated_poly.append([new_x, new_y])
+        
+        return rotated_poly
+
+    @staticmethod
+    def _bbox_to_poly(bbox: List[float]) -> List[List[float]]:
+        """
+        将 bbox 转换为多边形(4个角点,逆时针顺序)
+        
+        Args:
+            bbox: 边界框 [x_min, y_min, x_max, y_max]
+        
+        Returns:
+            多边形坐标 [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]
+            顺序:左上 -> 右上 -> 右下 -> 左下(逆时针)
+        
+        Example:
+            >>> bbox = [100, 200, 150, 250]
+            >>> poly = BBoxExtractor._bbox_to_poly(bbox)
+            >>> print(poly)
+            [[100, 200], [150, 200], [150, 250], [100, 250]]
+        """
+        if not bbox or len(bbox) < 4:
+            return []
+        
+        x_min, y_min, x_max, y_max = bbox[:4]
+        
+        # 🎯 4个角点(逆时针顺序)
+        poly = [
+            [x_min, y_min],  # 左上角
+            [x_max, y_min],  # 右上角
+            [x_max, y_max],  # 右下角
+            [x_min, y_max]   # 左下角
+        ]
+        
+        return poly
+
+    @staticmethod
     def _poly_to_bbox(poly: List[List[float]]) -> List[float]:
         """将多边形转换为 bbox [x_min, y_min, x_max, y_max]"""
         xs = [p[0] for p in poly]

+ 82 - 98
merger/data_processor.py

@@ -17,6 +17,19 @@ except ImportError:
 
 class DataProcessor:
     """数据处理器"""
+    """_summary_
+    1.负责处理 MinerU/PaddleOCR_VL/DotsOCR 数据,添加 table_cells bbox 信息, 其他类型的bbox信息依然使用vl自带的bbox
+    2.由于不同OCR工具的输出格式不同,DataProcessor 需要包含多个处理方法,分别处理 MinerU、DotsOCR 和 PaddleOCR_VL 数据, 都先转换成mineru格式再添加table cells bbox信息
+    3.使用 TextMatcher 进行文本匹配,TableCellMatcher 进行表单元格匹配
+    4.最终输出统一的 MinerU 格式数据
+    
+    由于VL模型minerU,dotsocr坐标都是使用的原图坐标,不是旋转后的坐标,PaddleVL使用的时旋转转换后的坐标,而ppstructure使用的ocr文本块是旋转后的坐标,
+    因此在处理VL数据时,
+    1.首先需要根据ppstructure的旋转角度和原图尺寸,将VL的table坐标转换为旋转后的坐标
+    2.通过TableCellMatcher 进行表单元格匹配
+    3.再将匹配到的单元格bbox逆向转换为原图坐标,存储在最终输出的MinerU格式数据中
+    4.其他类型的bbox信息依然使用vl自带的bbox
+    """
     
     def __init__(self, text_matcher: TextMatcher, look_ahead_window: int = 10, x_tolerance: int = 3, y_tolerance: int = 10):
         """
@@ -39,7 +52,7 @@ class DataProcessor:
         )
     
     def process_mineru_data(self, mineru_data: List[Dict], 
-                           paddle_text_boxes: List[Dict]) -> List[Dict]:
+                           paddle_text_boxes: List[Dict], rotation_angle: float, orig_image_size: Tuple[int, int]) -> List[Dict]:
         """
         处理 MinerU 数据,添加 bbox 信息
         
@@ -64,12 +77,27 @@ class DataProcessor:
             item_type = item.get('type', '')
             
             if item_type == 'table':
+                if rotation_angle != 0:
+                    inverse_table_bbox = BBoxExtractor.rotate_box_coordinates(item['bbox'], rotation_angle, orig_image_size)  
+                    inverse_item = item.copy()
+                    inverse_item['bbox'] = inverse_table_bbox
+                else:
+                    inverse_item = item
                 merged_item, paddle_pointer = self._process_table(
-                    item, paddle_text_boxes, paddle_pointer
+                    inverse_item, paddle_text_boxes, paddle_pointer
                 )
+                # 如果有旋转,需要将匹配到的单元格bbox逆向转换为原图坐标
+                if rotation_angle != 0:
+                    for cell in merged_item.get('table_cells', []):
+                        cell_bbox = cell.get('bbox', [])
+                        if cell_bbox:
+                            original_bbox = BBoxExtractor.inverse_rotate_box_coordinates(cell_bbox, rotation_angle, orig_image_size)
+                            cell['bbox'] = original_bbox
+                    merged_item['bbox'] = item['bbox']  # 保持表格的原始bbox不变        
+                            
                 merged_data.append(merged_item)
             
-            elif item_type in ['text', 'title']:
+            elif item_type in ['text', 'title', 'header', 'footer']:
                 merged_item, paddle_pointer, last_matched_index = self._process_text(
                     item, paddle_text_boxes, paddle_pointer, last_matched_index
                 )
@@ -87,57 +115,44 @@ class DataProcessor:
         return merged_data
     
     def process_dotsocr_data(self, dotsocr_data: List[Dict],
-                            paddle_text_boxes: List[Dict]) -> List[Dict]:
+                            paddle_text_boxes: List[Dict], 
+                            rotation_angle: float, 
+                            orig_image_size: Tuple[int, int]) -> List[Dict]:
         """
-        🎯 处理 DotsOCR 数据,转换为 MinerU 格式并添加 bbox 信息
+        处理 DotsOCR 数据(简化版:转换后复用 MinerU 处理逻辑)
         
         Args:
-            dotsocr_data: DotsOCR 数据
-            paddle_text_boxes: PaddleOCR 文字框列表
+            dotsocr_data: DotsOCR 输出数据
+            paddle_text_boxes: PaddleOCR 文本框
+            rotation_angle: 旋转角度
+            orig_image_size: 原始图片尺寸
         
         Returns:
-            MinerU 格式的合并数据
+            统一的 MinerU 格式数据(带 table_cells bbox)
         """
-        merged_data = []
-        paddle_pointer = 0
-        last_matched_index = 0
-        
-        # 按 bbox 排序
-        dotsocr_data.sort(
-            key=lambda x: (x['bbox'][1], x['bbox'][0])
-            if 'bbox' in x else (float('inf'), float('inf'))
-        )
+        print(f"📊 处理 DotsOCR 数据: {len(dotsocr_data)} 个块")
         
+        # 🎯 第一步:转换为 MinerU 格式
+        mineru_format_data = []
         for item in dotsocr_data:
-            # 🎯 转换为 MinerU 格式
-            mineru_item = self._convert_dotsocr_to_mineru(item)
-            category = mineru_item.get('type', '')
-            
-            # 🎯 根据类型处理
-            if category.lower() == 'table':
-                merged_item, paddle_pointer = self._process_table(
-                    mineru_item, paddle_text_boxes, paddle_pointer
-                )
-                merged_data.append(merged_item)
-            
-            elif category.lower() in ['text', 'title', 'header', 'footer']:
-                merged_item, paddle_pointer, last_matched_index = self._process_text(
-                    mineru_item, paddle_text_boxes, paddle_pointer, last_matched_index
-                )
-                merged_data.append(merged_item)
-            
-            elif category.lower() == 'list':
-                merged_item, paddle_pointer, last_matched_index = self._process_list(
-                    mineru_item, paddle_text_boxes, paddle_pointer, last_matched_index
-                )
-                merged_data.append(merged_item)
-            
-            else:
-                # Page-header, Page-footer, Picture 等
-                merged_data.append(mineru_item)
-        
-        return merged_data
-    
+            try:
+                converted_item = self._convert_dotsocr_to_mineru(item)
+                if converted_item:
+                    mineru_format_data.append(converted_item)
+            except Exception as e:
+                print(f"⚠️ DotsOCR 转换失败: {e}")
+                continue
+        
+        print(f"   ✓ 转换完成: {len(mineru_format_data)} 个块")
+        
+        # 🎯 第二步:复用 MinerU 处理逻辑
+        return self.process_mineru_data(
+            mineru_data=mineru_format_data,
+            paddle_text_boxes=paddle_text_boxes,
+            rotation_angle=rotation_angle,
+            orig_image_size=orig_image_size
+        )
+
     def _convert_dotsocr_to_mineru(self, dotsocr_item: Dict) -> Dict:
         """
         🎯 将 DotsOCR 格式转换为 MinerU 格式
@@ -199,7 +214,7 @@ class DataProcessor:
         return mineru_item
     
     def process_paddleocr_vl_data(self, paddleocr_vl_data: Dict,
-                                  paddle_text_boxes: List[Dict]) -> List[Dict]:
+                                  paddle_text_boxes: List[Dict], rotation_angle: float, orig_image_size: Tuple[int, int]) -> List[Dict]:
         """
         处理 PaddleOCR_VL 数据,添加 bbox 信息
         
@@ -216,12 +231,12 @@ class DataProcessor:
         
         # 🎯 获取旋转角度和原始图像尺寸
         rotation_angle = self._get_rotation_angle_from_vl(paddleocr_vl_data)
-        orig_image_size = None
+        vl_orig_image_size = None
         
         if rotation_angle != 0:
-            orig_image_size = self._get_original_image_size_from_vl(paddleocr_vl_data)
+            vl_orig_image_size = self._get_original_image_size_from_vl(paddleocr_vl_data)
             print(f"🔄 PaddleOCR_VL 检测到旋转角度: {rotation_angle}°")
-            print(f"📐 原始图像尺寸: {orig_image_size[0]} x {orig_image_size[1]}")
+            print(f"📐 原始图像尺寸: {vl_orig_image_size[0]} x {vl_orig_image_size[1]}")
         
         # 提取 parsing_res_list
         parsing_res_list = paddleocr_vl_data.get('parsing_res_list', [])
@@ -231,40 +246,26 @@ class DataProcessor:
             key=lambda x: (x['block_bbox'][1], x['block_bbox'][0])
             if 'block_bbox' in x else (float('inf'), float('inf'))
         )
-        
+        mineru_format_data = []
+    
         for item in parsing_res_list:
             # 🎯 先转换 bbox 坐标(如果需要)
             if rotation_angle != 0 and orig_image_size:
                 item = self._transform_vl_block_bbox(item, rotation_angle, orig_image_size)
-            
-            # 🎯 统一转换为 MinerU 格式
-            mineru_item = self._convert_paddleocr_vl_to_mineru(item)
-            item_type = mineru_item.get('type', '')
-            
-            # 🎯 根据类型处理(复用 MinerU 的通用方法)
-            if item_type == 'table':
-                merged_item, paddle_pointer = self._process_table(
-                    mineru_item, paddle_text_boxes, paddle_pointer
-                )
-                merged_data.append(merged_item)
-            
-            elif item_type in ['text', 'title', 'header', 'footer', 'equation']:
-                merged_item, paddle_pointer, last_matched_index = self._process_text(
-                    mineru_item, paddle_text_boxes, paddle_pointer, last_matched_index
-                )
-                merged_data.append(merged_item)
-            
-            elif item_type == 'list':
-                merged_item, paddle_pointer, last_matched_index = self._process_list(
-                    mineru_item, paddle_text_boxes, paddle_pointer, last_matched_index
-                )
-                merged_data.append(merged_item)
-            
-            else:
-                # 其他类型(image 等)直接添加
-                merged_data.append(mineru_item)
-        
-        return merged_data
+            converted_item = self._convert_paddleocr_vl_to_mineru(item)
+            if converted_item:
+                mineru_format_data.append(converted_item)
+    
+        print(f"   ✓ 转换完成: {len(mineru_format_data)} 个块")
+        
+        # 🎯 第三步:复用 MinerU 处理逻辑
+        return self.process_mineru_data(
+            mineru_data=mineru_format_data,
+            paddle_text_boxes=paddle_text_boxes,
+            rotation_angle=rotation_angle,
+            orig_image_size=orig_image_size
+        )    
+
     
     def _get_rotation_angle_from_vl(self, paddleocr_vl_data: Dict) -> float:
         """从 PaddleOCR_VL 数据中获取旋转角度"""
@@ -296,24 +297,7 @@ class DataProcessor:
         if len(block_bbox) < 4:
             return transformed_item
         
-        # block_bbox 格式: [x1, y1, x2, y2]
-        # 转换为 poly 格式进行旋转
-        poly = [
-            [block_bbox[0], block_bbox[1]],  # 左上
-            [block_bbox[2], block_bbox[1]],  # 右上
-            [block_bbox[2], block_bbox[3]],  # 右下
-            [block_bbox[0], block_bbox[3]]   # 左下
-        ]
-        
-        # 🎯 使用 BBoxExtractor 的坐标转换方法
-        transformed_poly = BBoxExtractor._inverse_rotate_coordinates(
-            poly, angle, orig_image_size
-        )
-        
-        # 转换回 bbox 格式
-        xs = [p[0] for p in transformed_poly]
-        ys = [p[1] for p in transformed_poly]
-        transformed_bbox = [min(xs), min(ys), max(xs), max(ys)]
+        transformed_bbox = BBoxExtractor.inverse_rotate_box_coordinates(block_bbox, angle, orig_image_size)
         
         transformed_item['block_bbox'] = transformed_bbox
         

+ 2 - 2
merger/dotsocr_merger.py

@@ -59,11 +59,11 @@ class DotsOCRMerger:
             paddle_data = json.load(f)
         
         # 🎯 提取 PaddleOCR 的文字框信息
-        paddle_text_boxes = self.bbox_extractor.extract_paddle_text_boxes(paddle_data)
+        paddle_text_boxes, rotation_angle, orig_image_size = self.bbox_extractor.extract_paddle_text_boxes(paddle_data)
         
         # 🎯 使用专门的 DotsOCR 处理方法(自动转换为 MinerU 格式)
         merged_data = self.data_processor.process_dotsocr_data(
-            dotsocr_data, paddle_text_boxes
+            dotsocr_data, paddle_text_boxes, rotation_angle, orig_image_size
         )
         
         return merged_data

+ 2 - 2
merger/merge_paddleocr_vl_paddleocr.py

@@ -285,8 +285,8 @@ if __name__ == "__main__":
     if len(sys.argv) == 1:
         # 默认配置
         default_config = {
-            "paddleocr-vl-file": "/Users/zhch158/workspace/data/流水分析/2023年度报告母公司/paddleocr_vl_results/2023年度报告母公司_page_005.json",
-            "paddle-file": "/Users/zhch158/workspace/data/流水分析/2023年度报告母公司/ppstructurev3_client_results/2023年度报告母公司_page_005.json",
+            "paddleocr-vl-file": "/Users/zhch158/workspace/data/流水分析/2023年度报告母公司/paddleocr_vl_results/2023年度报告母公司_page_003.json",
+            "paddle-file": "/Users/zhch158/workspace/data/流水分析/2023年度报告母公司/ppstructurev3_client_results/2023年度报告母公司_page_003.json",
             "output-dir": "/Users/zhch158/workspace/data/流水分析/2023年度报告母公司/paddleocr_vl_results_cell_bbox",
             "output-type": "both",
             "window": "15",

+ 2 - 2
merger/merger_core.py

@@ -55,11 +55,11 @@ class MinerUPaddleOCRMerger:
             paddle_data = json.load(f)
         
         # 提取 PaddleOCR 的文字框信息
-        paddle_text_boxes = self.bbox_extractor.extract_paddle_text_boxes(paddle_data)
+        paddle_text_boxes, rotation_angle, orig_image_size = self.bbox_extractor.extract_paddle_text_boxes(paddle_data)
         
         # 处理 MinerU 的数据
         merged_data = self.data_processor.process_mineru_data(
-            mineru_data, paddle_text_boxes
+            mineru_data, paddle_text_boxes, rotation_angle, orig_image_size
         )
         
         return merged_data

+ 2 - 2
merger/paddleocr_vl_merger.py

@@ -59,11 +59,11 @@ class PaddleOCRVLMerger:
             paddle_data = json.load(f)
         
         # 提取 PaddleOCR 的文字框信息
-        paddle_text_boxes = self.bbox_extractor.extract_paddle_text_boxes(paddle_data)
+        paddle_text_boxes, rotation_angle, orig_image_size = self.bbox_extractor.extract_paddle_text_boxes(paddle_data)
         
         # 处理 PaddleOCR_VL 的数据, merge后已是minerU json格式
         merged_data = self.data_processor.process_paddleocr_vl_data(
-            paddleocr_vl_data, paddle_text_boxes
+            paddleocr_vl_data, paddle_text_boxes, rotation_angle, orig_image_size
         )
         
         # 不用再转换,

+ 1 - 1
merger/table_cell_matcher.py

@@ -571,7 +571,7 @@ class TableCellMatcher:
         # 🎯 策略 1: 数量相等,简单 1:1 映射
         if len(html_rows) == len(grouped_boxes):
             for i in range(len(html_rows)):
-                mapping[i] = grouped_boxes[i]
+                mapping[i] = [i]
             return mapping
         
         # 🎯 策略 2: 第一遍 - 基于内容精确匹配(使用预处理后的组)

+ 4 - 4
ocr_validator_layout.py

@@ -666,7 +666,7 @@ class OCRLayoutManager:
     def _add_bboxes_to_plot_batch(self, fig: go.Figure, bboxes: List[List[int]], 
                                 image_height: int, 
                                 line_color: str = "blue", 
-                                line_width: int = 1, 
+                                line_width: int = 2, 
                                 fill_color: str = "rgba(0, 100, 200, 0.2)"):
         """
         批量添加边界框(性能优化版)
@@ -702,7 +702,7 @@ class OCRLayoutManager:
     def _add_bboxes_as_scatter(self, fig: go.Figure, bboxes: List[List[int]], 
                           image_height: int,
                           line_color: str = "blue", 
-                          line_width: int = 1,
+                          line_width: int = 2,
                           name: str = "boxes"):
         """
         使用 Scatter 绘制边界框(极致性能优化)
@@ -766,7 +766,7 @@ class OCRLayoutManager:
                 bboxes=all_boxes,
                 image_height=image.height,
                 line_color="rgba(0, 100, 200, 0.8)",
-                line_width=1,
+                line_width=2,
                 name="all_boxes"
             )
 
@@ -777,7 +777,7 @@ class OCRLayoutManager:
                 bboxes=selected_boxes,
                 image_height=image.height,
                 line_color="red",
-                line_width=1,
+                line_width=2,
                 fill_color="rgba(255, 0, 0, 0.3)"
             )