hmfayyaz commited on
Commit
db144cd
·
unverified ·
1 Parent(s): 5575d1a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -132
app.py CHANGED
@@ -1,145 +1,225 @@
1
- import sys
2
  import os
 
 
 
 
 
 
 
 
 
3
 
4
- # Ensure project root is in python path
5
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
6
-
7
- import streamlit as st
8
- import tempfile
9
- import pandas as pd
10
- import time
11
- from src.extractor import PassportExtractor
12
- from src.validators import validate_passport_data
13
- from src.formats import format_iraqi_airways, format_flydubai
14
-
15
- # Set page configuration
16
- st.set_page_config(
17
- page_title="Passport OCR Tool",
18
- page_icon="🛂",
19
- layout="wide"
20
  )
21
 
22
- # Initialize Extractor (cached to avoid reloading model)
23
- @st.cache_resource
24
- def get_extractor(use_gpu=False):
25
- return PassportExtractor(use_gpu=use_gpu)
26
-
27
- def save_uploaded_file(uploaded_file):
28
- """Save uploaded file to a temporary location and return the path."""
29
- try:
30
- suffix = os.path.splitext(uploaded_file.name)[1]
31
- with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file:
32
- tmp_file.write(uploaded_file.getvalue())
33
- return tmp_file.name
34
- except Exception as e:
35
- st.error(f"Error saving file: {e}")
36
- return None
37
-
38
- def main():
39
- st.title("🛂 Passport OCR Extractor")
40
-
41
- # Add a description
42
- st.markdown("""
43
- This tool extracts data from passport MRZ (Machine-Readable Zone) codes.
44
- Upload passport images or PDFs, and the app will return a structured table of the extracted information.
45
- You can also select an airline-specific format for the output data.
46
- """)
47
-
48
- # Sidebar Configuration
49
- st.sidebar.header("Settings")
50
- use_gpu = st.sidebar.checkbox("Enable GPU Acceleration", value=False)
51
- airline = st.sidebar.selectbox("Choose Airline Format", ["Default", "Iraqi Airways", "Flydubai"])
52
-
53
- # File Uploader
54
- uploaded_files = st.file_uploader(
55
- "Upload Passport Files",
56
- type=['png', 'jpg', 'jpeg', 'pdf', 'avif', 'webp', 'bmp', 'tiff'],
57
- accept_multiple_files=True
58
- )
59
-
60
- if uploaded_files:
61
- if st.button("Extract Data"):
62
- # Initialize extractor inside the button click to use the latest settings
63
- extractor = PassportExtractor(use_gpu=use_gpu)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- all_results = []
66
- progress_bar = st.progress(0)
 
 
 
 
 
67
 
68
- for i, file in enumerate(uploaded_files):
69
- st.write(f"Processing file {i+1} of {len(uploaded_files)}: {file.name}")
70
- st.write(f"File type detected: {file.type}") # Debug line
71
-
72
- with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.name)[1]) as tmp:
73
- tmp.write(file.getvalue())
74
- tmp_path = tmp.name
75
-
76
  try:
77
- # Check both MIME type and file extension for PDF detection
78
- is_pdf = file.type == "application/pdf" or file.name.lower().endswith('.pdf')
 
 
79
 
80
- if is_pdf:
81
- st.write(f"Processing as PDF: {file.name} (type: {file.type})")
82
- results = extractor.process_pdf(tmp_path)
83
- if not results:
84
- st.warning(f"⚠️ No passport data found in PDF: {file.name}")
 
85
  else:
86
- st.write(f"Processing as image: {file.name} (type: {file.type})")
87
- result = extractor.get_data(tmp_path)
88
- results = [result] if result else []
89
- if not results:
90
- st.warning(f"⚠️ No passport data found in image: {file.name}")
91
-
92
- for res in results:
93
- res['source_file'] = file.name
94
- all_results.extend(results)
95
-
96
  except Exception as e:
97
- st.error(f"❌ Error processing {file.name}: {str(e)}")
98
-
99
  finally:
100
- os.remove(tmp_path)
101
-
102
- progress_bar.progress((i + 1) / len(uploaded_files))
103
-
104
- if not all_results:
105
- st.warning("No data could be extracted. Please check the files or try again.")
106
- return
107
-
108
- # Format data based on airline selection
109
- if airline == "Iraqi Airways":
110
- df = format_iraqi_airways(all_results)
111
- elif airline == "Flydubai":
112
- df = format_flydubai(all_results)
113
- else:
114
- df = pd.DataFrame(all_results)
115
-
116
- st.dataframe(df)
117
-
118
- # Download buttons
119
- col1, col2 = st.columns(2)
120
 
