import cv2 import numpy as np import os import io import base64 from PIL import Image from dotenv import load_dotenv from google import genai from google.genai import types load_dotenv() # Initialize the Gemini client _client = None def get_client(): global _client if _client is None: api_key = os.getenv("GEMINI_API_KEY") if not api_key or api_key == "your_api_key_here": raise ValueError("GEMINI_API_KEY not set. Please add it to your .env file.") _client = genai.Client(api_key=api_key) return _client def upscale_image(img: np.ndarray) -> np.ndarray: """ Upscales the image using Google Gemini's image generation API. Sends the cropped card image to Gemini with a prompt to upscale it, then returns the AI-enhanced result. Handles both BGR and BGRA (transparent) images. Falls back to local upscaling if Gemini API fails. """ has_alpha = len(img.shape) == 3 and img.shape[2] == 4 if has_alpha: bgr = img[:, :, :3] alpha = img[:, :, 3] else: bgr = img alpha = None try: # Convert BGR (OpenCV) to RGB (PIL) rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) pil_image = Image.fromarray(rgb) # Call Gemini API to upscale upscaled_pil = _gemini_upscale(pil_image) # Convert back to OpenCV BGR upscaled_rgb = np.array(upscaled_pil) upscaled_bgr = cv2.cvtColor(upscaled_rgb, cv2.COLOR_RGB2BGR) if alpha is not None: # Resize alpha to match the upscaled image h, w = upscaled_bgr.shape[:2] upscaled_alpha = cv2.resize(alpha, (w, h), interpolation=cv2.INTER_LANCZOS4) _, upscaled_alpha = cv2.threshold(upscaled_alpha, 127, 255, cv2.THRESH_BINARY) return cv2.merge(( upscaled_bgr[:, :, 0], upscaled_bgr[:, :, 1], upscaled_bgr[:, :, 2], upscaled_alpha )) else: return upscaled_bgr except Exception as e: print(f"Gemini upscale failed: {e}") print("Falling back to local upscaling...") return _local_fallback_upscale(img) def _gemini_upscale(pil_image: Image.Image) -> Image.Image: """ Uses the Gemini API to upscale/enhance an image. """ client = get_client() response = client.models.generate_content( model="gemini-2.0-flash-exp", contents=[ "Upscale this credit card image to high resolution. " "Make the text sharp, crisp, and readable. " "Preserve all colors, logos, textures, and details exactly. " "Do not add any watermarks, borders, or extra elements. " "Do not change the content of the image in any way. " "Output only the enhanced image.", pil_image, ], config=types.GenerateContentConfig( response_modalities=["IMAGE", "TEXT"], ), ) # Extract the image from the response for part in response.candidates[0].content.parts: if part.inline_data is not None: img_bytes = part.inline_data.data return Image.open(io.BytesIO(img_bytes)) raise ValueError("Gemini did not return an image in the response") def _local_fallback_upscale(img: np.ndarray) -> np.ndarray: """ Fallback: local multi-pass Lanczos + sharpening if Gemini API is unavailable. """ has_alpha = len(img.shape) == 3 and img.shape[2] == 4 if has_alpha: bgr = img[:, :, :3] alpha = img[:, :, 3] else: bgr = img alpha = None h, w = bgr.shape[:2] upscaled = cv2.resize(bgr, (w * 2, h * 2), interpolation=cv2.INTER_LANCZOS4) upscaled = cv2.bilateralFilter(upscaled, d=5, sigmaColor=40, sigmaSpace=40) # Unsharp mask blurred = cv2.GaussianBlur(upscaled, (0, 0), 2.0) upscaled = cv2.addWeighted(upscaled, 2.0, blurred, -1.0, 0) if alpha is not None: uh, uw = upscaled.shape[:2] upscaled_alpha = cv2.resize(alpha, (uw, uh), interpolation=cv2.INTER_LANCZOS4) _, upscaled_alpha = cv2.threshold(upscaled_alpha, 127, 255, cv2.THRESH_BINARY) return cv2.merge((upscaled[:,:,0], upscaled[:,:,1], upscaled[:,:,2], upscaled_alpha)) return upscaled