id-ocr-engine / engine /mrz_reader.py
Kevin-KES
Deploy current main to HF Space
50c6ee2
Raw
History Blame Contribute Delete
3.53 kB
import logging
logger = logging.getLogger(__name__)
_mrz = None
def _get_mrz():
"""Lazy-initialize FastMRZ. First call loads ONNX model."""
global _mrz
if _mrz is None:
logger.info("Initializing FastMRZ...")
from fastmrz import FastMRZ
_mrz = FastMRZ()
logger.info("FastMRZ initialized")
return _mrz
def read_mrz(image_path: str) -> dict | None:
"""Read MRZ (Machine Readable Zone) from a passport image.
Returns dict with extracted fields, or None if MRZ not found/readable.
Fields returned:
passport_number, surname, given_names, nationality,
date_of_birth, sex, expiry_date, issuing_country,
mrz_valid (bool — check digits passed)
"""
try:
mrz = _get_mrz()
mrz_data = mrz.get_mrz(image_path)
if not mrz_data:
logger.info("No MRZ found in image")
return None
# FastMRZ returns a dict with standardized field names
result = {
"source": "mrz",
"passport_number": mrz_data.get("document_number", "").replace("<", "").strip() or None,
"surname": mrz_data.get("surname", "").replace("<", " ").strip() or None,
"given_names": mrz_data.get("given_name", "").replace("<", " ").strip() or None,
"nationality": mrz_data.get("nationality", "").strip() or None,
"date_of_birth": _format_mrz_date(mrz_data.get("birth_date", "")),
"sex": _normalize_sex(mrz_data.get("sex", "")),
"expiry_date": _format_mrz_date(mrz_data.get("expiry_date", "")),
"issuing_country": mrz_data.get("country", "").strip() or None,
"mrz_valid": mrz_data.get("valid_score", False),
}
# Clean up names
if result["surname"]:
result["surname"] = result["surname"].title()
if result["given_names"]:
result["given_names"] = result["given_names"].title()
logger.info(
"MRZ extracted: nationality=%s passport=%s valid=%s",
result["nationality"],
result["passport_number"],
result["mrz_valid"],
)
return result
except Exception as e:
logger.warning("MRZ reading failed: %s", e)
return None
def _format_mrz_date(date_str: str) -> str | None:
"""Convert MRZ date (YYMMDD) to ISO format (YYYY-MM-DD)."""
if not date_str or len(date_str) < 6:
return None
date_str = date_str.strip().replace("-", "").replace("/", "")
if len(date_str) < 6:
return None
try:
yy = int(date_str[:2])
mm = int(date_str[2:4])
dd = int(date_str[4:6])
# Century heuristic: same as SA ID parser
from datetime import datetime
current_yy = datetime.now().year % 100
year = 1900 + yy if yy > current_yy else 2000 + yy
from datetime import date
d = date(year, mm, dd)
return d.isoformat()
except (ValueError, IndexError):
return None
def _normalize_sex(sex_str: str) -> str | None:
"""Normalize MRZ sex field to 'Male' or 'Female'."""
s = sex_str.strip().upper()
if s in ("M", "MALE"):
return "Male"
if s in ("F", "FEMALE"):
return "Female"
return None
def warm_mrz() -> bool:
"""Pre-load FastMRZ model. Returns True if successful."""
try:
_get_mrz()
return True
except Exception as e:
logger.warning("FastMRZ warm-up failed: %s", e)
return False