Sivakkanth commited on
Commit
93d3ace
·
1 Parent(s): 596e9c7

updated the model with helper function to get the completed and the correct output

Browse files
Files changed (3) hide show
  1. README.md +38 -0
  2. app.py +151 -25
  3. requirements.txt +7 -6
README.md CHANGED
@@ -10,3 +10,41 @@ pinned: false
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
+
14
+ # ⚡ Receipt Extractor
15
+
16
+ A simple web application to extract information from receipt images using **YOLOv8** and **EasyOCR**. Built with **Gradio** for an interactive interface.
17
+
18
+ ---
19
+
20
+ ## Features
21
+
22
+ - Detects and extracts:
23
+ - Merchant Name
24
+ - Date
25
+ - Total Amount
26
+ - Items with prices
27
+ - Time, Discount, and Tax (if present)
28
+ - Handles receipts with multiple items.
29
+ - Interactive web interface via Gradio.
30
+
31
+ ---
32
+
33
+ ## Tech Stack
34
+
35
+ - **Python 3.12+**
36
+ - **YOLOv8** (Ultralytics) – Object Detection
37
+ - **EasyOCR** – Text Extraction
38
+ - **OpenCV** – Image Processing
39
+ - **Gradio** – Web Interface
40
+ - **NumPy** – Numerical Operations
41
+
42
+ ---
43
+
44
+ ## Installation (Local / Colab)
45
+
46
+ 1. Clone the repository:
47
+
48
+ ```bash
49
+ git clone git clone https://huggingface.co/spaces/Sivakkanth/receipt-extractor
50
+ cd receipt-extractor
app.py CHANGED
@@ -3,9 +3,11 @@ import cv2
3
  from ultralytics import YOLO
4
  import easyocr
5
  import numpy as np
 
 
6
 
7
- # Load YOLO model (replace with your custom weights path if needed)
8
- model = YOLO("model/best.pt")
9
 
10
  # Initialize OCR
11
  reader = easyocr.Reader(['en'])
@@ -13,53 +15,177 @@ reader = easyocr.Reader(['en'])
13
  # Class names
14
  class_names = ["Merchant","date","total","no","item"]
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  def extract_receipt(image):
17
- # Convert from PIL to OpenCV
18
  img = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
19
-
20
  results = model(img)[0]
21
-
22
- output = {
23
- "items": [],
24
- "name": "",
25
- "total": "",
26
- "date": "",
27
- "time": "",
28
- "discount": "",
29
- "tax": ""
30
- }
31
-
32
  for box, cls_id, conf in zip(results.boxes.xyxy, results.boxes.cls, results.boxes.conf):
33
  x1, y1, x2, y2 = [int(i) for i in box]
34
  cls_id = int(cls_id)
35
  cls_name = class_names[cls_id]
 
36
  crop = img[y1:y2, x1:x2]
37
  text_result = reader.readtext(crop)
38
  text = " ".join([t[1] for t in text_result])
39
-
40
  if cls_name == "Merchant":
41
  output["name"] = text
42
  elif cls_name == "date":
43
- output["date"] = text
 
 
44
  elif cls_name == "total":
45
  output["total"] = text
46
  elif cls_name == "no":
47
- output["time"] = text
 
 
48
  elif cls_name == "item":
49
- parts = text.rsplit(" ", 1)
50
- if len(parts) == 2 and parts[1].replace(".","").isdigit():
51
- output["items"].append({"product": parts[0], "price": parts[1]})
52
- else:
53
- output["items"].append({"product": text, "price": ""})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return output
55
 
56
- # Gradio interface
57
  iface = gr.Interface(
58
  fn=extract_receipt,
59
  inputs=gr.Image(type="pil"),
60
  outputs=gr.JSON(),
61
  title="Receipt Extractor",
62
- description="Upload a receipt image to extract merchant, date, total, and items."
63
  )
64
 
65
  iface.launch(share=True)
 
3
  from ultralytics import YOLO
4
  import easyocr
5
  import numpy as np
6
+ import re
7
+ from datetime import datetime
8
 
9
+ # Load YOLO model
10
+ model = YOLO("model/best.pt")
11
 
12
  # Initialize OCR
13
  reader = easyocr.Reader(['en'])
 
15
  # Class names
16
  class_names = ["Merchant","date","total","no","item"]
17
 
18
+ # Regex for numbers
19
+ NUMBER_RE = re.compile(r"\d+(?:\.\d+)?")
20
+ NUMBER_RE_PARSE = re.compile(r'[-+]?\d{1,3}(?:[,\d]*\d)?(?:[.,]\d{1,2})?')
21
+
22
+ # OCR fixes
23
+ OCR_FIXES = {'O':'0', 'o':'0', 'l':'1', '`':"'", 'S':'5', '$':'5', 'I':'1'}
24
+
25
+ # ---------- Helper functions ----------
26
+ def normalize_ocr_text(s: str) -> str:
27
+ s = s.replace('\n',' ').strip()
28
+ for k,v in OCR_FIXES.items():
29
+ s = s.replace(k,v)
30
+ s = re.sub(r'\s{2,}', ' ', s)
31
+ return s
32
+
33
+ def extract_numbers_parse(s: str):
34
+ tokens = NUMBER_RE_PARSE.findall(s)
35
+ nums = []
36
+ for t in tokens:
37
+ t_norm = t.replace(',', '')
38
+ if ',' in t and '.' not in t and re.search(r',\d{1,2}$', t):
39
+ t_norm = t.replace(',', '.')
40
+ try:
41
+ nums.append(float(t_norm))
42
+ except:
43
+ continue
44
+ return nums
45
+
46
+ def pick_price_from_numbers(numbers, original_str):
47
+ if not numbers:
48
+ return None
49
+ if len(numbers) > 1:
50
+ largest = max(numbers)
51
+ matches = NUMBER_RE_PARSE.finditer(original_str)
52
+ found = [m.group(0) for m in matches]
53
+ if found:
54
+ try:
55
+ t = found[-1].replace(',', '')
56
+ if ',' in found[-1] and '.' not in found[-1] and re.search(r',\d{1,2}$', found[-1]):
57
+ t = found[-1].replace(',', '.')
58
+ return float(t)
59
+ except:
60
+ return largest
61
+ return largest
62
+ else:
63
+ return numbers[0]
64
+
65
+ def clean_product_name(s: str):
66
+ s = re.sub(r'\b(x|qty|pcs|pc|nos|no|each)\b', '', s, flags=re.IGNORECASE)
67
+ s = re.sub(NUMBER_RE_PARSE, '', s)
68
+ s = re.sub(r'[\$₹£€:,()*`"“”]', ' ', s)
69
+ s = re.sub(r'\s{2,}', ' ', s).strip()
70
+ return s
71
+
72
+ def parse_line_item(raw_line: str):
73
+ raw = normalize_ocr_text(raw_line)
74
+ numbers = extract_numbers_parse(raw)
75
+ price = pick_price_from_numbers(numbers, raw)
76
+ product = clean_product_name(raw)
77
+ return {"product": product if product else raw_line, "price": f"{price:.2f}" if price is not None else ""}
78
+
79
+ def extract_total_amount(total_str: str):
80
+ if not total_str:
81
+ return None
82
+ matches = NUMBER_RE.findall(total_str)
83
+ for m in matches[::-1]:
84
+ try:
85
+ return float(m.replace(",",""))
86
+ except:
87
+ continue
88
+ return None
89
+
90
+ def parse_date(text):
91
+ text = text.replace('Date','').replace('date','').replace(':','').strip()
92
+ patterns = [r"(\d{2}[/-]\d{2}[/-]\d{2,4})", r"(\d{4}[/-]\d{2}[/-]\d{2})"]
93
+ for pat in patterns:
94
+ match = re.search(pat, text)
95
+ if match:
96
+ dt_str = match.group(1)
97
+ for fmt in ("%d/%m/%y", "%d/%m/%Y", "%Y-%m-%d"):
98
+ try:
99
+ dt = datetime.strptime(dt_str, fmt)
100
+ return dt.strftime("%Y-%m-%d")
101
+ except:
102
+ continue
103
+ return None
104
+
105
+ def parse_time(text):
106
+ text = text.replace('Time','').replace('time','').replace(':','').strip()
107
+ patterns = [r"(\d{1,2}:\d{2}(:\d{2})?)"]
108
+ for pat in patterns:
109
+ match = re.search(pat, text)
110
+ if match:
111
+ tm_str = match.group(1)
112
+ for fmt in ("%H:%M:%S","%H:%M"):
113
+ try:
114
+ tm = datetime.strptime(tm_str, fmt)
115
+ return tm.strftime("%H:%M:%S")
116
+ except:
117
+ continue
118
+ return None
119
+
120
+ # ---------- Main extraction function ----------
121
  def extract_receipt(image):
 
