Spaces:
No application file
No application file
| import cv2 | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont | |
| from backend.utilities import cv_to_pil, pil_to_cv | |
| from backend.text_editor.font_detector import load_matching_font | |
| def erase_text_in_bbox(img_bgr: np.ndarray, bbox: tuple, paper_color_bgr: list) -> np.ndarray: | |
| """ | |
| Erases text in the specified bounding box by creating a character-level binary mask | |
| and applying OpenCV Inpainting (Telea) to preserve background texture. | |
| """ | |
| h_img, w_img = img_bgr.shape[:2] | |
| x, y, w, h = bbox | |
| # Ensure coordinates are within image bounds | |
| x = max(0, min(x, w_img - 2)) | |
| y = max(0, min(y, h_img - 2)) | |
| w = max(1, min(w, w_img - x)) | |
| h = max(1, min(h, h_img - y)) | |
| # Create a full-image black mask | |
| full_mask = np.zeros((h_img, w_img), dtype=np.uint8) | |
| # Crop local patch | |
| crop = img_bgr[y:y+h, x:x+w] | |
| try: | |
| # Convert crop to grayscale | |
| gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) | |
| # Segment character strokes using Otsu's thresholding | |
| _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) | |
| # Ensure we target the ink strokes rather than the background | |
| fg_count = np.sum(thresh == 255) | |
| bg_count = np.sum(thresh == 0) | |
| if fg_count > bg_count: | |
| thresh = cv2.bitwise_not(thresh) | |
| # Dilate the character strokes to fully cover complex Devanagari horizontal bars and matras | |
| kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) | |
| dilated_thresh = cv2.dilate(thresh, kernel, iterations=3) | |
| # Inject local mask back into full-image mask | |
| full_mask[y:y+h, x:x+w] = dilated_thresh | |
| except Exception as e: | |
| print(f"Error during character mask generation: {e}") | |
| # Fallback: mask the entire bounding box | |
| full_mask[y:y+h, x:x+w] = 255 | |
| # Apply OpenCV Telea Inpainting to restore background paper textures | |
| inpainted = cv2.inpaint(img_bgr, full_mask, inpaintRadius=4, flags=cv2.INPAINT_TELEA) | |
| return inpainted | |
| def replace_text_in_image( | |
| img_bgr: np.ndarray, | |
| bbox: tuple, | |
| replacement_text: str, | |
| ink_color_bgr: list, | |
| paper_color_bgr: list, | |
| font_family: str = "Sans-Serif", | |
| size_multiplier: float = 0.85 | |
| ) -> np.ndarray: | |
| """ | |
| Erases text in a bounding box and renders replacement text with matching style. | |
| Returns: | |
| OpenCV BGR image containing the replacement text. | |
| """ | |
| x, y, w, h = bbox | |
| # 1. Erase original text using smart character mask inpainting | |
| inpainted_bgr = erase_text_in_bbox(img_bgr, bbox, paper_color_bgr) | |
| # 2. Convert to PIL Image for high-quality antialiased text drawing | |
| pil_img = cv_to_pil(inpainted_bgr) | |
| draw = ImageDraw.Draw(pil_img) | |
| # 3. Load matching font and scale based on bounding box height (with dynamic character-level font auto-resolver) | |
| font, font_size = load_matching_font(h, font_family, size_multiplier, text=replacement_text) | |
| # Convert BGR color to RGB for Pillow | |
| ink_color_rgb = (int(ink_color_bgr[2]), int(ink_color_bgr[1]), int(ink_color_bgr[0])) | |
| # 4. Measure replacement text to align it properly | |
| # Using modern draw.textbbox or fallback font.getbbox | |
| try: | |
| left, top, right, bottom = draw.textbbox((0, 0), replacement_text, font=font) | |
| text_w = right - left | |
| text_h = bottom - top | |
| except AttributeError: | |
| # Fallback for older PIL versions | |
| text_w, text_h = font.getsize(replacement_text) if hasattr(font, 'getsize') else (w, h) | |
| left, top = 0, 0 | |
| # Calculate drawing coordinates | |
| # Center text horizontally or left-align it | |
| # If text fits inside bounding box, we can left-align or center it | |
| x_draw = x | |
| # Center vertically inside the bounding box | |
| y_draw = y + (h - text_h) // 2 - top | |
| # Draw replacement text | |
| draw.text((x_draw, y_draw), replacement_text, fill=ink_color_rgb, font=font) | |
| # 5. Convert back to OpenCV BGR | |
| return pil_to_cv(pil_img) | |