data_processor.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. """
  2. 数据处理模块
  3. 负责处理 MinerU/PaddleOCR_VL/DotsOCR 数据,添加 bbox 信息
  4. """
  5. from typing import List, Dict, Tuple, Optional
  6. from bs4 import BeautifulSoup
  7. try:
  8. from .text_matcher import TextMatcher
  9. from .bbox_extractor import BBoxExtractor
  10. from .table_cell_matcher import TableCellMatcher
  11. except ImportError:
  12. from text_matcher import TextMatcher
  13. from bbox_extractor import BBoxExtractor
  14. from table_cell_matcher import TableCellMatcher
  15. class DataProcessor:
  16. """数据处理器"""
  17. def __init__(self, text_matcher: TextMatcher, look_ahead_window: int = 10, x_tolerance: int = 3, y_tolerance: int = 10):
  18. """
  19. Args:
  20. text_matcher: 文本匹配器
  21. look_ahead_window: 向前查找窗口
  22. x_tolerance: x轴容差
  23. """
  24. self.text_matcher = text_matcher
  25. self.look_ahead_window = look_ahead_window
  26. # X轴容差, 用于判断文本框是否在同一列
  27. self.x_tolerance = x_tolerance
  28. self.y_tolerance = y_tolerance # Y轴容差, 用于行分组
  29. # 🎯 创建表格单元格匹配器
  30. self.table_cell_matcher = TableCellMatcher(
  31. text_matcher=text_matcher,
  32. x_tolerance=x_tolerance,
  33. y_tolerance=y_tolerance
  34. )
  35. def process_mineru_data(self, mineru_data: List[Dict],
  36. paddle_text_boxes: List[Dict]) -> List[Dict]:
  37. """
  38. 处理 MinerU 数据,添加 bbox 信息
  39. Args:
  40. mineru_data: MinerU 数据
  41. paddle_text_boxes: PaddleOCR 文字框列表
  42. Returns:
  43. 合并后的数据, table cell使用paddle的bbox,其他类型只是移动指针,bbox还是沿用minerU的bbox
  44. """
  45. merged_data = []
  46. paddle_pointer = 0
  47. last_matched_index = 0
  48. # 按 bbox 排序
  49. mineru_data.sort(
  50. key=lambda x: (x['bbox'][1], x['bbox'][0])
  51. if 'bbox' in x else (float('inf'), float('inf'))
  52. )
  53. for item in mineru_data:
  54. item_type = item.get('type', '')
  55. if item_type == 'table':
  56. merged_item, paddle_pointer = self._process_table(
  57. item, paddle_text_boxes, paddle_pointer
  58. )
  59. merged_data.append(merged_item)
  60. elif item_type in ['text', 'title']:
  61. merged_item, paddle_pointer, last_matched_index = self._process_text(
  62. item, paddle_text_boxes, paddle_pointer, last_matched_index
  63. )
  64. merged_data.append(merged_item)
  65. elif item_type == 'list':
  66. merged_item, paddle_pointer, last_matched_index = self._process_list(
  67. item, paddle_text_boxes, paddle_pointer, last_matched_index
  68. )
  69. merged_data.append(merged_item)
  70. else:
  71. merged_data.append(item.copy())
  72. return merged_data
  73. def process_dotsocr_data(self, dotsocr_data: List[Dict],
  74. paddle_text_boxes: List[Dict]) -> List[Dict]:
  75. """
  76. 🎯 处理 DotsOCR 数据,转换为 MinerU 格式并添加 bbox 信息
  77. Args:
  78. dotsocr_data: DotsOCR 数据
  79. paddle_text_boxes: PaddleOCR 文字框列表
  80. Returns:
  81. MinerU 格式的合并数据
  82. """
  83. merged_data = []
  84. paddle_pointer = 0
  85. last_matched_index = 0
  86. # 按 bbox 排序
  87. dotsocr_data.sort(
  88. key=lambda x: (x['bbox'][1], x['bbox'][0])
  89. if 'bbox' in x else (float('inf'), float('inf'))
  90. )
  91. for item in dotsocr_data:
  92. # 🎯 转换为 MinerU 格式
  93. mineru_item = self._convert_dotsocr_to_mineru(item)
  94. category = mineru_item.get('type', '')
  95. # 🎯 根据类型处理
  96. if category.lower() == 'table':
  97. merged_item, paddle_pointer = self._process_table(
  98. mineru_item, paddle_text_boxes, paddle_pointer
  99. )
  100. merged_data.append(merged_item)
  101. elif category.lower() in ['text', 'title', 'header', 'footer']:
  102. merged_item, paddle_pointer, last_matched_index = self._process_text(
  103. mineru_item, paddle_text_boxes, paddle_pointer, last_matched_index
  104. )
  105. merged_data.append(merged_item)
  106. elif category.lower() == 'list':
  107. merged_item, paddle_pointer, last_matched_index = self._process_list(
  108. mineru_item, paddle_text_boxes, paddle_pointer, last_matched_index
  109. )
  110. merged_data.append(merged_item)
  111. else:
  112. # Page-header, Page-footer, Picture 等
  113. merged_data.append(mineru_item)
  114. return merged_data
  115. def _convert_dotsocr_to_mineru(self, dotsocr_item: Dict) -> Dict:
  116. """
  117. 🎯 将 DotsOCR 格式转换为 MinerU 格式
  118. DotsOCR:
  119. {
  120. "category": "Table",
  121. "bbox": [x1, y1, x2, y2],
  122. "text": "..."
  123. }
  124. MinerU:
  125. {
  126. "type": "table",
  127. "bbox": [x1, y1, x2, y2],
  128. "table_body": "...",
  129. "page_idx": 0
  130. }
  131. """
  132. category = dotsocr_item.get('category', '')
  133. # 🎯 Category 映射
  134. category_map = {
  135. 'Page-header': 'header',
  136. 'Page-footer': 'footer',
  137. 'Picture': 'image',
  138. 'Figure': 'image',
  139. 'Section-header': 'title',
  140. 'Table': 'table',
  141. 'Text': 'text',
  142. 'Title': 'title',
  143. 'List': 'list',
  144. 'Caption': 'title'
  145. }
  146. mineru_type = category_map.get(category, 'text')
  147. # 🎯 基础转换
  148. mineru_item = {
  149. 'type': mineru_type,
  150. 'bbox': dotsocr_item.get('bbox', []),
  151. 'page_idx': 0 # DotsOCR 默认单页
  152. }
  153. # 🎯 处理文本内容
  154. text = dotsocr_item.get('text', '')
  155. if mineru_type == 'table':
  156. # 表格:text -> table_body
  157. mineru_item['table_body'] = text
  158. else:
  159. # 其他类型:保持 text
  160. mineru_item['text'] = text
  161. # 标题级别
  162. if category == 'Section-header':
  163. mineru_item['text_level'] = 1
  164. return mineru_item
  165. def process_paddleocr_vl_data(self, paddleocr_vl_data: Dict,
  166. paddle_text_boxes: List[Dict]) -> List[Dict]:
  167. """
  168. 处理 PaddleOCR_VL 数据,添加 bbox 信息
  169. Args:
  170. paddleocr_vl_data: PaddleOCR_VL 数据 (JSON 对象)
  171. paddle_text_boxes: PaddleOCR 文字框列表
  172. Returns:
  173. 🎯 MinerU 格式的合并数据(统一输出格式)
  174. """
  175. merged_data = []
  176. paddle_pointer = 0
  177. last_matched_index = 0
  178. # 🎯 获取旋转角度和原始图像尺寸
  179. rotation_angle = self._get_rotation_angle_from_vl(paddleocr_vl_data)
  180. orig_image_size = None
  181. if rotation_angle != 0:
  182. orig_image_size = self._get_original_image_size_from_vl(paddleocr_vl_data)
  183. print(f"🔄 PaddleOCR_VL 检测到旋转角度: {rotation_angle}°")
  184. print(f"📐 原始图像尺寸: {orig_image_size[0]} x {orig_image_size[1]}")
  185. # 提取 parsing_res_list
  186. parsing_res_list = paddleocr_vl_data.get('parsing_res_list', [])
  187. # 按 bbox 排序
  188. parsing_res_list.sort(
  189. key=lambda x: (x['block_bbox'][1], x['block_bbox'][0])
  190. if 'block_bbox' in x else (float('inf'), float('inf'))
  191. )
  192. for item in parsing_res_list:
  193. # 🎯 先转换 bbox 坐标(如果需要)
  194. if rotation_angle != 0 and orig_image_size:
  195. item = self._transform_vl_block_bbox(item, rotation_angle, orig_image_size)
  196. # 🎯 统一转换为 MinerU 格式
  197. mineru_item = self._convert_paddleocr_vl_to_mineru(item)
  198. item_type = mineru_item.get('type', '')
  199. # 🎯 根据类型处理(复用 MinerU 的通用方法)
  200. if item_type == 'table':
  201. merged_item, paddle_pointer = self._process_table(
  202. mineru_item, paddle_text_boxes, paddle_pointer
  203. )
  204. merged_data.append(merged_item)
  205. elif item_type in ['text', 'title', 'header', 'footer', 'equation']:
  206. merged_item, paddle_pointer, last_matched_index = self._process_text(
  207. mineru_item, paddle_text_boxes, paddle_pointer, last_matched_index
  208. )
  209. merged_data.append(merged_item)
  210. elif item_type == 'list':
  211. merged_item, paddle_pointer, last_matched_index = self._process_list(
  212. mineru_item, paddle_text_boxes, paddle_pointer, last_matched_index
  213. )
  214. merged_data.append(merged_item)
  215. else:
  216. # 其他类型(image 等)直接添加
  217. merged_data.append(mineru_item)
  218. return merged_data
  219. def _get_rotation_angle_from_vl(self, paddleocr_vl_data: Dict) -> float:
  220. """从 PaddleOCR_VL 数据中获取旋转角度"""
  221. return BBoxExtractor._get_rotation_angle(paddleocr_vl_data)
  222. def _get_original_image_size_from_vl(self, paddleocr_vl_data: Dict) -> tuple:
  223. """从 PaddleOCR_VL 数据中获取原始图像尺寸"""
  224. return BBoxExtractor._get_original_image_size(paddleocr_vl_data)
  225. def _transform_vl_block_bbox(self, item: Dict, angle: float,
  226. orig_image_size: tuple) -> Dict:
  227. """
  228. 转换 PaddleOCR_VL 的 block_bbox 坐标
  229. Args:
  230. item: PaddleOCR_VL 的 block 数据
  231. angle: 旋转角度
  232. orig_image_size: 原始图像尺寸
  233. Returns:
  234. 转换后的 block 数据
  235. """
  236. transformed_item = item.copy()
  237. if 'block_bbox' not in item:
  238. return transformed_item
  239. block_bbox = item['block_bbox']
  240. if len(block_bbox) < 4:
  241. return transformed_item
  242. # block_bbox 格式: [x1, y1, x2, y2]
  243. # 转换为 poly 格式进行旋转
  244. poly = [
  245. [block_bbox[0], block_bbox[1]], # 左上
  246. [block_bbox[2], block_bbox[1]], # 右上
  247. [block_bbox[2], block_bbox[3]], # 右下
  248. [block_bbox[0], block_bbox[3]] # 左下
  249. ]
  250. # 🎯 使用 BBoxExtractor 的坐标转换方法
  251. transformed_poly = BBoxExtractor._inverse_rotate_coordinates(
  252. poly, angle, orig_image_size
  253. )
  254. # 转换回 bbox 格式
  255. xs = [p[0] for p in transformed_poly]
  256. ys = [p[1] for p in transformed_poly]
  257. transformed_bbox = [min(xs), min(ys), max(xs), max(ys)]
  258. transformed_item['block_bbox'] = transformed_bbox
  259. return transformed_item
  260. def _convert_paddleocr_vl_to_mineru(self, paddleocr_vl_item: Dict) -> Dict:
  261. """
  262. 🎯 将 PaddleOCR_VL 格式转换为 MinerU 格式
  263. 基于 PP-DocLayout_plus-L 的 20 种类别
  264. """
  265. block_label = paddleocr_vl_item.get('block_label', '')
  266. # 🎯 PP-DocLayout_plus-L 类别映射(共 20 种)
  267. label_map = {
  268. # 标题类(3种)
  269. 'paragraph_title': 'title',
  270. 'doc_title': 'title',
  271. 'figure_table_chart_title': 'title',
  272. # 文本类(9种)
  273. 'text': 'text',
  274. 'number': 'text',
  275. 'content': 'text',
  276. 'abstract': 'text',
  277. 'footnote': 'text',
  278. 'aside_text': 'text',
  279. 'algorithm': 'text',
  280. 'reference': 'text',
  281. 'reference_content': 'text',
  282. # 页眉页脚(2种)
  283. 'header': 'header',
  284. 'footer': 'footer',
  285. # 表格(1种)
  286. 'table': 'table',
  287. # 图片/图表(3种)
  288. 'image': 'image',
  289. 'chart': 'image',
  290. 'seal': 'image',
  291. # 公式(2种)
  292. 'formula': 'equation',
  293. 'formula_number': 'equation'
  294. }
  295. mineru_type = label_map.get(block_label, 'text')
  296. mineru_item = {
  297. 'type': mineru_type,
  298. 'bbox': paddleocr_vl_item.get('block_bbox', []),
  299. 'page_idx': 0
  300. }
  301. content = paddleocr_vl_item.get('block_content', '')
  302. if mineru_type == 'table':
  303. mineru_item['table_body'] = content
  304. else:
  305. mineru_item['text'] = content
  306. # 标题级别
  307. if block_label == 'doc_title':
  308. mineru_item['text_level'] = 1
  309. elif block_label == 'paragraph_title':
  310. mineru_item['text_level'] = 2
  311. elif block_label == 'figure_table_chart_title':
  312. mineru_item['text_level'] = 3
  313. return mineru_item
  314. def _process_table(self, item: Dict, paddle_text_boxes: List[Dict],
  315. start_pointer: int) -> Tuple[Dict, int]:
  316. """
  317. 处理表格类型(MinerU 格式)
  318. 策略:
  319. - 解析 HTML 表格
  320. - 为每个单元格匹配 PaddleOCR 的 bbox
  321. - 返回处理后的表格和新指针位置
  322. """
  323. table_body = item.get('table_body', '')
  324. if not table_body:
  325. print(f"⚠️ 表格内容为空,跳过")
  326. return item, start_pointer
  327. try:
  328. # 🔑 传入 table_bbox 用于筛选
  329. table_bbox = item.get('bbox') # MinerU 提供的表格边界
  330. # 🎯 委托给 TableCellMatcher
  331. enhanced_html, cells, new_pointer = \
  332. self.table_cell_matcher.enhance_table_html_with_bbox(
  333. table_body,
  334. paddle_text_boxes,
  335. start_pointer,
  336. table_bbox
  337. )
  338. # 更新 item
  339. item['table_body'] = enhanced_html
  340. item['table_cells'] = cells
  341. # 统计信息
  342. matched_count = len(cells)
  343. total_cells = len(BeautifulSoup(table_body, 'html.parser').find_all(['td', 'th']))
  344. print(f" 表格单元格: {matched_count}/{total_cells} 匹配")
  345. return item, new_pointer
  346. except Exception as e:
  347. print(f"⚠️ 表格处理失败: {e}")
  348. import traceback
  349. traceback.print_exc()
  350. return item, start_pointer
  351. def _process_text(self, item: Dict, paddle_text_boxes: List[Dict],
  352. paddle_pointer: int, last_matched_index: int) -> Tuple[Dict, int, int]:
  353. """处理文本"""
  354. merged_item = item.copy()
  355. text = item.get('text', '')
  356. matched_bbox, paddle_pointer, last_matched_index = \
  357. self.text_matcher.find_matching_bbox(
  358. text, paddle_text_boxes, paddle_pointer, last_matched_index,
  359. self.look_ahead_window
  360. )
  361. if matched_bbox:
  362. matched_bbox['used'] = True
  363. return merged_item, paddle_pointer, last_matched_index
  364. def _process_list(self, item: Dict, paddle_text_boxes: List[Dict],
  365. paddle_pointer: int, last_matched_index: int) -> Tuple[Dict, int, int]:
  366. """处理列表"""
  367. merged_item = item.copy()
  368. list_items = item.get('list_items', [])
  369. for list_item in list_items:
  370. matched_bbox, paddle_pointer, last_matched_index = \
  371. self.text_matcher.find_matching_bbox(
  372. list_item, paddle_text_boxes, paddle_pointer, last_matched_index,
  373. self.look_ahead_window
  374. )
  375. if matched_bbox:
  376. matched_bbox['used'] = True
  377. return merged_item, paddle_pointer, last_matched_index