1
0

7 Commits 4f888dc393 ... d28d0df3b4

Autor SHA1 Mensagem Data
  zhch158_admin d28d0df3b4 refactor: Streamline table line editor by consolidating analysis parameters and enhancing structure rendering logic há 13 horas atrás
  zhch158_admin 6a40e484f1 refactor: Ensure active output configuration is consistently displayed in save section, improving user feedback on save location há 13 horas atrás
  zhch158_admin 0193d9944c refactor: Update OCR data parsing to use automatic tool detection for improved flexibility há 13 horas atrás
  zhch158_admin 46bc29508c refactor: Improve user interface for image rotation adjustments by clarifying angle descriptions and enhancing input labels há 13 horas atrás
  zhch158_admin 03f776143b feat: Enhance OCR data parsing by introducing automatic tool detection and refining analysis method selection há 13 horas atrás
  zhch158_admin 97e4bee194 fix: Update tool name from 'ppstructv3' to 'ppstructure' in table_line_generator.yaml for consistency há 13 horas atrás
  zhch158_admin 01443814e4 refactor: Simplify image rotation caching by consolidating image and text bounding box mapping into a single dictionary structure há 16 horas atrás

+ 6 - 9
ocr_validator_layout.py

@@ -86,10 +86,6 @@ class OCRLayoutManager:
                 if isinstance(item, dict) and 'rotation_angle' in item:
                     return item['rotation_angle']
         
-        # 如果没有预设角度,尝试自动检测
-        if hasattr(self, 'rotated_angle'):
-            return self.rotated_angle
-        
         return 0.0
     
     def load_and_rotate_image(self, image_path: str) -> Optional[Image.Image]:
@@ -102,7 +98,8 @@ class OCRLayoutManager:
         cache_key = f"{image_path}_{rotation_angle}"
         
         if cache_key in self._rotated_image_cache:
-            return self._rotated_image_cache[cache_key]
+            self.validator.text_bbox_mapping = self._rotated_image_cache[cache_key]['text_bbox_mapping']
+            return self._rotated_image_cache[cache_key]['image']
         
         try:
             image = Image.open(image_path)
@@ -132,13 +129,13 @@ class OCRLayoutManager:
                     
                     if self.rotated_angle == 0:
                         # 坐标不需要变换,因为JSON中已经是正确的坐标
-                        self._rotated_image_cache[cache_key] = rotated_image
+                        self._rotated_image_cache[cache_key] = {'image': rotated_image, 'text_bbox_mapping': self.validator.text_bbox_mapping}
                         self._manage_cache_size()
                         return rotated_image
 
                     image = rotated_image  # 继续使用旋转后的图像进行后续处理
                 
-                # Dots OCR: 需要同时旋转图像和坐标
+                # VLM: 需要同时旋转图像和坐标
                 # 收集所有bbox坐标
                 all_bboxes = []
                 text_to_bbox_map = {}  # 记录文本到bbox索引的映射
@@ -164,13 +161,13 @@ class OCRLayoutManager:
                             self.validator.text_bbox_mapping[text][i]['bbox'] = rotated_bboxes[bbox_idx]
                 
                 # 缓存结果
-                self._rotated_image_cache[cache_key] = rotated_image
+                self._rotated_image_cache[cache_key] = {'image': rotated_image, 'text_bbox_mapping': self.validator.text_bbox_mapping}
                 self._manage_cache_size()
                 return rotated_image
                     
             else:
                 # 无需旋转,直接缓存原图
-                self._rotated_image_cache[cache_key] = image
+                self._rotated_image_cache[cache_key] = {'image': image, 'text_bbox_mapping': self.validator.text_bbox_mapping}
                 self._manage_cache_size()  # 检查并管理缓存大小
                 return image
                 

+ 11 - 6
table_line_generator/editor/adjustments.py

@@ -153,32 +153,37 @@ def create_adjustment_section(structure):
             st.success(f"✅ 已删除 {len(to_delete)} 条竖线")
 
     elif adjustment_action == "微调旋转":
