Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -106,100 +106,155 @@ def verify_password(password, hashed):
|
|
106 |
class ImageProcessor:
|
107 |
"""Handles image preprocessing for better OCR results"""
|
108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
@staticmethod
|
110 |
def process_receipt_image(image_file, phone):
|
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 |
except Exception as e:
|
191 |
-
print(f"
|
192 |
-
|
193 |
-
|
194 |
-
status_msg = f"β
Receipt processed! Confidence: {confidence:.1%}"
|
195 |
-
if confidence < 0.7:
|
196 |
-
status_msg += " β οΈ Low confidence - please verify"
|
197 |
-
|
198 |
-
return True, status_msg, extracted_data, image_preview
|
199 |
-
|
200 |
-
except Exception as e:
|
201 |
-
print(f"β Processing error: {traceback.format_exc()}")
|
202 |
-
return False, f"β Processing failed: {str(e)}", {}, None
|
203 |
|
204 |
@staticmethod
|
205 |
def extract_text_regions(image_path):
|
|
|
106 |
class ImageProcessor:
|
107 |
"""Handles image preprocessing for better OCR results"""
|
108 |
|
109 |
+
@staticmethod
|
110 |
+
def preprocess_receipt_image(image_path):
|
111 |
+
"""
|
112 |
+
Preprocess receipt image for optimal OCR
|
113 |
+
Returns: processed image path and preprocessing info
|
114 |
+
"""
|
115 |
+
try:
|
116 |
+
if not PIL_AVAILABLE:
|
117 |
+
return image_path, "No preprocessing - PIL not available"
|
118 |
+
|
119 |
+
# Load image
|
120 |
+
image = Image.open(image_path)
|
121 |
+
|
122 |
+
# Convert to RGB if needed
|
123 |
+
if image.mode != 'RGB':
|
124 |
+
image = image.convert('RGB')
|
125 |
+
|
126 |
+
# Enhance contrast
|
127 |
+
enhancer = ImageEnhance.Contrast(image)
|
128 |
+
image = enhancer.enhance(1.5)
|
129 |
+
|
130 |
+
# Enhance sharpness
|
131 |
+
enhancer = ImageEnhance.Sharpness(image)
|
132 |
+
image = enhancer.enhance(2.0)
|
133 |
+
|
134 |
+
# Convert to grayscale
|
135 |
+
image = image.convert('L')
|
136 |
+
|
137 |
+
# Apply Gaussian blur to reduce noise
|
138 |
+
image = image.filter(ImageFilter.GaussianBlur(radius=0.5))
|
139 |
+
|
140 |
+
# Convert to numpy array for OpenCV processing
|
141 |
+
if 'cv2' in globals() and cv2 is not None:
|
142 |
+
img_array = np.array(image)
|
143 |
+
|
144 |
+
# Apply threshold to get binary image
|
145 |
+
_, binary = cv2.threshold(img_array, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
146 |
+
|
147 |
+
# Morphological operations to clean up the image
|
148 |
+
kernel = np.ones((1,1), np.uint8)
|
149 |
+
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
|
150 |
+
|
151 |
+
# Convert back to PIL Image
|
152 |
+
image = Image.fromarray(binary)
|
153 |
+
|
154 |
+
# Save processed image
|
155 |
+
processed_path = image_path.replace('.', '_processed.')
|
156 |
+
image.save(processed_path)
|
157 |
+
|
158 |
+
return processed_path, "Enhanced contrast, sharpness, applied thresholding"
|
159 |
+
|
160 |
+
except Exception as e:
|
161 |
+
print(f"Image preprocessing error: {e}")
|
162 |
+
return image_path, f"Preprocessing failed: {str(e)}"
|
163 |
+
|
164 |
@staticmethod
|
165 |
def process_receipt_image(image_file, phone):
|
166 |
+
"""
|
167 |
+
Complete receipt processing pipeline that handles Gradio file objects
|
168 |
+
Returns: (success, status_message, extracted_data, image_preview)
|
169 |
+
"""
|
170 |
+
try:
|
171 |
+
if not phone:
|
172 |
+
return False, "β Please sign in first", {}, None
|
173 |
+
|
174 |
+
if not image_file:
|
175 |
+
return False, "β No image uploaded", {}, None
|
176 |
|
177 |
+
# Debug input type
|
178 |
+
print(f"\nπ Input type: {type(image_file)}")
|
179 |
+
|
180 |
+
# Handle different input types
|
181 |
+
if isinstance(image_file, str):
|
182 |
+
# Case 1: Direct file path (local testing)
|
183 |
+
image_path = image_file
|
184 |
+
elif hasattr(image_file, 'name'):
|
185 |
+
# Case 2: Gradio NamedString object (Hugging Face Spaces)
|
186 |
+
image_path = image_file.name
|
187 |
+
else:
|
188 |
+
return False, "β Unsupported file input type", {}, None
|
189 |
|
190 |
+
# Create receipts directory if needed
|
191 |
+
os.makedirs(RECEIPTS_DIR, exist_ok=True)
|
192 |
+
|
193 |
+
# Generate unique filename
|
194 |
+
timestamp = int(time.time())
|
195 |
+
filename = f"receipt_{phone}_{timestamp}{os.path.splitext(image_path)[1]}"
|
196 |
+
save_path = os.path.join(RECEIPTS_DIR, filename)
|
197 |
+
|
198 |
+
# Copy the uploaded file (works for both Gradio and direct paths)
|
199 |
+
with open(image_path, 'rb') as src, open(save_path, 'wb') as dst:
|
200 |
+
dst.write(src.read())
|
201 |
+
|
202 |
+
print(f"π Saved receipt to: {save_path}")
|
203 |
+
|
204 |
+
# Preprocess image
|
205 |
+
processed_path, preprocessing_info = ImageProcessor.preprocess_receipt_image(save_path)
|
206 |
+
print(f"πΌοΈ Preprocessing: {preprocessing_info}")
|
207 |
+
|
208 |
+
# Extract text using OCR
|
209 |
+
raw_text, confidence, extracted_data = ocr_service.extract_text_from_receipt(processed_path)
|
210 |
+
print(f"π OCR Confidence: {confidence:.1%}")
|
211 |
+
|
212 |
+
# Auto-categorize
|
213 |
+
if extracted_data.get('merchant'):
|
214 |
+
suggested_category = db.auto_categorize_receipt(
|
215 |
+
phone,
|
216 |
+
extracted_data['merchant'],
|
217 |
+
extracted_data.get('total_amount', 0)
|
218 |
+
)
|
219 |
+
extracted_data['suggested_category'] = suggested_category
|
220 |
+
print(f"π·οΈ Suggested category: {suggested_category}")
|
221 |
+
|
222 |
+
# Prepare receipt data for database
|
223 |
+
receipt_data = {
|
224 |
+
'image_path': save_path,
|
225 |
+
'processed_image_path': processed_path,
|
226 |
+
'merchant': extracted_data.get('merchant', ''),
|
227 |
+
'amount': extracted_data.get('total_amount', 0.0),
|
228 |
+
'date': extracted_data.get('date', ''),
|
229 |
+
'category': extracted_data.get('suggested_category', 'Miscellaneous'),
|
230 |
+
'confidence': confidence,
|
231 |
+
'raw_text': raw_text,
|
232 |
+
'extracted_data': extracted_data,
|
233 |
+
'is_validated': False
|
234 |
+
}
|
235 |
+
|
236 |
+
# Save to database
|
237 |
+
receipt_id = db.save_receipt(phone, receipt_data)
|
238 |
+
extracted_data['receipt_id'] = receipt_id
|
239 |
+
print(f"πΎ Saved to DB with ID: {receipt_id}")
|
240 |
+
|
241 |
+
# Create image preview
|
242 |
+
try:
|
243 |
+
image_preview = Image.open(save_path)
|
244 |
+
image_preview.thumbnail((400, 600)) # Resize for display
|
245 |
+
except Exception as e:
|
246 |
+
print(f"β οΈ Preview generation failed: {e}")
|
247 |
+
image_preview = None
|
248 |
+
|
249 |
+
status_msg = f"β
Receipt processed! Confidence: {confidence:.1%}"
|
250 |
+
if confidence < 0.7:
|
251 |
+
status_msg += " β οΈ Low confidence - please verify"
|
252 |
+
|
253 |
+
return True, status_msg, extracted_data, image_preview
|
254 |
+
|
255 |
except Exception as e:
|
256 |
+
print(f"β Processing error: {traceback.format_exc()}")
|
257 |
+
return False, f"β Processing failed: {str(e)}", {}, None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
258 |
|
259 |
@staticmethod
|
260 |
def extract_text_regions(image_path):
|