Seg2Click / app.py
jcwang0602's picture
Deploy Seg2Click interactive demo
48d01ef verified
Raw History Blame Contribute Delete
5.79 kB
"""Interactive Seg2Click demo for Hugging Face Spaces."""
# ruff: noqa: I001 -- spaces must be imported before torch and its transitive importers.
import spaces # Import before torch so ZeroGPU can configure the runtime.
import time
import gradio as gr
import numpy as np
import torch
from huggingface_hub import snapshot_download
from PIL import Image, ImageDraw
from seg2click.predictor import Seg2ClickPredictor
MODEL_ID = "jcwang0602/Seg2Click-Qwen3.5-0.8B"
MASK_THRESHOLD = 0.5
EXAMPLES = [
["examples/vscode_refresh.png", "Refresh the file explorer."],
["examples/excel_sort.png", "open the sort dialog"],
["examples/word_insert_picture.png", "the pictures inserting function in word"],
["examples/photoshop_shape_mode.png", "change to shape mode"],
["examples/macos_cancel_extraction.png", "cancel extraction"],
]
print(f"Downloading {MODEL_ID} ...", flush=True)
checkpoint_dir = snapshot_download(MODEL_ID)
print(f"Loading model from {checkpoint_dir} ...", flush=True)
predictor = Seg2ClickPredictor.from_pretrained(
checkpoint_dir,
device="cuda",
attn_implementation="sdpa",
dtype=torch.bfloat16,
)
print("Seg2Click is ready.", flush=True)
def render_overlay(image: Image.Image, probability: np.ndarray, x: float, y: float) -> Image.Image:
"""Overlay the action-mask heatmap and predicted click on a screenshot."""
canvas = image.convert("RGB")
width, height = canvas.size
probability_image = Image.fromarray(
(np.clip(probability, 0, 1) * 255).astype(np.uint8), mode="L"
).resize((width, height), resample=Image.Resampling.BILINEAR)
values = np.asarray(probability_image, dtype=np.float32) / 255.0
heatmap = np.stack(
[
np.clip(1.5 - np.abs(4.0 * values - 3.0), 0, 1),
np.clip(1.5 - np.abs(4.0 * values - 2.0), 0, 1),
np.clip(1.5 - np.abs(4.0 * values - 1.0), 0, 1),
],
axis=-1,
)
base = np.asarray(canvas, dtype=np.float32)
blended = base * (1 - 0.55 * values[..., None]) + heatmap * 255 * (0.55 * values[..., None])
foreground = (values >= MASK_THRESHOLD).astype(np.float32)
blended *= 0.75 + 0.25 * foreground[..., None]
canvas = Image.fromarray(blended.astype(np.uint8), mode="RGB")
px = round(x * width)
py = round(y * height)
marker = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(marker)
draw.ellipse((px - 14, py - 14, px + 14, py + 14), fill=(255, 0, 0, 190))
draw.ellipse((px - 26, py - 26, px + 26, py + 26), outline=(255, 215, 0, 255), width=5)
return Image.alpha_composite(canvas.convert("RGBA"), marker).convert("RGB")
@spaces.GPU(duration=30)
def predict_click(screenshot: Image.Image, instruction: str):
"""Predict an action mask and executable click point."""
if screenshot is None:
raise gr.Error("Please upload a screenshot first.")
instruction = (instruction or "").strip()
if not instruction:
raise gr.Error("Please enter an instruction, such as 'click the Submit button'.")
image = screenshot.convert("RGB")
started = time.perf_counter()
with torch.inference_mode():
prediction = predictor.predict(image, instruction)
elapsed = time.perf_counter() - started
x, y = prediction.point
width, height = image.size
overlay = render_overlay(image, prediction.probability.numpy(), x, y)
report = (
f"**Click position:** `({x:.4f}, {y:.4f})` (normalized) \n"
f"**Pixel position:** `({round(x * width)}, {round(y * height)})` \n"
f"**Decode:** `{prediction.decode_reason}` \n"
f"**Inference time:** `{elapsed:.1f}s`"
)
return overlay, report
CSS = "#app-container { max-width: 1200px; margin: 0 auto; }"
with gr.Blocks(title="Seg2Click — GUI Visual Grounding") as demo:
gr.Markdown(
"""
# 🖱️ Seg2Click: Pixel-Level Action Masks for GUI Visual Grounding
Upload a GUI screenshot and describe what you want to click. Seg2Click predicts the complete
executable region as a pixel-level action mask, then returns its centroid as the click point.
This demo runs [Seg2Click-Qwen3.5-0.8B](https://huggingface.co/jcwang0602/Seg2Click-Qwen3.5-0.8B).
[Paper](https://arxiv.org/abs/2610.16234) · [Code](https://github.com/jcwang0602/Seg2Click)
"""
)
with gr.Row(elem_id="app-container"):
with gr.Column():
screenshot_input = gr.Image(
type="pil",
label="GUI screenshot",
height=440,
sources=["upload", "clipboard"],
)
instruction_input = gr.Textbox(
label="Instruction",
placeholder="e.g. click the refresh button in the sidebar",
)
predict_button = gr.Button("Predict click", variant="primary")
with gr.Column():
output_image = gr.Image(type="pil", label="Action mask and click point", height=440)
output_report = gr.Markdown()
gr.Markdown("## Examples\nClick an example to run fresh model inference on the selected screenshot.")
gr.Examples(
examples=EXAMPLES,
inputs=[screenshot_input, instruction_input],
outputs=[output_image, output_report],
fn=predict_click,
cache_examples=False,
run_on_click=True,
)
predict_button.click(
predict_click,
inputs=[screenshot_input, instruction_input],
outputs=[output_image, output_report],
api_name="predict_click",
)
instruction_input.submit(
predict_click,
inputs=[screenshot_input, instruction_input],
outputs=[output_image, output_report],
)
demo.launch(theme=gr.themes.Citrus(), css=CSS)