Spaces:
Runtime error
Runtime error
| import cv2 | |
| import numpy as np | |
| from PIL import Image, ImageOps | |
| def _order_points(pts): | |
| """Orders 4 points as: [top-left, top-right, bottom-right, bottom-left]""" | |
| pts = pts.reshape((4, 2)) | |
| rect = np.zeros((4, 2), dtype="float32") | |
| s = pts.sum(axis=1) | |
| rect[0] = pts[np.argmin(s)] | |
| rect[2] = pts[np.argmax(s)] | |
| diff = np.diff(pts, axis=1) | |
| rect[1] = pts[np.argmin(diff)] | |
| rect[3] = pts[np.argmax(diff)] | |
| return rect | |
| def _load_as_bgr(image_input): | |
| """ | |
| Accepts a file path, a PIL Image, or a numpy (OpenCV) array, and | |
| returns a BGR numpy array ready for OpenCV. | |
| File paths and PIL Images go through Pillow first so EXIF rotation | |
| gets baked into the actual pixels — cv2.imread/imdecode ignore that | |
| metadata entirely, so a photo that looks upright everywhere else | |
| can load sideways or upside-down here without any error. | |
| """ | |
| if isinstance(image_input, str): | |
| pil_img = Image.open(image_input) | |
| elif isinstance(image_input, Image.Image): | |
| pil_img = image_input | |
| elif isinstance(image_input, np.ndarray): | |
| return image_input | |
| else: | |
| raise ValueError("Input must be a file path, a PIL Image, or a numpy image array.") | |
| pil_img = ImageOps.exif_transpose(pil_img).convert("RGB") | |
| return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) | |
| def _find_quadrilateral(hull): | |
| """ | |
| Tries to simplify a contour's convex hull down to exactly 4 points, | |
| trying progressively looser tolerances until one works. | |
| Returns the 4-point approximation, or None if no tolerance in the | |
| range produces a clean quadrilateral. We deliberately never fall | |
| back to picking raw hull extremes (e.g. via sum/diff of all hull | |
| points) — noise such as a shadow or glare merging into the grid's | |
| silhouette can push a hull point past the real edge of the page, | |
| and warpPerspective will then sample outside the source image, | |
| filling the gap with solid black (visible as a sharp diagonal wedge | |
| in the warped output). | |
| """ | |
| peri = cv2.arcLength(hull, True) | |
| for eps_factor in (0.02, 0.03, 0.05, 0.08, 0.1): | |
| approx = cv2.approxPolyDP(hull, eps_factor * peri, True) | |
| if len(approx) == 4: | |
| return approx | |
| return None | |
| def process_image(image_input, side_length=450): | |
| """ | |
| Reads an image from a path, a PIL Image, or a numpy array. | |
| Isolates the Sudoku grid robustly and returns a warped perspective. | |
| """ | |
| img = _load_as_bgr(image_input) | |
| if img is None: | |
| raise FileNotFoundError(f"Could not read image from: {image_input}") | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
| blurred = cv2.GaussianBlur(gray, (5, 5), 0) | |
| # Dynamic thresholding based on average brightness | |
| avg_brightness = np.mean(gray) | |
| if avg_brightness < 127: | |
| thresh_adaptive = cv2.adaptiveThreshold( | |
| blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, | |
| cv2.THRESH_BINARY, 11, 5 | |
| ) | |
| else: | |
| thresh_adaptive = cv2.adaptiveThreshold( | |
| blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, | |
| cv2.THRESH_BINARY_INV, 11, 5 | |
| ) | |
| # Find external contours | |
| contours, _ = cv2.findContours(thresh_adaptive, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| sorted_contours = sorted(contours, key=cv2.contourArea, reverse=True) | |
| sudoku_contour = None | |
| for c in sorted_contours: | |
| if cv2.contourArea(c) < (img.shape[0] * img.shape[1] * 0.1): | |
| continue | |
| hull = cv2.convexHull(c) | |
| approx = _find_quadrilateral(hull) | |
| if approx is not None: | |
| sudoku_contour = approx | |
| break | |
| if sudoku_contour is None: | |
| raise ValueError("Error: Could not identify a 4-sided grid shape in the image.") | |
| src_pts = _order_points(sudoku_contour) | |
| dst_pts = np.array([ | |
| [0, 0], [side_length - 1, 0], [side_length - 1, side_length - 1], [0, side_length - 1] | |
| ], dtype="float32") | |
| matrix = cv2.getPerspectiveTransform(src_pts, dst_pts) | |
| warped = cv2.warpPerspective(gray, matrix, (side_length, side_length)) | |
| return warped |