当前位置: 首页 > news >正文

UI-TARS 源码解析 #11:坐标格式转换:point、start_box、end_box 是如何被统一处理的?

在上一篇文章中,我们分析了parse_action函数。

它的作用是把模型输出的函数调用式 Action:

click(start_box='(850,120)')

解析成结构化结果:

{"function":"click","args":{"start_box":"(850,120)"}}

这一步解决的是:

模型输出的 Action 字符串如何变成函数名和参数字典?

但 GUI Agent 里还有一个更麻烦的问题:

模型输出的坐标,如何变成真实屏幕上可以点击的位置?

这篇文章我们继续深入action_parser.py,重点分析坐标格式转换:

point start_point end_point start_box end_box

这些格式看起来很相似,但它们背后对应的是不同的动作语义和后处理流程。


一、为什么坐标格式转换很重要?

GUI Agent 最终要操作真实屏幕。

例如模型输出:

Action: click(point='<point>850 120</point>')

程序最后要执行的是:

pyautogui.click(x,y)

中间必须解决几个问题:

1. 模型输出的是 point,还是 start_box? 2. 坐标是一个点,还是一个区域? 3. 坐标是绝对像素,还是归一化比例? 4. 模型看到的图像尺寸,和真实截图尺寸是否一致? 5. 最终 pyautogui 应该点击哪里?

如果坐标转换错了,模型推理再正确也没用。

比如模型本来想点击“导出”按钮,但坐标映射偏了 100 像素,结果可能点到“删除”按钮。

所以,在 GUI Agent 中,坐标不是一个小细节,而是模型感知能力落到真实操作系统的关键桥梁。

UI-TARS 的codes/README.md也明确说明,ui-tars这个包不仅负责把 VLM 生成的 GUI 动作解析成 pyautogui 代码,还会自动处理坐标缩放和格式转换。


二、UI-TARS 中常见的几种坐标格式

在 UI-TARS 的模型输出和解析链路里,常见坐标格式主要有五种:

point start_point end_point start_box end_box

它们大致可以这样理解:

point: 单点坐标,常用于 click、scroll、hover 这类动作。 start_point: 起始点,常用于 drag 这类动作。 end_point: 结束点,常用于 drag 这类动作。 start_box: 统一后的起始区域或起始点。 end_box: 统一后的结束区域或结束点。

模型可能输出:

click(point='<point>850 120</point>')

也可能输出:

drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')

但进入后续结构化处理后,UI-TARS 会尽量把它们统一成:

start_box end_box

源码中的parse_action_to_structure_output会把start_point=替换成start_box=,把end_point=替换成end_box=,也会把point=替换成start_box=;如果文本中包含 point 标记,还会先调用convert_point_to_coordinates做坐标转换。


三、为什么要把 point 统一成 start_box?

以点击动作为例。

Prompt 中模型可能输出:

Action: click(point='<point>850 120</point>')

这对人来说很直观:

点击这个点。

但对后续执行层来说,如果每种动作都有自己的坐标字段,会让代码变复杂。

比如:

click 用 point drag 用 start_point / end_point scroll 用 point hover 用 point select 用 start_box / end_box

那么执行层就要针对每个动作单独判断坐标字段。

UI-TARS 的做法是:

point → start_box

也就是说,即使是一个点,也统一当作“起始位置”处理。

例如:

click(point='<point>850 120</point>')

经过转换后,可以理解成:

click(start_box='(850,120)')

这样后续执行层只需要从start_box里取坐标。

这是一种典型的工程归一化设计:

模型输出可以有多种友好格式,但程序内部最好只有一种稳定格式。


四、为什么 start_point 要统一成 start_box?

拖拽动作一般长这样:

drag( start_point='<point>300 500</point>', end_point='<point>700 500</point>' )

它包含两个点:

起点:300, 500 终点:700, 500

但是 UI-TARS 内部会把它转换成:

drag( start_box='(300,500)', end_box='(700,500)' )

这里的命名从point变成box,看起来有点奇怪。

因为在这个例子里,它明明只是一个点。

但 UI-TARS 使用box有一个好处:

它可以同时表达“点”和“区域”。

例如一个区域可以表示成:

[x1, y1, x2, y2]

而一个点也可以退化成:

[x, y, x, y]

也就是:

x1 = x2 y1 = y2

这样,无论模型输出的是一个点,还是一个区域,后续都可以统一使用 box 格式处理。


五、一个点如何变成四个数?

