| import gradio as gr |
| import easyocr |
| import cv2 |
| import numpy as np |
| from PIL import Image, ImageDraw, ImageFont |
| import math |
| from sklearn.cluster import KMeans |
| import urllib.request |
| import os |
|
|
| |
| print("Initializing EasyOCR...") |
| reader = easyocr.Reader(['en'], gpu=False) |
|
|
| |
| FONT_PATH = "Roboto-Regular.ttf" |
| if not os.path.exists(FONT_PATH): |
| print("Downloading default font...") |
| urllib.request.urlretrieve( |
| "https://github.com/googlefonts/roboto/raw/main/src/hinted/Roboto-Regular.ttf", |
| FONT_PATH |
| ) |
|
|
| def get_text_color(crop_img, mask): |
| pixels = crop_img.reshape((-1, 3)) |
| mask_pixels = mask.reshape((-1)) |
| text_pixels = pixels[mask_pixels > 0] |
| |
| if len(text_pixels) == 0: |
| return (0, 0, 0) |
| |
| kmeans = KMeans(n_clusters=1, n_init=10) |
| kmeans.fit(text_pixels) |
| dominant_color = kmeans.cluster_centers_[0] |
| return tuple(map(int, dominant_color)) |
|
|
| def process_image(image, old_text, new_text): |
| if image is None or not old_text or not new_text: |
| return image, "Please provide an image and both text fields." |
|
|
| img_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) |
| img_h, img_w = img_cv.shape[:2] |
|
|
| results = reader.readtext(img_cv) |
| |
| target_bbox = None |
| for (bbox, text, prob) in results: |
| if old_text.lower() in text.lower(): |
| target_bbox = bbox |
| break |
| |
| if not target_bbox: |
| return image, f"Text '{old_text}' not found in the image." |
|
|
| (tl, tr, br, bl) = target_bbox |
| tl, tr, br, bl = (int(tl[0]), int(tl[1])), (int(tr[0]), int(tr[1])), (int(br[0]), int(br[1])), (int(bl[0]), int(bl[1])) |
|
|
| dx = tr[0] - tl[0] |
| dy = tr[1] - tl[1] |
| angle = math.degrees(math.atan2(dy, dx)) |
| |
| width = int(math.hypot(dx, dy)) |
| height = int(math.hypot(tl[0] - bl[0], tl[1] - bl[1])) |
|
|
| full_mask = np.zeros((img_h, img_w), dtype=np.uint8) |
| pts = np.array([tl, tr, br, bl], dtype=np.int32) |
| cv2.fillPoly(full_mask, [pts], 255) |
|
|
| x_coords, y_coords = [p[0] for p in pts], [p[1] for p in pts] |
| x_min, x_max = max(0, min(x_coords)), min(img_w, max(x_coords)) |
| y_min, y_max = max(0, min(y_coords)), min(img_h, max(y_coords)) |
| |
| crop_img = img_cv[y_min:y_max, x_min:x_max] |
| gray_crop = cv2.cvtColor(crop_img, cv2.COLOR_BGR2GRAY) |
| _, text_mask_crop = cv2.threshold(gray_crop, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) |
| |
| text_color_bgr = get_text_color(crop_img, text_mask_crop) |
| text_color_rgb = (text_color_bgr[2], text_color_bgr[1], text_color_bgr[0]) |
|
|
| kernel = np.ones((3,3), np.uint8) |
| text_mask_crop = cv2.dilate(text_mask_crop, kernel, iterations=1) |
|
|
| precise_full_mask = np.zeros((img_h, img_w), dtype=np.uint8) |
| precise_full_mask[y_min:y_max, x_min:x_max] = text_mask_crop |
| precise_full_mask = cv2.bitwise_and(precise_full_mask, full_mask) |
|
|
| inpainted_img_cv = cv2.inpaint(img_cv, precise_full_mask, inpaintRadius=3, flags=cv2.INPAINT_NS) |
| inpainted_img_rgb = cv2.cvtColor(inpainted_img_cv, cv2.COLOR_BGR2RGB) |
| final_image = Image.fromarray(inpainted_img_rgb) |
|
|
| txt_overlay = Image.new('RGBA', final_image.size, (255, 255, 255, 0)) |
| draw = ImageDraw.Draw(txt_overlay) |
|
|
| font_size = 10 |
| font = ImageFont.truetype(FONT_PATH, font_size) |
| while True: |
| bbox = draw.textbbox((0, 0), new_text, font=font) |
| if bbox[3] - bbox[1] >= height * 0.8: |
| break |
| font_size += 1 |
| font = ImageFont.truetype(FONT_PATH, font_size) |
|
|
| text_bbox = draw.textbbox((0, 0), new_text, font=font) |
| text_w = text_bbox[2] - text_bbox[0] |
| text_h = text_bbox[3] - text_bbox[1] |
| |
| text_img = Image.new('RGBA', (text_w, text_h), (255, 255, 255, 0)) |
| text_draw = ImageDraw.Draw(text_img) |
| text_draw.text((0, 0), new_text, font=font, fill=text_color_rgb) |
|
|
| rotated_text_img = text_img.rotate(-angle, expand=True, resample=Image.BICUBIC) |
| |
| center_x, center_y = sum(x_coords) // 4, sum(y_coords) // 4 |
| paste_x = center_x - (rotated_text_img.width // 2) |
| paste_y = center_y - (rotated_text_img.height // 2) |
|
|
| txt_overlay.paste(rotated_text_img, (paste_x, paste_y), rotated_text_img) |
| final_image = Image.alpha_composite(final_image.convert('RGBA'), txt_overlay).convert('RGB') |
|
|
| return final_image, "Success!" |
|
|
| with gr.Blocks(theme=gr.themes.Soft()) as app: |
| gr.Markdown("# 🪄 Seamless Text-in-Image Replacement AI") |
| gr.Markdown("Upload an image, specify the text you want to replace, and enter the new text. The AI will erase the old text, reconstruct the background, and render the new text matching the original color, size, and angle.") |
| |
| with gr.Row(): |
| with gr.Column(): |
| image_input = gr.Image(type="pil", label="Upload Image") |
| old_text_input = gr.Textbox(label="Old Text (to remove)", placeholder="e.g., Trisoy") |
| new_text_input = gr.Textbox(label="New Text (to insert)", placeholder="e.g., Ridoy") |
| submit_btn = gr.Button("Replace Text", variant="primary") |
| |
| with gr.Column(): |
| image_output = gr.Image(type="pil", label="Result Image") |
| status_output = gr.Textbox(label="Status", interactive=False) |
|
|
| submit_btn.click( |
| fn=process_image, |
| inputs=[image_input, old_text_input, new_text_input], |
| outputs=[image_output, status_output] |
| ) |
|
|
| if __name__ == "__main__": |
| app.launch() |