Spaces:
Sleeping
Sleeping
Download doc_intelligence/src/classification/features.py from vettri06/Document_intelligence: direct link, hf CLI and curl.
- Browser
- Download file 4.21 kB
-
https://huggingface.co/spaces/vettri06/Document_intelligence/resolve/main/doc_intelligence/src/classification/features.py
- Command line
-
hf download hf://spaces/vettri06/Document_intelligence/doc_intelligence/src/classification/features.py
-
curl -L -o features.py https://huggingface.co/spaces/vettri06/Document_intelligence/resolve/main/doc_intelligence/src/classification/features.py
4.21 kB
| import os | |
| import re | |
| import numpy as np | |
| import cv2 | |
| from typing import List, Dict, Any | |
| from sklearn.base import BaseEstimator, TransformerMixin | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| CLASS_INDICATIVE_KEYWORDS = [ | |
| "invoice", "receipt", "application", "applicant", "bill", "total", "form", | |
| "fatura", "tax", "cashier", "subtotal", "change", "signature" | |
| ] | |
| class LayoutFeatureExtractor(BaseEstimator, TransformerMixin): | |
| """ | |
| Extracts layout and statistical features from OCR tokens and document records: | |
| - token count | |
| - digit ratio | |
| - keyword presence indicators | |
| - aspect ratio | |
| - line count estimate | |
| """ | |
| def __init__(self): | |
| self.invoice_kw = ["invoice", "tax", "po", "bill", "due", "balance", "vendor"] | |
| self.receipt_kw = ["receipt", "change", "cash", "cashier", "subtotal", "card", "store"] | |
| self.form_kw = ["applicant", "signature", "employee", "title", "department", "date", "form"] | |
| def fit(self, X, y=None): | |
| return self | |
| def transform(self, records: List[Any]) -> np.ndarray: | |
| feats = [] | |
| for r in records: | |
| words = r.ocr_words if hasattr(r, "ocr_words") else r.get("ocr_words", []) | |
| text_tokens = [w.text if hasattr(w, "text") else w.get("text", "") for w in words] | |
| full_text = " ".join(text_tokens).lower() | |
| num_tokens = len(text_tokens) | |
| total_chars = max(len(full_text), 1) | |
| num_digits = sum(c.isdigit() for c in full_text) | |
| digit_ratio = num_digits / total_chars | |
| has_inv = float(any(kw in full_text for kw in self.invoice_kw)) | |
| has_rec = float(any(kw in full_text for kw in self.receipt_kw)) | |
| has_frm = float(any(kw in full_text for kw in self.form_kw)) | |
| # Bounding box layout estimation | |
| y_coords = [w.bbox[1] if hasattr(w, "bbox") else w.get("bbox", [0, 0, 0, 0])[1] for w in words] | |
| unique_lines = len(set([y // 20 for y in y_coords])) if y_coords else 0 | |
| # Aspect ratio from image if available | |
| aspect_ratio = 1.4 # standard portrait default | |
| img_path = r.image_path if hasattr(r, "image_path") else r.get("image_path", "") | |
| if img_path and os.path.exists(img_path): | |
| try: | |
| from PIL import Image | |
| with Image.open(img_path) as im: | |
| w_img, h_img = im.size | |
| aspect_ratio = float(w_img / max(h_img, 1)) | |
| except Exception: | |
| pass | |
| feats.append([ | |
| float(num_tokens), | |
| float(digit_ratio), | |
| has_inv, | |
| has_rec, | |
| has_frm, | |
| float(unique_lines), | |
| float(aspect_ratio) | |
| ]) | |
| return np.array(feats, dtype=np.float32) | |
| def mask_class_keywords(text: str) -> str: | |
| """Masks obvious class-indicative keywords for robustness ablation.""" | |
| pattern = re.compile(r'\b(' + '|'.join(CLASS_INDICATIVE_KEYWORDS) + r')\b', re.IGNORECASE) | |
| return pattern.sub("[MASKED]", text) | |
| def apply_image_augmentations(img: np.ndarray, aug_type: str = "all") -> np.ndarray: | |
| """ | |
| Applies image distortions for robustness testing: | |
| - blur (Gaussian blur) | |
| - rotation (+/- 5 degrees) | |
| - compression (JPEG encoding at low quality) | |
| - noise (Gaussian random noise) | |
| """ | |
| out = img.copy() | |
| h, w = out.shape[:2] | |
| if aug_type in ["rotation", "all"]: | |
| angle = float(np.random.choice([-5.0, 5.0])) | |
| M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) | |
| out = cv2.warpAffine(out, M, (w, h), borderMode=cv2.BORDER_REPLICATE) | |
| if aug_type in ["blur", "all"]: | |
| out = cv2.GaussianBlur(out, (5, 5), 0) | |
| if aug_type in ["compression", "all"]: | |
| encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 30] | |
| _, enc = cv2.imencode('.jpg', out, encode_param) | |
| out = cv2.imdecode(enc, 1) | |
| if aug_type in ["noise", "all"]: | |
| noise = np.random.normal(0, 15, out.shape).astype(np.float32) | |
| out = np.clip(out.astype(np.float32) + noise, 0, 255).astype(np.uint8) | |
| return out | |