121
- with col1:
122
- csv = df.to_csv(index=False).encode('utf-8')
123
- st.download_button(
124
- label="Download data as CSV",
125
- data=csv,
126
- file_name=f"passport_data_{airline.lower()}.csv",
127
- mime="text/csv",
128
- )
129
 
130
- with col2:
131
- # Create an in-memory Excel file
132
- from io import BytesIO
133
- output = BytesIO()
134
- with pd.ExcelWriter(output, engine='openpyxl') as writer:
135
- df.to_excel(writer, index=False, sheet_name='PassportData')
136
-
137
- st.download_button(
138
- label="Download data as Excel",
139
- data=output.getvalue(),
140
- file_name=f"passport_data_{airline.lower()}.xlsx",
141
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
142
- )
143
-
144
- if __name__ == "__main__":
145
- main()
 
 
1
  import os
2
+ import cv2
3
+ import numpy as np
4
+ import easyocr
5
+ import warnings
6
+ import ssl
7
+ import re
8
+ from passporteye import read_mrz
9
+ from pdf2image import convert_from_path
10
+ import string as st
11
 
12
+ from src.utils import (
13
+ clean_string,
14
+ clean_mrz_line,
15
+ parse_date,
16
+ get_country_name,
17
+ get_sex,
18
+ setup_logger,
19
+ clean_name_field
 
 
 
 
 
 
 
 
20
  )
21
 
22
+ from src.fallback_mrz import FallbackMRZ
23
+ from config.settings import USE_GPU, OCR_LANGUAGES, TEMP_DIR
24
+
25
+ warnings.filterwarnings("ignore")
26
+ logger = setup_logger(__name__)
27
+
28
+
29
+ # Fix SSL issue (Mac EasyOCR model download fix)
30
+ try:
31
+ _create_unverified_https_context = ssl._create_unverified_context
32
+ except AttributeError:
33
+ pass
34
+ else:
35
+ ssl._create_default_https_context = _create_unverified_https_context
36
+
37
+
38
+ class PassportExtractor:
39
+
40
+ def __init__(self, use_gpu=USE_GPU, languages=None):
41
+ self.languages = languages if languages else OCR_LANGUAGES
42
+
43
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
44
+ model_dir = os.path.join(base_dir, "data", "models")
45
+ os.makedirs(model_dir, exist_ok=True)
46
+
47
+ logger.info(f"Initializing EasyOCR Reader (GPU={use_gpu})...")
48
+ self.reader = easyocr.Reader(
49
+ self.languages,
50
+ gpu=use_gpu,
51
+ model_storage_directory=model_dir
52
+ )
53
+ logger.info("EasyOCR initialized.")
54
+
55
+ # ---------------------------------------------------
56
+ # VISUAL GIVEN NAME EXTRACTION (PRIMARY SOURCE)
57
+ # ---------------------------------------------------
58
+ def extract_given_names_from_visual(self, img_path):
59
+ try:
60
+ results = self.reader.readtext(img_path, detail=0)
61
+ lines = [r.strip() for r in results if r.strip()]
62
+
63
+ for i, line in enumerate(lines):
64
+ upper_line = line.upper()
65
+
66
+ if "GIVEN" in upper_line and "NAME" in upper_line:
67
+
68
+ if ":" in line:
69
+ candidate = line.split(":")[1].strip()
70
+ else:
71
+ if i + 1 < len(lines):
72
+ candidate = lines[i + 1].strip()
73
+ else:
74
+ return ""
75
+
76
+ # Keep only letters and spaces
77
+ candidate = re.sub(r'[^A-Za-z\s]', '', candidate)
78
+
79
+ # Remove trailing single letter only if it's clearly an OCR artifact (not part of a name)
80
+ # This is more conservative - only removes single letters that are likely OCR errors
81
+ candidate = re.sub(r'([A-Z]{2,})[K]$', r'\1', candidate) # K is common OCR error for <
82
+
83
+ return candidate.strip()
84
+
85
+ return ""
86
+
87
+ except Exception as e:
88
+ logger.error(f"Given Names extraction failed: {e}")
89
+ return ""
90
+
91
+ # ---------------------------------------------------
92
+ # MRZ EXTRACTION
93
+ # ---------------------------------------------------
94
+ def extract_mrz_from_roi(self, img_path):
95
+ try:
96
+ mrz = read_mrz(img_path, save_roi=True)
97
+
98
+ if not mrz:
99
+ return None, None, None
100
+
101
+ roi = mrz.aux["roi"]
102
+
103
+ if roi.dtype != np.uint8:
104
+ roi = (roi * 255).astype(np.uint8)
105
+
106
+ img_resized = cv2.resize(roi, (1110, 140))
107
+ allow = st.ascii_uppercase + st.digits + "<"
108
+
109
+ code = self.reader.readtext(
110
+ img_resized,
111
+ detail=0,
112
+ allowlist=allow
113
+ )
114
+
115
+ if len(code) < 2:
116
+ return None, None, mrz
117
+
118
+ line1 = clean_mrz_line(code[0])
119
+ line2 = clean_mrz_line(code[1])
120
+
121
+ return line1, line2, mrz
122
+
123
+ except Exception as e:
124
+ logger.error(f"MRZ extraction failed: {e}")
125
+ return None, None, None
126
+
127
+ # ---------------------------------------------------
128
+ # MAIN DATA FUNCTION
129
+ # ---------------------------------------------------
130
+ def get_data(self, img_path):
131
+
132
+ if not os.path.exists(img_path):
133
+ logger.error(f"File not found: {img_path}")
134
+ return None
135
+
136
+ line1, line2, mrz = self.extract_mrz_from_roi(img_path)
137
+
138
+ if line1 and line2:
139
+ mrz = FallbackMRZ(line1, line2)
140
+
141
+ if mrz is None:
142
+ logger.warning("MRZ not detected.")
143
+ return None
144
+
145
+ surname = clean_name_field(getattr(mrz, "surname", ""))
146
+
147
+ # 🔥 ALWAYS prefer visual name
148
+ visual_name = self.extract_given_names_from_visual(img_path)
149
+
150
+ if visual_name:
151
+ name = visual_name
152
+ else:
153
+ name = clean_name_field(
154
+ getattr(mrz, "names", getattr(mrz, "name", ""))
155
+ )
156
+
157
+ # Final defensive cleanup - only remove trailing K which is a common OCR artifact
158
+ name = re.sub(r'([A-Z]{2,})[K]$', r'\1', name)
159
+
160
+ data = {
161
+ "surname": surname,
162
+ "name": name,
163
+ "country": get_country_name(getattr(mrz, "country", "")),
164
+ "nationality": get_country_name(getattr(mrz, "nationality", "")),
165
+ "passport_number": clean_string(getattr(mrz, "number", "")),
166
+ "sex": get_sex(getattr(mrz, "sex", "")),
167
+ "date_of_birth": parse_date(getattr(mrz, "date_of_birth", "")),
168
+ "expiration_date": parse_date(getattr(mrz, "expiration_date", "")),
169
+ "mrz_full_string": (line1 or "") + (line2 or ""),
170
+ "valid_score": getattr(mrz, "valid_score", 0),
171
+ }
172
+
173
+ return data
174
+
175
+ # ---------------------------------------------------
176
+ # PDF PROCESSING
177
+ # ---------------------------------------------------
178
+ def process_pdf(self, pdf_path):
179
+ """Process a PDF file and extract passport data from all pages."""
180
+ try:
181
+ logger.info(f"Processing PDF: {pdf_path}")
182
+
183
+ # Ensure temp directory exists
184
+ os.makedirs(TEMP_DIR, exist_ok=True)
185
 
