# Gui Actor Grounding

> Enable GUI agents to ground actions without generating pixel coordinates by using attention-based patch-level alignment and a verifier for selecting optimal action regions from candidates.

- Skill: `adu2021/gui-actor-grounding` (Agent Skill)
- Install (CLI): `npx skillmds@latest add adu2021/gui-actor-grounding`
- Raw SKILL.md: https://api.skillmd.com/api/skills/adu2021/gui-actor-grounding/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: adu2021 (https://skillmd.com/u/adu2021)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/adu2021/gui-actor-grounding

---


# GUI-Actor: Coordinate-Free Visual Grounding for GUI Agents

## Core Concept

GUI-Actor addresses a fundamental mismatch in VLM-powered GUI agents: generating precise pixel coordinates for screen actions. Traditional approaches output (x, y) coordinates, but Vision Transformers work with patch-level features, creating a gap between dense screen pixels and sparse patch embeddings.

GUI-Actor eliminates coordinate generation entirely. Instead, it uses an attention mechanism to align a dedicated `<ACTOR>` token with relevant visual patches, enabling patch-based action grounding. A separate verifier selects the optimal action region from candidates. This approach achieves superior performance (44.6 on ScreenSpot-Pro vs. 38.1 for larger competitors) while maintaining frozen VLM backbones.

## Architecture Overview

- **Attention-Based Action Head**: Align `<ACTOR>` token with screen patches using cross-attention
- **Patch-Level Grounding**: Work directly with ViT patch granularity rather than dense pixels
- **Grounding Verifier**: Evaluate and rank action region candidates for selection
- **Frozen VLM Backbone**: Preserve pretrained vision-language model weights
- **Multi-Action Support**: Generate multiple action proposals in parallel
- **Minimal Fine-Tuning**: Only ~100M parameters trainable; rest frozen

## Implementation

The following steps outline how to implement coordinate-free visual grounding for GUI agents:

1. **Extract patch embeddings** - Get Vision Transformer patch features from the screen image
2. **Create action candidates** - Generate multiple action proposals using the attention mechanism
3. **Score with verifier** - Evaluate candidate quality and select optimal actions
4. **Execute action** - Perform the selected action without explicit coordinates
5. **Process feedback** - Update understanding based on action outcomes
6. **Iterate** - Continue grounding new actions as agent progresses through task