-        st.info("📐 微调图片的旋转角度 (基于当前角度), 正值逆时针旋转,负值顺时针旋转")
+        st.info("📐 手动修正图片的倾斜角度。系统会根据设定的倾斜角度,进行**反向旋转**来校正图片。")
         
         if 'generator' in st.session_state:
             generator = st.session_state.generator
             current_skew = generator.ocr_data.get('skew_angle', 0.0)
             original_skew = generator.ocr_data.get('original_skew_angle', 0.0)
             
+            # 计算实际执行的旋转角度
+            applied_rotation = -current_skew
+            
             st.markdown(f"""
-            **当前总倾斜校正:** `{current_skew:.2f}°`  --   *(原始文件读取: `{original_skew:.2f}°`)*
+            *   **原始识别倾斜:** `{original_skew:.2f}°` (OCR模型检测到的原始角度)
+            *   **当前设定倾斜:** `{current_skew:.2f}°` (系统认为图片歪了多少)
+            *   **实际执行旋转:** `{applied_rotation:.2f}°` (系统实际执行的**反向**校正旋转)
             """)
             
             col1, col2 = st.columns([1, 1], width=300, gap="small")
             with col1:
                 delta_angle = st.number_input(
-                    "旋转偏移量 (度)",
+                    "调整设定值 (度)",
                     min_value=-10.0,
                     max_value=10.0,
                     value=0.0,
-                    step=0.01,
+                    step=0.05,
                     format="%.2f",
-                    help="正值逆时针旋转,负值顺时针旋转",
+                    help="增加设定值 -> 系统认为图片逆时针歪得更多 -> 执行更大幅度的顺时针旋转",
                     label_visibility="collapsed",
                     key="rotate_delta_input"
                 )
             with col2:
-                if st.button("🔄 应用", key="apply_rotate_btn"):
+                if st.button("🔄 应用调整", key="apply_rotate_btn"):
                     if delta_angle != 0:
                         # 更新 skew_angle
                         generator.ocr_data['skew_angle'] = current_skew + delta_angle

+ 1 - 1
table_line_generator/editor/file_handlers.py

@@ -37,7 +37,7 @@ def handle_json_upload(uploaded_json):
             else:
                 st.json(raw_data[:3] if len(raw_data) > 3 else raw_data)
         
-        ocr_data = TableLineGenerator.parse_ocr_data(raw_data, tool="ppstructv3")
+        table_bbox, ocr_data = TableLineGenerator.parse_ocr_data(raw_data, tool="auto")
         
         if not ocr_data:
             st.error("❌ 无法解析 OCR 数据,请检查 JSON 格式")

+ 2 - 2
table_line_generator/editor/save_controls.py

