Chapter 03

截图工具与视觉理解

构建高效的截图管道,让 Claude 准确感知屏幕状态

截图工具选型

主要 Python 截图库对比

平台 速度 特点
mss Win/Mac/Linux 极快 纯 Python,无外部依赖,专为截图设计
Pillow (ImageGrab) Win/Mac 功能全面的图像处理库,截图只是其中一个功能
pyautogui Win/Mac/Linux 中等 集成截图+鼠标控制,一体化方案
scrot (Linux) Linux 命令行工具,需要 subprocess 调用

推荐方案:mss + Pillow

使用 mss 进行高速截图,使用 Pillow 进行图像处理(缩放、压缩)。

# 安装依赖
pip install mss Pillow

截图实现

基础截图实现

import mss
import mss.tools
from PIL import Image
import io
import base64
from typing import Optional


class ScreenshotTool:
    """高效的截图工具,针对 Computer Use 优化"""

    def __init__(
        self,
        target_width: int = 1280,
        quality: int = 75,
        format: str = "JPEG"
    ):
        """
        Args:
            target_width: 截图缩放到的目标宽度(像素)
            quality: JPEG 压缩质量(1-95),75 是速度和质量的平衡点
            format: 图像格式,JPEG 更小,PNG 无损
        """
        self.target_width = target_width
        self.quality = quality
        self.format = format
        self._sct = mss.mss()  # 复用 mss 实例,避免重复初始化

    def take_screenshot(
        self,
        monitor: int = 1,
        region: Optional[dict] = None
    ) -> bytes:
        """
        截取屏幕截图并返回压缩后的字节数据。

        Args:
            monitor: 显示器编号(1 为主显示器)
            region: 截取区域 {'top': y, 'left': x, 'width': w, 'height': h}
                   None 表示截取整个显示器

        Returns:
            压缩后的图像字节数据
        """
        # 确定截图区域
        if region:
            screenshot_region = region
        else:
            screenshot_region = self._sct.monitors[monitor]

        # 执行截图
        sct_img = self._sct.grab(screenshot_region)

        # 转换为 PIL Image
        img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")

        # 缩放以减少 Token 消耗
        if img.width > self.target_width:
            ratio = self.target_width / img.width
            new_height = int(img.height * ratio)
            img = img.resize(
                (self.target_width, new_height),
                Image.Resampling.LANCZOS
            )

        # 压缩并返回字节数据
        buffer = io.BytesIO()
        if self.format == "JPEG":
            img.save(buffer, format="JPEG", quality=self.quality, optimize=True)
        else:
            img.save(buffer, format="PNG", optimize=True)
        return buffer.getvalue()

    def take_screenshot_b64(self, **kwargs) -> str:
        """截图并返回 base64 编码字符串(用于 API 传输)"""
        screenshot_bytes = self.take_screenshot(**kwargs)
        return base64.standard_b64encode(screenshot_bytes).decode()

    def get_screen_size(self, monitor: int = 1) -> tuple[int, int]:
        """获取显示器实际分辨率"""
        m = self._sct.monitors[monitor]
        return m["width"], m["height"]

坐标系缩放:重要细节

当我们将截图缩放到较小尺寸时,Claude 返回的点击坐标也是基于缩放后的图像大小。在实际执行鼠标操作时,需要将坐标转换回实际屏幕坐标:

class CoordinateTransformer:
    """在截图坐标和实际屏幕坐标之间转换"""

    def __init__(
        self,
        screenshot_width: int,
        screenshot_height: int,
        screen_width: int,
        screen_height: int
    ):
        self.scale_x = screen_width / screenshot_width
        self.scale_y = screen_height / screenshot_height

    def to_screen(self, x: int, y: int) -> tuple[int, int]:
        """将截图坐标转换为屏幕坐标"""
        screen_x = int(x * self.scale_x)
        screen_y = int(y * self.scale_y)
        return screen_x, screen_y

# 使用示例
screenshot_tool = ScreenshotTool(target_width=1280)
screen_w, screen_h = screenshot_tool.get_screen_size()
# 截图后的宽度为 1280,高度等比缩放
screenshot_h = int(screen_h * (1280 / screen_w))

transformer = CoordinateTransformer(
    screenshot_width=1280,
    screenshot_height=screenshot_h,
    screen_width=screen_w,
    screen_height=screen_h
)

# Claude 说要点击 (640, 400)(基于截图坐标)
actual_x, actual_y = transformer.to_screen(640, 400)
# actual_x, actual_y 是实际屏幕坐标

图像优化策略

分辨率与 Token 的权衡

1920×1080 全分辨率
~1200 tokens/张。适合需要精确识别小元素(如密集表格、小按钮)的场景。成本最高。
1280×720(推荐)
~600 tokens/张。大多数 UI 元素清晰可辨,成本适中。这是大多数实际项目的最佳平衡点。
960×540
~350 tokens/张。成本低,但小文字和小按钮可能难以识别,导致 Claude 需要更多截图确认。

