specs / app.py
Peace-Network's picture
Update app.py
200710a verified
Raw
History Blame Contribute Delete
6.77 kB
"""
Specs Remover — single-purpose app.
Upload a photo -> glasses are automatically removed -> download the result.
Built on the same qwenimage/ pipeline as the duplicated FireRed Space
(fast, FA3-accelerated). The edit prompt is fixed internally — the user
never sees or edits it.
"""
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 qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
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(
"FireRedTeam/FireRed-Image-Edit-1.1",
subfolder="transformer",
torch_dtype=dtype,
)
print("Loading pipeline...")
pipe = QwenImageEditPlusPipeline.from_pretrained(
"FireRedTeam/FireRed-Image-Edit-1.1",
transformer=transformer,
torch_dtype=dtype,
).to(device)
try:
pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
print("Flash Attention 3 Processor set successfully.")
except Exception as e:
print(f"Warning: Could not set FA3 processor: {e}")
print("Pipeline ready.")
# --- Face-region masking for identity-preserving blend ---
_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, # left eyebrow
300, 293, 334, 296, 336, 285, 295, 282, 283, 276, # right eyebrow
33, 133, 160, 159, 158, 157, 173, 246, # left eye
362, 263, 387, 386, 385, 384, 398, 466, # right eye
6, 197, 195, 5, 4, # nose bridge
127, 234, 93, 356, 454, 323, # temples
]
def build_glasses_region_mask(image_pil: Image.Image):
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: Image.Image, ai_output_pil: Image.Image):
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=60)
def remove_glasses(
input_image,
seed=42,
randomize_seed=True,
num_inference_steps=8,
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=8,
)
run_button.click(
fn=remove_glasses,
inputs=[input_image, seed, randomize_seed, num_inference_steps],
outputs=[output_image],
)
if __name__ == "__main__":
demo.launch()