8 Commits 58d9568b0f ... c11f2ea045

Autore SHA1 Messaggio Data
  zhch158_admin c11f2ea045 feat: 添加 detect_page_type 方法以检测 PDF 页面类型(文本或扫描OCR) 6 giorni fa
  zhch158_admin f2079d9e4f feat: 添加 detect_page_type 函数以检测 PDF 页面的类型(文字页或图片页) 6 giorni fa
  zhch158_admin ca0374db5f feat: 添加 pdf_type 参数以支持不同的 PDF 处理模式,优化识别逻辑 6 giorni fa
  zhch158_admin 54f5b5943d fix: 将日志级别从 info 更改为 debug,以减少输出冗余 6 giorni fa
  zhch158_admin 9d7afeff31 feat: 添加 pdf_type 参数以支持不同的 OCR 模式,优化二次 OCR 逻辑 6 giorni fa
  zhch158_admin bfd018969b fix: 将日志级别从 info 更改为 debug,以减少输出冗余 6 giorni fa
  zhch158_admin e4304a8c0e feat: 增强 PDF 文本提取逻辑,添加页级别类型检测,优化 OCR 使用条件 6 giorni fa
  zhch158_admin d68f33b382 feat: 添加 PDF 类型检测功能,优化文档处理逻辑,支持文本和 OCR 页面的识别 6 giorni fa

+ 2 - 0
ocr_tools/universal_doc_parser/core/element_processors.py

@@ -326,6 +326,7 @@ class ElementProcessors:
         layout_item: Dict[str, Any],
         scale: float,
         pre_matched_spans: Optional[List[Dict[str, Any]]] = None,
+        pdf_type: str = 'ocr', # 'ocr' 或 'txt'
         output_dir: Optional[str] = None,
         basename: Optional[str] = None
     ) -> Dict[str, Any]:
@@ -376,6 +377,7 @@ class ElementProcessors:
                 table_image=cropped_table,
                 # ocr_boxes=ocr_boxes_for_wired,
                 ocr_boxes=ocr_boxes,
+                pdf_type=pdf_type,
                 debug_options=debug_opts_override
             )
             

+ 31 - 18
ocr_tools/universal_doc_parser/core/pipeline_manager_v2.py

@@ -272,7 +272,7 @@ class EnhancedDocPipeline:
             self,
             image_dict: Dict[str, Any],
             page_idx: int,
-            pdf_type: str,
+            pdf_type: str, # 'ocr' 或 'txt'
             pdf_doc: Optional[Any] = None,
             page_name: Optional[str] = None,
             output_dir: Optional[str] = None
@@ -362,25 +362,35 @@ class EnhancedDocPipeline:
         all_text_spans = []
         all_ocr_spans = []
         should_run_ocr = True
+        actual_page_type = None
         
         if pdf_type == 'txt' and pdf_doc is not None:
             # 文字 PDF:直接从 PDF 提取文本块
             try:
-                pdf_text_blocks, rotation_angle = PDFUtils.extract_all_text_blocks(
-                    pdf_doc, page_idx, scale=scale
-                )
-                # 保存rotation角度
-                page_result['angle'] = rotation_angle
-                if rotation_angle != 0:
-                    logger.info(f"📐 Page {page_idx}: PDF rotation {rotation_angle}°")
+                # 页级别检测:该页是否真的有文字
+                actual_page_type = PDFUtils.detect_page_type(pdf_doc, page_idx)
+                
+                if actual_page_type == 'txt':
+                    # 正常提取文字
+                    all_text_spans, rotation = PDFUtils.extract_all_text_blocks(
+                        pdf_doc, page_idx, scale=scale
+                    )
+                    # 保存rotation角度
+                    page_result['angle'] = rotation
+                    if rotation != 0:
+                        logger.info(f"📐 Page {page_idx}: PDF rotation {rotation}°")
 
