main.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import html
  2. import logging
  3. import os
  4. import time
  5. import traceback
  6. from dataclasses import dataclass, asdict
  7. from typing import List, Optional, Union, Dict, Any
  8. import numpy as np
  9. import cv2
  10. from PIL import Image
  11. from loguru import logger
  12. from ..slanet_plus.main import RapidTableInput, RapidTable
  13. from .table_structure_unet import TSRUnet
  14. from mineru.utils.enum_class import ModelPath
  15. from mineru.utils.models_download_utils import auto_download_and_get_model_root_path
  16. from .table_recover import TableRecover
  17. from .utils import InputType, LoadImage, VisTable
  18. from .utils_table_recover import (
  19. match_ocr_cell,
  20. plot_html_table,
  21. box_4_2_poly_to_box_4_1,
  22. sorted_ocr_boxes,
  23. gather_ocr_list_by_row,
  24. )
  25. @dataclass
  26. class WiredTableInput:
  27. model_path: str
  28. device: str = "cpu"
  29. @dataclass
  30. class WiredTableOutput:
  31. pred_html: Optional[str] = None
  32. cell_bboxes: Optional[np.ndarray] = None
  33. logic_points: Optional[np.ndarray] = None
  34. elapse: Optional[float] = None
  35. class WiredTableRecognition:
  36. def __init__(self, config: WiredTableInput, ocr_engine=None):
  37. self.table_structure = TSRUnet(asdict(config))
  38. self.load_img = LoadImage()
  39. self.table_recover = TableRecover()
  40. self.ocr_engine = ocr_engine
  41. def __call__(
  42. self,
  43. img: InputType,
  44. ocr_result: Optional[List[Union[List[List[float]], str, str]]] = None,
  45. **kwargs,
  46. ) -> WiredTableOutput:
  47. s = time.perf_counter()
  48. need_ocr = True
  49. col_threshold = 15
  50. row_threshold = 10
  51. if kwargs:
  52. need_ocr = kwargs.get("need_ocr", True)
  53. col_threshold = kwargs.get("col_threshold", 15)
  54. row_threshold = kwargs.get("row_threshold", 10)
  55. img = self.load_img(img)
  56. polygons, rotated_polygons = self.table_structure(img, **kwargs)
  57. if polygons is None:
  58. logging.warning("polygons is None.")
  59. return WiredTableOutput("", None, None, 0.0)
  60. try:
  61. table_res, logi_points = self.table_recover(
  62. rotated_polygons, row_threshold, col_threshold
  63. )
  64. # 将坐标由逆时针转为顺时针方向,后续处理与无线表格对齐
  65. polygons[:, 1, :], polygons[:, 3, :] = (
  66. polygons[:, 3, :].copy(),
  67. polygons[:, 1, :].copy(),
  68. )
  69. if not need_ocr:
  70. sorted_polygons, idx_list = sorted_ocr_boxes(
  71. [box_4_2_poly_to_box_4_1(box) for box in polygons]
  72. )
  73. return WiredTableOutput(
  74. "",
  75. sorted_polygons,
  76. logi_points[idx_list],
  77. time.perf_counter() - s,
  78. )
  79. cell_box_det_map, not_match_orc_boxes = match_ocr_cell(ocr_result, polygons)
  80. # 如果有识别框没有ocr结果,直接进行rec补充
  81. cell_box_det_map = self.fill_blank_rec(img, polygons, cell_box_det_map)
  82. # 转换为中间格式,修正识别框坐标,将物理识别框,逻辑识别框,ocr识别框整合为dict,方便后续处理
  83. t_rec_ocr_list = self.transform_res(cell_box_det_map, polygons, logi_points)
  84. # 将每个单元格中的ocr识别结果排序和同行合并,输出的html能完整保留文字的换行格式
  85. t_rec_ocr_list = self.sort_and_gather_ocr_res(t_rec_ocr_list)
  86. logi_points = [t_box_ocr["t_logic_box"] for t_box_ocr in t_rec_ocr_list]
  87. cell_box_det_map = {
  88. i: [ocr_box_and_text[1] for ocr_box_and_text in t_box_ocr["t_ocr_res"]]
  89. for i, t_box_ocr in enumerate(t_rec_ocr_list)
  90. }
  91. pred_html = plot_html_table(logi_points, cell_box_det_map)
  92. polygons = np.array(polygons).reshape(-1, 8)
  93. logi_points = np.array(logi_points)
  94. elapse = time.perf_counter() - s
  95. except Exception:
  96. logging.warning(traceback.format_exc())
  97. return WiredTableOutput("", None, None, 0.0)
  98. return WiredTableOutput(pred_html, polygons, logi_points, elapse)
  99. def transform_res(
  100. self,
  101. cell_box_det_map: Dict[int, List[any]],
  102. polygons: np.ndarray,
  103. logi_points: List[np.ndarray],
  104. ) -> List[Dict[str, any]]:
  105. res = []
  106. for i in range(len(polygons)):
  107. ocr_res_list = cell_box_det_map.get(i)
  108. if not ocr_res_list:
  109. continue
  110. xmin = min([ocr_box[0][0][0] for ocr_box in ocr_res_list])
  111. ymin = min([ocr_box[0][0][1] for ocr_box in ocr_res_list])
  112. xmax = max([ocr_box[0][2][0] for ocr_box in ocr_res_list])
  113. ymax = max([ocr_box[0][2][1] for ocr_box in ocr_res_list])
  114. dict_res = {
  115. # xmin,xmax,ymin,ymax
  116. "t_box": [xmin, ymin, xmax, ymax],
  117. # row_start,row_end,col_start,col_end
  118. "t_logic_box": logi_points[i].tolist(),
  119. # [[xmin,xmax,ymin,ymax], text]
  120. "t_ocr_res": [
  121. [box_4_2_poly_to_box_4_1(ocr_det[0]), ocr_det[1]]
  122. for ocr_det in ocr_res_list
  123. ],
  124. }
  125. res.append(dict_res)
  126. return res
  127. def sort_and_gather_ocr_res(self, res):
  128. for i, dict_res in enumerate(res):
  129. _, sorted_idx = sorted_ocr_boxes(
  130. [ocr_det[0] for ocr_det in dict_res["t_ocr_res"]], threhold=0.3
  131. )
  132. dict_res["t_ocr_res"] = [dict_res["t_ocr_res"][i] for i in sorted_idx]
  133. dict_res["t_ocr_res"] = gather_ocr_list_by_row(
  134. dict_res["t_ocr_res"], threhold=0.3
  135. )
  136. return res
  137. # def fill_blank_rec(
  138. # self,
  139. # img: np.ndarray,
  140. # sorted_polygons: np.ndarray,
  141. # cell_box_map: Dict[int, List[str]],
  142. # ) -> Dict[int, List[Any]]:
  143. # """找到poly对应为空的框,尝试将直接将poly框直接送到识别中"""
  144. # for i in range(sorted_polygons.shape[0]):
  145. # if cell_box_map.get(i):
  146. # continue
  147. # box = sorted_polygons[i]
  148. # cell_box_map[i] = [[box, "", 1]]
  149. # continue
  150. # return cell_box_map
  151. def fill_blank_rec(
  152. self,
  153. img: np.ndarray,
  154. sorted_polygons: np.ndarray,
  155. cell_box_map: Dict[int, List[str]],
  156. ) -> Dict[int, List[Any]]:
  157. """找到poly对应为空的框,尝试将直接将poly框直接送到识别中"""
  158. bgr_img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
  159. img_crop_info_list = []
  160. img_crop_list = []
  161. for i in range(sorted_polygons.shape[0]):
  162. if cell_box_map.get(i):
  163. continue
  164. box = sorted_polygons[i]
  165. if self.ocr_engine is None:
  166. logger.warning(f"No OCR engine provided for box {i}: {box}")
  167. continue
  168. # 从img中截取对应的区域
  169. x1, y1, x2, y2 = int(box[0][0]), int(box[0][1]), int(box[2][0]), int(box[2][1])
  170. if x1 >= x2 or y1 >= y2:
  171. logger.warning(f"Invalid box coordinates: {box}")
  172. continue
  173. # 判断长宽比
  174. if (x2 - x1) / (y2 - y1) > 20 or (y2 - y1) / (x2 - x1) > 20:
  175. logger.warning(f"Box {i} has invalid aspect ratio: {x1, y1, x2, y2}")
  176. continue
  177. img_crop = bgr_img[int(y1):int(y2), int(x1):int(x2)]
  178. img_crop_list.append(img_crop)
  179. img_crop_info_list.append([i, box])
  180. if len(img_crop_list) > 0:
  181. # 进行ocr识别
  182. ocr_result = self.ocr_engine.ocr(img_crop_list, det=False)
  183. if not ocr_result or not isinstance(ocr_result, list) or len(ocr_result) == 0:
  184. logger.warning("OCR engine returned no results or invalid result for image crops.")
  185. return cell_box_map
  186. ocr_res_list = ocr_result[0]
  187. if not isinstance(ocr_res_list, list) or len(ocr_res_list) != len(img_crop_list):
  188. logger.warning("OCR result list length does not match image crop list length.")
  189. return cell_box_map
  190. for j, ocr_res in enumerate(ocr_res_list):
  191. img_crop_info_list[j].append(ocr_res)
  192. for i, box, ocr_res in img_crop_info_list:
  193. # 处理ocr结果
  194. ocr_text, ocr_score = ocr_res
  195. # logger.debug(f"OCR result for box {i}: {ocr_text} with score {ocr_score}")
  196. if ocr_score < 0.9 or ocr_text in ['1']:
  197. # logger.warning(f"Low confidence OCR result for box {i}: {ocr_text} with score {ocr_score}")
  198. box = sorted_polygons[i]
  199. cell_box_map[i] = [[box, "", 0.5]]
  200. continue
  201. cell_box_map[i] = [[box, ocr_text, ocr_score]]
  202. return cell_box_map
  203. def escape_html(input_string):
  204. """Escape HTML Entities."""
  205. return html.escape(input_string)
  206. def count_table_cells_physical(html_code):
  207. """计算表格的物理单元格数量(合并单元格算一个)"""
  208. if not html_code:
  209. return 0
  210. # 简单计数td和th标签的数量
  211. html_lower = html_code.lower()
  212. td_count = html_lower.count('<td')
  213. th_count = html_lower.count('<th')
  214. return td_count + th_count
  215. class UnetTableModel:
  216. def __init__(self, ocr_engine):
  217. model_path = os.path.join(auto_download_and_get_model_root_path(ModelPath.unet_structure), ModelPath.unet_structure)
  218. wired_input_args = WiredTableInput(model_path=model_path)
  219. self.wired_table_model = WiredTableRecognition(wired_input_args, ocr_engine)
  220. slanet_plus_model_path = os.path.join(auto_download_and_get_model_root_path(ModelPath.slanet_plus), ModelPath.slanet_plus)
  221. wireless_input_args = RapidTableInput(model_type='slanet_plus', model_path=slanet_plus_model_path)
  222. self.wireless_table_model = RapidTable(wireless_input_args)
  223. self.ocr_engine = ocr_engine
  224. def predict(self, input_img, table_cls_score, wireless_html_code):
  225. if isinstance(input_img, Image.Image):
  226. np_img = np.asarray(input_img)
  227. elif isinstance(input_img, np.ndarray):
  228. np_img = input_img
  229. else:
  230. raise ValueError("Input must be a pillow object or a numpy array.")
  231. bgr_img = cv2.cvtColor(np_img, cv2.COLOR_RGB2BGR)
  232. ocr_result = self.ocr_engine.ocr(bgr_img)[0]
  233. if ocr_result:
  234. ocr_result = [
  235. [item[0], escape_html(item[1][0]), item[1][1]]
  236. for item in ocr_result
  237. if len(item) == 2 and isinstance(item[1], tuple)
  238. ]
  239. else:
  240. ocr_result = None
  241. if ocr_result:
  242. try:
  243. wired_table_results = self.wired_table_model(np_img, ocr_result)
  244. wired_html_code = wired_table_results.pred_html
  245. wired_len = count_table_cells_physical(wired_html_code)
  246. wireless_len = count_table_cells_physical(wireless_html_code)
  247. # logger.debug(f"wired table cell bboxes: {wired_len}, wireless table cell bboxes: {wireless_len}")
  248. # 计算两种模型检测的单元格数量差异
  249. gap_of_len = wireless_len - wired_len
  250. # 判断是否使用无线表格模型的结果
  251. if (
  252. wired_len <= int(wireless_len * 0.55)+1 # 有线模型检测到的单元格数太少(低于无线模型的50%)
  253. # or ((round(wireless_len*1.2) < wired_len) and (wired_len < (2 * wireless_len)) and table_cls_score <= 0.94) # 有线模型检测到的单元格数反而更多
  254. or (0 <= gap_of_len <= 5 and wired_len <= round(wireless_len * 0.75)) # 两者相差不大但有线模型结果较少
  255. or (gap_of_len == 0 and wired_len <= 4) # 单元格数量完全相等且总量小于等于4
  256. ):
  257. # logger.debug("fall back to wireless table model")
  258. html_code = wireless_html_code
  259. else:
  260. html_code = wired_html_code
  261. return html_code
  262. except Exception as e:
  263. logger.exception(e)
  264. return None