Peace-Network's picture
Update app.py
a943fd6 verified
Raw
History Blame Contribute Delete
6.5 kB
"""
Specs Remover — single-purpose app.
Upload a photo -> glasses are automatically removed -> download the result.
"""
import os
import random
import time
import cv2
import gradio as gr
import mediapipe as mp
import numpy as np
import spaces
import torch
from PIL import Image, ImageFilter
from diffusers import QwenImageEditPlusPipeline
from diffusers.models import QwenImageTransformer2DModel
GLASSES_REMOVAL_PROMPT = (
"Remove the eyeglasses from the image while preserving the background "
"and remaining elements, maintaining realism and original details. "
"Keep the eyes, eyebrows, eyelashes, nose, skin tone, lighting, "
"hairstyle, and facial identity exactly the same and fully natural."
)
NEGATIVE_PROMPT = " "
MAX_SEED = np.iinfo(np.int32).max
dtype = torch.bfloat16
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Loading transformer...")
transformer = QwenImageTransformer2DModel.from_pretrained(
"Qwen/Qwen-Image-Edit-2511",
subfolder="transformer",
torch_dtype=dtype,
)
print("Loading pipeline...")
pipe = QwenImageEditPlusPipeline.from_pretrained(
"Qwen/Qwen-Image-Edit-2511",
transformer=transformer,
torch_dtype=dtype,
).to(device)
print("Loading Lightning LoRA...")
pipe.load_lora_weights(
"lightx2v/Qwen-Image-Edit-2511-Lightning",
weight_name="Qwen-Image-Edit-2511-Lightning-4steps-V1.0-bf16.safetensors",
adapter_name="lightning",
)
print("Loading Object-Remover LoRA...")
pipe.load_lora_weights(
"prithivMLmods/Qwen-Image-Edit-2511-Object-Remover",
adapter_name="object_remover",
)
pipe.set_adapters(["lightning", "object_remover"], adapter_weights=[1.0, 1.0])
pipe.fuse_lora(adapter_names=["lightning", "object_remover"])
print("Pipeline ready.")
_FACE_MESH = mp.solutions.face_mesh.FaceMesh(
static_image_mode=True,
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5,
)
_GLASSES_REGION_INDICES = [
70, 63, 105, 66, 107, 55, 65, 52, 53, 46,
300, 293, 334, 296, 336, 285, 295, 282, 283, 276,
33, 133, 160, 159, 158, 157, 173, 246,
362, 263, 387, 386, 385, 384, 398, 466,
6, 197, 195, 5, 4,
127, 234, 93, 356, 454, 323,
]
def build_glasses_region_mask(image_pil):
image_bgr = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
h, w = image_bgr.shape[:2]
results = _FACE_MESH.process(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB))
if not results.multi_face_landmarks:
return None
landmarks = results.multi_face_landmarks[0].landmark
points = np.array(
[[landmarks[i].x * w, landmarks[i].y * h] for i in _GLASSES_REGION_INDICES],
dtype=np.float32,
)
hull = cv2.convexHull(points.astype(np.int32))
mask = np.zeros((h, w), dtype=np.uint8)
cv2.fillConvexPoly(mask, hull, 255)
pad = max(8, int(0.03 * min(h, w)))
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (pad * 2, pad * 2))
mask = cv2.dilate(mask, kernel)
mask_pil = Image.fromarray(mask, mode="L")
feather_radius = max(4, int(0.015 * min(h, w)))
mask_pil = mask_pil.filter(ImageFilter.GaussianBlur(radius=feather_radius))
return mask_pil
def blend_with_original(original_pil, ai_output_pil):
if ai_output_pil.size != original_pil.size:
ai_output_pil = ai_output_pil.resize(original_pil.size, Image.LANCZOS)
mask = build_glasses_region_mask(original_pil)
if mask is None:
return ai_output_pil
return Image.composite(ai_output_pil, original_pil, mask)
@spaces.GPU(duration=300)
def remove_glasses(
input_image,
seed=42,
randomize_seed=True,
num_inference_steps=4,
progress=gr.Progress(track_tqdm=True),
):
print("remove_glasses called")
if input_image is None:
raise gr.Error("Please upload a photo first.")
try:
if randomize_seed:
seed = random.randint(0, MAX_SEED)
generator = torch.Generator(device=device).manual_seed(seed)
if isinstance(input_image, str):
pil_image = Image.open(input_image).convert("RGB")
else:
pil_image = input_image.convert("RGB")
progress(0.1, desc="Removing glasses...")
result = pipe(
image=[pil_image],
prompt=GLASSES_REMOVAL_PROMPT,
negative_prompt=NEGATIVE_PROMPT,
num_inference_steps=num_inference_steps,
generator=generator,
true_cfg_scale=1.0,
num_images_per_prompt=1,
).images
output_image = result[0]
progress(0.9, desc="Preserving original details...")
output_image = blend_with_original(pil_image, output_image)
os.makedirs("outputs", exist_ok=True)
output_path = f"outputs/result_{seed}_{int(time.time() * 1000)}.png"
output_image.save(output_path)
print("remove_glasses done")
return output_path
except Exception as e:
import traceback
traceback.print_exc()
raise gr.Error(f"Generation failed: {e}")
css = """
#col-container {
margin: 0 auto;
max-width: 640px;
}
#title {
text-align: center;
}
"""
with gr.Blocks(css=css, title="Specs Remover") as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown(
"""
# 👓 Specs Remover
Upload a photo. Glasses are automatically removed —
everything else about the face stays natural.
""",
elem_id="title",
)
input_image = gr.Image(
label="Upload a photo",
type="filepath",
interactive=True,
)
run_button = gr.Button("Remove Glasses", variant="primary")
output_image = gr.Image(label="Result", type="filepath")
with gr.Accordion("Advanced (optional)", open=False):
seed = gr.Slider(
label="Seed",
minimum=0,
maximum=MAX_SEED,
step=1,
value=0,
)
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
num_inference_steps = gr.Slider(
label="Inference steps",
minimum=1,
maximum=40,
step=1,
value=4,
)
run_button.click(
fn=remove_glasses,
inputs=[input_image, seed, randomize_seed, num_inference_steps],
outputs=[output_image],
)
if __name__ == "__main__":
demo.launch()