122
  img = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
 
123
  results = model(img)[0]
124
+
125
+ output = {"items": [], "name": "", "total": "", "date": "", "time": "", "discount": 0.0, "tax": 0.0}
126
+
 
 
 
 
 
 
 
 
127
  for box, cls_id, conf in zip(results.boxes.xyxy, results.boxes.cls, results.boxes.conf):
128
  x1, y1, x2, y2 = [int(i) for i in box]
129
  cls_id = int(cls_id)
130
  cls_name = class_names[cls_id]
131
+
132
  crop = img[y1:y2, x1:x2]
133
  text_result = reader.readtext(crop)
134
  text = " ".join([t[1] for t in text_result])
135
+
136
  if cls_name == "Merchant":
137
  output["name"] = text
138
  elif cls_name == "date":
139
+ parsed_date = parse_date(text)
140
+ if parsed_date:
141
+ output["date"] = parsed_date
142
  elif cls_name == "total":
143
  output["total"] = text
144
  elif cls_name == "no":
145
+ parsed_time = parse_time(text)
146
+ if parsed_time:
147
+ output["time"] = parsed_time
148
  elif cls_name == "item":
149
+ parsed = parse_line_item(text)
150
+ new_product = parsed["product"]
151
+ new_price = float(parsed["price"]) if parsed["price"] else None
152
+ output["items"].append({"product": new_product, "price": new_price if new_price is not None else ""})
153
+
154
+ # ---------- Post-processing totals ----------
155
+ model_total = extract_total_amount(output.get("total",""))
156
+ item_sum = sum([it["price"] for it in output["items"] if it.get("price") not in ("",None)])
157
+
158
+ if model_total is None or model_total > item_sum*10:
159
+ model_total = round(item_sum,2)
160
+ tax, discount = 0.0, 0.0
161
+ else:
162
+ if abs(model_total - item_sum) < 0.01:
163
+ tax, discount = 0.0, 0.0
164
+ elif model_total > item_sum:
165
+ tax, discount = round(model_total - item_sum,2), 0.0
166
+ else:
167
+ tax, discount = 0.0, round(item_sum - model_total,2)
168
+
169
+ output["total"] = model_total
170
+ output["tax"] = tax
171
+ output["discount"] = discount
172
+
173
+ # ---------- Fill missing date/time ----------
174
+ now = datetime.now()
175
+ if not output["date"]:
176
+ output["date"] = now.strftime("%Y-%m-%d")
177
+ if not output["time"]:
178
+ output["time"] = now.strftime("%H:%M:%S")
179
+
180
  return output
181
 
182
+ # ---------- Gradio Interface ----------
183
  iface = gr.Interface(
184
  fn=extract_receipt,
185
  inputs=gr.Image(type="pil"),
186
  outputs=gr.JSON(),
187
  title="Receipt Extractor",
188
+ description="Upload a receipt image to extract merchant, date, total, time, and items."
189
  )
190
 
191
  iface.launch(share=True)
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
- torch
2
- torchvision
3
- ultralytics
4
- opencv-python-headless
5
- easyocr
6
- gradio
 
 
1
+ torch==2.8.0+cu126
2
+ torchvision==0.23.0+cu126
3
+ ultralytics==8.3.203
4
+ opencv-python-headless==4.12.0.88
5
+ easyocr==1.7.2
6
+ gradio==5.46.0
7
+ numpy==2.0.2