mirror of
https://github.com/opendatalab/MinerU.git
synced 2026-09-21 12:42:22 +08:00
feat: implement post-OCR fallback mechanism for private use text handling
This commit is contained in:
@@ -20,6 +20,10 @@ from mineru.utils.cut_image import cut_image_and_table
|
||||
from mineru.utils.enum_class import ContentType, BlockType
|
||||
from mineru.utils.hash_utils import bytes_md5
|
||||
from mineru.utils.ocr_utils import OcrConfidence, rotate_vertical_crop_if_needed
|
||||
from mineru.utils.span_pre_proc import (
|
||||
_clear_post_ocr_fallback,
|
||||
_restore_post_ocr_fallback,
|
||||
)
|
||||
from mineru.utils.title_level_postprocess import apply_title_leveling_to_pdf_info
|
||||
from mineru.utils.pdfium_guard import close_pdfium_child, close_pdfium_document, pdfium_guard
|
||||
from mineru.version import __version__
|
||||
@@ -152,6 +156,9 @@ def _apply_post_ocr(pdf_info_list, hybrid_pipeline_model):
|
||||
if ocr_score > OcrConfidence.min_confidence:
|
||||
span['content'] = ocr_text
|
||||
span['score'] = float(f"{ocr_score:.3f}")
|
||||
_clear_post_ocr_fallback(span)
|
||||
elif _restore_post_ocr_fallback(span):
|
||||
continue
|
||||
else:
|
||||
span['content'] = ''
|
||||
span['score'] = 0.0
|
||||
|
||||
@@ -19,6 +19,10 @@ from mineru.utils.ocr_utils import OcrConfidence, rotate_vertical_crop_if_needed
|
||||
from mineru.version import __version__
|
||||
from mineru.utils.hash_utils import bytes_md5
|
||||
from mineru.utils.pdfium_guard import close_pdfium_child, close_pdfium_document, pdfium_guard
|
||||
from mineru.utils.span_pre_proc import (
|
||||
_clear_post_ocr_fallback,
|
||||
_restore_post_ocr_fallback,
|
||||
)
|
||||
|
||||
|
||||
def page_model_info_to_page_info(page_model_info, image_dict, page, image_writer, page_index, ocr_enable=False):
|
||||
@@ -245,6 +249,9 @@ def _apply_post_ocr(pdf_info_list, lang=None):
|
||||
if ocr_score > OcrConfidence.min_confidence:
|
||||
span['content'] = ocr_text
|
||||
span['score'] = float(f"{ocr_score:.3f}")
|
||||
_clear_post_ocr_fallback(span)
|
||||
elif _restore_post_ocr_fallback(span):
|
||||
continue
|
||||
else:
|
||||
span['content'] = ''
|
||||
span['score'] = 0.0
|
||||
|
||||
@@ -15,6 +15,15 @@ from mineru.utils.pdf_text_tool import get_lines_from_chars, get_page_chars
|
||||
from mineru.utils.pdfium_guard import close_pdfium_child, pdfium_guard
|
||||
|
||||
MAX_NATIVE_TEXT_CHARS_PER_PAGE = 65535
|
||||
PRIVATE_USE_AREA_START = 0xE000
|
||||
PRIVATE_USE_AREA_END = 0xF8FF
|
||||
PRIVATE_USE_TEXT_COUNT_THRESHOLD = 2
|
||||
PRIVATE_USE_TEXT_RATIO_THRESHOLD = 0.05
|
||||
PRIVATE_USE_TEXT_RUN_THRESHOLD = 2
|
||||
POST_OCR_FALLBACK_CONTENT_KEY = '_post_ocr_fallback_content'
|
||||
POST_OCR_FALLBACK_SCORE_KEY = '_post_ocr_fallback_score'
|
||||
POST_OCR_REASON_KEY = '_post_ocr_reason'
|
||||
POST_OCR_REASON_PRIVATE_USE_TEXT = 'private_use_text'
|
||||
|
||||
|
||||
def __replace_ligatures(text: str):
|
||||
@@ -141,6 +150,8 @@ def _prepare_post_ocr_spans(need_ocr_spans, spans, pil_img, scale):
|
||||
span_img = cv2.cvtColor(np.array(span_pil_img), cv2.COLOR_RGB2BGR)
|
||||
# 计算span的对比度,低于0.17的span不进行ocr,等于0.17的临界框保留给后置OCR。
|
||||
if calculate_contrast(span_img, img_mode='bgr') < 0.17:
|
||||
if _restore_post_ocr_fallback(span):
|
||||
continue
|
||||
if span in spans:
|
||||
spans.remove(span)
|
||||
continue
|
||||
@@ -267,9 +278,18 @@ def fill_char_in_spans(spans, all_chars, median_span_height):
|
||||
|
||||
need_ocr_spans = []
|
||||
for span in spans:
|
||||
private_use_signal = _get_private_use_text_signal(span['chars'])
|
||||
should_post_ocr_private_use = _should_fallback_to_post_ocr_for_private_use_text(
|
||||
private_use_signal
|
||||
)
|
||||
chars_to_content(span)
|
||||
if should_post_ocr_private_use and span.get('content'):
|
||||
span[POST_OCR_FALLBACK_CONTENT_KEY] = span['content']
|
||||
span[POST_OCR_FALLBACK_SCORE_KEY] = span.get('score', 1.0)
|
||||
span[POST_OCR_REASON_KEY] = POST_OCR_REASON_PRIVATE_USE_TEXT
|
||||
need_ocr_spans.append(span)
|
||||
# 有的span中虽然没有字但有一两个空的占位符,用宽高和content长度过滤
|
||||
if len(span['content']) * span['height'] < span['width'] * 0.5:
|
||||
elif len(span['content']) * span['height'] < span['width'] * 0.5:
|
||||
# logger.info(f"maybe empty span: {len(span['content'])}, {span['height']}, {span['width']}")
|
||||
need_ocr_spans.append(span)
|
||||
del span['height'], span['width']
|
||||
@@ -284,6 +304,79 @@ SCRIPT_BODY_HEIGHT_RATIO = 0.9
|
||||
SCRIPT_CENTER_TOLERANCE_RATIO = 0.12
|
||||
|
||||
|
||||
def _is_private_use_char(char: str) -> bool:
|
||||
"""判断单个字符是否落在 Unicode 私用区,用于识别字体映射异常。"""
|
||||
return (
|
||||
len(char) == 1
|
||||
and PRIVATE_USE_AREA_START <= ord(char) <= PRIVATE_USE_AREA_END
|
||||
)
|
||||
|
||||
|
||||
def _get_private_use_text_signal(chars):
|
||||
"""统计 span 字符中的私用区信号,供局部后置 OCR 决策使用。"""
|
||||
pua_count = 0
|
||||
text_char_count = 0
|
||||
current_pua_run = 0
|
||||
max_pua_run = 0
|
||||
|
||||
for char in chars:
|
||||
for text_char in char.get('char', ''):
|
||||
if text_char.isspace():
|
||||
current_pua_run = 0
|
||||
continue
|
||||
|
||||
text_char_count += 1
|
||||
if _is_private_use_char(text_char):
|
||||
pua_count += 1
|
||||
current_pua_run += 1
|
||||
max_pua_run = max(max_pua_run, current_pua_run)
|
||||
else:
|
||||
current_pua_run = 0
|
||||
|
||||
pua_ratio = 0.0
|
||||
if text_char_count > 0:
|
||||
pua_ratio = pua_count / text_char_count
|
||||
|
||||
return {
|
||||
'pua_count': pua_count,
|
||||
'text_char_count': text_char_count,
|
||||
'pua_ratio': pua_ratio,
|
||||
'max_pua_run': max_pua_run,
|
||||
}
|
||||
|
||||
|
||||
def _should_fallback_to_post_ocr_for_private_use_text(signal) -> bool:
|
||||
"""连续或高占比 PUA 才转后置 OCR,降低孤立私用符号误召回。"""
|
||||
pua_count = signal['pua_count']
|
||||
if pua_count < PRIVATE_USE_TEXT_COUNT_THRESHOLD:
|
||||
return False
|
||||
|
||||
return (
|
||||
signal['max_pua_run'] >= PRIVATE_USE_TEXT_RUN_THRESHOLD
|
||||
or signal['pua_ratio'] >= PRIVATE_USE_TEXT_RATIO_THRESHOLD
|
||||
)
|
||||
|
||||
|
||||
def _clear_post_ocr_fallback(span):
|
||||
"""清理后置 OCR 内部兜底字段,避免进入最终 middle-json 输出。"""
|
||||
span.pop(POST_OCR_FALLBACK_CONTENT_KEY, None)
|
||||
span.pop(POST_OCR_FALLBACK_SCORE_KEY, None)
|
||||
span.pop(POST_OCR_REASON_KEY, None)
|
||||
|
||||
|
||||
def _restore_post_ocr_fallback(span) -> bool:
|
||||
"""在后置 OCR 无法使用时恢复原始文本兜底,返回是否已恢复。"""
|
||||
if POST_OCR_FALLBACK_CONTENT_KEY not in span:
|
||||
_clear_post_ocr_fallback(span)
|
||||
return False
|
||||
|
||||
span['content'] = span[POST_OCR_FALLBACK_CONTENT_KEY]
|
||||
if POST_OCR_FALLBACK_SCORE_KEY in span:
|
||||
span['score'] = span[POST_OCR_FALLBACK_SCORE_KEY]
|
||||
_clear_post_ocr_fallback(span)
|
||||
return True
|
||||
|
||||
|
||||
def calculate_char_in_span(char_bbox, span_bbox, char, span_height_ratio=Span_Height_Ratio):
|
||||
char_center_x = (char_bbox[0] + char_bbox[2]) / 2
|
||||
char_center_y = (char_bbox[1] + char_bbox[3]) / 2
|
||||
|
||||
Reference in New Issue
Block a user