|
|
@@ -4,6 +4,7 @@
|
|
|
提供从表格线提取单元格和恢复网格结构的功能。
|
|
|
"""
|
|
|
from typing import List, Dict, Optional
|
|
|
+from pathlib import Path
|
|
|
import cv2
|
|
|
import numpy as np
|
|
|
from loguru import logger
|
|
|
@@ -22,6 +23,8 @@ class GridRecovery:
|
|
|
debug_dir: Optional[str] = None,
|
|
|
debug_prefix: str = "",
|
|
|
crop_padding: int = 10, # 新增:裁剪时的padding值(原图坐标系)
|
|
|
+ ocr_bboxes: Optional[List[Dict]] = None, # 🆕 整页OCR结果
|
|
|
+ enable_ocr_edge_compensation: bool = True, # 🆕 是否启用OCR边缘补偿
|
|
|
) -> List[List[float]]:
|
|
|
"""
|
|
|
基于矢量重构的连通域分析 (Advanced Vector-based Recovery)
|
|
|
@@ -32,6 +35,7 @@ class GridRecovery:
|
|
|
3. 线段归并/连接 (adjust_lines)
|
|
|
4. 几何延长线段 (Custom final_adjust_lines with larger threshold)
|
|
|
5. 重绘Mask并进行连通域分析
|
|
|
+ 6. 🆕 OCR补偿未封闭的边缘单元格
|
|
|
|
|
|
Args:
|
|
|
hpred_up: 横线预测mask(上采样后)
|
|
|
@@ -42,14 +46,17 @@ class GridRecovery:
|
|
|
debug_dir: 调试输出目录 (Optional)
|
|
|
debug_prefix: 调试文件名前缀 (Optional)
|
|
|
crop_padding: 裁剪时的padding值(原图坐标系,默认10px)
|
|
|
+ ocr_bboxes: 🆕 整页OCR结果 [{'bbox': [x1,y1,x2,y2], 'text': str, 'confidence': float}, ...]
|
|
|
+ enable_ocr_edge_compensation: 🆕 是否启用OCR边缘补偿(默认True)
|
|
|
|
|
|
注意:
|
|
|
- hpred_up/vpred_up 是上采样后的mask,坐标系已经放大了 upscale 倍
|
|
|
- crop_padding 是原图坐标系的值,需要乘以 upscale 转换到mask坐标系
|
|
|
- edge_margin 用于过滤贴近图像边缘的线条(padding区域的噪声)
|
|
|
+ - ocr_bboxes坐标应为原图坐标系,补偿算法会自动处理坐标转换
|
|
|
|
|
|
Returns:
|
|
|
- 单元格bbox列表 [[x1, y1, x2, y2], ...]
|
|
|
+ 单元格bbox列表 [[x1, y1, x2, y2], ...] (原图坐标系)
|
|
|
"""
|
|
|
import numpy as np
|
|
|
import cv2
|
|
|
@@ -489,6 +496,40 @@ class GridRecovery:
|
|
|
else:
|
|
|
logger.info(f"矢量重构分析提取到 {len(bboxes)} 个单元格 (Dynamic Alpha: {dynamic_alpha}, upscale={upscale:.3f})")
|
|
|
|
|
|
+ # 🆕 Step 6: OCR补偿未封闭的边缘单元格
|
|
|
+ if enable_ocr_edge_compensation and ocr_bboxes and orig_h is not None and orig_w is not None:
|
|
|
+ logger.info("━━━━━━━━ 🔍 OCR边缘补偿 ━━━━━━━━")
|
|
|
+
|
|
|
+ # 转换线条坐标到原图坐标系 (从mask坐标系转换)
|
|
|
+ rowboxes_orig = [
|
|
|
+ [line[0] / scale_w, line[1] / scale_h, line[2] / scale_w, line[3] / scale_h]
|
|
|
+ for line in rowboxes
|
|
|
+ ]
|
|
|
+ colboxes_orig = [
|
|
|
+ [line[0] / scale_w, line[1] / scale_h, line[2] / scale_w, line[3] / scale_h]
|
|
|
+ for line in colboxes
|
|
|
+ ]
|
|
|
+
|
|
|
+ # 调用OCR补偿算法 (所有坐标均为原图坐标系)
|
|
|
+ compensated_bboxes = GridRecovery._compensate_unclosed_cells(
|
|
|
+ existing_bboxes=bboxes, # 已有bbox (原图坐标系)
|
|
|
+ ocr_bboxes=ocr_bboxes, # OCR结果 (原图坐标系)
|
|
|
+ rowboxes=rowboxes_orig, # 水平线 (原图坐标系)
|
|
|
+ colboxes=colboxes_orig, # 垂直线 (原图坐标系)
|
|
|
+ img_h=orig_h,
|
|
|
+ img_w=orig_w,
|
|
|
+ debug_dir=debug_dir,
|
|
|
+ debug_prefix=debug_prefix
|
|
|
+ )
|
|
|
+
|
|
|
+ if compensated_bboxes:
|
|
|
+ logger.info(f"✅ OCR补偿成功: +{len(compensated_bboxes)}个边缘单元格")
|
|
|
+ bboxes.extend(compensated_bboxes)
|
|
|
+ # 重新排序
|
|
|
+ bboxes.sort(key=lambda b: (int(b[1] / 10), b[0]))
|
|
|
+ else:
|
|
|
+ logger.info("ℹ️ OCR补偿: 无需补偿边缘单元格")
|
|
|
+
|
|
|
return bboxes
|
|
|
|
|
|
@staticmethod
|
|
|
@@ -722,3 +763,353 @@ class GridRecovery:
|
|
|
new_cells.append(new_cell)
|
|
|
|
|
|
return new_cells
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _compensate_unclosed_cells(
|
|
|
+ existing_bboxes: List[List[float]],
|
|
|
+ ocr_bboxes: List[Dict],
|
|
|
+ rowboxes: List[List[float]],
|
|
|
+ colboxes: List[List[float]],
|
|
|
+ img_h: float,
|
|
|
+ img_w: float,
|
|
|
+ min_confidence: float = 0.7,
|
|
|
+ debug_dir: Optional[str] = None,
|
|
|
+ debug_prefix: str = ""
|
|
|
+ ) -> List[List[float]]:
|
|
|
+ """
|
|
|
+ 基于网格矩阵补偿未封闭的边缘单元格
|
|
|
+
|
|
|
+ 新算法思路:
|
|
|
+ 1. 从rowboxes/colboxes构建网格矩阵
|
|
|
+ 2. 将existing_bboxes映射到网格单元
|
|
|
+ 3. 检测空的边缘单元格(与已有单元格相邻)
|
|
|
+ 4. 用OCR填充这些空单元格
|
|
|
+
|
|
|
+ Args:
|
|
|
+ existing_bboxes: 连通域检测到的bbox列表 (原图坐标系)
|
|
|
+ ocr_bboxes: 整页OCR结果
|
|
|
+ rowboxes: 水平线列表 (原图坐标系)
|
|
|
+ colboxes: 垂直线列表 (原图坐标系)
|
|
|
+ img_h, img_w: 原图尺寸
|
|
|
+ min_confidence: OCR最小置信度阈值
|
|
|
+ debug_dir, debug_prefix: Debug可视化参数
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ 补偿的bbox列表 (原图坐标系)
|
|
|
+ """
|
|
|
+ if not ocr_bboxes or not rowboxes or not colboxes:
|
|
|
+ logger.debug("📊 OCR补偿: 缺少必要数据")
|
|
|
+ return []
|
|
|
+
|
|
|
+ logger.info(f"🔧 OCR补偿参数: img_size=({img_w:.0f}×{img_h:.0f})")
|
|
|
+
|
|
|
+ # Step 1: 过滤OCR
|
|
|
+ valid_ocr = [
|
|
|
+ ocr for ocr in ocr_bboxes
|
|
|
+ if ocr.get('confidence', 1.0) >= min_confidence
|
|
|
+ and len(ocr.get('text', '').strip()) > 0
|
|
|
+ ]
|
|
|
+
|
|
|
+ if not valid_ocr:
|
|
|
+ logger.debug(f"📊 OCR补偿: 过滤后无有效OCR")
|
|
|
+ return []
|
|
|
+
|
|
|
+ # Step 2: 构建网格(使用线条中点作为分割线)
|
|
|
+ row_dividers = sorted(set((line[1] + line[3]) / 2 for line in rowboxes))
|
|
|
+ col_dividers = sorted(set((line[0] + line[2]) / 2 for line in colboxes))
|
|
|
+
|
|
|
+ # 添加图像边界
|
|
|
+ if not row_dividers or row_dividers[0] > 5:
|
|
|
+ row_dividers.insert(0, 0.0)
|
|
|
+ if not row_dividers or row_dividers[-1] < img_h - 5:
|
|
|
+ row_dividers.append(img_h)
|
|
|
+ if not col_dividers or col_dividers[0] > 5:
|
|
|
+ col_dividers.insert(0, 0.0)
|
|
|
+ if not col_dividers or col_dividers[-1] < img_w - 5:
|
|
|
+ col_dividers.append(img_w)
|
|
|
+
|
|
|
+ logger.debug(f"📊 网格: {len(row_dividers)-1}行 × {len(col_dividers)-1}列")
|
|
|
+
|
|
|
+ # Step 3: 将existing_bboxes映射到网格单元(支持跨行跨列)
|
|
|
+ grid = {} # {(row, col): True} - 标记已占用的单元格
|
|
|
+
|
|
|
+ def find_overlapping_cells(bbox: List[float]) -> List[tuple]:
|
|
|
+ """找到bbox覆盖的所有网格单元[(row, col), ...]"""
|
|
|
+ x1, y1, x2, y2 = bbox
|
|
|
+ cells = []
|
|
|
+
|
|
|
+ for i in range(len(row_dividers) - 1):
|
|
|
+ # 检查垂直方向重叠
|
|
|
+ grid_y1, grid_y2 = row_dividers[i], row_dividers[i + 1]
|
|
|
+ if max(y1, grid_y1) < min(y2, grid_y2): # 有重叠
|
|
|
+ for j in range(len(col_dividers) - 1):
|
|
|
+ # 检查水平方向重叠
|
|
|
+ grid_x1, grid_x2 = col_dividers[j], col_dividers[j + 1]
|
|
|
+ if max(x1, grid_x1) < min(x2, grid_x2): # 有重叠
|
|
|
+ cells.append((i, j))
|
|
|
+
|
|
|
+ return cells
|
|
|
+
|
|
|
+ # 标记所有existing_bbox占用的网格单元
|
|
|
+ for bbox in existing_bboxes:
|
|
|
+ cells = find_overlapping_cells(bbox)
|
|
|
+ for cell in cells:
|
|
|
+ grid[cell] = True
|
|
|
+
|
|
|
+ logger.debug(f"📊 已占用: {len(grid)}个网格单元 (共{(len(row_dividers)-1)*(len(col_dividers)-1)}个)")
|
|
|
+
|
|
|
+ # Step 4: 第一遍 - 为所有OCR找到其覆盖的空单元格(不扩展)
|
|
|
+ ocr_to_empty_cells = {} # {ocr_index: [empty_cells]}
|
|
|
+
|
|
|
+ for idx, ocr in enumerate(valid_ocr):
|
|
|
+ ocr_bbox = ocr['bbox']
|
|
|
+ ocr_text = ocr.get('text', '')[:30]
|
|
|
+
|
|
|
+ # 找到OCR覆盖的所有网格单元
|
|
|
+ overlapping_cells = find_overlapping_cells(ocr_bbox)
|
|
|
+
|
|
|
+ if not overlapping_cells:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 找出未被占用的单元格
|
|
|
+ empty_cells = [cell for cell in overlapping_cells if cell not in grid]
|
|
|
+
|
|
|
+ if not empty_cells:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 检查是否是边缘单元格(至少一个空单元格与已占用单元格相邻)
|
|
|
+ has_neighbor = False
|
|
|
+ for row, col in empty_cells:
|
|
|
+ for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
|
|
+ neighbor = (row + dr, col + dc)
|
|
|
+ if neighbor in grid:
|
|
|
+ has_neighbor = True
|
|
|
+ break
|
|
|
+ if has_neighbor:
|
|
|
+ break
|
|
|
+
|
|
|
+ if not has_neighbor:
|
|
|
+ logger.debug(f"⏭️ 跳过OCR '{ocr_text}': 无相邻单元格")
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 记录这个OCR的初始空单元格
|
|
|
+ ocr_to_empty_cells[idx] = {
|
|
|
+ 'ocr': ocr,
|
|
|
+ 'empty_cells': empty_cells
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.debug(f"📊 第一遍完成: {len(ocr_to_empty_cells)}个OCR需要补偿")
|
|
|
+
|
|
|
+ # Step 5: 第二遍 - 对所有标记的OCR区域统一扩展
|
|
|
+ # 🆕 辅助函数:检查侧边相邻列/行的已占用单元格边界
|
|
|
+ def get_side_boundary_for_vertical_expansion(current_min_col, current_max_col, direction='up'):
|
|
|
+ """向上/下扩展时,检查左右两侧相邻列的单元格边界"""
|
|
|
+ boundary_rows = []
|
|
|
+
|
|
|
+ # 检查左侧相邻列(current_min_col - 1)
|
|
|
+ if current_min_col > 0:
|
|
|
+ left_col = current_min_col - 1
|
|
|
+ occupied_rows_in_left = [r for r, c in grid.keys() if c == left_col]
|
|
|
+ if occupied_rows_in_left:
|
|
|
+ if direction == 'up':
|
|
|
+ boundary_rows.append(min(occupied_rows_in_left))
|
|
|
+ else: # down
|
|
|
+ boundary_rows.append(max(occupied_rows_in_left))
|
|
|
+
|
|
|
+ # 检查右侧相邻列(current_max_col + 1)
|
|
|
+ if current_max_col < len(col_dividers) - 2:
|
|
|
+ right_col = current_max_col + 1
|
|
|
+ occupied_rows_in_right = [r for r, c in grid.keys() if c == right_col]
|
|
|
+ if occupied_rows_in_right:
|
|
|
+ if direction == 'up':
|
|
|
+ boundary_rows.append(min(occupied_rows_in_right))
|
|
|
+ else: # down
|
|
|
+ boundary_rows.append(max(occupied_rows_in_right))
|
|
|
+
|
|
|
+ return boundary_rows
|
|
|
+
|
|
|
+ def get_side_boundary_for_horizontal_expansion(current_min_row, current_max_row, direction='left'):
|
|
|
+ """向左/右扩展时,检查上下两侧相邻行的单元格边界"""
|
|
|
+ boundary_cols = []
|
|
|
+
|
|
|
+ # 检查上侧相邻行(current_min_row - 1)
|
|
|
+ if current_min_row > 0:
|
|
|
+ top_row = current_min_row - 1
|
|
|
+ occupied_cols_in_top = [c for r, c in grid.keys() if r == top_row]
|
|
|
+ if occupied_cols_in_top:
|
|
|
+ if direction == 'left':
|
|
|
+ boundary_cols.append(min(occupied_cols_in_top))
|
|
|
+ else: # right
|
|
|
+ boundary_cols.append(max(occupied_cols_in_top))
|
|
|
+
|
|
|
+ # 检查下侧相邻行(current_max_row + 1)
|
|
|
+ if current_max_row < len(row_dividers) - 2:
|
|
|
+ bottom_row = current_max_row + 1
|
|
|
+ occupied_cols_in_bottom = [c for r, c in grid.keys() if r == bottom_row]
|
|
|
+ if occupied_cols_in_bottom:
|
|
|
+ if direction == 'left':
|
|
|
+ boundary_cols.append(min(occupied_cols_in_bottom))
|
|
|
+ else: # right
|
|
|
+ boundary_cols.append(max(occupied_cols_in_bottom))
|
|
|
+
|
|
|
+ return boundary_cols
|
|
|
+
|
|
|
+ # 对每个OCR区域进行扩展
|
|
|
+ for idx, ocr_data in ocr_to_empty_cells.items():
|
|
|
+ empty_cells = ocr_data['empty_cells']
|
|
|
+ ocr = ocr_data['ocr']
|
|
|
+ ocr_text = ocr.get('text', '')[:30]
|
|
|
+
|
|
|
+ # 向上下左右扩展连续的空单元格(必须与侧边已有单元格对齐)
|
|
|
+ expanded = set(empty_cells)
|
|
|
+ changed = True
|
|
|
+ while changed:
|
|
|
+ changed = False
|
|
|
+ current_min_row = min(r for r, c in expanded)
|
|
|
+ current_max_row = max(r for r, c in expanded)
|
|
|
+ current_min_col = min(c for r, c in expanded)
|
|
|
+ current_max_col = max(c for r, c in expanded)
|
|
|
+
|
|
|
+ # 🆕 尝试向上扩展(整行都是空的,且不超过左右侧单元格的上边界)
|
|
|
+ if current_min_row > 0:
|
|
|
+ row_above = current_min_row - 1
|
|
|
+ # 检查该行是否都是空的
|
|
|
+ if all((row_above, col) not in grid for col in range(current_min_col, current_max_col + 1)):
|
|
|
+ # 🆕 检查左右侧相邻列的单元格最小行(上边界)
|
|
|
+ side_boundaries = get_side_boundary_for_vertical_expansion(
|
|
|
+ current_min_col, current_max_col, 'up'
|
|
|
+ )
|
|
|
+ can_expand = True
|
|
|
+ if side_boundaries:
|
|
|
+ # 左右侧单元格的最小行,不能扩展超过它
|
|
|
+ min_side_row = min(side_boundaries)
|
|
|
+ if row_above < min_side_row:
|
|
|
+ can_expand = False
|
|
|
+
|
|
|
+ if can_expand:
|
|
|
+ for col in range(current_min_col, current_max_col + 1):
|
|
|
+ expanded.add((row_above, col))
|
|
|
+ changed = True
|
|
|
+
|
|
|
+ # 🆕 尝试向下扩展(整行都是空的,且不超过左右侧单元格的下边界)
|
|
|
+ if current_max_row < len(row_dividers) - 2:
|
|
|
+ row_below = current_max_row + 1
|
|
|
+ if all((row_below, col) not in grid for col in range(current_min_col, current_max_col + 1)):
|
|
|
+ side_boundaries = get_side_boundary_for_vertical_expansion(
|
|
|
+ current_min_col, current_max_col, 'down'
|
|
|
+ )
|
|
|
+ can_expand = True
|
|
|
+ if side_boundaries:
|
|
|
+ max_side_row = max(side_boundaries)
|
|
|
+ if row_below > max_side_row:
|
|
|
+ can_expand = False
|
|
|
+
|
|
|
+ if can_expand:
|
|
|
+ for col in range(current_min_col, current_max_col + 1):
|
|
|
+ expanded.add((row_below, col))
|
|
|
+ changed = True
|
|
|
+
|
|
|
+ # 🆕 尝试向左扩展(整列都是空的,且不超过上下侧单元格的左边界)
|
|
|
+ if current_min_col > 0:
|
|
|
+ col_left = current_min_col - 1
|
|
|
+ if all((row, col_left) not in grid for row in range(current_min_row, current_max_row + 1)):
|
|
|
+ side_boundaries = get_side_boundary_for_horizontal_expansion(
|
|
|
+ current_min_row, current_max_row, 'left'
|
|
|
+ )
|
|
|
+ can_expand = True
|
|
|
+ if side_boundaries:
|
|
|
+ min_side_col = min(side_boundaries)
|
|
|
+ if col_left < min_side_col:
|
|
|
+ can_expand = False
|
|
|
+
|
|
|
+ if can_expand:
|
|
|
+ for row in range(current_min_row, current_max_row + 1):
|
|
|
+ expanded.add((row, col_left))
|
|
|
+ changed = True
|
|
|
+
|
|
|
+ # 🆕 尝试向右扩展(整列都是空的,且不超过上下侧单元格的右边界)
|
|
|
+ if current_max_col < len(col_dividers) - 2:
|
|
|
+ col_right = current_max_col + 1
|
|
|
+ if all((row, col_right) not in grid for row in range(current_min_row, current_max_row + 1)):
|
|
|
+ side_boundaries = get_side_boundary_for_horizontal_expansion(
|
|
|
+ current_min_row, current_max_row, 'right'
|
|
|
+ )
|
|
|
+ can_expand = True
|
|
|
+ if side_boundaries:
|
|
|
+ max_side_col = max(side_boundaries)
|
|
|
+ if col_right > max_side_col:
|
|
|
+ can_expand = False
|
|
|
+
|
|
|
+ if can_expand:
|
|
|
+ for row in range(current_min_row, current_max_row + 1):
|
|
|
+ expanded.add((row, col_right))
|
|
|
+ changed = True
|
|
|
+
|
|
|
+ # 更新扩展后的空单元格
|
|
|
+ ocr_to_empty_cells[idx]['expanded_cells'] = list(expanded)
|
|
|
+
|
|
|
+ logger.debug(f"📊 第二遍完成: 所有OCR区域已扩展")
|
|
|
+
|
|
|
+ # Step 6: 第三遍 - 生成补偿bbox
|
|
|
+ compensated_bboxes = []
|
|
|
+
|
|
|
+ for idx, ocr_data in ocr_to_empty_cells.items():
|
|
|
+ empty_cells = ocr_data['expanded_cells']
|
|
|
+ ocr = ocr_data['ocr']
|
|
|
+ ocr_text = ocr.get('text', '')[:30]
|
|
|
+
|
|
|
+ # 找到所有空单元格的边界范围
|
|
|
+ min_row = min(r for r, c in empty_cells)
|
|
|
+ max_row = max(r for r, c in empty_cells)
|
|
|
+ min_col = min(c for r, c in empty_cells)
|
|
|
+ max_col = max(c for r, c in empty_cells)
|
|
|
+
|
|
|
+ # 使用网格边界作为bbox(精确对齐)
|
|
|
+ y1 = row_dividers[min_row]
|
|
|
+ y2 = row_dividers[max_row + 1]
|
|
|
+ x1 = col_dividers[min_col]
|
|
|
+ x2 = col_dividers[max_col + 1]
|
|
|
+
|
|
|
+ compensated_bbox = [x1, y1, x2, y2]
|
|
|
+ compensated_bboxes.append(compensated_bbox)
|
|
|
+
|
|
|
+ # 标记这些单元格为已占用
|
|
|
+ for row, col in empty_cells:
|
|
|
+ grid[(row, col)] = True
|
|
|
+
|
|
|
+ logger.info(
|
|
|
+ f"✅ 补偿单元格[{min_row}-{max_row},{min_col}-{max_col}]: '{ocr_text}' | "
|
|
|
+ f"bbox=[{x1:.1f},{y1:.1f},{x2:.1f},{y2:.1f}] | "
|
|
|
+ f"占据{len(empty_cells)}个网格单元"
|
|
|
+ )
|
|
|
+
|
|
|
+ # Step 5: Debug可视化
|
|
|
+ if debug_dir and compensated_bboxes:
|
|
|
+ try:
|
|
|
+ from pathlib import Path
|
|
|
+ vis_img = np.ones((int(img_h), int(img_w), 3), dtype=np.uint8) * 255
|
|
|
+
|
|
|
+ # 绘制网格线(灰色虚线)
|
|
|
+ for y in row_dividers:
|
|
|
+ cv2.line(vis_img, (0, int(y)), (int(img_w), int(y)), (200, 200, 200), 1, cv2.LINE_AA)
|
|
|
+ for x in col_dividers:
|
|
|
+ cv2.line(vis_img, (int(x), 0), (int(x), int(img_h)), (200, 200, 200), 1, cv2.LINE_AA)
|
|
|
+
|
|
|
+ # 绘制现有bbox(蓝色)
|
|
|
+ for bbox in existing_bboxes:
|
|
|
+ x1, y1, x2, y2 = [int(v) for v in bbox]
|
|
|
+ cv2.rectangle(vis_img, (x1, y1), (x2, y2), (255, 0, 0), 2)
|
|
|
+
|
|
|
+ # 绘制补偿bbox(绿色)
|
|
|
+ for bbox in compensated_bboxes:
|
|
|
+ x1, y1, x2, y2 = [int(v) for v in bbox]
|
|
|
+ cv2.rectangle(vis_img, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
|
+
|
|
|
+ out_path = Path(debug_dir) / f"{debug_prefix}step06_ocr_compensation.png"
|
|
|
+ cv2.imwrite(str(out_path), vis_img)
|
|
|
+ logger.debug(f"💾 Debug图: {out_path}")
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"⚠️ Debug可视化失败: {e}")
|
|
|
+
|
|
|
+ logger.info(f"🎉 OCR补偿完成: +{len(compensated_bboxes)}个边缘单元格")
|
|
|
+ return compensated_bboxes
|