-                # 将 PDF 文本块转换为 span 格式
-                all_text_spans = self._convert_pdf_blocks_to_spans(
-                    pdf_text_blocks, detection_image.shape
-                )
-                logger.info(f"📝 Page {page_idx}: PDF extracted {len(all_text_spans)} text blocks")
-                if self.debug_mode:
-                    # 调试模式:同时运行 OCR 对比
+                    # 将 PDF 文本块转换为 span 格式
+                    all_text_spans = self._convert_pdf_blocks_to_spans(
+                        all_text_spans, detection_image.shape
+                    )
+                    logger.info(f"📝 Page {page_idx}: PDF extracted {len(all_text_spans)} text blocks")
+                    if self.debug_mode:
+                        # 调试模式:同时运行 OCR 对比
+                        should_run_ocr = True
+                else:
+                    # 该页是扫描页,使用OCR
+                    logger.info(f"Page {page_idx}: detected as scanned page, using OCR")
                     should_run_ocr = True
             except Exception as e:
                 logger.warning(f"⚠️ PDF text extraction failed, fallback to OCR: {e}")
@@ -405,7 +415,10 @@ class EnhancedDocPipeline:
                 self._compare_ocr_and_pdf_text(
                     page_idx, pdf_doc, all_ocr_spans, detection_image, output_dir, page_name, scale
                 )
-        
+        # 根据实际情况决定使用 OCR 结果还是 PDF 提取结果
+        if actual_page_type and actual_page_type == 'ocr':
+            pdf_type = 'ocr'
+            
         if pdf_type == 'ocr':
             all_text_spans = all_ocr_spans
             
@@ -683,7 +696,7 @@ class EnhancedDocPipeline:
         self,
         detection_image: np.ndarray,
         classified_elements: Dict[str, List[Dict[str, Any]]],
-        pdf_type: str,
+        pdf_type: str,  # 'ocr' 或 'txt'
         pdf_doc: Optional[Any],
         page_idx: int,
         scale: float,
@@ -778,7 +791,7 @@ class EnhancedDocPipeline:
                     # 有线表格路径:UNet 识别
                     logger.info(f"🔷 Using wired UNet table recognition (configured)")
                     element = self.element_processors.process_table_element_wired(
-                        detection_image, item, scale, pre_matched_spans=spans,
+                        detection_image, item, scale, pre_matched_spans=spans, pdf_type=pdf_type,
                         output_dir=output_dir, basename=f"{basename}_{idx}"
                     )
                     

+ 4 - 1
ocr_tools/universal_doc_parser/models/adapters/mineru_wired_table.py

@@ -181,6 +181,7 @@ class MinerUWiredTableRecognizer:
         self,
         table_image: np.ndarray,
         ocr_boxes: List[Dict[str, Any]],
+        pdf_type: str = 'ocr', # 'ocr' 或 'txt'
         debug_options: Optional[Dict[str, Any]] = None,
     ) -> Dict[str, Any]:
         """
@@ -387,6 +388,7 @@ class MinerUWiredTableRecognizer:
                 texts = self.text_filler.second_pass_ocr_fill(
                     table_image, bboxes_merged, texts, scores, 
                     need_reocr_indices=need_reocr_indices,
+                    pdf_type=pdf_type,
                     force_all=False,  # Force Per-Cell OCR
                     output_dir=output_dir
                 )
@@ -454,6 +456,7 @@ class MinerUWiredTableRecognizer:
         self,
         table_image: np.ndarray,
         ocr_boxes: List[Dict[str, Any]],
+        pdf_type: str = 'ocr',
         debug_options: Optional[Dict[str, Any]] = None,
     ) -> Dict[str, Any]:
         """
@@ -470,7 +473,7 @@ class MinerUWiredTableRecognizer:
                     self.config, 
                     override=debug_options or self.debug_options.__dict__
                 )
-                return self.recognize_v4(table_image, ocr_boxes, debug_options=merged_debug_opts.__dict__)
+                return self.recognize_v4(table_image, ocr_boxes, pdf_type=pdf_type, debug_options=merged_debug_opts.__dict__)
             except Exception:
                 # 回退
                 return self.recognize_legacy(table_image, ocr_boxes)

