feat(seal): add double verification and institution name cleaning

Key improvements:
1. Double verification mechanism for OCR failures
   - When unwarp OCR fails (empty text), automatically try PaddleOCRVL backup on crop
   - Fixes issue where correct seal was ignored due to unwarp image distortion
   - Test result: 4% → 93.8% similarity on problematic PDFs

2. Institution name cleaning
   - Remove unwanted suffixes: 检验检测专用章, 专用章, etc.
   - Clean names before adding to results and similarity calculation
   - Improves matching accuracy

3. Enhanced logging for institution selection
   - Show all extracted institutions with similarity scores
   - Track why specific institution was selected
   - Better debugging and transparency

Example impact:
- Before: "成都虹之川科技有限公司" (wrong seal, 4% similarity)
- After: "中科测试技术(广东)集团有限公司" (correct seal, 93.8% similarity)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
黄仁欢 2026-02-08 13:46:56 +08:00
parent 5a493b8d67
commit 52f283c7c9
1 changed files with 95 additions and 8 deletions

View File

@ -717,8 +717,10 @@ def extract_seals_and_institutions(page_img, output_dir, ocr_model="ppocr_v5", v
result['seals'].append(seal_data)
if ocr_result['success']:
result['institutions'].append(ocr_result['text'])
logger.info(f" ✓ Seal #{i} SUCCESS: {ocr_result['text'][:50]}... (confidence: {ocr_result['score']:.4f})")
# Clean the institution name before adding
cleaned_name = clean_institution_name(ocr_result['text'])
result['institutions'].append(cleaned_name)
logger.info(f" ✓ Seal #{i} SUCCESS: {cleaned_name[:50]}... (confidence: {ocr_result['score']:.4f})")
else:
logger.warning(f" ✗ Seal #{i} FAILED: Could not extract institution name")
@ -872,8 +874,9 @@ def extract_seals_and_institutions(page_img, output_dir, ocr_model="ppocr_v5", v
marked_path = os.path.join(output_dir, f"seal_marked_{i}.png")
imwrite_safe(marked_path, marked)
# OCR recognition
# OCR recognition with double verification
ocr_result = {'text': '', 'score': 0.0, 'success': False}
ocr_method_used = method_used
if unwarp is not None:
# Standard path: Recognize unwarp image
@ -885,13 +888,35 @@ def extract_seals_and_institutions(page_img, output_dir, ocr_model="ppocr_v5", v
else:
ocr_result = run_ocr_recognition(unwarp_path, rec_model)
logger.info(f" Seal #{i} OCR Result:")
ocr_method_used = f"{method_used}_unwarp"
logger.info(f" Seal #{i} OCR Result (unwarp):")
logger.info(f" - Text: '{ocr_result['text']}'")
logger.info(f" - Score: {ocr_result['score']:.4f}")
logger.info(f" - Success: {ocr_result['success']}")
logger.info(f" - Text length: {len(ocr_result['text'])} chars")
if used_fallback:
logger.info(f" - ** Used fallback angle range (7:30 to 4:30) **")
# ============ DOUBLE VERIFICATION: Try PaddleOCRVL on crop if unwarp OCR fails ============
# If unwarp OCR failed (empty text or success=False), try PaddleOCRVL backup on crop
if (not ocr_result['success'] or len(ocr_result['text'].strip()) == 0) and vl_pipeline is not None and PADDLEOCRVL_AVAILABLE:
logger.warning(f" Seal #{i}: Unwarp OCR failed (empty result), trying PaddleOCRVL backup on crop image")
seal_crop_path = os.path.join(output_dir, f"seal_crop_{i}.png")
backup_result = run_ocr_recognition_vl(seal_crop_path, vl_pipeline)
logger.info(f" Seal #{i} PaddleOCRVL Backup Result (crop):")
logger.info(f" - Text: '{backup_result['text']}'")
logger.info(f" - Score: {backup_result['score']:.4f}")
logger.info(f" - Success: {backup_result['success']}")
logger.info(f" - Text length: {len(backup_result['text'])} chars")
# Use backup result if it's better (non-empty text)
if backup_result['success'] and len(backup_result['text'].strip()) > 0:
logger.info(f" Seal #{i}: ** Using PaddleOCRVL backup result (unwarp failed) **")
ocr_result = backup_result
ocr_method_used = f"{method_used}_crop_backup"
else:
logger.warning(f" Seal #{i}: ** Both unwarp and crop OCR failed **")
else:
# ============ BACKUP: Use PaddleOCRVL directly on seal crop ============
logger.warning(f" Seal #{i}: No unwarp image available (polar unwarp failed)")
@ -900,6 +925,7 @@ def extract_seals_and_institutions(page_img, output_dir, ocr_model="ppocr_v5", v
logger.info(f" Seal #{i}: Using PaddleOCRVL backup - directly recognize seal crop image")
seal_crop_path = os.path.join(output_dir, f"seal_crop_{i}.png")
ocr_result = run_ocr_recognition_vl(seal_crop_path, vl_pipeline)
ocr_method_used = f"{method_used}_crop_backup"
logger.info(f" Seal #{i} PaddleOCRVL Backup Result:")
logger.info(f" - Text: '{ocr_result['text']}'")
logger.info(f" - Score: {ocr_result['score']:.4f}")
@ -919,7 +945,7 @@ def extract_seals_and_institutions(page_img, output_dir, ocr_model="ppocr_v5", v
'text': ocr_result['text'],
'confidence': float(ocr_result['score']),
'success': bool(ocr_result['success']),
'method_used': method_used, # Add method tracking
'method_used': ocr_method_used, # Track actual OCR method used
'used_fallback': used_fallback, # Track if fallback was used
'debug_info': {
'center': center,
@ -934,8 +960,10 @@ def extract_seals_and_institutions(page_img, output_dir, ocr_model="ppocr_v5", v
result['seals'].append(seal_data)
if ocr_result['success']:
result['institutions'].append(ocr_result['text'])
logger.info(f" ✓ Seal #{i} SUCCESS: {ocr_result['text'][:50]}... (confidence: {ocr_result['score']:.4f})")
# Clean the institution name before adding
cleaned_name = clean_institution_name(ocr_result['text'])
result['institutions'].append(cleaned_name)
logger.info(f" ✓ Seal #{i} SUCCESS: {cleaned_name[:50]}... (confidence: {ocr_result['score']:.4f})")
else:
logger.warning(f" ✗ Seal #{i} FAILED: Could not extract institution name")
@ -943,6 +971,56 @@ def extract_seals_and_institutions(page_img, output_dir, ocr_model="ppocr_v5", v
return result
# ============ Text Cleaning Functions ============
def clean_institution_name(text: str) -> str:
"""
Clean extracted institution name by removing unwanted suffixes.
Removes common seal-related text that is not part of the institution name:
- 检验检测专用章
- 检验检测专用
- 专用章
- 及其他变体
Args:
text: Raw extracted institution name
Returns:
Cleaned institution name
"""
if not text:
return text
# Define patterns to remove (order matters: most specific first)
patterns_to_remove = [
'检验检测专用章',
'检验检测专用',
'检测专用章',
'检验专用章',
'专用章',
'(检验检测)',
'(检验检测)',
'【检验检测】',
'[检验检测]',
]
cleaned = text
for pattern in patterns_to_remove:
if pattern in cleaned:
cleaned = cleaned.replace(pattern, '')
logger.debug(f"Removed pattern '{pattern}' from institution name")
# Strip whitespace
cleaned = cleaned.strip()
# Log if cleaning occurred
if cleaned != text:
logger.info(f"Cleaned institution name: '{text}''{cleaned}'")
return cleaned
# ============ Similarity and Matching Functions ============
def calculate_similarity(str1: str, str2: str) -> float:
@ -1122,23 +1200,32 @@ def process_single_pdf(pdf_name: str, expected_cma: str, expected_inst: str,
# Select best institution match
if seal_result['institutions']:
logger.info(f" Institution Extraction:")
logger.info(f" - Expected: {expected_inst if expected_inst else 'N/A'}")
logger.info(f" - Found {len(seal_result['institutions'])} institution(s) from seals")
# Find best matching institution
best_inst = None
best_similarity = 0.0
for inst in seal_result['institutions']:
for idx, inst in enumerate(seal_result['institutions']):
if expected_inst and expected_inst != "":
sim = calculate_similarity(inst, expected_inst)
logger.info(f" - Inst #{idx+1}: '{inst[:50]}...' → Similarity: {sim:.1f}%")
if sim > best_similarity:
best_similarity = sim
best_inst = inst
logger.info(f" → New best match! ({sim:.1f}% > {best_similarity:.1f}%)")
elif not best_inst:
best_inst = inst
logger.info(f" - Inst #{idx+1}: '{inst[:50]}...' (no expected value for comparison)")
# Fallback: if best_inst is still None (all similarities were 0), use first institution
if best_inst is None and seal_result['institutions']:
best_inst = seal_result['institutions'][0]
logger.warning(f" - All similarities were 0%, using first institution: '{best_inst[:50]}...'")
logger.info(f" - Selected: '{best_inst[:50]}...' (similarity: {best_similarity:.1f}%)")
result['extracted']['institution'] = best_inst
# Compare institution