File size: 5,524 Bytes
90b9784
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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

# Initialize EasyOCR reader (Set to CPU for free web hosting)
print("Initializing EasyOCR...")
reader = easyocr.Reader(['en'], gpu=False) 

# Download a default scalable font if it doesn't exist
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()