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: # Fallback: treat whole image as receipt 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") # Order points: top-left, top-right, bottom-right, bottom-left 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)) # Slight contrast enhancement and grayscale for OCR readiness warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) warped_eq = cv2.equalizeHist(warped_gray) # Encode as JPEG 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")