截图缓存机制

import hashlib
import time
from dataclasses import dataclass


@dataclass
class CachedScreenshot:
    data: bytes
    timestamp: float
    hash: str


class CachingScreenshotTool(ScreenshotTool):
    """带缓存的截图工具,减少重复截图的 Token 消耗"""

    def __init__(self, cache_ttl: float = 0.5, **kwargs):
        super().__init__(**kwargs)
        self.cache_ttl = cache_ttl  # 缓存有效期(秒)
        self._cache: Optional[CachedScreenshot] = None

    def take_screenshot(self, **kwargs) -> bytes:
        now = time.time()
        if (self._cache and
                now - self._cache.timestamp < self.cache_ttl):
            return self._cache.data

        data = super().take_screenshot(**kwargs)
        data_hash = hashlib.md5(data).hexdigest()
        self._cache = CachedScreenshot(data=data, timestamp=now, hash=data_hash)
        return data

    @property
    def screen_changed(self) -> bool:
        """检查屏幕是否发生变化(通过对比截图哈希)"""
        if not self._cache:
            return True
        old_hash = self._cache.hash
        # 强制重新截图
        self._cache = None
        new_data = self.take_screenshot()
        new_hash = hashlib.md5(new_data).hexdigest()
        return old_hash != new_hash

多显示器支持

def list_monitors() -> list[dict]:
    """列出所有可用的显示器及其信息"""
    with mss.mss() as sct:
        monitors = []
        for i, monitor in enumerate(sct.monitors):
            if i == 0:
                continue  # index 0 是"所有显示器"的虚拟显示器
            monitors.append({
                "id": i,
                "width": monitor["width"],
                "height": monitor["height"],
                "left": monitor["left"],
                "top": monitor["top"],
            })
        return monitors

# 示例:在多显示器环境中选择合适的显示器
monitors = list_monitors()
# [{"id": 1, "width": 1920, "height": 1080, "left": 0, "top": 0},
#  {"id": 2, "width": 2560, "height": 1440, "left": 1920, "top": 0}]
截图时机的最佳实践

不应该在每次操作前后都截图,这会大量增加 token 消耗和执行时间。建议的策略:执行点击/输入操作后,等待 0.5-1 秒,然后截图确认。只在有疑问或需要确认状态时才截图。Claude 本身会在需要时请求截图,不要预先提供太多截图。

截图质量对 AI 识别的影响

什么因素影响 Claude 的视觉识别准确率

截图的质量不仅仅关乎分辨率,还涉及多个影响 AI 理解准确性的因素。了解这些因素有助于你优化截图策略:

文字清晰度
小于 12px 的文字在 720p 截图中可能变得模糊,导致 Claude 读取错误。特别是高 DPI(Retina)屏幕上的内容,在截图缩放后文字可能失真。解决方法:对于需要精确读取文字的任务,使用较高分辨率(1280×720 以上)。
对比度和颜色
低对比度的 UI(如浅灰色文字在白色背景上)会降低 Claude 的识别准确率。高对比度界面(深色主题、高亮按钮)识别效果最好。这也是 Claude 有时在识别禁用状态的按钮时出错的原因。
JPEG 压缩伪影
过度压缩的 JPEG(quality < 50)会在文字边缘产生块状伪影,严重影响文字识别。推荐质量设置为 70-85,这是文件大小与识别质量的最佳平衡点。对于需要精确读取的内容(如验证码、数字),使用 PNG 格式。
动态内容截图时机
加载动画、过渡效果、实时更新的数据流——在这些状态下截图会让 Claude 看到"中间态",导致错误判断。应等待页面完全稳定后再截图(参考第4章的 wait_for_stable_screen 实现)。

HiDPI(Retina 屏)的特殊处理

macOS Retina 屏幕的设备像素比(DPR)为 2,意味着物理 1920×1200 的屏幕,逻辑分辨率是 960×600。如果截图时不处理 DPR,坐标会出现 2 倍偏差:

import mss
import subprocess
import sys
from PIL import Image


def get_display_info():
    """
    获取显示器的逻辑分辨率和设备像素比。
    macOS Retina 屏幕物理分辨率是逻辑分辨率的 2 倍。
    """
    if sys.platform == "darwin":  # macOS
        # 通过系统命令获取逻辑分辨率
        result = subprocess.run(
            ["system_profiler", "SPDisplaysDataType"],
            capture_output=True, text=True
        )
        # 也可以通过 mss 获取物理分辨率
        with mss.mss() as sct:
            physical = sct.monitors[1]
            physical_w = physical["width"]
            physical_h = physical["height"]
    return physical_w, physical_h


