File size: 5,711 Bytes
25b208e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# main.py

import cv2
import numpy as np
import easyocr
import logging
import re
import datetime
import mediapipe as mp

# ----------------- Logging -----------------
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ----------------- Settings -----------------
class Settings:
    threshold_distance = 0.58
    max_image_size = (1600, 1600)
    min_depth_variation = 0.001
    min_texture_score = 0.5
    action_threshold = {
        "smile": 0.045,
        "blink": 0.20,
        "head_turn": 15.0
    }
    target_size = (160, 160)

settings = Settings()

# ----------------- Utils -----------------
def process_image(image_bytes: bytes) -> np.ndarray:
    nparr = np.frombuffer(image_bytes, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
    return img

def cosine_similarity(a, b):
    a = np.array(a)
    b = np.array(b)
    a = a / np.linalg.norm(a)
    b = b / np.linalg.norm(b)
    return float(np.dot(a, b))

easyocr_reader = easyocr.Reader(['ar', 'en'], gpu=False)

def ocr_text(image: np.ndarray, region: tuple = None) -> str:
    """Run OCR on full image or region. Converts Arabic numerals to English."""
    if region:
        h, w = image.shape[:2]
        y1, y2, x1, x2 = region
        image = image[int(h*y1):int(h*y2), int(w*x1):int(w*x2)]
    result = easyocr_reader.readtext(image)
    arabic_to_western = str.maketrans('٠١٢٣٤٥٦٧٨٩', '0123456789')
    return " ".join([item[1].translate(arabic_to_western) for item in result])
# ----------------- Factory Number Extraction -----------------

def extract_factory_number(image: np.ndarray) -> str | None:
    """Extract the factory number robustly from the full image using OCR."""
    try:
        text = ocr_text(image)
        logger.info(f"[OCR] Full image text for factory number: {text}")

        # OCR confusion correction
        confusion_map = str.maketrans({
            '$': 'S', '§': 'S', '5': 'S', 's': 'S',
            '1': 'I', 'l': 'I', '|': 'I', '!': 'I',
            '0': 'O', 'O': '0', 'Q': '0', 'D': '0',
            '8': 'B', 'B': '8', '2': 'Z', 'Z': '2',
            '6': 'G', 'G': '6', '9': 'G', 'g': 'G',
            '4': 'A', 'A': '4', '7': 'T', 'T': '7',
        })
        norm_text = text.translate(confusion_map).upper()

        # Pattern 1: starts with 1–2 letters + 5–9 digits
        pattern_letters = r'([A-Z]{1,2}[0-9]{5,9})'
        candidates_letters = re.findall(pattern_letters, norm_text)

        # Pattern 2: fallback 7–11 alphanumerics with at least 5 digits
        pattern_any = r'([A-Z0-9]{7,11})'
        candidates_any = [
            c for c in re.findall(pattern_any, norm_text)
            if sum(x.isdigit() for x in c) >= 5 and sum(x.isalpha() for x in c) >= 1
        ]

        # Prefer pattern with letters
        candidate = None
        if candidates_letters:
            candidate = candidates_letters[-1]
        elif candidates_any:
            candidate = candidates_any[-1]

        # Extra fallback: first char suspicious correction
        suspicious_map = {
            '|': 'I', '$': 'S', '§': 'S', '5': 'S', '1': 'I',
            'l': 'I', '!': 'I', '0': 'O', '8': 'B', '2': 'Z',
            '6': 'G', '9': 'G', '4': 'A', '7': 'T'
        }
        if candidate:
            if candidate[0] in suspicious_map:
                candidate = suspicious_map[candidate[0]] + candidate[1:]
            return candidate.upper()

        # Extra fallback: search full text again
        fallback_pattern = r'([\|\$§5sl!0182g46947][A-Z0-9]{6,10})'
        fallback_candidates = re.findall(fallback_pattern, text)
        for fc in fallback_candidates:
            fc_up = fc.upper()
            if sum(x.isdigit() for x in fc_up) >= 5:
                if fc_up[0] in suspicious_map:
                    fc_up = suspicious_map[fc_up[0]] + fc_up[1:]
                return fc_up

        return None
    except Exception as e:
        logger.error(f"Factory number extraction error: {str(e)}")
        return None
# ----------------- Gradio Interface -----------------

import gradio as gr

def verify_id_card_gradio(id_img: np.ndarray) -> dict:
    """Gradio-friendly function to extract factory number from ID card image."""
    try:
        h, w = id_img.shape[:2]
        if w > settings.max_image_size[0] or h > settings.max_image_size[1]:
            return {
                "verified": False,
                "factory_number": None,
                "message": f"Image too large. Max allowed: {settings.max_image_size[0]}x{settings.max_image_size[1]} pixels."
            }

        factory_number = extract_factory_number(id_img)

        if factory_number:
            return {
                "verified": True,
                "factory_number": factory_number,
                "message": "Factory number extracted successfully."
            }
        else:
            return {
                "verified": False,
                "factory_number": None,
                "message": "Factory number not found in the image."
            }

    except Exception as e:
        logger.error(f"Gradio ID card error: {str(e)}")
        return {
            "verified": False,
            "factory_number": None,
            "message": "An error occurred while processing the image."
        }


iface = gr.Interface(
    fn=verify_id_card_gradio,
    inputs=gr.Image(type="numpy", label="Upload Egyptian ID Card"),
    outputs="json",
    title="Egyptian ID Factory Number Extractor",
    description="Upload an Egyptian ID card image to extract the factory number. Only returns: match status and extracted factory number.",
    allow_flagging="never",
    examples=None
)

if __name__ == "__main__":
    iface.launch()