Commit ·
d68e5fd
1
Parent(s): b4d4ce5
Confidence + Bucket
Browse filesIt crops to the highest confidence score instead of largest box.
The bucket or receipts will now be stored in their correct folders corresponding to their category
app.py
CHANGED
|
@@ -349,28 +349,62 @@ def parse_total(value: str | float | int | None) -> float:
|
|
| 349 |
return float(m[-1]) if m else 0.0
|
| 350 |
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
def detect_and_crop_largest_receipt(img_array: np.ndarray):
|
| 353 |
-
"""Detect
|
| 354 |
|
| 355 |
Returns (cropped_array, bbox_dict)
|
| 356 |
-
bbox_dict = {"x1": int, "y1": int, "x2": int, "y2": int}
|
| 357 |
"""
|
| 358 |
results = model(img_array)
|
| 359 |
-
# Default fallback to full image
|
| 360 |
cropped = img_array
|
| 361 |
bbox = None
|
|
|
|
| 362 |
for r in results:
|
| 363 |
if len(r.boxes.xyxy) == 0:
|
| 364 |
continue
|
| 365 |
-
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
x1, y1, x2, y2 = map(int, boxes[i].tolist())
|
| 368 |
cropped = img_array[y1:y2, x1:x2]
|
| 369 |
-
bbox = {
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
return cropped, bbox
|
| 372 |
|
| 373 |
|
|
|
|
| 374 |
def encode_image_to_data_url(img_array: np.ndarray, format: str = "JPEG") -> str:
|
| 375 |
"""Encode an RGB image array to a data URL suitable for GPT-4o image input."""
|
| 376 |
pil_img = Image.fromarray(img_array)
|
|
@@ -540,11 +574,27 @@ async def process_receipt_vision(
|
|
| 540 |
content_str = getattr(result, "content", str(result))
|
| 541 |
extracted_obj = parse_extracted_json(content_str)
|
| 542 |
|
| 543 |
-
# Upload CROPPED image to Storage (JPEG)
|
| 544 |
cropped_bytes = encode_image_to_jpeg_bytes(cropped)
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
try:
|
| 547 |
stored = upload_to_storage(storage_path, cropped_bytes, "image/jpeg")
|
|
|
|
| 548 |
except Exception as exc:
|
| 549 |
from fastapi import HTTPException
|
| 550 |
raise HTTPException(status_code=502, detail=f"Storage upload failed: {exc}")
|
|
|
|
| 349 |
return float(m[-1]) if m else 0.0
|
| 350 |
|
| 351 |
|
| 352 |
+
# def detect_and_crop_largest_receipt(img_array: np.ndarray):
|
| 353 |
+
# """Detect the largest bounding box via YOLO and return crop and bbox.
|
| 354 |
+
|
| 355 |
+
# Returns (cropped_array, bbox_dict)
|
| 356 |
+
# bbox_dict = {"x1": int, "y1": int, "x2": int, "y2": int}
|
| 357 |
+
# """
|
| 358 |
+
# results = model(img_array)
|
| 359 |
+
# # Default fallback to full image
|
| 360 |
+
# cropped = img_array
|
| 361 |
+
# bbox = None
|
| 362 |
+
# for r in results:
|
| 363 |
+
# if len(r.boxes.xyxy) == 0:
|
| 364 |
+
# continue
|
| 365 |
+
# boxes = r.boxes.xyxy
|
| 366 |
+
# i = max(range(len(boxes)), key=lambda j: (boxes[j][2]-boxes[j][0]) * (boxes[j][3]-boxes[j][1]))
|
| 367 |
+
# x1, y1, x2, y2 = map(int, boxes[i].tolist())
|
| 368 |
+
# cropped = img_array[y1:y2, x1:x2]
|
| 369 |
+
# bbox = {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
|
| 370 |
+
# break
|
| 371 |
+
# return cropped, bbox
|
| 372 |
+
|
| 373 |
def detect_and_crop_largest_receipt(img_array: np.ndarray):
|
| 374 |
+
"""Detect receipt via YOLO and return the crop of the highest-confidence box.
|
| 375 |
|
| 376 |
Returns (cropped_array, bbox_dict)
|
| 377 |
+
bbox_dict = {"x1": int, "y1": int, "x2": int, "y2": int, "conf": float}
|
| 378 |
"""
|
| 379 |
results = model(img_array)
|
|
|
|
| 380 |
cropped = img_array
|
| 381 |
bbox = None
|
| 382 |
+
|
| 383 |
for r in results:
|
| 384 |
if len(r.boxes.xyxy) == 0:
|
| 385 |
continue
|
| 386 |
+
|
| 387 |
+
boxes = r.boxes.xyxy.cpu().numpy()
|
| 388 |
+
scores = r.boxes.conf.cpu().numpy()
|
| 389 |
+
|
| 390 |
+
# Pick index of highest confidence
|
| 391 |
+
i = int(scores.argmax())
|
| 392 |
+
|
| 393 |
x1, y1, x2, y2 = map(int, boxes[i].tolist())
|
| 394 |
cropped = img_array[y1:y2, x1:x2]
|
| 395 |
+
bbox = {
|
| 396 |
+
"x1": x1,
|
| 397 |
+
"y1": y1,
|
| 398 |
+
"x2": x2,
|
| 399 |
+
"y2": y2,
|
| 400 |
+
"conf": float(scores[i]),
|
| 401 |
+
}
|
| 402 |
+
break # only use first image in batch
|
| 403 |
+
|
| 404 |
return cropped, bbox
|
| 405 |
|
| 406 |
|
| 407 |
+
|
| 408 |
def encode_image_to_data_url(img_array: np.ndarray, format: str = "JPEG") -> str:
|
| 409 |
"""Encode an RGB image array to a data URL suitable for GPT-4o image input."""
|
| 410 |
pil_img = Image.fromarray(img_array)
|
|
|
|
| 574 |
content_str = getattr(result, "content", str(result))
|
| 575 |
extracted_obj = parse_extracted_json(content_str)
|
| 576 |
|
|
|
|
| 577 |
cropped_bytes = encode_image_to_jpeg_bytes(cropped)
|
| 578 |
+
|
| 579 |
+
# Build storage path with category slug
|
| 580 |
+
category = extracted_obj.get("category")
|
| 581 |
+
def category_to_slug(category: str | None) -> str:
|
| 582 |
+
if not category:
|
| 583 |
+
return "uncategorized"
|
| 584 |
+
return (
|
| 585 |
+
category.strip().lower()
|
| 586 |
+
.replace("&", "and")
|
| 587 |
+
.replace("/", "_")
|
| 588 |
+
.replace(" ", "_")
|
| 589 |
+
.replace("-", "_")
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
category_slug = category_to_slug(category)
|
| 593 |
+
storage_path = f"{user_id}/{category_slug}/{int(time.time()*1000)}_cropped.jpg"
|
| 594 |
+
|
| 595 |
try:
|
| 596 |
stored = upload_to_storage(storage_path, cropped_bytes, "image/jpeg")
|
| 597 |
+
|
| 598 |
except Exception as exc:
|
| 599 |
from fastapi import HTTPException
|
| 600 |
raise HTTPException(status_code=502, detail=f"Storage upload failed: {exc}")
|