Spaces:
Sleeping
Sleeping
Download src/extractor.py from Hztech/passport-extractor1: direct link, hf CLI and curl.
- Browser
- Download file 8.28 kB
-
https://huggingface.co/spaces/Hztech/passport-extractor1/resolve/main/src/extractor.py
- Command line
-
hf download hf://spaces/Hztech/passport-extractor1/src/extractor.py
-
curl -L -o extractor.py https://huggingface.co/spaces/Hztech/passport-extractor1/resolve/main/src/extractor.py
8.28 kB
| import os | |
| import cv2 | |
| import numpy as np | |
| import easyocr | |
| import warnings | |
| import ssl | |
| import re | |
| from passporteye import read_mrz | |
| from pdf2image import convert_from_path | |
| from PIL import Image | |
| import string as st | |
| from src.utils import ( | |
| clean_string, | |
| clean_mrz_line, | |
| parse_date, | |
| get_country_name, | |
| get_sex, | |
| setup_logger, | |
| clean_name_field | |
| ) | |
| from src.fallback_mrz import FallbackMRZ | |
| from config.settings import USE_GPU, OCR_LANGUAGES, TEMP_DIR | |
| warnings.filterwarnings("ignore") | |
| logger = setup_logger(__name__) | |
| # Fix SSL issue (Mac EasyOCR model download fix) | |
| try: | |
| _create_unverified_https_context = ssl._create_unverified_context | |
| except AttributeError: | |
| pass | |
| else: | |
| ssl._create_default_https_context = _create_unverified_https_context | |
| class PassportExtractor: | |
| def __init__(self, use_gpu=USE_GPU, languages=None): | |
| self.languages = languages if languages else OCR_LANGUAGES | |
| base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| model_dir = os.path.join(base_dir, "data", "models") | |
| os.makedirs(model_dir, exist_ok=True) | |
| logger.debug(f"Initializing EasyOCR Reader (GPU={use_gpu})...") | |
| # Create custom user network directory to avoid permission issues | |
| user_network_dir = os.path.join(base_dir, "data", "easyocr_user_network") | |
| os.makedirs(user_network_dir, exist_ok=True) | |
| self.reader = easyocr.Reader( | |
| self.languages, | |
| gpu=use_gpu, | |
| model_storage_directory=model_dir, | |
| user_network_directory=user_network_dir, | |
| verbose=False | |
| ) | |
| logger.debug("EasyOCR initialized.") | |
| # --------------------------------------------------- | |
| # VISUAL GIVEN NAME EXTRACTION | |
| # --------------------------------------------------- | |
| def extract_given_names_from_visual(self, img_path): | |
| try: | |
| results = self.reader.readtext(img_path, detail=0) | |
| lines = [r.strip() for r in results if r.strip()] | |
| for i, line in enumerate(lines): | |
| upper_line = line.upper() | |
| if "GIVEN" in upper_line and "NAME" in upper_line: | |
| if ":" in line: | |
| candidate = line.split(":")[1].strip() | |
| else: | |
| if i + 1 < len(lines): | |
| candidate = lines[i + 1].strip() | |
| else: | |
| return "" | |
| candidate = re.sub(r'[^A-Za-z\s]', '', candidate) | |
| candidate = re.sub(r'\s+', ' ', candidate).strip() | |
| candidate = re.sub(r'([A-Z]{2,})[K]$', r'\1', candidate) | |
| return candidate.strip() | |
| return "" | |
| except Exception as e: | |
| logger.error(f"Given Names extraction failed: {e}") | |
| return "" | |
| # --------------------------------------------------- | |
| # MRZ EXTRACTION (SMART FALLBACK) | |
| # --------------------------------------------------- | |
| def extract_mrz_easyocr(self, img_path): | |
| """Find MRZ lines using EasyOCR when Tesseract is missing.""" | |
| try: | |
| # Use EasyOCR to read ALL text | |
| allow = st.ascii_uppercase + st.digits + "<" | |
| results = self.reader.readtext(img_path, detail=0, allowlist=allow) | |
| # 1. Clean and filter candidates | |
| candidates = [] | |
| for res in results: | |
| # Remove spaces and keep uppercase/digits/< | |
| clean = "".join([c for c in res.upper().replace(" ", "") if c in allow]) | |
| if len(clean) >= 30: | |
| candidates.append(clean) | |
| # 2. Look for Line 1 (starts with P<) | |
| line1, line2 = None, None | |
| for i, c in enumerate(candidates): | |
| if c.startswith("P<"): | |
| line1 = c | |
| # Line 2 is almost always the next one or one after | |
| if i + 1 < len(candidates): | |
| line2 = candidates[i+1] | |
| break | |
| # 3. If no P< found, look for any two 44-char lines | |
| if not line1 and len(candidates) >= 2: | |
| for i in range(len(candidates)-1): | |
| if len(candidates[i]) >= 40 and len(candidates[i+1]) >= 40: | |
| line1, line2 = candidates[i], candidates[i+1] | |
| break | |
| return line1, line2 | |
| except Exception as e: | |
| logger.error(f"EasyOCR fallback error: {e}") | |
| return None, None | |
| def extract_mrz_from_roi(self, img_path): | |
| try: | |
| # First attempt: PassportEye (If Tesseract works) | |
| try: | |
| mrz = read_mrz(img_path, save_roi=True) | |
| if mrz and getattr(mrz, 'valid_score', 0) > 20: | |
| l1 = clean_mrz_line(getattr(mrz, 'line1', '')) | |
| l2 = clean_mrz_line(getattr(mrz, 'line2', '')) | |
| if l1 and l2: | |
| return l1, l2, mrz | |
| except: | |
| pass | |
| # Second attempt: EasyOCR Manual Search | |
| l1, l2 = self.extract_mrz_easyocr(img_path) | |
| if l1 and l2: | |
| # Standardize to 44 chars for FallbackMRZ | |
| l1_clean = clean_mrz_line(l1) | |
| l2_clean = clean_mrz_line(l2) | |
| return l1_clean, l2_clean, FallbackMRZ(l1_clean, l2_clean) | |
| return None, None, None | |
| except Exception as e: | |
| logger.error(f"MRZ extraction failed: {e}") | |
| return None, None, None | |
| # --------------------------------------------------- | |
| # MAIN DATA FUNCTION | |
| # --------------------------------------------------- | |
| def get_data(self, img_path, airline="iraqi"): | |
| if not os.path.exists(img_path): | |
| return None | |
| line1, line2, mrz = self.extract_mrz_from_roi(img_path) | |
| if mrz is None: | |
| return { | |
| "surname": "•••", "name": "•••", "country": "•••", "nationality": "•••", | |
| "passport_number": "•••", "sex": "•••", "date_of_birth": "•••", | |
| "expiration_date": "•••", "personal_number": "•••", | |
| "mrz_full_string": "", "valid_score": 0, "mrz_found": False | |
| } | |
| data = { | |
| "surname": clean_name_field(getattr(mrz, "surname", "")), | |
| "name": self.extract_given_names_from_visual(img_path) or clean_name_field(getattr(mrz, "names", getattr(mrz, "name", ""))), | |
| "country": get_country_name(getattr(mrz, "country", "")), | |
| "nationality": get_country_name(getattr(mrz, "nationality", "")), | |
| "passport_number": clean_string(getattr(mrz, "number", "")), | |
| "sex": get_sex(getattr(mrz, "sex", "")), | |
| "date_of_birth": parse_date(getattr(mrz, "date_of_birth", ""), airline=airline), | |
| "expiration_date": parse_date(getattr(mrz, "expiration_date", ""), airline=airline), | |
| "mrz_full_string": (line1 or "") + (line2 or ""), | |
| "valid_score": getattr(mrz, "valid_score", 0), | |
| "mrz_found": True, | |
| } | |
| return data | |
| def process_pdf(self, pdf_path, progress_callback=None, airline="iraqi"): | |
| os.makedirs(TEMP_DIR, exist_ok=True) | |
| try: | |
| from pdf2image import pdfinfo_from_path, convert_from_path | |
| info = pdfinfo_from_path(pdf_path) | |
| results = [] | |
| for page in range(1, info["Pages"] + 1): | |
| if progress_callback: progress_callback(page / info["Pages"]) | |
| imgs = convert_from_path(pdf_path, dpi=150, first_page=page, last_page=page) | |
| if not imgs: continue | |
| tmp = os.path.join(TEMP_DIR, f"tmp_{page}.jpg") | |
| imgs[0].save(tmp, "JPEG", quality=70) | |
| res = self.get_data(tmp, airline=airline) | |
| if res: | |
| res["page_number"] = page | |
| results.append(res) | |
| if os.path.exists(tmp): os.remove(tmp) | |
| return results | |
| except Exception as e: | |
| logger.error(f"PDF failed: {e}"); return [] | |