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)