| import cv2 |
| import numpy as np |
| from typing import Tuple, Dict |
| import io |
|
|
|
|
| def crop_receipt_from_image(image_bytes: bytes) -> Tuple[bytes, Dict]: |
| """Detect receipt edges and return a perspective-corrected JPEG. |
| |
| Returns (jpeg_bytes, meta) |
| meta: { confidence: float, width: int, height: int } |
| """ |
| image_array = np.frombuffer(image_bytes, dtype=np.uint8) |
| img = cv2.imdecode(image_array, cv2.IMREAD_COLOR) |
| if img is None: |
| raise ValueError("Invalid image data") |
|
|
| orig = img.copy() |
| ratio = 500.0 / max(img.shape[0], img.shape[1]) |
| if ratio < 1.0: |
| img = cv2.resize(img, None, fx=ratio, fy=ratio, interpolation=cv2.INTER_AREA) |
|
|
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
| gray = cv2.GaussianBlur(gray, (5, 5), 0) |
| edged = cv2.Canny(gray, 50, 150) |
|
|
| contours, _ = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) |
| contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10] |
|
|
| receipt_contour = None |
| for c in contours: |
| peri = cv2.arcLength(c, True) |
| approx = cv2.approxPolyDP(c, 0.02 * peri, True) |
| if len(approx) == 4: |
| receipt_contour = approx |
| break |
|
|
| if receipt_contour is None: |
| |
| h, w = img.shape[:2] |
| receipt_contour = np.array([[[0, 0]], [[w - 1, 0]], [[w - 1, h - 1]], [[0, h - 1]]]) |
| confidence = 0.2 |
| else: |
| confidence = 0.8 |
|
|
| pts = receipt_contour.reshape(4, 2).astype("float32") |
| |
| rect = _order_points(pts) |
| (tl, tr, br, bl) = rect |
|
|
| width_top = np.linalg.norm(tr - tl) |
| width_bottom = np.linalg.norm(br - bl) |
| max_width = int(max(width_top, width_bottom)) |
|
|
| height_right = np.linalg.norm(br - tr) |
| height_left = np.linalg.norm(bl - tl) |
| max_height = int(max(height_right, height_left)) |
|
|
| dst = np.array( |
| [[0, 0], [max_width - 1, 0], [max_width - 1, max_height - 1], [0, max_height - 1]], |
| dtype="float32", |
| ) |
|
|
| M = cv2.getPerspectiveTransform(rect, dst) |
| warped = cv2.warpPerspective(img, M, (max_width, max_height)) |
|
|
| |
| warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) |
| warped_eq = cv2.equalizeHist(warped_gray) |
|
|
| |
| success, jpeg = cv2.imencode('.jpg', warped_eq, [int(cv2.IMWRITE_JPEG_QUALITY), 90]) |
| if not success: |
| raise ValueError("Failed to encode JPEG") |
|
|
| return jpeg.tobytes(), {"confidence": float(confidence), "width": int(max_width), "height": int(max_height)} |
|
|
|
|
| def _order_points(pts: np.ndarray) -> np.ndarray: |
| x_sorted = pts[np.argsort(pts[:, 0]), :] |
| left = x_sorted[:2, :] |
| right = x_sorted[2:, :] |
|
|
| tl, bl = left[np.argsort(left[:, 1]), :] |
| tr, br = right[np.argsort(right[:, 1]), :] |
| return np.array([tl, tr, br, bl], dtype="float32") |
|
|
|
|
|
|