为 Vision Agent 编写自定义工具:模板匹配(Template Matching)Custom Tool 实战指南
【免费下载链接】vision-agentThis tool has been deprecated. Use Agentic Document Extraction instead.项目地址: https://gitcode.com/GitHub_Trending/vi/vision-agent
导读
本指南以 examples/custom_tools/README.md 为主线,完整讲解如何在 vision-agent 中开发一个"模板匹配(Template Matching)"自定义视觉工具,并让VisionAgentCoderV2在求解问题时自动调用它。读完本文你将掌握:自定义工具的注册方式、register_tool中 imports 参数的底层原理(独立进程代码注入机制)、旋转模板匹配算法的完整实现细节,以及如何在对话中显式指定工具以获得稳定可靠的结果。
一、示例概览:这个 Custom Tool 能做什么
官方在 examples/custom_tools/README.md 中提供了一个名为Template Matching Custom Tool的演示项目。它的业务目标是:
让 Vision Agent 使用自定义的
template_match_工具,在大图pid.png中找到小模板图pid_template.png出现的位置,并进一步回答"是否有匹配结果靠近 'NOTE 5' 区域"这类空间关系问题。
模板匹配本身并不是 Agent 内置工具列表里的能力,而是由开发者通过register_tool机制注入给 Agent 的自定义工具。这一模式说明了 vision-agent 的工具系统是开放、可扩展的:你可以把任意 Python 视觉算法封装成工具,交给 Agent 在生成代码时按需调用。
对应的示例目录(examples/custom_tools/)包含四个关键文件:
| 文件 | 作用 |
|---|---|
run_custom_tool.py | 自定义工具的注册代码 + 启动 Agent 的主流程 |
template_match.py | 纯算法实现:带旋转的模板匹配 |
requirements.txt | 依赖声明(torch、torchvision) |
pid.png/pid_template.png | 演示用目标图与模板图 |
二、环境准备与快速运行
README 给出了两条最直接的运行命令。
第一步:安装依赖
pip install -r requirements.txt查看 examples/custom_tools/requirements.txt 可知,示例的核心依赖非常精简,只有两个:
torch torchvision其中torchvision用于调用torchvision.ops.nms做检测框的非极大值抑制(NMS),torch是其底层张量库。除此之外,示例还依赖cv2(OpenCV)与numpy,它们分别用于cv2.matchTemplate模板匹配和数组运算;vision_agent本体则作为主框架提供注册与 Agent 能力。
第二步:运行示例
python run_custom_tool.py脚本会完成两件事:注册template_match自定义工具;然后启动VisionAgentCoderV2,向它提问"在 pid.png 中找到 pid_template.png 的位置,并判断是否有结果靠近 'NOTE 5'"。Agent 会根据工具描述决定调用该自定义工具、生成并执行代码、最终返回带边界框的结果。
说明:与示例配套的完整问答流程同样可以在仓库内的 chat-app 演示(examples/chat/)中看到类似交互形态,但本文聚焦于 custom_tools 这一最小可复现示例。
三、注册自定义工具:run_custom_tool.py 逐段解析
examples/custom_tools/run_custom_tool.py 是自定义工具的"标准模板",我们逐段拆解。
3.1 导入与装饰器注册
import numpy as np from template_match import template_matching_with_rotation import vision_agent as va import vision_agent.tools as T import vision_agent.tools.planner_tools as pt from vision_agent.models import AgentMessage from vision_agent.utils.image_utils import get_image_size, normalize_bbox @va.tools.register_tool( imports=[ "import numpy as np", "from vision_agent.utils.image_utils import get_image_size, normalize_bbox", "from template_match import template_matching_with_rotation", ] ) def template_match(target_image: np.ndarray, template_image: np.ndarray) -> dict: ...几个关键点:
- 注册入口是
va.tools.register_tool。vision_agent.tools包在 vision_agent/tools/init.py 中导出register_tool,同时通过from .tools import ...导入大量内置工具(目标检测、分割、OCR、视频追踪等)。 imports参数必须显式列出工具运行所需的全部导入语句。这是 README 强调的核心坑点,后面第四节会深入原理。- 函数必须有规范的 docstring。Agent(LLM)正是通过 docstring 理解"这个工具是干什么的、参数是什么、返回什么",从而在写代码时决定是否调用它。示例的 docstring 给出了参数说明、返回值说明以及带
cv2.imread的用法示例,这是值得复用的最佳实践。
3.2 工具函数体:归一化边界框
image_size = get_image_size(target_image) matches = template_matching_with_rotation(target_image, template_image) matches["bboxes"] = [normalize_bbox(box, image_size) for box in matches["bboxes"]] return matches工具函数体做三件事:
- 用
vision_agent.utils.image_utils的get_image_size获取目标图尺寸; - 调用算法层
template_matching_with_rotation得到原始像素坐标的边界框与得分; - 用
normalize_bbox把每个边界框归一化到 0~1 区间,再原样返回。
归一化是关键约定:vision-agent 内置工具返回的 bbox 统一采用归一化坐标,Agent 生成的代码(例如画框、计算距离)也都基于这一约定。自定义工具遵循同样的返回格式({"bboxes": [...], "scores": [...]}),就能无缝融入 Agent 的代码生成与工具调用流程。
3.3 主流程:通过 AgentMessage 发起任务
if __name__ == "__main__": agent = va.agent.VisionAgentCoderV2(verbose=True) result = agent.generate_code( [ AgentMessage( role="user", content="Can you find the locations of the pid_template.png in pid.png and tell me if any are nearby 'NOTE 5'?", media=["pid.png", "pid_template.png"], ) ] )va.agent包(vision_agent/agent/init.py)对外暴露VisionAgentCoderV2等 Agent 类。这里通过AgentMessage把用户问题与媒体文件(media列表)一起传入generate_code。Agent 内部会结合工具文档、规划结果生成可执行代码并运行,最终给出带边界框的答案。
四、核心算法:template_match.py 的旋转模板匹配实现
examples/custom_tools/template_match.py 是纯算法文件,不依赖 vision-agent 的任何 API,可以在任何 Python 环境单独复用。
4.1 图像旋转辅助函数rotate_image
def rotate_image(mat, angle): height, width = mat.shape[:2] image_center = (width / 2, height / 2) rotation_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) abs_cos = abs(rotation_mat[0, 0]) abs_sin = abs(rotation_mat[0, 1]) bound_w = int(height * abs_sin + width * abs_cos) bound_h = int(height * abs_cos + width * abs_sin) rotation_mat[0, 2] += bound_w / 2 - image_center[0] rotation_mat[1, 2] += bound_h / 2 - image_center[1] rotated_mat = cv2.warpAffine(mat, rotation_mat, (bound_w, bound_h)) return rotated_mat该函数对图像按指定角度旋转,并通过计算旋转后的外接宽高(bound_w/bound_h)自动扩边,避免旋转裁切。这是模板匹配支持任意旋转角度的前置条件。
4.2 主函数template_matching_with_rotation
def template_matching_with_rotation( main_image, template, max_rotation=360, step=90, threshold=0.75, visualize=False, ) -> dict:核心参数与默认值如下:
| 参数 | 默认值 | 含义与影响 |
|---|---|---|
max_rotation | 360 | 旋转搜索的最大角度(度) |
step | 90 | 角度步长。默认 90 度即只搜 0/90/180/270 四个方向,可大幅降低耗时;对任意角度模板可调小(如 15/30 度) |
threshold | 0.75 | cv2.matchTemplate归一化相关系数的命中阈值,越高越严格 |
visualize | False | 为True时用 OpenCV 窗口绘制并展示所有命中框(调试用) |
算法流程:
- 灰度化:
cv2.cvtColor(..., cv2.COLOR_BGR2GRAY)把目标图与模板转成灰度,供cv2.matchTemplate使用; - 多角度循环:从
0到max_rotation按step步进旋转模板,若旋转后的模板尺寸大于目标图则跳过; - 模板匹配:对每个角度执行
cv2.matchTemplate(main_image_gray, rotated_template, cv2.TM_CCOEFF_NORMED),用np.where(result >= threshold)找出所有得分超过阈值的坐标,收集为(x, y, x+w, y+h)的边界框; - NMS 去重:同一目标在多个角度/邻域可能产生多个重叠框,因此调用
torchvision.ops.nms(IOU 阈值0.2)合并重叠框,同时保留对应得分; - 返回结果:
{"bboxes": boxes, "scores": scores}。
返回值结构正是第三节中normalize_bbox与 Agent 所消费的标准格式:bboxes为[x1, y1, x2, y2]列表,scores为对应置信度列表。
五、在对话中显式指定工具:解决"工具选择困难"
README 特别提醒了一个实战问题:
Tool choice can be difficult for the agent to get, so sometimes it helps to explicitly call out which tool you want to use.
即:让 LLM 自己从众多工具中选对自定义工具并不总是可靠。当存在大量内置工具(目标检测、OCR、分割等)时,Agent 可能选错或编造不存在的工具名。缓解办法是在提问时点名工具:
import vision_agent as va agent = va.agent.VisionAgentCoderV2(verbosity=2) agent( "Can you use the 'template_match_' tool to find the location of pid_template.png in pid.png?", media="pid.png", )注意两点:
- 指令中写的是
'template_match_'(带下划线后缀),这是告诉 Agent 去工具列表里寻找以该前缀命名的工具; media参数把图片传递给 Agent 会话;如果需要同时传入模板图,可像run_custom_tool.py的AgentMessage那样用media=["pid.png", "pid_template.png"]列表形式。
verbosity=2(README 示例)与verbose=True(run_custom_tool.py 示例)均为 Agent 的输出控制参数,实际使用以你所安装版本的签名为准。显式点名工具之后,Agent 生成代码时会优先匹配template_match_工具及其 docstring,从而显著提升稳定性。
六、底层原理:为什么必须传 imports?独立进程的代码注入机制
README 的 Details 一节解释了自定义工具注册最核心的机制,值得深挖:
Because we execute code on a separate process, we need to re-register the tools inside the new process. To do this,
register_toolscopies the source code and prepends it to the code that is executed in the new process. But there's a catch, it cannot copy the imports needed to run the tool code.
翻译过来即:
- 代码在独立进程中执行:
VisionAgentCoderV2生成的代码交由CodeInterpreter(见 vision_agent/utils/execute.py)在独立进程中运行; - 新进程不认识自定义工具:因此注册机制会把工具源码复制并前置拼接到新进程要执行的代码之前,实现"重新注册";
- 但源码复制不包含 import:函数体里用到的
cv2、numpy、template_match等依赖,无法仅靠复制函数源码带入新进程,所以必须由开发者在register_tool(imports=[...])里显式声明。
6.1 register_tool 的实现
看 vision_agent/tools/init.py#L73-L88 的源码:
def register_tool(imports: Optional[List] = None) -> Callable: def decorator(tool: Callable) -> Callable: import inspect global TOOLS, TOOLS_DF, TOOL_DESCRIPTIONS, TOOL_DOCSTRING, TOOLS_INFO from vision_agent.tools.tools import TOOLS if tool not in TOOLS: TOOLS.append(tool) globals()[tool.__name__] = tool if imports is not None: for import_ in imports: __new_tools__.append(import_) __new_tools__.append(inspect.getsource(tool)) return tool return decorator逐行解读其行为:
- 把工具函数追加进全局
TOOLS列表,同时globals()[tool.__name__] = tool使其可直接按名字访问; - 将
imports中的每条导入语句依次追加到__new_tools__; - 用
inspect.getsource(tool)取出工具完整源码,也追加到__new_tools__; __new_tools__初始内容为["import vision_agent as va", "from vision_agent.tools import register_tool"](见 vision_agent/tools/init.py#L67-L70),保证注入代码自身可解析。
6.2 注入点:DefaultImports.prepend_imports
最终这些字符串被拼接到什么位置?见 vision_agent/utils/agent.py#L195-L217 的DefaultImports:
class DefaultImports: common_imports = [ "import os", "import numpy as np", "from vision_agent.tools import *", "from vision_agent.tools.planner_tools import judge_od_results", "from typing import *", "from pillow_heif import register_heif_opener", "register_heif_opener()", ] @staticmethod def to_code_string() -> str: return "\n".join(DefaultImports.common_imports + T.__new_tools__) @staticmethod def prepend_imports(code: str) -> str: return DefaultImports.to_code_string() + "\n\n" + code可以看到:Agent 每次执行代码前,都会把common_imports(含from vision_agent.tools import *)与__new_tools__(即所有已注册自定义工具的 imports + 源码)拼接,再前置到目标代码上。这就是"重新注册"的完整链路:
common_imports(默认导入) + 自定义工具 imports(你显式传入的导入语句) + 自定义工具源码(inspect.getsource 复制) + 换行 + Agent 生成的业务代码由此也验证了 README 的告诫:凡是工具函数体用到的第三方库,都必须写进imports,否则新进程执行注入代码时会直接NameError。反过来,imports里只应放执行该工具所必需的导入,避免污染执行环境。
6.3 一条完整的"最小注册"示例
README 给出了去掉业务逻辑的最小骨架:
import vision_agent as va @va.register_tool( imports=["import cv2"], ) def custom_tool(*args): # Your tool code here pass与run_custom_tool.py中完整写法(@va.tools.register_tool)等价,只是注册路径的写法不同。两者的要点一致:装饰器 + imports 列表 + 规范 docstring。
七、实战要点与最佳实践总结
综合 README 与源码,编写可被 Vision Agent 稳定调用的自定义工具,应遵循以下要点:
- 注册三要素缺一不可:
@va.tools.register_tool装饰器、完整的imports列表、描述清晰的 docstring(含参数、返回与用法示例)。 - 返回格式对齐内置约定:边界框归一化到 0~1(用
normalize_bbox),返回{"bboxes": ..., "scores": ...}结构,Agent 的后续代码(画框、算距离、判断邻近关系)才能正确消费。 - 算法层与注册层分离:template_match.py 只依赖
cv2/numpy/torch,与 vision-agent 解耦,便于独立测试与复用;run_custom_tool.py 只负责包装与注册。 - 对话中显式点名工具:提问时明确写
'template_match_'这样的工具名,降低 Agent 工具选择的随机性。 - 依赖声明完整:示例依赖
torch/torchvision(见 requirements.txt),实际使用还应确保opencv-python、numpy与vision_agent本体安装到位。 - 性能与精度权衡:
step决定角度搜索粒度——step=90快但只覆盖四个方向,需要任意角度匹配时调小步长(同时匹配时间近似线性增长);threshold=0.75可根据实际误检情况上下调整。
八、结语
本文以 examples/custom_tools/README.md 为骨架,从运行方式、注册代码、旋转模板匹配算法到register_tool的独立进程代码注入原理,完整还原了"为 Vision Agent 添加自定义工具"的端到端流程。核心结论可以浓缩为一句话:自定义工具 = 规范 docstring 的函数 +register_tool注册 + 显式 imports + 归一化 bbox 返回格式。掌握了这套模式,你就能把任意 OpenCV / PyTorch 视觉算法快速武装成 Agent 的可调用能力。
【免费下载链接】vision-agentThis tool has been deprecated. Use Agentic Document Extraction instead.项目地址: https://gitcode.com/GitHub_Trending/vi/vision-agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考