UFO 源码实战 (2):它怎么“看”懂屏幕的?UI 截图与标注代码详解

前言

在上一篇文章中,我们跑通了 UFO 的 Hello World。很多同学在后台问:“UFO 到底是怎么知道‘文件’菜单在哪里的?是靠 OCR 文字识别,还是靠图像识别?”

答案可能会让你惊讶:它两者都不是,或者说,它比单纯的图像识别更‘懂’系统。

UFO 采用了一种**“底层 UI 树 + 视觉标注 (Set-of-Marks)”** 的混合策略。它不光截了图,还在图上给每个可点击的按钮贴了个“数字标签”。

今天这篇,我们深入 ufo/automator/ui_control 模块,拆解它**“截图 -> 探查控件 -> 绘制标注”**的三步走核心逻辑。


📸 核心原理:为什么只给截图还不够?

如果直接把一张原始的 Windows 截图丢给 GPT-4V,并问它:“文件按钮的坐标是多少?”,GPT-4V 虽然能看懂图,但很难给出精确到像素的坐标。一旦点歪了,任务就挂了。

UFO 的解决方案非常聪明,遵循了 Set-of-Marks (SoM) 的设计思想:

  1. 透视眼:利用 Windows API 获取当前窗口所有控件的真实坐标。

  2. 打标签:在截图中,用红框把控件圈出来,并标上序号(比如 #1, #2)。

  3. 看图说话:把处理过的图发给 GPT-4V,说:“你要操作的功能是几号?”

  4. 精确打击:GPT-4V 回复“#5”,UFO 查表得知 #5 的中心坐标是 (100, 200),执行点击。


🔍 第一步:开启“透视眼” (Inspector)

UFO 获取控件信息的代码主要集中在 ufo/automator/ui_control/inspector.py (或类似命名,视具体版本而定) 以及依赖库 pywinauto 的调用上。

我们来看它是如何遍历当前窗口的 UI 树的。

1. 锁定目标窗口

UFO 不会无脑截全屏,它会先聚焦到你指定的 App。

Python

# 伪代码逻辑演示
from pywinauto import Desktop

def get_application_window(app_name):
    # 连接到桌面
    desktop = Desktop(backend="uia") # 重点:使用 uia (UI Automation) 后端
    # 模糊匹配窗口标题
    window = desktop.window(title_re=f".*{app_name}.*")
    window.set_focus() # 强制前台显示
    return window

💡 源码划重点:注意 backend="uia"。Windows 自动化有两种后端:win32 (老旧) 和 uia (新一代)。UFO 使用 uia,因为它可以获取更丰富的控件属性(如按钮类型、文本内容、甚至是否可用)。

2. 遍历 UI 元素

拿到窗口对象后,UFO 需要递归查找里面的所有子控件(按钮、输入框、菜单)。

Python

# ufo/automator/ui_control/inspector.py 核心逻辑

def inspect_ui_elements(window):
    # 获取所有后代控件
    # control_type 过滤:我们只关心 Button, Edit, MenuItem 等可交互元素
    elements = window.descendants(control_type=["Button", "Edit", "MenuItem", ...])
    
    valid_elements = []
    for elem in elements:
        # 过滤掉不可见或尺寸为0的幽灵控件
        if elem.is_visible() and elem.rectangle().width() > 0:
            valid_elements.append(elem)
            
    return valid_elements

这一步结束后,UFO 手里不仅有了截图,还有了一张**“藏宝图”**:它知道屏幕上每一个按钮的精确 (left, top, right, bottom) 坐标。


🎨 第二步:绘制“数字标签” (Annotator)

拿到控件列表后,UFO 需要把这些信息“画”在图片上。这部分代码通常涉及 PIL (Pillow) 库的操作。

我们深入 screenshot.pydrawer 相关模块:

1. 截图与绘制

UFO 会对原始截图进行深加工。

Python

from PIL import Image, ImageDraw, ImageFont

def draw_labels(original_image, element_list):
    # 创建一个可以在上面画画的对象
    draw = ImageDraw.Draw(original_image)
    
    # 建立一个映射表:ID -> 控件对象
    control_map = {}
    
    for index, element in enumerate(element_list):
        # 获取控件的矩形坐标
        rect = element.rectangle()
        box = (rect.left, rect.top, rect.right, rect.bottom)
        
        # 1. 画红框 (Bounding Box)
        draw.rectangle(box, outline="red", width=2)
        
        # 2. 画数字背景和数字
        label_text = str(index + 1)
        # ... (此处省略计算文字位置的代码,通常在左上角)
        draw.text((box[0], box[1]), label_text, fill="yellow")
        
        # 3. 记录映射关系,供后续点击使用
        control_map[index + 1] = element

    return original_image, control_map

2. 视觉效果分析

经过这一步处理,原本干干净净的记事本界面,变成了一张**“麻子脸”**:到处都是红框和黄色的数字。

  • 对人类来说:这图乱糟糟的,很丑。

  • 对 GPT-4V 来说:这是神图!它不需要猜测“保存”按钮在哪里,它只需要看到“保存”按钮上写着 15,然后告诉 UFO:“去按 15 号”。


🧠 第三步:构建 Prompt (提示词)

有了标注图,UFO 还需要把辅助信息打包成 Prompt 发给大脑。

ufo/prompter/agent_prompter.py 中,你会看到类似的构造逻辑:

Python

# 构造发给 GPT-4V 的消息
messages = [
    {
        "role": "system",
        "content": "You are an agent controlling Windows..."
    },
    {
        "role": "user",
        "content": [
            {"type": "text", "text": "Here is the screenshot with annotated labels..."},
            # 传入那张画满红框的图
            {"type": "image_url", "image_url": annotated_image_base64} 
        ]
    }
]

关键点:UFO 不仅传图片,还会把控件的文本列表作为辅助文本传过去。

比如:Index 15: Button "Save", Position: (200, 300)。

这构成了双重保障:GPT 既看图,又看文字描述,极大降低了幻觉概率。


🛠️ 源码调试小技巧

如果你想亲眼看看 UFO 到底截了什么图,可以在源码中插桩。

找到 screenshot.py 里的截图保存逻辑,加入一行代码:

Python

# 在图片发送给 GPT 之前
annotated_image.save("debug_annotated_screen.png")
print("已保存调试截图!")

运行一次任务,打开这张 debug_annotated_screen.png,你就能以“AI 的视角”看世界了。你会发现,有时候红框可能会重叠,或者把不该框的框进去了,这就是当前 AI Agent 偶尔操作失误的根本原因——视觉预处理的噪声。


📝 总结与下期预告

通过阅读源码,我们发现 UFO 的“视觉”其实是一个障眼法

  1. 它利用 Pywinauto 扒取底层数据,获得绝对精准的坐标。

  2. 它利用 Pillow 绘制红框和数字,充当 GPT-4V 的“眼镜”。

  3. 它通过 ID 映射,把复杂的“点击坐标 (x,y)”问题,简化成了“做选择题 (选 ID)”的问题。

思考题:

如果 GPT-4V 告诉我们要点击 ID: 15,UFO 是如何控制鼠标平滑移动过去并执行双击的?如果点击失败了怎么办?

下期预告:

下期我们将进入“肢体动作”篇,剖析 automator 模块。

《UFO 源码实战 (3):它怎么“点”鼠标的?通过源码掌握 Windows 自动化控制》,带你研究如何用 Python 模拟人类真实的鼠标轨迹!


喜欢这个系列的源码解读吗?欢迎点赞、收藏,你的支持是我更新的动力! 👋

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值