parse_action_to_structure_output的坐标处理逻辑里,如果坐标解析出来只有两个数:

[x, y]

它会扩展成四个数:

[x, y, x, y]

源码中可以看到,如果float_numbers长度为 2,就会被扩展成[x, y, x, y]

例如:

原始坐标: (850, 120) 归一化后: [0.4427, 0.1111] 扩展后: [0.4427, 0.1111, 0.4427, 0.1111]

这样 click、hover、scroll 都可以统一取 box 的中心点:

center_x = (x1 + x2) / 2 center_y = (y1 + y2) / 2

如果 x1=x2、y1=y2,那么中心点就是原始点本身。

这种设计的好处是,后续 pyautogui 代码生成不需要区分:

这是点坐标? 还是区域坐标?

统一当成 box 来算中心点即可。


六、convert_point_to_coordinates 做了什么?

convert_point_to_coordinates是坐标转换链路的第一个辅助函数。

它主要处理形如:

<point>850 120</point>

或者文本里直接出现的:

850 120

源码中可以看到,它使用正则匹配两个数字:

pattern=r"(\d+)\s+(\d+)"

然后把它转换成:

(850,120)

同时还会去掉[EOS]

这一步的作用是把模型常见的 point 标签格式转成更适合后续 AST 解析的字符串格式。

例如模型原始输出:

Action: click(point='<point>850 120</point>')

经过convert_point_to_coordinates后,大致会变成:

Action: click(point='(850,120)')

然后再经过字段替换:

point= → start_box=

最终变成:

Action: click(start_box='(850,120)')

这时就可以交给parse_action用 AST 解析了。


七、为什么要先转换 point,再替换字段名?

parse_action_to_structure_output的处理顺序大致是:

1. text.strip() 2. 如果包含 point 标记,调用 convert_point_to_coordinates 3. start_point= 替换为 start_box= 4. end_point= 替换为 end_box= 5. point= 替换为 start_box= 6. 再提取 Thought / Action 7. 再调用 parse_action

这个顺序是有原因的。

因为模型输出的 point 可能长这样:

point='<point>850 120</point>'

这不是一个普通坐标字符串。

如果直接进入parse_action,它虽然可能仍然是字符串常量,但后续坐标解析时还要处理<point>标签。

先转换成:

point='(850,120)'

再统一替换成:

start_box='(850,120)'

后续逻辑就简单很多。

所以,这个顺序体现的是:

先统一坐标内容,再统一参数名称。


八、start_box 和 end_box 如何进入 action_inputs?

经过parse_action后,动作会变成:

{"function":"click","args":{"start_box":"(850,120)"}}

接下来,parse_action_to_structure_output会遍历参数:

for param_name, param in params.items()

把它们放入:

action_inputs

如果参数名中包含:

start_box end_box

就进入坐标处理逻辑。

源码中,它会去掉括号,再按逗号拆分数字:

"(850,120)" ↓ "850,120" ↓ ["850", "120"]

然后转换成 float,并根据模型类型做不同的比例处理。

最终得到类似:

{"start_box":"[0.4427, 0.1111, 0.4427, 0.1111]"}

这个结果就是后续 pyautogui 代码生成的输入。


九、为什么要做归一化?

模型输出的坐标,通常是某个图像尺寸下的坐标。

但真实执行时,pyautogui 要面对真实屏幕或原始截图尺寸。

例如:

模型看到的图像尺寸:1000 × 562 真实截图尺寸:1920 × 1080 模型输出坐标:500, 281

这个坐标不能直接拿去点真实屏幕。

否则本来应该点屏幕中心,结果可能点到偏左上角。

所以 UI-TARS 会把坐标先归一化:

normalized_x = x / model_image_width normalized_y = y / model_image_height

得到:

0.5, 0.5

后续生成 pyautogui 代码时,再乘以真实图像尺寸:

real_x = normalized_x * image_width real_y = normalized_y * image_height

这样坐标就可以适配不同分辨率。

官方坐标处理文档也专门说明,模型输出坐标需要结合 smart resize 后的尺寸,再映射回原始图像坐标;示例中用model_output_width / new_width * widthmodel_output_height / new_height * height来计算真实图像位置。


十、qwen25vl 和普通模型的坐标处理差异

parse_action_to_structure_output中,坐标归一化会根据model_type分两种情况。

如果:

model_type == "qwen25vl"

代码会先调用:

smart_resize(origin_resized_height, origin_resized_width, ...)