@@ -24,11 +24,11 @@ def create_save_section(work_mode: str, structure: Dict, image, line_width: int,
     st.divider()
 
     # 🔑 优先使用当前数据源的输出配置
-    if 'current_output_config' in st.session_state:
+    if 'current_output_config' in st.session_state and st.session_state.current_output_config:
         active_output_config = st.session_state.current_output_config
-        st.info(f"📂 保存位置:{active_output_config.get('directory', 'N/A')}")
     else:
         active_output_config = output_config
+    st.info(f"📂 保存位置:{active_output_config.get('directory', 'N/A')}")
 
     defaults = active_output_config.get("defaults", {})
     line_colors = active_output_config.get("line_colors") or [

+ 31 - 42
table_line_generator/streamlit_table_line_editor.py

@@ -200,48 +200,37 @@ def create_table_line_editor():
         if st.session_state.ocr_data and st.session_state.image:
             st.info(f"📂 已加载: {st.session_state.loaded_json_name}")
             
-            # 🔧 显示分析参数设置(统一处理)
-            st.sidebar.subheader("🔬 分析参数")
-            
-            analysis_method = st.sidebar.selectbox(
-                "分析算法",
-                ["auto", "cluster", "mineru"],
-                format_func=lambda x: {
-                    "auto": "🤖 自动选择(推荐)",
-                    "cluster": "📊 聚类算法(通用)",
-                    "mineru": "🎯 MinerU 索引算法"
-                }[x]
+            # 使用统一的 setup_new_annotation_mode
+            _, structure, _, line_width, display_mode, zoom_level, show_line_numbers = setup_new_annotation_mode(
+                st.session_state.ocr_data,
+                st.session_state.image,
+                TABLE_EDITOR_CONFIG["display"]
             )
             
-            if analysis_method in ["auto", "cluster"]:
-                y_tolerance = st.sidebar.slider("Y轴容差", 1, 20, 5)
-                x_tolerance = st.sidebar.slider("X轴容差", 1, 30, 10)
-                min_row_height = st.sidebar.slider("最小行高", 10, 50, 20)
-            
-            # 🎯 分析按钮
-            if st.button("🔍 分析表格结构"):
-                with st.spinner("正在分析..."):
-                    # 统一的分析流程
-                    generator = TableLineGenerator(
-                        st.session_state.image, 
-                        st.session_state.ocr_data
-                    )
-                    
-                    if analysis_method == "auto":
-                        # 根据数据特征自动选择
-                        has_cell_index = any('row' in item for item in st.session_state.ocr_data)
-                        method = "mineru" if has_cell_index else "cluster"
-                    else:
-                        method = analysis_method
-                    
-                    st.session_state.structure = generator.analyze_table_structure(
-                        y_tolerance=y_tolerance if method == "cluster" else 5,
-                        x_tolerance=x_tolerance if method == "cluster" else 10,
-                        min_row_height=min_row_height if method == "cluster" else 20,
-                        method=method
-                    )
-                    
-                    st.success(f"✅ 分析完成(使用 {method} 算法)")
+            # 如果生成了结构(点击了分析按钮),更新 session_state
+            if structure:
+                st.session_state.structure = structure
+                
+            # 渲染视图
+            if 'structure' in st.session_state and st.session_state.structure:
+                render_table_structure_view(
+                    st.session_state.structure,
+                    st.session_state.image,
+                    line_width,
+                    display_mode,
+                    zoom_level,
+                    show_line_numbers,
+                    VIEWPORT_WIDTH,
+                    VIEWPORT_HEIGHT
+                )
+                
+                create_save_section(
+                    "new",
+                    st.session_state.structure,
+                    st.session_state.image,
+                    line_width,
+                    TABLE_EDITOR_CONFIG["output"]
+                )
         
         return
     
@@ -280,7 +269,7 @@ def create_table_line_editor():
         
         render_table_structure_view(
             st.session_state.structure,
-            image,
+            image or Image.new('RGB', (2000, 2000), 'white'),
             line_width,
             display_mode,
             zoom_level,
@@ -289,7 +278,7 @@ def create_table_line_editor():
             VIEWPORT_HEIGHT
         )
         create_save_section(
-            work_mode,
+            "edit",
             st.session_state.structure,
             image,
             line_width,

+ 38 - 8
table_line_generator/table_line_generator.py

@@ -62,7 +62,7 @@ class TableLineGenerator:
 
 
     @staticmethod
-    def parse_ocr_data(ocr_result: Dict, tool: str = "ppstructv3") -> Tuple[List[int], Dict]:
+    def parse_ocr_data(ocr_result: Dict, tool: str = "auto") -> Tuple[List[int], Dict]:
         """
         统一的 OCR 数据解析接口(第一步:仅读取数据)
         
@@ -73,14 +73,29 @@ class TableLineGenerator:
         Returns:
             (table_bbox, ocr_data): 表格边界框和文本框列表
         """
-        if tool.lower() == "mineru":
-            return TableLineGenerator._parse_mineru_data(ocr_result)
-        elif tool.lower() in ["ppstructv3", "ppstructure"]:
+        if tool.lower() == "auto":
+            tool_type = TableLineGenerator.detect_ocr_tool_type(ocr_result)
+        else:
+            tool_type = tool.lower() if tool else None
+        
+        if tool_type == "ppstructure":
             return TableLineGenerator._parse_ppstructure_data(ocr_result)
+        elif tool_type == "mineru":
+            return TableLineGenerator._parse_mineru_data(ocr_result)
         else:
             raise ValueError(f"不支持的工具类型: {tool}")
     
     @staticmethod
+    def detect_ocr_tool_type(ocr_result: Dict) -> str:
+        """
+        检测 OCR 工具类型
+        """
+        if 'parsing_res_list' in ocr_result and 'overall_ocr_res' in ocr_result:
+            return "ppstructure"
+        else:
+            return "mineru"
+
+    @staticmethod
     def _parse_mineru_data(mineru_result: Union[Dict, List]) -> Tuple[List[int], Dict]:
         """
         解析 MinerU 格式数据(仅提取数据,不分析结构)
@@ -205,6 +220,14 @@ class TableLineGenerator:
         return table_bbox, ocr_data
     
     # ==================== 统一接口:第二步 - 分析结构 ====================
+    def detect_analysis_method(self) -> str:
+        """
+        检测分析方法
+        """
+        if 'text_boxes' in self.ocr_data:
+            return "mineru" if any('row' in item and 'col' in item for item in self.ocr_data['text_boxes']) else "cluster"
+        else:
+            return "cluster"
     
     def analyze_table_structure(self, 
                                y_tolerance: int = 5,
@@ -231,8 +254,7 @@ class TableLineGenerator:
         # 🔑 自动选择方法
         if method == "auto":
             # 根据数据特征自动选择
-            has_cell_index = any('row' in item and 'col' in item for item in self.ocr_data.get('text_boxes', []))
-            method = "mineru" if has_cell_index else "cluster"
+            method = self.detect_analysis_method()
             print(f"🤖 自动选择分析方法: {method}")
         
         # 🔑 根据方法选择算法
@@ -327,11 +349,18 @@ class TableLineGenerator:
         # 3. 执行倾斜校正 (skew_angle)
         if abs(skew_angle) > 0.1:
             print(f"   📐 执行倾斜校正: {skew_angle:.2f}°")
-            # 图片逆时针歪了 skew_angle 度,需要顺时针转 skew_angle 度校正
+            # 逻辑说明:
+            # skew_angle 表示图片内容的倾斜角度。
+            # 正值(+) 表示内容逆时针倾斜(左高右低,或者坐标系定义下的逆时针)。
+            # 为了校正,我们需要向相反方向旋转,即顺时针旋转。
+            # PIL.Image.rotate(angle) 中,正值是逆时针旋转,负值是顺时针旋转。
+            # 所以 correction_angle = -skew_angle
             correction_angle = -skew_angle
             current_image = current_image.rotate(correction_angle, expand=False, fillcolor='white')
             
             # 更新 bbox 坐标
+            # 注意: BBoxExtractor.rotate_point 已修正为符合 PIL 的正=逆时针逻辑
+            # 所以这里传入 correction_angle 即可保持一致
             working_text_boxes = BBoxExtractor.correct_boxes_skew(
                 working_text_boxes, 
                 correction_angle, 
@@ -911,7 +940,8 @@ def _parse_table_body_structure(table_body: str) -> Tuple[int, int]:
         
         num_rows = len(rows)
         first_row = rows[0]
-        num_cols = len(first_row.find_all(['td', 'th']))
+        # 寻找最大列数,避免某些行缺失列
+        num_cols = max(len(row.find_all(['td', 'th'])) for row in rows)
         
         return num_rows, num_cols
         

+ 1 - 1
table_line_generator/table_line_generator.yaml

@@ -43,7 +43,7 @@ table_editor:
         image_suffix: ".png"
 
     - name: "康强_北京农村商业银行"
-      tool: "ppstructv3"
+      tool: "ppstructure"
       base_dir: "/Users/zhch158/workspace/data/流水分析"
       json_dir: "{{name}}/ppstructurev3_client_results"
       image_dir: "{{name}}/ppstructurev3_client_results/{{name}}"