class RetinaAwareScreenshotTool:
    """
    处理 HiDPI 屏幕的截图工具。
    核心问题:macOS Retina 屏幕截图的物理分辨率是显示分辨率的 2 倍,
    但鼠标坐标系基于逻辑分辨率。截图需要缩放到逻辑分辨率,
    并告知 Claude 使用逻辑分辨率的坐标。
    """

    def __init__(self):
        with mss.mss() as sct:
            m = sct.monitors[1]
            # mss 在 macOS 上返回的是物理像素分辨率
            self.physical_width = m["width"]
            self.physical_height = m["height"]

        # 通过 PyObjC 或系统 API 获取逻辑分辨率(macOS)
        try:
            import AppKit
            screen = AppKit.NSScreen.mainScreen()
            frame = screen.frame()
            self.logical_width = int(frame.size.width)
            self.logical_height = int(frame.size.height)
        except ImportError:
            # 如果没有 PyObjC,假设 DPR 为 2
            self.logical_width = self.physical_width // 2
            self.logical_height = self.physical_height // 2

        self.dpr = self.physical_width / self.logical_width

    def take_screenshot_for_claude(self) -> tuple[bytes, int, int]:
        """
        截图并缩放到逻辑分辨率。
        返回:(图像字节数据, 逻辑宽度, 逻辑高度)
        Claude 工具定义应使用返回的逻辑宽高。
        """
        with mss.mss() as sct:
            sct_img = sct.grab(sct.monitors[1])
            img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")

        # 缩放到逻辑分辨率(Retina 屏缩小 2 倍)
        img = img.resize(
            (self.logical_width, self.logical_height),
            Image.Resampling.LANCZOS
        )

        import io
        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=80)
        return buf.getvalue(), self.logical_width, self.logical_height

# computer 工具定义应使用逻辑分辨率
# tool = {"type": "computer_20241022", "name": "computer",
#         "display_width_px": logical_width,   # 逻辑宽度(如 1440)
#         "display_height_px": logical_height}  # 逻辑高度(如 900)
# Claude 输出的点击坐标也基于逻辑坐标,直接用于 pyautogui 即可

截图区域裁切与局部分析

对于只需要分析屏幕某个区域的任务(如填写一个表单区域),可以只截取该区域,大幅减少 token 消耗:

class RegionalScreenshotTool:
    """支持区域截图的工具,用于减少 token 消耗"""

    def take_region_screenshot(
        self,
        x: int, y: int,
        width: int, height: int
    ) -> bytes:
        """
        截取屏幕指定区域。
        例如:只截取一个 400×300 的对话框,
        而不是整个 1280×720 的屏幕。
        Token 消耗约为全屏的 10-20%。

        Args:
            x, y: 区域左上角坐标(屏幕坐标)
            width, height: 区域宽高
        """
        region = {"top": y, "left": x, "width": width, "height": height}
        with mss.mss() as sct:
            sct_img = sct.grab(region)
            img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
        import io
        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=80)
        return buf.getvalue()

    def annotate_screenshot(
        self,
        screenshot_bytes: bytes,
        annotations: list[dict]
    ) -> bytes:
        """
        在截图上叠加标注(用于调试和可观测性)。
        例如:画出上一步点击的位置,帮助调试坐标偏差问题。

        Args:
            annotations: 标注列表,格式:
                [{"type": "circle", "x": 100, "y": 200, "r": 10, "color": "red"},
                 {"type": "text", "x": 100, "y": 180, "text": "点击目标", "color": "red"}]
        """
        from PIL import ImageDraw, ImageFont
        import io

        img = Image.open(io.BytesIO(screenshot_bytes))
        draw = ImageDraw.Draw(img)

        for ann in annotations:
            if ann["type"] == "circle":
                x, y, r = ann["x"], ann["y"], ann.get("r", 10)
                color = ann.get("color", "red")
                draw.ellipse([x-r, y-r, x+r, y+r], outline=color, width=3)
            elif ann["type"] == "text":
                draw.text((ann["x"], ann["y"]), ann["text"],
                           fill=ann.get("color", "red"))

        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=80)
        return buf.getvalue()

不同场景的截图策略选择

场景对应最佳策略

场景 推荐格式 推荐分辨率 理由
一般 Web 页面操作 JPEG q=75 1280×720 平衡质量和成本,大多数 UI 元素清晰
需要精确读取文字(如邮件内容) JPEG q=85 1920×1080 文字清晰度优先
表单填写(焦点区域) JPEG q=80 局部截图 400×300 减少无关内容,降低 token 消耗
桌面程序(复杂界面) PNG 实际屏幕分辨率 避免 JPEG 伪影影响 UI 元素识别
视频播放/动态内容 JPEG q=60 960×540 快速截图,内容本身不需要精确识别
包含图表/图像的页面 JPEG q=85 1280×720 图像内容需要更高质量才能被正确理解
章节小结

本章讲解了如何为 Computer Use 构建高效的截图管道。关键要点: