deankk's picture
Update app.py
c08672a verified
Raw
History Blame Contribute Delete
11.4 kB
import gradio as gr
import numpy as np
import cv2
from PIL import Image
import tempfile
import os
def dilate_mask(mask, strength=5):
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (strength, strength))
return cv2.dilate(mask, kernel, iterations=1)
def patch_based_inpaint(img_bgr, mask, dilate_strength=5):
"""
Patch Clone: untuk watermark kecil, ambil patch dari sekitar dan tempel.
Jauh lebih tajam daripada cv2.inpaint yang blur.
"""
mask = dilate_mask(mask, dilate_strength)
h, w = img_bgr.shape[:2]
# Bounding box watermark
x, y, mw, mh = cv2.boundingRect(mask)
if mw == 0 or mh == 0:
return img_bgr
# Perluas bbox sedikit untuk blending
pad = max(mw, mh) // 2
x1 = max(0, x - pad)
y1 = max(0, y - pad)
x2 = min(w, x + mw + pad)
y2 = min(h, y + mh + pad)
# Cari patch sumber: ambil dari area di sekitar bbox yang TIDAK kena mask
# Prioritas: kiri -> atas -> kanan -> bawah (arah yang paling jauh dari tepi gambar)
patch_h = y2 - y1
patch_w = x2 - x1
candidates = []
# Kiri
if x1 - patch_w >= 0:
patch = img_bgr[y1:y2, x1-patch_w:x1].copy()
if patch.shape[0] == patch_h and patch.shape[1] == patch_w:
candidates.append(patch)
# Atas
if y1 - patch_h >= 0:
patch = img_bgr[y1-patch_h:y1, x1:x2].copy()
if patch.shape[0] == patch_h and patch.shape[1] == patch_w:
candidates.append(patch)
# Kanan
if x2 + patch_w <= w:
patch = img_bgr[y1:y2, x2:x2+patch_w].copy()
if patch.shape[0] == patch_h and patch.shape[1] == patch_w:
candidates.append(patch)
# Bawah
if y2 + patch_h <= h:
patch = img_bgr[y2:y2+patch_h, x1:x2].copy()
if patch.shape[0] == patch_h and patch.shape[1] == patch_w:
candidates.append(patch)
if not candidates:
# Fallback: cv2.inpaint NS
return cv2.inpaint(img_bgr, mask, 3, cv2.INPAINT_NS)
# Pilih patch yang paling mirip dengan tepi area watermark (L2 distance minimal)
# Ambil ring 5px di sekitar bbox sebagai referensi
best_patch = candidates[0]
if len(candidates) > 1:
ring_mask = np.zeros((h, w), dtype=np.uint8)
cv2.rectangle(ring_mask, (x1, y1), (x2, y2), 255, 3)
ring_mask = cv2.bitwise_and(ring_mask, cv2.bitwise_not(mask))
if cv2.countNonZero(ring_mask) > 0:
ref_mean = cv2.mean(img_bgr, ring_mask)[:3]
best_dist = float('inf')
for cand in candidates:
# Resize candidate ke ukuran bbox untuk compare
cand_small = cv2.resize(cand, (x2-x1, y2-y1))
# Ambil border candidate
cm = np.zeros((y2-y1, x2-x1), dtype=np.uint8)
cv2.rectangle(cm, (0,0), (x2-x1-1, y2-y1-1), 255, 3)
if cv2.countNonZero(cm) > 0:
dist = sum(abs(a-b) for a,b in zip(cv2.mean(cand_small, cm)[:3], ref_mean))
if dist < best_dist:
best_dist = dist
best_patch = cand
# Resize patch ke ukuran bbox
patch_resized = cv2.resize(best_patch, (x2-x1, y2-y1), interpolation=cv2.INTER_CUBIC)
# Buat mask untuk blending (feathered edge)
roi_mask = mask[y1:y2, x1:x2].copy()
if roi_mask.shape[0] != patch_resized.shape[0] or roi_mask.shape[1] != patch_resized.shape[1]:
roi_mask = cv2.resize(roi_mask, (patch_resized.shape[1], patch_resized.shape[0]), interpolation=cv2.INTER_NEAREST)
# Feather mask untuk blending halus
roi_mask_f = roi_mask.astype(np.float32) / 255.0
roi_mask_f = cv2.GaussianBlur(roi_mask_f, (0,0), sigmaX=max(3, dilate_strength//2))
# Blend patch ke gambar asli
result = img_bgr.copy()
roi = result[y1:y2, x1:x2].astype(np.float32)
patch_f = patch_resized.astype(np.float32)
for c in range(3):
roi[:,:,c] = roi[:,:,c] * (1 - roi_mask_f) + patch_f[:,:,c] * roi_mask_f
result[y1:y2, x1:x2] = roi.astype(np.uint8)
# Post: slight sharpening untuk area hasil
kernel_sharp = np.array([[0,-1,0],[-1,5,-1],[0,-1,0]], dtype=np.float32)
sharpened = cv2.filter2D(result, -1, kernel_sharp)
blend_sharp = roi_mask_f * 0.3
for c in range(3):
result[y1:y2, x1:x2, c] = result[y1:y2, x1:x2, c] * (1-blend_sharp) + sharpened[y1:y2, x1:x2, c] * blend_sharp
return result
def ns_sharpen_inpaint(img_bgr, mask, dilate_strength=2):
"""
Navier-Stokes inpaint + unsharp mask + CLAHE.
Lebih baik untuk tekstur metal/kompleks daripada Telea.
"""
mask = dilate_mask(mask, dilate_strength)
# NS lebih tajam untuk edge/tekstur daripada Telea
result = cv2.inpaint(img_bgr, mask, 3, cv2.INPAINT_NS)
# Unsharp mask untuk mengurangi blur
gaussian = cv2.GaussianBlur(result, (0,0), sigmaX=2.0)
result = cv2.addWeighted(result, 1.5, gaussian, -0.5, 0)
# CLAHE untuk kontras lokal (membantu tekstur metal)
lab = cv2.cvtColor(result, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
l = clahe.apply(l)
result = cv2.cvtColor(cv2.merge([l,a,b]), cv2.COLOR_LAB2BGR)
return result
def remove_watermark(editor_data, method_name, dilate_strength, detail_strength):
if editor_data is None:
return None, None, None
try:
if not isinstance(editor_data, dict):
return None, None, None
bg = editor_data.get("background")
layers = editor_data.get("layers", [])
if bg is None:
return None, None, None
# Parse background
if isinstance(bg, np.ndarray):
img = Image.fromarray(bg).convert("RGB")
elif hasattr(bg, "convert"):
img = bg.convert("RGB")
else:
img = Image.fromarray(np.array(bg)).convert("RGB")
if len(layers) == 0:
return img, None, None
# Parse mask dari layer brush
mask_layer = layers[-1]
mask = np.array(mask_layer) if not isinstance(mask_layer, np.ndarray) else mask_layer
img_np = np.array(img)
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
if len(mask.shape) == 3:
if mask.shape[2] == 4:
mask_gray = cv2.cvtColor(mask, cv2.COLOR_RGBA2GRAY)
else:
mask_gray = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY)
else:
mask_gray = mask
_, mask_bin = cv2.threshold(mask_gray, 5, 255, cv2.THRESH_BINARY)
if cv2.countNonZero(mask_bin) == 0:
return img, None, None
# Pilih metode
if method_name == "Patch Clone (Tajam)":
result_bgr = patch_based_inpaint(img_bgr, mask_bin, dilate_strength)
elif method_name == "NS + Sharpen (Smooth)":
result_bgr = ns_sharpen_inpaint(img_bgr, mask_bin, dilate_strength)
else:
# Telea original
mask_d = dilate_mask(mask_bin, dilate_strength)
result_bgr = cv2.inpaint(img_bgr, mask_d, 3, cv2.INPAINT_TELEA)
result_rgb = cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB)
result_pil = Image.fromarray(result_rgb)
# Simpan ke file temporary untuk download
png_path = os.path.join(tempfile.gettempdir(), "result.png")
jpg_path = os.path.join(tempfile.gettempdir(), "result.jpg")
result_pil.save(png_path, format="PNG")
result_pil.save(jpg_path, format="JPEG", quality=95, optimize=True)
return result_pil, png_path, jpg_path
except Exception as e:
# Fallback aman
try:
if isinstance(editor_data, dict) and editor_data.get("background"):
bg = editor_data["background"]
if isinstance(bg, np.ndarray):
fallback = Image.fromarray(bg).convert("RGB")
else:
fallback = bg.convert("RGB") if hasattr(bg, "convert") else None
return fallback, None, None
except Exception:
return None, None, None
# ==========================================
# GRADIO 6.x UI
# ==========================================
with gr.Blocks(title="Watermark Remover CPU - Patch Clone") as demo:
gr.Markdown("""
# 🧽 Watermark Remover (Patch Clone Edition)
### CPU-Friendly untuk Hugging Face Space Gratis
**Cara pakai:**
1. Upload gambar di canvas (ikon πŸ“Ž pojok kanan bawah)
2. Pilih ikon **kuas (brush)** dan coret area watermark
3. Pilih metode:
- **Patch Clone (Tajam)** β†’ ambil tekstur dari sekitar, tidak blur
- **NS + Sharpen (Smooth)** β†’ Navier-Stokes + sharpening, blur minimal
4. Atur **Dilate Mask** (2-5 untuk watermark kecil, 10+ untuk besar)
5. Klik **✨ Hapus Watermark**
6. Download **PNG** atau **JPG**
""")
with gr.Row():
with gr.Column(scale=1):
editor = gr.ImageEditor(
label="πŸ“Έ Upload & Brush Area Watermark",
height=420,
)
with gr.Row():
method = gr.Radio(
["Patch Clone (Tajam)", "NS + Sharpen (Smooth)", "Telea (Blur)"],
value="Patch Clone (Tajam)",
label="Metode"
)
dilate = gr.Slider(
minimum=1, maximum=25, value=3, step=1,
label="πŸ”§ Dilate Mask (2-5 untuk watermark kecil)"
)
detail = gr.Slider(
minimum=0, maximum=100, value=80, step=5,
label="🎨 Detail Strength (unused, reserved)",
visible=False # hidden, reserved for future
)
btn_remove = gr.Button("✨ Hapus Watermark", variant="primary", size="lg")
btn_clear = gr.Button("πŸ—‘οΈ Reset")
with gr.Column(scale=1):
output_img = gr.Image(
label="βœ… Hasil Preview",
height=350,
)
gr.Markdown("### πŸ“₯ Download Hasil")
with gr.Row():
output_png = gr.File(label="Download PNG", interactive=False)
output_jpg = gr.File(label="Download JPG", interactive=False)
gr.Markdown("""
**Kenapa tidak blur lagi?**
- 🧩 **Patch Clone** mengambil tekstur asli dari sekitar watermark dan menempelkannya dengan blending halus
- ⚑ **NS + Sharpen** menggunakan Navier-Stokes (lebih tajam dari Telea) + unsharp mask + CLAHE
- 🎯 Untuk watermark kecil di pojok/pojok: **Patch Clone** adalah solusi terbaik untuk CPU
- πŸ’Ύ Output tersedia dalam **PNG (lossless)** dan **JPG (quality 95)**
""")
btn_remove.click(
fn=remove_watermark,
inputs=[editor, method, dilate, detail],
outputs=[output_img, output_png, output_jpg]
)
btn_clear.click(
lambda: (None, None, None),
outputs=[output_img, output_png, output_jpg]
)
if __name__ == "__main__":
demo.launch()