Spaces:
Sleeping
Sleeping
| """PII masking using OCR bounding boxes — no YOLO dependency.""" | |
| from pathlib import Path | |
| import re | |
| import cv2 | |
| import numpy as np | |
| from PIL import Image, ImageDraw | |
| from ml_utils.ocr import OcrResult | |
| # Regex patterns for PII fields to mask | |
| PII_PATTERNS: dict[str, list[re.Pattern]] = { | |
| "aadhaar": [ | |
| re.compile(r"\d{4}\s?\d{4}\s?\d{4}"), # Full Aadhaar number | |
| ], | |
| "pan": [ | |
| re.compile(r"[A-Z]{5}\d{4}[A-Z]"), # PAN number | |
| ], | |
| } | |
| # Additional keyword-based masking: if an OCR block near these labels, | |
| # mask the VALUE block (the one to the right/below) | |
| PII_LABELS: dict[str, list[str]] = { | |
| "aadhaar": ["address", "पता"], | |
| "pan": [], | |
| } | |
| def mask_pii_on_image( | |
| image_bgr: np.ndarray, | |
| ocr_results: list[OcrResult], | |
| doc_type: str, | |
| ) -> np.ndarray: | |
| """Mask PII fields on the image using OCR bounding boxes.""" | |
| img_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) | |
| img_pil = Image.fromarray(img_rgb) | |
| draw = ImageDraw.Draw(img_pil) | |
| patterns = PII_PATTERNS.get(doc_type, []) | |
| for result in ocr_results: | |
| for pat in patterns: | |
| if pat.search(result.text): | |
| _draw_mask(draw, result.bbox, img_pil.size) | |
| break | |
| # Mask address region for Aadhaar (bottom-right blocks after "Address" label) | |
| if doc_type == "aadhaar": | |
| _mask_address_region(draw, ocr_results, img_pil.size) | |
| return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) | |
| def _draw_mask(draw: ImageDraw.ImageDraw, bbox: list[list[int]], img_size: tuple[int, int]): | |
| """Draw a black rectangle over a bounding box.""" | |
| w, h = img_size | |
| xs = [max(0, min(p[0], w)) for p in bbox] | |
| ys = [max(0, min(p[1], h)) for p in bbox] | |
| x1, x2 = min(xs), max(xs) | |
| y1, y2 = min(ys), max(ys) | |
| # Add small padding | |
| pad = 3 | |
| draw.rectangle([x1 - pad, y1 - pad, x2 + pad, y2 + pad], fill=(0, 0, 0)) | |
| def _mask_address_region(draw: ImageDraw.ImageDraw, results: list[OcrResult], img_size: tuple[int, int]): | |
| """Mask text blocks in the address region of an Aadhaar card.""" | |
| # Find "Address" or "पता" label | |
| address_label = None | |
| for r in results: | |
| if any(kw in r.text.lower() for kw in ["address", "पता"]): | |
| address_label = r | |
| break | |
| if address_label is None: | |
| return | |
| # Mask all blocks below the address label (within reasonable distance) | |
| _, label_y1, _, label_y2 = address_label.rect | |
| img_h = img_size[1] | |
| max_y = label_y2 + (img_h - label_y2) # everything below | |
| for r in results: | |
| _, ry1, _, _ = r.rect | |
| if ry1 >= label_y1 and r is not address_label: | |
| _draw_mask(draw, r.bbox, img_size) | |
| def save_masked_image(image_bgr: np.ndarray, output_path: Path) -> str: | |
| """Save masked image to disk.""" | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| cv2.imwrite(str(output_path), image_bgr) | |
| return str(output_path) | |