Spaces:
Sleeping
Sleeping
File size: 7,055 Bytes
0e39d80 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | """Spatial field extraction engine β replaces YOLO detection.
Uses OCR bounding box positions + regex to locate and extract fields.
"""
from __future__ import annotations
import re
from ml_utils.ocr import OcrResult, group_by_lines
# ββ Core Spatial Helpers ββββββββββββββββββββββββββββββββββββββββββββββββ
def _image_dimensions(results: list[OcrResult]) -> tuple[int, int]:
"""Estimate image dimensions from OCR bounding boxes."""
if not results:
return (1, 1)
max_x = max(max(p[0] for p in r.bbox) for r in results)
max_y = max(max(p[1] for p in r.bbox) for r in results)
return (max(max_x, 1), max(max_y, 1))
def find_by_regex(results: list[OcrResult], pattern: str, flags: int = 0) -> list[tuple[str, OcrResult]]:
"""Find all OCR blocks whose text matches a regex pattern."""
compiled = re.compile(pattern, flags)
matches: list[tuple[str, OcrResult]] = []
for r in results:
m = compiled.search(r.text)
if m:
matches.append((m.group(0), r))
return matches
def find_keyword(results: list[OcrResult], keywords: list[str]) -> OcrResult | None:
"""Find first OCR block containing any of the keywords (case-insensitive)."""
lower_kw = [k.lower() for k in keywords]
for r in results:
text_lower = r.text.lower()
for kw in lower_kw:
if kw in text_lower:
return r
return None
def find_value_near_label(
results: list[OcrResult],
label_keywords: list[str],
direction: str = "right_or_below",
max_distance_pct: float = 0.15,
) -> str | None:
"""Find the text block nearest to a label keyword.
direction: 'right', 'below', 'right_or_below'
max_distance_pct: max distance as fraction of image dimension
"""
label_block = find_keyword(results, label_keywords)
if label_block is None:
return None
img_w, img_h = _image_dimensions(results)
lx1, ly1, lx2, ly2 = label_block.rect
max_dx = img_w * max_distance_pct
max_dy = img_h * max_distance_pct
candidates: list[tuple[float, OcrResult]] = []
for r in results:
if r is label_block:
continue
rx1, ry1, rx2, ry2 = r.rect
if direction in ("right", "right_or_below"):
# Block is to the right and roughly same vertical position
if rx1 >= lx2 - 10 and abs(r.center_y - label_block.center_y) < max_dy:
dist = rx1 - lx2
if dist < max_dx * 3: # more lenient horizontally
candidates.append((dist, r))
if direction in ("below", "right_or_below"):
# Block is below and roughly same horizontal position
if ry1 >= ly2 - 10 and abs(r.center_x - label_block.center_x) < max_dx * 2:
dist = ry1 - ly2
if dist < max_dy * 2:
candidates.append((dist + 10000, r)) # prefer right over below
if not candidates:
# Fallback: check if label text contains value after colon
colon_match = re.search(r'[:\-]\s*(.+)', label_block.text)
if colon_match:
value = colon_match.group(1).strip()
if value:
return value
return None
candidates.sort(key=lambda x: x[0])
return candidates[0][1].text
def get_text_in_region(
results: list[OcrResult],
y_start_pct: float = 0.0,
y_end_pct: float = 1.0,
x_start_pct: float = 0.0,
x_end_pct: float = 1.0,
) -> str:
"""Get all text in a rectangular region (percentages of image dimensions)."""
img_w, img_h = _image_dimensions(results)
y_start = img_h * y_start_pct
y_end = img_h * y_end_pct
x_start = img_w * x_start_pct
x_end = img_w * x_end_pct
region_results = [
r for r in results
if y_start <= r.center_y <= y_end and x_start <= r.center_x <= x_end
]
lines = group_by_lines(region_results)
return "\n".join(" ".join(r.text for r in line) for line in lines)
def get_all_text_blocks_in_region(
results: list[OcrResult],
y_start_pct: float = 0.0,
y_end_pct: float = 1.0,
x_start_pct: float = 0.0,
x_end_pct: float = 1.0,
) -> list[OcrResult]:
"""Get all OCR blocks in a rectangular region."""
img_w, img_h = _image_dimensions(results)
y_start = img_h * y_start_pct
y_end = img_h * y_end_pct
x_start = img_w * x_start_pct
x_end = img_w * x_end_pct
return [
r for r in results
if y_start <= r.center_y <= y_end and x_start <= r.center_x <= x_end
]
def get_nearby_text(
results: list[OcrResult],
anchor: OcrResult,
direction: str = "below",
max_blocks: int = 5,
max_distance_pct: float = 0.2,
) -> list[OcrResult]:
"""Get text blocks near an anchor block in a given direction."""
img_w, img_h = _image_dimensions(results)
ax1, ay1, ax2, ay2 = anchor.rect
max_dist = img_h * max_distance_pct if direction in ("below", "above") else img_w * max_distance_pct
candidates: list[tuple[float, OcrResult]] = []
for r in results:
if r is anchor:
continue
rx1, ry1, rx2, ry2 = r.rect
if direction == "below" and ry1 >= ay2 - 5:
dist = ry1 - ay2
if dist < max_dist:
candidates.append((dist, r))
elif direction == "right" and rx1 >= ax2 - 5:
dist = rx1 - ax2
if dist < max_dist and abs(r.center_y - anchor.center_y) < anchor.height * 1.5:
candidates.append((dist, r))
candidates.sort(key=lambda x: x[0])
return [c[1] for c in candidates[:max_blocks]]
# ββ Document-Specific Extraction Router βββββββββββββββββββββββββββββββββ
def extract_fields(ocr_results: list[OcrResult], doc_type: str) -> dict:
"""Route to the correct document-specific extractor."""
if doc_type == "aadhaar":
from ml_utils.validators.aadhaar import parse_aadhaar_fields
return parse_aadhaar_fields(ocr_results)
elif doc_type == "pan":
from ml_utils.validators.pan import parse_pan_fields
return parse_pan_fields(ocr_results)
elif doc_type == "caste":
from ml_utils.validators.caste import parse_caste_fields_from_ocr
return parse_caste_fields_from_ocr(ocr_results)
elif doc_type == "experience":
from ml_utils.validators.experience import parse_experience_fields_from_ocr
return parse_experience_fields_from_ocr(ocr_results)
elif doc_type == "education":
from ml_utils.validators.education import parse_education_fields
return parse_education_fields(ocr_results)
elif doc_type == "resume":
from ml_utils.validators.resume import parse_resume_fields
return parse_resume_fields(ocr_results)
else:
from ml_utils.validators.general import parse_general_fields
return parse_general_fields(ocr_results)
|