Monike123's picture
Deploy DocVerify FastAPI backend (EasyOCR + Gemini 3 Flash)
0e39d80
Raw
History Blame Contribute Delete
6.46 kB
"""PAN card field extraction and validation — pure OCR-based."""
from __future__ import annotations
import re
from ml_utils.ocr import OcrResult, get_full_text, get_average_confidence
from ml_utils.extract import find_by_regex, find_value_near_label, get_text_in_region
# ── Patterns ────────────────────────────────────────────────────────────
PAN_RE = re.compile(r"[A-Z]{5}\d{4}[A-Z]", re.IGNORECASE)
DOB_RE = re.compile(r"\b(\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4})\b")
NAME_LABELS = ["name", "नाम", "naam"]
FATHER_LABELS = ["father", "father's name", "पिता", "father name"]
DOB_LABELS = ["date of birth", "dob", "d.o.b", "birth", "जन्म तिथि"]
def _fix_pan_chars(value: str) -> str:
"""Fix common OCR misreads in PAN numbers."""
if len(value) < 10:
return value
chars = list(value[:10])
# Positions 0-4 should be letters
for i in range(5):
if chars[i].isdigit():
chars[i] = {"0": "O", "1": "I", "5": "S", "8": "B"}.get(chars[i], chars[i])
# Positions 5-8 should be digits
for i in range(5, 9):
if not chars[i].isdigit():
chars[i] = {"O": "0", "I": "1", "S": "5", "B": "8", "l": "1", "o": "0"}.get(chars[i], chars[i])
# Position 9 should be a letter
if len(chars) == 10 and not chars[9].isalpha():
chars[9] = {"0": "O", "1": "I"}.get(chars[9], chars[9])
return "".join(chars)
def _cleanup_pan_text(text: str) -> str:
"""Clean and extract PAN number from OCR text."""
compact = text.upper().replace(" ", "").replace("-", "").replace(".", "")
match = PAN_RE.search(compact)
if match:
return _fix_pan_chars(match.group(0).upper())
# Try with OCR error correction
for candidate in re.findall(r"[A-Z0-9]{10}", compact):
fixed = _fix_pan_chars(candidate)
if PAN_RE.fullmatch(fixed):
return fixed
if len(compact) >= 10:
return _fix_pan_chars(compact[-10:])
return compact
# ── Parser ──────────────────────────────────────────────────────────────
def parse_pan_fields(ocr_results: list[OcrResult]) -> dict:
"""Extract PAN card fields from OCR results."""
full_text = get_full_text(ocr_results)
fields: dict = {}
# PAN Number — regex on all text
pan_matches = find_by_regex(ocr_results, r"[A-Z0-9]{5}\s?[A-Z0-9]{4}\s?[A-Z0-9]")
pan_found = False
for raw_match, _ in pan_matches:
cleaned = _cleanup_pan_text(raw_match)
if PAN_RE.fullmatch(cleaned):
fields["pan_number"] = cleaned
pan_found = True
break
if not pan_found:
# Try on full text
compact = re.sub(r"\s", "", full_text.upper())
match = PAN_RE.search(compact)
if match:
fields["pan_number"] = _fix_pan_chars(match.group(0))
else:
for candidate in re.findall(r"[A-Z0-9]{10}", compact):
fixed = _fix_pan_chars(candidate)
if PAN_RE.fullmatch(fixed):
fields["pan_number"] = fixed
break
# Name
name = find_value_near_label(ocr_results, NAME_LABELS)
if name:
cleaned = re.sub(r"[^A-Za-z\s.]", "", name).strip()
if len(cleaned) >= 2 and not PAN_RE.search(cleaned):
fields["name"] = cleaned
if "name" not in fields:
# PAN cards: name is usually the 2nd or 3rd line from top
top_text = get_text_in_region(ocr_results, y_start_pct=0.15, y_end_pct=0.50)
for line in top_text.split("\n"):
cleaned = re.sub(r"[^A-Za-z\s.]", "", line).strip()
if (len(cleaned) >= 4 and " " in cleaned
and not any(k in cleaned.lower() for k in ["income", "tax", "govt", "india", "permanent"])):
fields["name"] = cleaned
break
# Father's Name
father = find_value_near_label(ocr_results, FATHER_LABELS)
if father:
cleaned = re.sub(r"[^A-Za-z\s.]", "", father).strip()
if len(cleaned) >= 2:
fields["father_name"] = cleaned
# DOB
dob = find_value_near_label(ocr_results, DOB_LABELS)
if dob:
dob_match = DOB_RE.search(dob)
if dob_match:
fields["dob"] = dob_match.group(1)
if "dob" not in fields:
all_dobs = DOB_RE.findall(full_text)
if all_dobs:
fields["dob"] = all_dobs[0]
# Signature presence (bottom region text)
bottom_text = get_text_in_region(ocr_results, y_start_pct=0.75, y_end_pct=1.0)
fields["signature_present"] = bool(
re.search(r"signature|sign|हस्ताक्षर", bottom_text, re.IGNORECASE)
)
return fields
# ── Validator ───────────────────────────────────────────────────────────
REQUIRED_FIELDS = {"pan_number", "name"}
def validate_pan(fields: dict, ocr_results: list[OcrResult]) -> tuple[float, list[str]]:
"""Validate PAN extraction. Returns (validation_score_0_to_20, flags)."""
flags: list[str] = []
checks_total = 0
checks_passed = 0
# Required fields
for req in REQUIRED_FIELDS:
checks_total += 1
if fields.get(req):
checks_passed += 1
else:
flags.append(f"MISSING_{req.upper()}")
# PAN format validation
pan = fields.get("pan_number", "")
if pan:
checks_total += 1
if PAN_RE.fullmatch(pan):
checks_passed += 1
fields["pan_validated"] = True
else:
flags.append("INVALID_PAN_FORMAT")
fields["pan_validated"] = False
else:
flags.append("NO_PAN_OCR")
fields["pan_validated"] = False
# DOB format
dob = fields.get("dob", "")
if dob:
checks_total += 1
if DOB_RE.search(dob):
checks_passed += 1
else:
flags.append("INVALID_DOB_FORMAT")
# OCR confidence
avg_conf = get_average_confidence(ocr_results)
if avg_conf < 0.4:
flags.append("LOW_OCR_CONFIDENCE")
validation_ratio = checks_passed / max(checks_total, 1)
return validation_ratio * 20.0, flags