|
|
""" |
|
|
SD3.5 Watermark Remover - Hugging Face Space |
|
|
|
|
|
Uses Stable Diffusion 3.5 img2img regeneration to remove watermarks |
|
|
while preserving image quality. Powered by ZeroGPU. |
|
|
|
|
|
Based on: https://github.com/XuandongZhao/WatermarkAttacker |
|
|
""" |
|
|
|
|
|
import os |
|
|
import gradio as gr |
|
|
import numpy as np |
|
|
import spaces |
|
|
import torch |
|
|
from PIL import Image |
|
|
from huggingface_hub import login |
|
|
|
|
|
|
|
|
if os.environ.get("HF_TOKEN"): |
|
|
login(token=os.environ["HF_TOKEN"]) |
|
|
print("Logged in with HF_TOKEN") |
|
|
|
|
|
from diffusers import StableDiffusion3Img2ImgPipeline |
|
|
|
|
|
|
|
|
MODEL_ID = "stabilityai/stable-diffusion-3.5-large" |
|
|
MAX_IMAGE_SIZE = 1536 |
|
|
MAX_SEED = np.iinfo(np.int32).max |
|
|
|
|
|
|
|
|
device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
torch_dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32 |
|
|
|
|
|
|
|
|
print(f"Loading SD3.5 model: {MODEL_ID}") |
|
|
pipe = StableDiffusion3Img2ImgPipeline.from_pretrained( |
|
|
MODEL_ID, |
|
|
torch_dtype=torch_dtype, |
|
|
) |
|
|
pipe = pipe.to(device) |
|
|
print("Model loaded successfully!") |
|
|
|
|
|
|
|
|
def preprocess_image(image: Image.Image, max_size: int = 1536) -> tuple[Image.Image, tuple[int, int], float]: |
|
|
""" |
|
|
Resize large images and pad to multiple of 64 for SD3 compatibility. |
|
|
|
|
|
Returns: |
|
|
Tuple of (processed_image, original_size, scale_factor) |
|
|
""" |
|
|
original_size = image.size |
|
|
w, h = original_size |
|
|
|
|
|
|
|
|
scale_factor = 1.0 |
|
|
if max(w, h) > max_size: |
|
|
scale_factor = max_size / max(w, h) |
|
|
new_w = int(w * scale_factor) |
|
|
new_h = int(h * scale_factor) |
|
|
image = image.resize((new_w, new_h), Image.Resampling.LANCZOS) |
|
|
print(f"Resized from {w}x{h} to {new_w}x{new_h} (scale: {scale_factor:.3f})") |
|
|
w, h = new_w, new_h |
|
|
|
|
|
|
|
|
pad_w = (w + 63) // 64 * 64 |
|
|
pad_h = (h + 63) // 64 * 64 |
|
|
|
|
|
if (pad_w, pad_h) != (w, h): |
|
|
padded_img = Image.new('RGB', (pad_w, pad_h), (0, 0, 0)) |
|
|
padded_img.paste(image, (0, 0)) |
|
|
return padded_img, original_size, scale_factor |
|
|
|
|
|
return image, original_size, scale_factor |
|
|
|
|
|
|
|
|
def postprocess_image(image: Image.Image, original_size: tuple[int, int], scale_factor: float) -> Image.Image: |
|
|
"""Crop padding and resize back to original dimensions.""" |
|
|
w, h = image.size |
|
|
original_w, original_h = original_size |
|
|
|
|
|
|
|
|
if scale_factor < 1.0: |
|
|
scaled_w = int(original_w * scale_factor) |
|
|
scaled_h = int(original_h * scale_factor) |
|
|
image = image.crop((0, 0, scaled_w, scaled_h)) |
|
|
|
|
|
image = image.resize((original_w, original_h), Image.Resampling.LANCZOS) |
|
|
print(f"Upscaled back to original size: {original_w}x{original_h}") |
|
|
else: |
|
|
|
|
|
if image.size != original_size: |
|
|
image = image.crop((0, 0, original_w, original_h)) |
|
|
|
|
|
return image |
|
|
|
|
|
|
|
|
@spaces.GPU(duration=90) |
|
|
def remove_watermark( |
|
|
input_image: Image.Image, |
|
|
strength: float = 0.3, |
|
|
num_inference_steps: int = 28, |
|
|
seed: int = 42, |
|
|
randomize_seed: bool = True, |
|
|
progress=gr.Progress(track_tqdm=True), |
|
|
) -> tuple[Image.Image, int]: |
|
|
""" |
|
|
Remove watermark from image using SD3.5 img2img regeneration. |
|
|
|
|
|
Args: |
|
|
input_image: Input image with watermark |
|
|
strength: Denoising strength (0.0-1.0). Lower = higher quality, less change. |
|
|
num_inference_steps: Number of denoising steps |
|
|
seed: Random seed for reproducibility |
|
|
randomize_seed: Whether to randomize the seed |
|
|
|
|
|
Returns: |
|
|
Tuple of (output image, seed used) |
|
|
""" |
|
|
if input_image is None: |
|
|
raise gr.Error("Please upload an image first!") |
|
|
|
|
|
|
|
|
if input_image.mode != 'RGB': |
|
|
input_image = input_image.convert('RGB') |
|
|
|
|
|
|
|
|
original_w, original_h = input_image.size |
|
|
print(f"Original image size: {original_w}x{original_h}") |
|
|
|
|
|
|
|
|
if randomize_seed: |
|
|
seed = np.random.randint(0, MAX_SEED) |
|
|
|
|
|
generator = torch.Generator(device=device).manual_seed(seed) |
|
|
|
|
|
|
|
|
processed_image, original_size, scale_factor = preprocess_image(input_image) |
|
|
padded_w, padded_h = processed_image.size |
|
|
print(f"Processed image size: {padded_w}x{padded_h} (scale: {scale_factor:.3f})") |
|
|
|
|
|
|
|
|
result = pipe( |
|
|
prompt="", |
|
|
image=processed_image, |
|
|
strength=strength, |
|
|
num_inference_steps=num_inference_steps, |
|
|
guidance_scale=0.0, |
|
|
generator=generator, |
|
|
).images[0] |
|
|
|
|
|
print(f"Pipeline output size: {result.size}") |
|
|
|
|
|
|
|
|
result = postprocess_image(result, original_size, scale_factor) |
|
|
print(f"Final output size: {result.size}") |
|
|
|
|
|
return result, seed |
|
|
|
|
|
|
|
|
|
|
|
examples = [ |
|
|
["examples/watermarked_sample.png", 0.3, 28, 42, True], |
|
|
] |
|
|
|
|
|
|
|
|
css = """ |
|
|
#col-container { |
|
|
margin: 0 auto; |
|
|
max-width: 900px; |
|
|
} |
|
|
|
|
|
.gr-button-primary { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
|
|
} |
|
|
|
|
|
footer { |
|
|
visibility: hidden; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo: |
|
|
with gr.Column(elem_id="col-container"): |
|
|
gr.Markdown(""" |
|
|
# π¨ SD3.5 Watermark Remover |
|
|
|
|
|
Remove watermarks from images using **Stable Diffusion 3.5** regeneration. |
|
|
|
|
|
This tool uses img2img to regenerate the image while preserving its content, |
|
|
effectively removing watermarks without manual editing. |
|
|
|
|
|
**How it works:** Lower strength values produce higher quality outputs closer to the original. |
|
|
Start with 0.3 and adjust as needed. |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
input_image = gr.Image( |
|
|
label="Input Image (with watermark)", |
|
|
type="pil", |
|
|
height=400, |
|
|
) |
|
|
|
|
|
run_button = gr.Button("π Remove Watermark", variant="primary", size="lg") |
|
|
|
|
|
with gr.Accordion("βοΈ Advanced Settings", open=False): |
|
|
strength = gr.Slider( |
|
|
label="Strength", |
|
|
info="Lower = higher quality, less change. Higher = more aggressive removal.", |
|
|
minimum=0.1, |
|
|
maximum=0.6, |
|
|
step=0.05, |
|
|
value=0.3, |
|
|
) |
|
|
|
|
|
num_inference_steps = gr.Slider( |
|
|
label="Inference Steps", |
|
|
info="More steps = higher quality, slower processing.", |
|
|
minimum=10, |
|
|
maximum=50, |
|
|
step=1, |
|
|
value=28, |
|
|
) |
|
|
|
|
|
seed = gr.Slider( |
|
|
label="Seed", |
|
|
minimum=0, |
|
|
maximum=MAX_SEED, |
|
|
step=1, |
|
|
value=42, |
|
|
) |
|
|
|
|
|
randomize_seed = gr.Checkbox( |
|
|
label="Randomize seed", |
|
|
value=True, |
|
|
) |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
output_image = gr.Image( |
|
|
label="Output Image (watermark removed)", |
|
|
type="pil", |
|
|
height=400, |
|
|
) |
|
|
|
|
|
output_seed = gr.Number(label="Seed Used", interactive=False) |
|
|
|
|
|
gr.Markdown(""" |
|
|
### π‘ Tips |
|
|
- **Strength 0.2-0.3**: Best for subtle watermarks, preserves most detail |
|
|
- **Strength 0.4-0.5**: Better for prominent watermarks, may alter some details |
|
|
- **Increase steps**: If quality is poor, try 35-40 steps |
|
|
|
|
|
--- |
|
|
|
|
|
Based on [WatermarkAttacker](https://github.com/XuandongZhao/WatermarkAttacker) research. |
|
|
Uses Stable Diffusion 3.5 Large from [Stability AI](https://huggingface.co/stabilityai/stable-diffusion-3.5-large). |
|
|
""") |
|
|
|
|
|
|
|
|
gr.on( |
|
|
triggers=[run_button.click], |
|
|
fn=remove_watermark, |
|
|
inputs=[ |
|
|
input_image, |
|
|
strength, |
|
|
num_inference_steps, |
|
|
seed, |
|
|
randomize_seed, |
|
|
], |
|
|
outputs=[output_image, output_seed], |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch() |
|
|
|