186
+ # Convert PDF to images with error handling
187
+ try:
188
+ images = convert_from_path(pdf_path, dpi=200) # Increased DPI for better quality
189
+ logger.info(f"Converted PDF to {len(images)} pages")
190
+ except Exception as e:
191
+ logger.error(f"Failed to convert PDF to images: {e}")
192
+ return []
193
 
194
+ results = []
195
+
196
+ for i, image in enumerate(images):
 
 
 
 
 
197
  try:
198
+ # Save temporary image
199
+ temp_image_path = os.path.join(TEMP_DIR, f"temp_page_{i}.jpg")
200
+ image.save(temp_image_path, 'JPEG', quality=95)
201
+ logger.info(f"Processing page {i+1}")
202
 
203
+ # Extract data from this page
204
+ result = self.get_data(temp_image_path)
205
+ if result:
206
+ result['page_number'] = i + 1
207
+ results.append(result)
208
+ logger.info(f"Successfully extracted data from page {i+1}")
209
  else:
210
+ logger.warning(f"No data extracted from page {i+1}")
211
+
 
 
 
 
 
 
 
 
212
  except Exception as e:
213
+ logger.error(f"Error processing page {i+1}: {e}")
214
+ continue
215
  finally:
216
+ # Clean up temporary file
217
+ if os.path.exists(temp_image_path):
218
+ os.remove(temp_image_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
+ logger.info(f"PDF processing complete. Found {len(results)} valid pages")
221
+ return results
 
 
 
 
 
 
222
 
223
+ except Exception as e:
224
+ logger.error(f"PDF processing failed for {pdf_path}: {e}")
225
+ return []