Woolv7007 commited on
Commit
25b208e
·
verified ·
1 Parent(s): e6e9b58

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +166 -0
app.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+
3
+ import cv2
4
+ import numpy as np
5
+ import easyocr
6
+ import logging
7
+ import re
8
+ import datetime
9
+ import mediapipe as mp
10
+
11
+ # ----------------- Logging -----------------
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # ----------------- Settings -----------------
16
+ class Settings:
17
+ threshold_distance = 0.58
18
+ max_image_size = (1600, 1600)
19
+ min_depth_variation = 0.001
20
+ min_texture_score = 0.5
21
+ action_threshold = {
22
+ "smile": 0.045,
23
+ "blink": 0.20,
24
+ "head_turn": 15.0
25
+ }
26
+ target_size = (160, 160)
27
+
28
+ settings = Settings()
29
+
30
+ # ----------------- Utils -----------------
31
+ def process_image(image_bytes: bytes) -> np.ndarray:
32
+ nparr = np.frombuffer(image_bytes, np.uint8)
33
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
34
+ return img
35
+
36
+ def cosine_similarity(a, b):
37
+ a = np.array(a)
38
+ b = np.array(b)
39
+ a = a / np.linalg.norm(a)
40
+ b = b / np.linalg.norm(b)
41
+ return float(np.dot(a, b))
42
+
43
+ easyocr_reader = easyocr.Reader(['ar', 'en'], gpu=False)
44
+
45
+ def ocr_text(image: np.ndarray, region: tuple = None) -> str:
46
+ """Run OCR on full image or region. Converts Arabic numerals to English."""
47
+ if region:
48
+ h, w = image.shape[:2]
49
+ y1, y2, x1, x2 = region
50
+ image = image[int(h*y1):int(h*y2), int(w*x1):int(w*x2)]
51
+ result = easyocr_reader.readtext(image)
52
+ arabic_to_western = str.maketrans('٠١٢٣٤٥٦٧٨٩', '0123456789')
53
+ return " ".join([item[1].translate(arabic_to_western) for item in result])
54
+ # ----------------- Factory Number Extraction -----------------
55
+
56
+ def extract_factory_number(image: np.ndarray) -> str | None:
57
+ """Extract the factory number robustly from the full image using OCR."""
58
+ try:
59
+ text = ocr_text(image)
60
+ logger.info(f"[OCR] Full image text for factory number: {text}")
61
+
62
+ # OCR confusion correction
63
+ confusion_map = str.maketrans({
64
+ '$': 'S', '§': 'S', '5': 'S', 's': 'S',
65
+ '1': 'I', 'l': 'I', '|': 'I', '!': 'I',
66
+ '0': 'O', 'O': '0', 'Q': '0', 'D': '0',
67
+ '8': 'B', 'B': '8', '2': 'Z', 'Z': '2',
68
+ '6': 'G', 'G': '6', '9': 'G', 'g': 'G',
69
+ '4': 'A', 'A': '4', '7': 'T', 'T': '7',
70
+ })
71
+ norm_text = text.translate(confusion_map).upper()
72
+
73
+ # Pattern 1: starts with 1–2 letters + 5–9 digits
74
+ pattern_letters = r'([A-Z]{1,2}[0-9]{5,9})'
75
+ candidates_letters = re.findall(pattern_letters, norm_text)
76
+
77
+ # Pattern 2: fallback 7–11 alphanumerics with at least 5 digits
78
+ pattern_any = r'([A-Z0-9]{7,11})'
79
+ candidates_any = [
80
+ c for c in re.findall(pattern_any, norm_text)
81
+ if sum(x.isdigit() for x in c) >= 5 and sum(x.isalpha() for x in c) >= 1
82
+ ]
83
+
84
+ # Prefer pattern with letters
85
+ candidate = None
86
+ if candidates_letters:
87
+ candidate = candidates_letters[-1]
88
+ elif candidates_any:
89
+ candidate = candidates_any[-1]
90
+
91
+ # Extra fallback: first char suspicious correction
92
+ suspicious_map = {
93
+ '|': 'I', '$': 'S', '§': 'S', '5': 'S', '1': 'I',
94
+ 'l': 'I', '!': 'I', '0': 'O', '8': 'B', '2': 'Z',
95
+ '6': 'G', '9': 'G', '4': 'A', '7': 'T'
96
+ }
97
+ if candidate:
98
+ if candidate[0] in suspicious_map:
99
+ candidate = suspicious_map[candidate[0]] + candidate[1:]
100
+ return candidate.upper()
101
+
102
+ # Extra fallback: search full text again
103
+ fallback_pattern = r'([\|\$§5sl!0182g46947][A-Z0-9]{6,10})'
104
+ fallback_candidates = re.findall(fallback_pattern, text)
105
+ for fc in fallback_candidates:
106
+ fc_up = fc.upper()
107
+ if sum(x.isdigit() for x in fc_up) >= 5:
108
+ if fc_up[0] in suspicious_map:
109
+ fc_up = suspicious_map[fc_up[0]] + fc_up[1:]
110
+ return fc_up
111
+
112
+ return None
113
+ except Exception as e:
114
+ logger.error(f"Factory number extraction error: {str(e)}")
115
+ return None
116
+ # ----------------- Gradio Interface -----------------
117
+
118
+ import gradio as gr
119
+
120
+ def verify_id_card_gradio(id_img: np.ndarray) -> dict:
121
+ """Gradio-friendly function to extract factory number from ID card image."""
122
+ try:
123
+ h, w = id_img.shape[:2]
124
+ if w > settings.max_image_size[0] or h > settings.max_image_size[1]:
125
+ return {
126
+ "verified": False,
127
+ "factory_number": None,
128
+ "message": f"Image too large. Max allowed: {settings.max_image_size[0]}x{settings.max_image_size[1]} pixels."
129
+ }
130
+
131
+ factory_number = extract_factory_number(id_img)
132
+
133
+ if factory_number:
134
+ return {
135
+ "verified": True,
136
+ "factory_number": factory_number,
137
+ "message": "Factory number extracted successfully."
138
+ }
139
+ else:
140
+ return {
141
+ "verified": False,
142
+ "factory_number": None,
143
+ "message": "Factory number not found in the image."
144
+ }
145
+
146
+ except Exception as e:
147
+ logger.error(f"Gradio ID card error: {str(e)}")
148
+ return {
149
+ "verified": False,
150
+ "factory_number": None,
151
+ "message": "An error occurred while processing the image."
152
+ }
153
+
154
+
155
+ iface = gr.Interface(
156
+ fn=verify_id_card_gradio,
157
+ inputs=gr.Image(type="numpy", label="Upload Egyptian ID Card"),
158
+ outputs="json",
159
+ title="Egyptian ID Factory Number Extractor",
160
+ description="Upload an Egyptian ID card image to extract the factory number. Only returns: match status and extracted factory number.",
161
+ allow_flagging="never",
162
+ examples=None
163
+ )
164
+
165
+ if __name__ == "__main__":
166
+ iface.launch()