```python
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import List, Tuple, Optional

class PatchExtractor:
    """Extract patch embeddings from Vision Transformer."""

    def __init__(self, patch_size: int = 16, image_size: int = 512):
        self.patch_size = patch_size
        self.image_size = image_size
        self.num_patches = (image_size // patch_size) ** 2

    def extract(self, image: torch.Tensor, vit_model) -> torch.Tensor:
        """Extract patch embeddings from image."""
        with torch.no_grad():
            # Get patch embeddings from ViT
            patches = vit_model.get_patch_embeddings(image)
        return patches


class ActionHead(nn.Module):
    """Attention-based action head for GUI grounding."""

    def __init__(self, embedding_dim: int = 768, num_patches: int = 1024, num_actions: int = 5):
        super().__init__()
        self.embedding_dim = embedding_dim
        self.num_patches = num_patches
        self.num_actions = num_actions

        # ACTOR token
        self.actor_token = nn.Parameter(torch.randn(1, 1, embedding_dim))

        # Cross-attention mechanism
        self.actor_attention = nn.MultiheadAttention(embedding_dim, num_heads=8, batch_first=True)

        # Action proposal generation
        self.action_mlp = nn.Sequential(
            nn.Linear(embedding_dim, 512),
            nn.ReLU(),
            nn.Linear(512, num_patches)
        )

    def forward(self, patch_embeddings: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        Generate action candidates via attention alignment.

        Args:
            patch_embeddings: (batch_size, num_patches, embedding_dim)

        Returns:
            action_logits: (batch_size, num_patches) - logits for each patch
            actor_embeddings: (batch_size, embedding_dim) - refined actor token
        """
        batch_size = patch_embeddings.shape[0]

        # Expand ACTOR token for batch
        actor = self.actor_token.expand(batch_size, -1, -1)

        # Cross-attention: ACTOR attends to patches
        actor_attended, _ = self.actor_attention(actor, patch_embeddings, patch_embeddings)

        # Generate action logits for each patch
        action_logits = self.action_mlp(actor_attended.squeeze(1))

        return action_logits, actor_attended.squeeze(1)


class GroundingVerifier(nn.Module):
    """Verify and rank action candidates."""

    def __init__(self, embedding_dim: int = 768):
        super().__init__()
        self.scoring_network = nn.Sequential(
            nn.Linear(embedding_dim * 2, 256),
            nn.ReLU(),
            nn.Dropout(0.1),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )

    def verify_candidates(self, actor_embedding: torch.Tensor, patch_embeddings: torch.Tensor,
                         action_logits: torch.Tensor, top_k: int = 3) -> Tuple[torch.Tensor, List[int]]:
        """
        Verify and rank action candidates.

        Args:
            actor_embedding: (batch_size, embedding_dim)
            patch_embeddings: (batch_size, num_patches, embedding_dim)
            action_logits: (batch_size, num_patches)
            top_k: Number of top candidates to return

        Returns:
            verified_scores: (batch_size, top_k) - confidence scores
            selected_patches: (batch_size, top_k) - patch indices
        """
        batch_size, num_patches, _ = patch_embeddings.shape

        # Get top-k candidates by logits
        top_logits, top_indices = torch.topk(action_logits, k=top_k, dim=1)

        # Score candidates using verifier
        verified_scores = []
        for b in range(batch_size):
            scores = []
            for idx in top_indices[b]:
                patch_emb = patch_embeddings[b, idx, :]
                combined = torch.cat([actor_embedding[b], patch_emb])
                score = self.scoring_network(combined)
                scores.append(score.item())
            verified_scores.append(scores)

        verified_scores = torch.tensor(verified_scores)

        return verified_scores, top_indices

    def select_action(self, verified_scores: torch.Tensor, patch_indices: torch.Tensor) -> torch.Tensor:
        """Select best action from verified candidates."""
        best_indices = torch.argmax(verified_scores, dim=1)
        selected_patches = torch.gather(patch_indices, 1, best_indices.unsqueeze(1))
        return selected_patches


class GUIActor(nn.Module):
    """Coordinate-free GUI action grounding."""

    def __init__(self, vit_model, embedding_dim: int = 768, num_patches: int = 1024):
        super().__init__()
        self.vit = vit_model
        self.action_head = ActionHead(embedding_dim, num_patches)
        self.verifier = GroundingVerifier(embedding_dim)
        self.patch_extractor = PatchExtractor()

    def ground_action(self, image: torch.Tensor) -> Tuple[int, float]:
        """
        Ground an action in the GUI without explicit coordinates.

        Args:
            image: Screenshot tensor (batch_size, 3, H, W)

        Returns:
            patch_id: Selected patch index
            confidence: Confidence score for the action
        """
        # Extract patch embeddings from frozen ViT backbone
        with torch.no_grad():
            patch_embeddings = self.vit.get_patch_embeddings(image)

        # Generate action candidates
        action_logits, actor_embedding = self.action_head(patch_embeddings)

        # Verify and select best action
        verified_scores, top_indices = self.verifier.verify_candidates(
            actor_embedding, patch_embeddings, action_logits, top_k=5
        )

        selected_patch = self.verifier.select_action(verified_scores, top_indices)

        return selected_patch.squeeze(), verified_scores.max().item()

    def forward(self, images: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        """Forward pass for batch processing."""
        batch_size = images.shape[0]

        # Extract patch embeddings
        with torch.no_grad():
            patch_embeddings = self.vit.get_patch_embeddings(images)

        # Generate action candidates
        action_logits, actor_embedding = self.action_head(patch_embeddings)

        # Verify candidates
        verified_scores, top_indices = self.verifier.verify_candidates(
            actor_embedding, patch_embeddings, action_logits, top_k=5
        )

        selected_patches = self.verifier.select_action(verified_scores, top_indices)

        return selected_patches, verified_scores


def patch_to_coordinates(patch_id: int, patch_size: int = 16, image_size: int = 512) -> Tuple[int, int]:
    """Convert patch ID back to approximate coordinates."""
    patches_per_side = image_size // patch_size
    row = (patch_id // patches_per_side) * patch_size + patch_size // 2
    col = (patch_id % patches_per_side) * patch_size + patch_size // 2
    return col, row
```

## Practical Guidance

**Fine-tuning strategy:**
- **Frozen backbone**: Keep VLM frozen during training; only update action head (~100M params)
- **Learning rate**: 1e-3 to 1e-4 for minimal backbone disturbance
- **Batch size**: 32-64 sufficient given limited trainable parameters
- **Epochs**: 3-10 epochs typically converge; monitor validation performance

**Verifier configuration:**
- **Top-k candidates**: 3-5 candidates; too few limits options, too many adds noise
- **Scoring network depth**: 2-3 layers; balance expressiveness with overfitting risk
- **Confidence threshold**: 0.7+ for filtering low-confidence actions

**When to use:**
- GUI automation and interaction tasks
- Mobile app testing and automation
- Web scraping requiring visual understanding
- Interactive systems where coordinate precision is difficult
- Transfer learning to new interfaces without coordinate retraining

**When NOT to use:**
- Pixel-level precision required (e.g., precise drawing tools)
- Dynamic interfaces with rapidly changing layouts
- Tasks where coordinate output is explicitly needed downstream
- Real-time systems where attention computation overhead is prohibitive

**Common pitfalls:**
- **Frozen VLM limitation**: Backbone features may not be optimal for action grounding; consider light fine-tuning
- **Patch resolution tradeoff**: Larger patches lose fine-grained localization; smaller patches increase computation
- **Verifier collapse**: Scoring network may converge to trivial solutions; use diverse training data
- **Generalization**: Models may overfit to specific UI layouts; validate on diverse interfaces
- **Confidence calibration**: Verified scores may not reflect true reliability; apply temperature scaling

## Reference

GUI-Actor achieves 44.6 on ScreenSpot-Pro with Qwen2.5-VL, surpassing larger models (UI-TARS-72B at 38.1). The approach improves generalization to unseen resolutions and demonstrates effective capability transfer from frozen VLMs.

Original paper: "GUI-Actor: Coordinate-Free Visual Grounding for GUI Agents" (arxiv.org/abs/2506.03143)

