File size: 5,791 Bytes
0378d77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0684ac
0378d77
f0684ac
48d01ef
 
 
 
 
f0684ac
0378d77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48d01ef
0378d77
 
 
 
 
 
 
f0684ac
0378d77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0684ac
 
 
 
 
 
 
 
 
 
0378d77
 
 
 
 
 
 
 
 
 
 
 
48d01ef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""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)