截图工具选型
主要 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 构建高效的截图管道。关键要点:
- mss + Pillow 是最佳组合:mss 负责高速截图,Pillow 负责缩放和压缩
- 截图后必须处理坐标缩放:Claude 的点击坐标基于截图尺寸,需转换为屏幕实际坐标
- HiDPI(Retina)屏幕需要特殊处理:截图应缩放至逻辑分辨率,工具定义也应使用逻辑分辨率
- JPEG 75 质量、1280×720 分辨率是大多数场景的最佳平衡点,每张约 600 token
- 文字清晰度、对比度、JPEG 压缩伪影是影响识别准确率的主要视觉因素
- 截图缓存(500ms TTL)可以避免相同页面的重复传输,显著降低成本