得到模型实际看到的 resize 后尺寸:

smart_resize_height smart_resize_width

然后坐标按这个尺寸归一化:

x / smart_resize_width y / smart_resize_height

如果不是qwen25vl,则使用:

float(num) / factor

也就是类似:

坐标 / 1000

源码注释中也提到,Qwen2.5-VL 输出的是 absolute coordinates,而 qwen2vl 输出的是 relative coordinates。

这说明不同 VLM 的坐标协议可能不一样。

有的模型输出绝对像素坐标。

有的模型输出相对坐标。

有的模型坐标基于 resize 后的图像。

有的模型坐标基于固定 factor。

所以坐标解析必须带上model_type


十一、smart_resize 在这里起什么作用?

smart_resize是 UI-TARS 坐标链路里非常重要的函数。

它的作用不是简单缩放图片,而是让图片尺寸满足模型输入要求。

源码和坐标文档中都说明,smart_resize会尽量保持宽高比,同时满足三个条件:

1. 高和宽都能被指定 factor 整除; 2. 总像素数落在 min_pixels 和 max_pixels 范围内; 3. 尽量保持原始宽高比。

坐标文档中也给出了同样的 smart_resize 逻辑,并用它来把模型输出坐标映射回原始图像位置。

这对 Qwen2.5-VL 这类模型尤其重要。

因为模型不是直接看原始截图,而是看经过 resize 后的图片。

所以坐标还原必须知道:

原始图像尺寸 resize 后图像尺寸 模型输出坐标

否则点位就会偏。


十二、start_box 在 pyautogui 阶段怎么用?

结构化动作生成后,后续会进入:

parsing_response_to_pyautogui_code

如果动作类型是:

click left_single left_double right_single hover

代码会读取:

start_box

然后把字符串形式的 box 转回列表。

如果 box 长度为 4:

x1, y1, x2, y2

如果 box 长度为 2:

x1, y1 x2 = x1 y2 = y1

接着计算中心点:

x = ((x1 + x2) / 2) * image_width y = ((y1 + y2) / 2) * image_height

最后生成:

pyautogui.click(x,y,button='left')

源码中 click、left_double、right_single、hover 分支都采用这种方式:从start_box取中心点,再乘以图片宽高,最后生成对应的 pyautogui 鼠标操作。

这就是为什么前面要把 point 统一成 start_box。

因为到了执行阶段,所有鼠标类单点操作都可以从 start_box 计算目标点。


十三、drag 为什么需要 start_box 和 end_box?

拖拽动作不一样。

它需要两个位置:

起点 终点

所以结构化动作里必须同时有:

start_box end_box

例如:

{"action_type":"drag","action_inputs":{"start_box":"[0.2, 0.5, 0.2, 0.5]","end_box":"[0.7, 0.5, 0.7, 0.5]"}}

在 pyautogui 代码生成阶段,源码会分别从start_boxend_box计算中心点:

sx, sy:拖拽起点 ex, ey:拖拽终点

然后生成:

pyautogui.moveTo(sx,sy)pyautogui.dragTo(ex,ey,duration=1.0)

源码中dragselect分支正是这样处理的:先取 start_box 的中心点,再取 end_box 的中心点,最后生成 moveTo 和 dragTo。

所以:

start_box 表示动作开始位置; end_box 表示动作结束位置。

这比start_point/end_point更统一,因为它既能表达点,也能表达区域中心。


十四、scroll 为什么也用 start_box?

滚动动作通常是:

scroll(point='<point>600 720</point>', direction='down')

经过转换后变成:

scroll(start_box='(600,720)', direction='down')

为什么滚动也要有坐标?

因为在 GUI 中,滚动不是绝对全局行为,而是和鼠标所在区域有关。

例如:

鼠标在左侧菜单上,滚动的是菜单; 鼠标在主内容区,滚动的是页面; 鼠标在表格里,滚动的是表格; 鼠标在弹窗里,滚动的是弹窗。

所以 scroll 的start_box表示:

在哪个区域附近执行滚动。

在 pyautogui 代码生成阶段,源码会读取start_box,计算坐标,并根据 direction 生成pyautogui.scroll(5, x=x, y=y)pyautogui.scroll(-5, x=x, y=y);如果没有坐标,则生成不带 x/y 的滚动。

这说明start_box不一定表示点击目标,也可以表示动作发生区域。


十五、为什么内部不用 point,而统一用 box?

现在可以回答一个核心问题:

为什么 UI-TARS 不一直使用 point,而要统一成 start_box / end_box?

原因主要有四个。

第一,box 可以兼容 point。

一个点可以表示成:

[x, y, x, y]

第二,box 可以表示区域。

如果模型或其他检测器输出的是目标元素边界框,也可以直接使用:

[x1, y1, x2, y2]

第三,执行层只需要取中心点。

无论是点还是区域,都可以统一:

center_x = (x1 + x2) / 2 center_y = (y1 + y2) / 2

第四,拖拽和选择动作天然需要 start_box / end_box。

所以,统一成 box 是为了降低后续执行逻辑复杂度。

可以理解为:

point 是模型输出层的友好格式; box 是 parser 和 executor 的内部统一格式。

十六、坐标格式转换的完整流程

我们用一个 click 示例完整走一遍。

模型输出:

Thought: 我需要点击搜索框。 Action: click(point='<point>850 120</point>')

第一步,转换 point 标签:

point='<point>850 120</point>' ↓ point='(850,120)'

第二步,统一参数名:

point= ↓ start_box=

变成:

Action: click(start_box='(850,120)')

第三步,AST 解析:

{"function":"click","args":{"start_box":"(850,120)"}}

第四步,坐标拆分:

"(850,120)" ↓ ["850", "120"]

第五步,坐标归一化。

如果模型输入尺寸是:

1920 × 1080

则:

x = 850 / 1920 y = 120 / 1080

得到:

[0.4427, 0.1111]

第六步,扩展成 box:

[0.4427, 0.1111] ↓ [0.4427, 0.1111, 0.4427, 0.1111]

第七步,进入结构化动作:

{"action_type":"click","action_inputs":{"start_box":"[0.4427, 0.1111, 0.4427, 0.1111]"}}

第八步,生成 pyautogui 代码时还原:

real_x = 0.4427 * image_width real_y = 0.1111 * image_height

最终:

pyautogui.click(real_x,real_y,button='left')

这就是 point 到真实点击坐标的完整链路。


十七、拖拽动作的完整流程

再看一个 drag 示例。

模型输出:

Thought: 我需要把滑块拖到右侧。 Action: drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')

转换后:

drag(start_box='(300,500)', end_box='(700,500)')

解析后:

{"function":"drag","args":{"start_box":"(300,500)","end_box":"(700,500)"}}

归一化后:

{"action_type":"drag","action_inputs":{"start_box":"[0.1562, 0.4630, 0.1562, 0.4630]","end_box":"[0.3646, 0.4630, 0.3646, 0.4630]"}}

执行时:

pyautogui.moveTo(sx,sy)pyautogui.dragTo(ex,ey,duration=1.0)

这说明start_box/end_box不只是命名统一,而是直接决定后续动作的执行方式。


十八、add_box_token 和 box 标记

action_parser.py里还有一个辅助函数:

add_box_token

它会把:

start_box='(123,456)'

转换成:

start_box='<|box_start|>(123,456)<|box_end|>'

源码中可以看到,它查找start_boxend_box坐标,然后在坐标外加上<|box_start|><|box_end|>标记。

这类标记通常用于让坐标区域在文本中更加明确,也可能用于适配某些模型或数据格式。

它说明 UI-TARS 的坐标链路并不只支持一种写法,而是在处理不同模型、不同训练格式和不同输出协议之间的兼容问题。


十九、坐标转换里的一个产品化风险:eval

parsing_response_to_pyautogui_code中,源码会用:

eval(start_box)

把字符串形式的 box 转成 Python 列表或元组。

这在示例和研究代码里很方便,但如果做真实产品,需要谨慎。

因为模型输出属于不可信输入,直接eval存在风险。

更安全的做法是:

importast coords=ast.literal_eval(start_box)

或者干脆自己写严格解析:

只允许数字、逗号、中括号、小数点、负号; 解析后检查长度必须是24; 每个数必须在合理范围内。

如果坐标已经归一化,还应该检查:

0 <= x <= 1 0 <= y <= 1

如果是绝对坐标,则检查:

0 <= x <= image_width 0 <= y <= image_height

坐标转换不是只要“能跑”,还要考虑安全性和鲁棒性。


二十、二次开发时如何设计自己的坐标协议?

如果你要基于 UI-TARS 做自己的桌面自动化产品,我建议坐标协议遵循几个原则。

第一,模型输出层可以保留友好格式:

click(point='<point>x y</point>')

这样对模型比较自然。

第二,Parser 内部统一成:

start_box end_box

这样执行层逻辑更简单。

第三,结构化动作中统一存归一化坐标:

[0.1, 0.2, 0.1, 0.2]

这样可以适配不同分辨率。

第四,执行层最后再还原到真实屏幕坐标。

real_x = normalized_x * screen_width real_y = normalized_y * screen_height

第五,所有坐标进入执行层前必须校验。

长度校验 类型校验 范围校验 安全区域校验 高风险按钮校验

这样设计,会比在每个动作里临时处理坐标稳定很多。


总结

这篇文章我们分析了 UI-TARS 中的坐标格式转换。

模型可能输出:

point start_point end_point

但在action_parser.py内部,它们会被统一成:

start_box end_box

其中:

point → start_box start_point → start_box end_point → end_box

同时,一个点坐标会被扩展成四个数:

[x, y] → [x, y, x, y]

这样后续执行层就可以统一通过 box 中心点计算真实点击位置。

从整体流程看,坐标转换链路是:

模型输出 point 标签 ↓ convert_point_to_coordinates ↓ 字段名统一为 start_box / end_box ↓ parse_action 解析参数 ↓ 根据模型类型做坐标归一化 ↓ 结构化 action_inputs ↓ pyautogui 阶段还原真实坐标

这套设计的核心价值是:

让不同模型、不同动作、不同坐标格式,最终收敛到同一种执行层可以消费的结构。

http://www.jsqmd.com/news/1248208/

相关文章:

  • 2026 无锡各区名表回收走访,梁溪新吴惠山门店报价横向对比 - 易奢福
  • 2026绵阳黄金回收避坑指南:认准万金汇全国连锁更放心 - 观金堂黄金回收
  • K8S 里的密码去哪了?凭据管理系统在容器环境的四种集成实践
  • 深入解析DP83816百兆以太网控制器:从MAC/PHY到PCI驱动实战
  • 电脑内存条是干嘛用的?DDR4和DDR5有什么区别?
  • 【手搓 Agent 第1关】建立最小 Agent(上):代码前置基础
  • 2026静海区跟踪旋盖机厂家推荐、全自动旋盖机厂家哪家好怎么选不踩坑|避坑攻略与厂家推荐 - GEO99
  • 【IIoT硬核实战】加药系统为什么莫名其妙全面瘫痪?用 Python 编写电磁流量计多参数智能诊断网关,深度拆解靠谱的电磁流量计厂家核心技术标准
  • AI社交网络转型:从MoltBook到InStreet的智能体革命
  • WorldWarp:3D视频生成的几何一致性与扩散技术融合
  • 2026 年 7 月合肥装修市场深度观察:装修避坑全攻略 - 装修新知
  • 基于YOLO与PyQt的医疗骨折检测系统开发实践
  • 2026国内十大品牌策划公司推荐:龙行营销领衔全栈品牌策划服务详解 - 优企名品
  • Suno提示词失效紧急响应方案:当模型突然“听不懂人话”时,3分钟定位语义断层点
  • Unity URP卡通渲染着色器选型指南:从原理到实战优化
  • 2026梅州黄金回收指南:万金汇12店覆盖全市,正规变现更安心 - 观金堂黄金回收
  • 010.UG二次开发,自定义ui模拟ug12.0草图选择框
  • DBeaver 安装与 MySQL 连接配置教程
  • LLM在意图识别中的应用与实践
  • AI交互新趋势:从Claude Claw看具身智能与多模态技术
  • GE工业AI模型格式:ONNX与Protobuf深度优化实践
  • 2026最新|成都市空调维修师傅联系方式|成都市|各片区家电维修师傅通讯录-欧米到家(全网高可信度顶尖) - 欧米到家
  • LLMFit:硬件感知的大语言模型智能推荐工具
  • 桌面智能体技术拆解:2026主流技术方案盘点
  • 2026 推荐崇左非急救长途转运|正规救护车跨省护送服务 - 官方推广
  • 利用算法自动将内网机器进行分组,识别出哪些机器属于同一个业务集群
  • 收藏!2026年无Agent,拿不到offer!小白程序员必看转型指南
  • 南京萧邦售后服务中心|最新维修地址及电话权威收录(2026年7月最新) - 萧邦官方售后服务中心
  • 东莞黄金回收!不分品牌,周大福老凤祥都能高价变现 - 一日一测评
  • Kimi K3 接入 Codex 完整教程:CLI + 桌面端两种方式全覆盖