OCR
This commit is contained in:
144
app.py
144
app.py
@@ -9,6 +9,9 @@ import psutil
|
||||
import secrets
|
||||
import hashlib
|
||||
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from pillow_heif import register_heif_opener
|
||||
|
||||
from datetime import datetime, timedelta, UTC, timezone
|
||||
@@ -49,6 +52,11 @@ from sqlalchemy import func, extract
|
||||
from collections import defaultdict, deque
|
||||
from functools import wraps
|
||||
|
||||
# OCR
|
||||
from collections import Counter
|
||||
import pytesseract
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
register_heif_opener() # pillow_heif dla HEIC
|
||||
@@ -335,6 +343,89 @@ def _receipt_error(message):
|
||||
return redirect(request.referrer or url_for("main_page"))
|
||||
|
||||
|
||||
############# OCR ###########################
|
||||
|
||||
|
||||
def preprocess_image_for_tesseract(pil_image):
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
# Konwersja PIL.Image → NumPy grayscale
|
||||
image = np.array(pil_image.convert("L"))
|
||||
|
||||
# Zwiększenie skali dla lepszej czytelności OCR
|
||||
image = cv2.resize(image, None, fx=2.0, fy=2.0, interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# Adaptacyjne progowanie (lepsze niż THRESH_BINARY przy nierównym tle)
|
||||
image = cv2.adaptiveThreshold(
|
||||
image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize=15, C=10
|
||||
)
|
||||
|
||||
# Konwersja z powrotem na PIL.Image (dla pytesseract)
|
||||
return Image.fromarray(image)
|
||||
|
||||
|
||||
def extract_total_tesseract(image):
|
||||
|
||||
text = pytesseract.image_to_string(image, lang="pol", config="--psm 6")
|
||||
lines = text.splitlines()
|
||||
candidates = []
|
||||
|
||||
fuzzy_regex = re.compile(r"[\dOo][.,:;g9zZ][\d]{2}")
|
||||
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
matches = re.findall(r"\d{1,4}[.,]\d{2}", line)
|
||||
for match in matches:
|
||||
try:
|
||||
val = float(match.replace(",", "."))
|
||||
if 0.1 <= val <= 100000:
|
||||
candidates.append((val, line))
|
||||
except:
|
||||
continue
|
||||
|
||||
fuzzy_matches = fuzzy_regex.findall(line)
|
||||
for match in fuzzy_matches:
|
||||
cleaned = (
|
||||
match.replace("O", "0")
|
||||
.replace("o", "0")
|
||||
.replace(":", ".")
|
||||
.replace(";", ".")
|
||||
.replace(",", ".")
|
||||
.replace("g", "9")
|
||||
.replace("z", "9")
|
||||
.replace("Z", "9")
|
||||
)
|
||||
try:
|
||||
val = float(cleaned)
|
||||
if 0.1 <= val <= 100:
|
||||
candidates.append((val, line))
|
||||
except:
|
||||
continue
|
||||
|
||||
preferred = [
|
||||
val
|
||||
for val, line in candidates
|
||||
if re.search(r"sum[aąo]?|razem|zapłaty", line.lower())
|
||||
]
|
||||
|
||||
if preferred:
|
||||
max_val = round(max(preferred), 2)
|
||||
return max_val, lines
|
||||
|
||||
if candidates:
|
||||
max_val = round(max([val for val, _ in candidates]), 2)
|
||||
return max_val, lines
|
||||
|
||||
return 0.0, lines
|
||||
|
||||
|
||||
############# END OCR #######################
|
||||
|
||||
|
||||
# zabezpieczenie logowani do systemu - błędne hasła
|
||||
def is_ip_blocked(ip):
|
||||
now = time.time()
|
||||
@@ -1037,6 +1128,59 @@ def reorder_items():
|
||||
return jsonify(success=True)
|
||||
|
||||
|
||||
# OCR
|
||||
@app.route("/lists/<int:list_id>/analyze", methods=["POST"])
|
||||
@login_required
|
||||
def analyze_receipts_for_list(list_id):
|
||||
list_obj = db.session.get(ShoppingList, list_id)
|
||||
if not list_obj or list_obj.owner_id != current_user.id:
|
||||
return jsonify({"error": "Brak dostępu"}), 403
|
||||
|
||||
receipt_objs = Receipt.query.filter_by(list_id=list_id).all()
|
||||
results = []
|
||||
total = 0.0
|
||||
|
||||
for receipt in receipt_objs:
|
||||
filepath = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename)
|
||||
if not os.path.exists(filepath):
|
||||
continue
|
||||
|
||||
temp_path = None
|
||||
|
||||
try:
|
||||
if filepath.lower().endswith(".webp"):
|
||||
|
||||
raw_image = Image.open(filepath).convert("RGB")
|
||||
image = preprocess_image_for_tesseract(raw_image)
|
||||
else:
|
||||
|
||||
raw_image = Image.open(filepath).convert("RGB")
|
||||
image = preprocess_image_for_tesseract(raw_image)
|
||||
|
||||
value, lines = extract_total_tesseract(image)
|
||||
|
||||
except Exception as e:
|
||||
print(f"OCR error for {receipt.filename}: {e}")
|
||||
value = 0.0
|
||||
lines = []
|
||||
|
||||
finally:
|
||||
if temp_path and os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"id": receipt.id,
|
||||
"filename": receipt.filename,
|
||||
"amount": round(value, 2),
|
||||
"debug_text": lines,
|
||||
}
|
||||
)
|
||||
total += value
|
||||
|
||||
return jsonify({"results": results, "total": round(total, 2)})
|
||||
|
||||
|
||||
@app.route("/admin")
|
||||
@login_required
|
||||
@admin_required
|
||||
|
Reference in New Issue
Block a user