+ 6 - 6
ocr_tools/universal_doc_parser/models/adapters/wired_table/grid_recovery.py

@@ -488,7 +488,7 @@ class GridRecovery:
             coverage_h = max_y - min_y
             expected_h = orig_h if orig_h else h / upscale
             
-            logger.info(
+            logger.debug(
                 f"📏 单元格Y轴覆盖验证:\n"
                 f"  - 最小Y: {min_y:.1f}\n"
                 f"  - 最大Y: {max_y:.1f}\n"
@@ -533,14 +533,14 @@ class GridRecovery:
                 name = f"{debug_prefix}_coordinate_verification_mask.png" if debug_prefix else "coordinate_verification_mask.png"
                 path = os.path.join(debug_dir, name)
                 cv2.imwrite(path, vis_mask)
-                logger.info(f"保存坐标验证图像(上采样mask): {path}")
+                logger.debug(f"保存坐标验证图像(上采样mask): {path}")
                 
             except Exception as e:
                 logger.warning(f"保存坐标验证图像失败: {e}")
         
         # 详细的坐标转换调试日志
         if len(bboxes) > 0:
-            logger.info(
+            logger.debug(
                 f"🔍 坐标转换验证:\n"
                 f"  - mask尺寸: [{h}, {w}]\n"
                 f"  - 原图尺寸: [{orig_h}, {orig_w}]\n"
@@ -553,7 +553,7 @@ class GridRecovery:
             sample_indices = [0, 1, 2] + [len(bboxes) - 3, len(bboxes) - 2, len(bboxes) - 1]
             sample_indices = [i for i in sample_indices if 0 <= i < len(bboxes)]
             
-            logger.info("🔍 样本单元格坐标转换详情:")
+            logger.debug("🔍 样本单元格坐标转换详情:")
             for idx in sample_indices:
                 bbox_orig = bboxes[idx]
                 # 反推上采样坐标(用于验证)
@@ -562,7 +562,7 @@ class GridRecovery:
                 w_up = (bbox_orig[2] - bbox_orig[0]) * scale_w
                 h_up = (bbox_orig[3] - bbox_orig[1]) * scale_h
                 
-                logger.info(
+                logger.debug(
                     f"  单元格 {idx}: 原图坐标 [{bbox_orig[0]:.1f}, {bbox_orig[1]:.1f}, "
                     f"{bbox_orig[2]:.1f}, {bbox_orig[3]:.1f}] "
                     f"(尺寸: {bbox_orig[2]-bbox_orig[0]:.1f}x{bbox_orig[3]-bbox_orig[1]:.1f}) "
@@ -576,7 +576,7 @@ class GridRecovery:
                 last_y = bboxes[-1][3]
                 expected_height = last_y - first_y
                 actual_image_height = orig_h if orig_h else h / upscale
-                logger.info(
+                logger.debug(
                     f"🔍 系统性偏移检查:\n"
                     f"  - 第一个单元格y1: {first_y:.1f}\n"
                     f"  - 最后一个单元格y2: {last_y:.1f}\n"

+ 8 - 2
ocr_tools/universal_doc_parser/models/adapters/wired_table/text_filling.py

@@ -360,6 +360,7 @@ class TextFiller:
         texts: List[str],
         scores: Optional[List[float]] = None,
         need_reocr_indices: Optional[List[int]] = None,
+        pdf_type: str = 'ocr',  # 'ocr' 或 'txt'
         force_all: bool = False,
         output_dir: Optional[str] = None,
     ) -> List[str]:
@@ -378,6 +379,7 @@ class TextFiller:
             texts: 当前文本列表
             scores: 当前置信度列表
             need_reocr_indices: 需要二次 OCR 的单元格索引列表(OCR 误合并检测结果)
+            pdf_type: str,  # 'ocr' 或 'txt'
             force_all: 是否强制对所有单元格进行 OCR (Default: False)
             output_dir: 输出目录,如果提供则保存单元格OCR图片到 {output_dir}/tablecell_ocr/ 目录
         """
@@ -424,8 +426,12 @@ class TextFiller:
                 else:
                     # 1. 文本为空且置信度不是极高
                     if (not t or not t.strip()) and scores[i] < 0.95:
-                        need_reocr = True
-                        reocr_reason = "空文本"
+                        if pdf_type == 'txt':
+                            # PDF文本模式下,空文本不触发二次OCR
+                            need_reocr = False
+                        else:
+                            need_reocr = True
+                            reocr_reason = "空文本"
                     # 2. 置信度过低
                     elif scores[i] < trigger_score_thresh:
                         need_reocr = True

+ 4 - 4
ocr_tools/universal_doc_parser/models/adapters/wired_table/visualization.py

@@ -40,7 +40,7 @@ class WiredTableVisualizer:
         vis_img[vpred > 128] = [255, 0, 0]  # 蓝色竖线
         
         cv2.imwrite(output_path, vis_img)
-        logger.info(f"表格线可视化: {output_path}")
+        logger.debug(f"表格线可视化: {output_path}")
         
         return vis_img
     
@@ -112,7 +112,7 @@ class WiredTableVisualizer:
                 )
 
         cv2.imwrite(output_path, vis)
-        logger.info(f"连通域可视化: {output_path} (共 {len(bboxes)} 个单元格)")
+        logger.debug(f"连通域可视化: {output_path} (共 {len(bboxes)} 个单元格)")
     
     @staticmethod
     def visualize_grid_structure(
@@ -157,7 +157,7 @@ class WiredTableVisualizer:
             cv2.putText(vis, info, (tx, ty), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 255, 255), thickness)
             
         cv2.imwrite(output_path, vis)
-        logger.info(f"表格结构可视化: {output_path}")
+        logger.debug(f"表格结构可视化: {output_path}")
     
     @staticmethod
     def visualize_with_text(
@@ -200,7 +200,7 @@ class WiredTableVisualizer:
         
         if output_path:
             cv2.imwrite(output_path, vis_img)
-            logger.info(f"文本填充可视化已保存: {output_path}")
+            logger.debug(f"文本填充可视化已保存: {output_path}")
         
         return vis_img
 

+ 19 - 0
ocr_utils/pdf_text_extraction.py

@@ -411,3 +411,22 @@ def extract_all_text_blocks_fitz(
         import traceback
         logger.debug(traceback.format_exc())
         return [], 0
+
+
+def detect_page_type(
+    pdf_doc: Any, 
+    page_idx: int,
+    char_threshold: int = 50
+) -> str:
+    """
+    检测PDF指定页是文字页还是图片页
+    
+    基于字符密度的简单可靠方法
+    """
+    try:
+        text_blocks, _ = extract_all_text_blocks(pdf_doc, page_idx, scale=1.0)
+        total_chars = sum(len(block.get('text', '')) for block in text_blocks)
+        
+        return 'txt' if total_chars >= char_threshold else 'ocr'
+    except:
+        return 'ocr'

+ 15 - 2
ocr_utils/pdf_utils.py

@@ -41,6 +41,7 @@ from .pdf_text_extraction import (
     extract_all_text_blocks,
     extract_all_text_blocks_pypdfium2,
     extract_all_text_blocks_fitz,
+    detect_page_type,
 )
 
 from .pdf_image_rendering import (
@@ -371,8 +372,20 @@ class PDFUtils:
             pdf_bytes, dpi, start_page_id, end_page_id, image_type
         )
     
-    # ========================================================================
-    # 其他功能
+    @staticmethod
+    def detect_page_type(
+        pdf_doc: Any, 
+        page_idx: int,
+        char_threshold: int = 50
+    ) -> str:
+        """
+        检测页面类型(文本PDF或扫描OCR)
+        
+        Returns:
+            页面类型:'txt' 或 'ocr'
+        """
+        return detect_page_type(pdf_doc, page_idx, char_threshold)
+
     # ========================================================================
     # 其他功能
     # ========================================================================