Skip to content

Commit c07b67a

Browse files
committed
Add masked template matching (ignore background via mask/alpha)
1 parent 6b0437c commit c07b67a

15 files changed

Lines changed: 400 additions & 3 deletions

File tree

‎README/WHATS_NEW_zh-CN.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 本次更新 — AutoControl
22

3+
## 本次更新 (2026-06-23) — 遮罩模板匹配
4+
5+
不论背景如何都能匹配图标。完整参考:[`docs/source/Zh/doc/new_features/v129_features_doc.rst`](../docs/source/Zh/doc/new_features/v129_features_doc.rst)。
6+
7+
- **`match_masked` / `match_masked_all`**(`AC_match_masked`、`AC_match_masked_all`):一般模板匹配会计分*每个*像素,因此从某背景裁切出的图标在不同背景上会匹配失败。本功能只计算你标记为相关的像素——明确的灰阶 `mask`,或 RGBA 模板的 alpha 通道——让透明 /「不在乎」的像素不再拉低分数。返回与计分模板匹配相同的 `Match`(score/center);使用 OpenCV 遮罩 `TM_CCORR_NORMED`,NaN 归零。可注入 haystack → 无头可测。
8+
39
## 本次更新 (2026-06-23) — 依颜色定位屏幕区域
410

511
依颜色找出绿色状态药丸 / 红色横幅。完整参考:[`docs/source/Zh/doc/new_features/v128_features_doc.rst`](../docs/source/Zh/doc/new_features/v128_features_doc.rst)。

‎README/WHATS_NEW_zh-TW.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 本次更新 — AutoControl
22

3+
## 本次更新 (2026-06-23) — 遮罩模板比對
4+
5+
不論背景如何都能比對圖示。完整參考:[`docs/source/Zh/doc/new_features/v129_features_doc.rst`](../docs/source/Zh/doc/new_features/v129_features_doc.rst)。
6+
7+
- **`match_masked` / `match_masked_all`**(`AC_match_masked`、`AC_match_masked_all`):一般模板比對會計分*每個*像素,因此從某背景裁切出的圖示在不同背景上會比對失敗。本功能只計算你標記為相關的像素——明確的灰階 `mask`,或 RGBA 模板的 alpha 通道——讓透明 /「不在乎」的像素不再拉低分數。回傳與計分模板比對相同的 `Match`(score/center);使用 OpenCV 遮罩 `TM_CCORR_NORMED`,NaN 歸零。可注入 haystack → 無頭可測。
8+
39
## 本次更新 (2026-06-23) — 依顏色定位螢幕區域
410

511
依顏色找出綠色狀態藥丸 / 紅色橫幅。完整參考:[`docs/source/Zh/doc/new_features/v128_features_doc.rst`](../docs/source/Zh/doc/new_features/v128_features_doc.rst)。

‎WHATS_NEW.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# What's New — AutoControl
22

3+
## What's new (2026-06-23) — Masked Template Matching
4+
5+
Match icons regardless of their background. Full reference: [`docs/source/Eng/doc/new_features/v129_features_doc.rst`](docs/source/Eng/doc/new_features/v129_features_doc.rst).
6+
7+
- **`match_masked` / `match_masked_all`** (`AC_match_masked`, `AC_match_masked_all`): plain template matching scores *every* pixel, so an icon clipped from one background fails over a different one. These count only the pixels you mark relevant — an explicit grayscale `mask`, or an RGBA template's alpha channel — so transparent / "don't care" pixels stop dragging the score down. Returns the same `Match` (score/center) as scored template matching; OpenCV masked `TM_CCORR_NORMED`, NaNs zeroed. Injectable haystack → headless-testable.
8+
39
## What's new (2026-06-23) — Locate On-Screen Regions by Colour
410

511
Find the green status pill / red banner by colour. Full reference: [`docs/source/Eng/doc/new_features/v128_features_doc.rst`](docs/source/Eng/doc/new_features/v128_features_doc.rst).
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Masked Template Matching (Ignore the Background)
2+
================================================
3+
4+
Plain template matching scores *every* pixel of the template, so an icon clipped
5+
from one background fails to match the same icon over a different one — a toolbar
6+
glyph on a hovered vs. idle button, a cursor over arbitrary content, a logo on a
7+
themed surface. ``match_masked`` counts only the pixels you mark as relevant: an
8+
explicit grayscale ``mask`` (non-zero = use), or — if you pass an RGBA template —
9+
its alpha channel. The transparent / "don't care" pixels stop dragging the score
10+
down.
11+
12+
It builds on the same ``Match`` result as :doc:`v127_features_doc` (top-left,
13+
size, ``score``, ``center``) and runs on an injectable ``haystack`` (ndarray /
14+
path / PIL), so it is unit-testable on synthetic arrays. Matching uses OpenCV's
15+
masked ``TM_CCORR_NORMED`` (the only normed metric that accepts a mask without
16+
producing NaNs); non-finite cells are zeroed. OpenCV + NumPy come in via
17+
``je_open_cv``; imports no ``PySide6``.
18+
19+
Headless API
20+
------------
21+
22+
.. code-block:: python
23+
24+
from je_auto_control import match_masked, match_masked_all
25+
26+
# A PNG icon with transparency — its alpha is the mask automatically.
27+
hit = match_masked("save_icon.png", min_score=0.9)
28+
if hit:
29+
click(*hit.center)
30+
31+
# An explicit mask: only the white pixels of mask.png are compared.
32+
for hit in match_masked_all("cursor.png", mask="cursor_mask.png",
33+
min_score=0.95):
34+
print(hit.x, hit.y, hit.score)
35+
36+
``match_masked`` returns the single best ``Match`` at or above ``min_score`` (or
37+
``None``); ``match_masked_all`` returns every match with overlaps removed by
38+
non-maximum suppression, highest score first, capped at ``max_results``. A mask
39+
whose shape does not match the template raises ``ValueError``.
40+
41+
Executor commands
42+
-----------------
43+
44+
``AC_match_masked`` / ``AC_match_masked_all`` take ``template`` (and optional
45+
``mask``) plus ``min_score`` / ``region`` (and ``max_results`` / ``nms_iou`` for
46+
the *all* form). They are exposed as the MCP tools ``ac_match_masked`` /
47+
``ac_match_masked_all`` and as Script Builder commands under **Image**.

‎docs/source/Eng/eng_index.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Comprehensive guides for all AutoControl features.
151151
doc/new_features/v126_features_doc
152152
doc/new_features/v127_features_doc
153153
doc/new_features/v128_features_doc
154+
doc/new_features/v129_features_doc
154155
doc/ocr_backends/ocr_backends_doc
155156
doc/observability/observability_doc
156157
doc/operations_layer/operations_layer_doc
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
遮罩模板比對(忽略背景)
2+
========================
3+
4+
一般模板比對會計分模板的*每個*像素,因此從某背景裁切出的圖示無法比對到同一圖示在不同背景上的情形——
5+
工具列圖示在 hover 與閒置按鈕上、游標疊在任意內容上、Logo 在主題化表面上。``match_masked`` 只計算你標記為
6+
相關的像素:明確的灰階 ``mask``(非零 = 使用),或——若傳入 RGBA 模板——其 alpha 通道。透明 /「不在乎」的
7+
像素就不會再把分數拉低。
8+
9+
它沿用與 :doc:`v127_features_doc` 相同的 ``Match`` 結果(左上角、尺寸、``score``、``center``),並在可注入的
10+
``haystack``(ndarray / 路徑 / PIL)上執行,因此可對合成陣列做單元測試。比對使用 OpenCV 的遮罩
11+
``TM_CCORR_NORMED``(唯一能接受遮罩且不產生 NaN 的正規化度量);非有限值會被歸零。OpenCV + NumPy 透過
12+
``je_open_cv`` 引入;不匯入 ``PySide6``。
13+
14+
無頭 API
15+
--------
16+
17+
.. code-block:: python
18+
19+
from je_auto_control import match_masked, match_masked_all
20+
21+
# 帶透明度的 PNG 圖示——其 alpha 自動作為遮罩。
22+
hit = match_masked("save_icon.png", min_score=0.9)
23+
if hit:
24+
click(*hit.center)
25+
26+
# 明確遮罩:只比對 mask.png 的白色像素。
27+
for hit in match_masked_all("cursor.png", mask="cursor_mask.png",
28+
min_score=0.95):
29+
print(hit.x, hit.y, hit.score)
30+
31+
``match_masked`` 回傳達到 ``min_score`` 的單一最佳 ``Match``(或 ``None``);``match_masked_all`` 回傳每個
32+
比對,以非極大值抑制移除重疊,分數由高到低,上限 ``max_results``。遮罩形狀與模板不符會丟出 ``ValueError``。
33+
34+
執行器命令
35+
----------
36+
37+
``AC_match_masked`` / ``AC_match_masked_all`` 接受 ``template``(及選用 ``mask``)以及
38+
``min_score`` / ``region``(*all* 形式另有 ``max_results`` / ``nms_iou``)。它們以 MCP 工具
39+
``ac_match_masked`` / ``ac_match_masked_all`` 以及 Script Builder 中 **Image** 分類下的命令提供。

‎docs/source/Zh/zh_index.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ AutoControl 所有功能的完整使用指南。
151151
doc/new_features/v126_features_doc
152152
doc/new_features/v127_features_doc
153153
doc/new_features/v128_features_doc
154+
doc/new_features/v129_features_doc
154155
doc/ocr_backends/ocr_backends_doc
155156
doc/observability/observability_doc
156157
doc/operations_layer/operations_layer_doc

‎je_auto_control/__init__.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@
275275
from je_auto_control.utils.grid_locator import cluster_grid, locate_cell
276276
# Confidence-returning template matching (score / multi-scale / find-all + NMS)
277277
from je_auto_control.utils.visual_match import (
278-
best_matches, match_template, match_template_all,
278+
best_matches, match_masked, match_masked_all, match_template,
279+
match_template_all,
279280
)
280281
from je_auto_control.utils.visual_match import Match as TemplateMatch
281282
# Locate on-screen regions by colour (mask + connected components)
@@ -1074,6 +1075,8 @@ def start_autocontrol_gui(*args, **kwargs):
10741075
"TemplateMatch",
10751076
"match_template",
10761077
"match_template_all",
1078+
"match_masked",
1079+
"match_masked_all",
10771080
"best_matches",
10781081
"find_color_region",
10791082
"find_color_regions",

‎je_auto_control/gui/script_builder/command_schema.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,31 @@ def _add_image_specs(specs: List[CommandSpec]) -> None:
278278
),
279279
description="Find every occurrence of a template (scored, NMS-deduped).",
280280
))
281+
specs.append(CommandSpec(
282+
"AC_match_masked", "Image", "Match Masked Template",
283+
fields=(
284+
FieldSpec("template", FieldType.FILE_PATH),
285+
FieldSpec("mask", FieldType.FILE_PATH, optional=True),
286+
FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9,
287+
min_value=0.0, max_value=1.0),
288+
FieldSpec("region", FieldType.STRING, optional=True,
289+
placeholder="[left, top, right, bottom]"),
290+
),
291+
description="Match counting only opaque/masked pixels (alpha or mask).",
292+
))
293+
specs.append(CommandSpec(
294+
"AC_match_masked_all", "Image", "Match Masked Template All",
295+
fields=(
296+
FieldSpec("template", FieldType.FILE_PATH),
297+
FieldSpec("mask", FieldType.FILE_PATH, optional=True),
298+
FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9,
299+
min_value=0.0, max_value=1.0),
300+
FieldSpec("max_results", FieldType.INT, optional=True, default=20),
301+
FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3,
302+
min_value=0.0, max_value=1.0),
303+
),
304+
description="Find every masked match of a template (NMS-deduped).",
305+
))
281306
specs.append(CommandSpec(
282307
"AC_find_color_region", "Image", "Find Colour Region",
283308
fields=(

‎je_auto_control/utils/executor/action_executor.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3229,6 +3229,34 @@ def _match_template_all(template: str, min_score: Any = 0.8,
32293229
return {"count": len(matches), "matches": [m.to_dict() for m in matches]}
32303230

32313231

3232+
def _match_masked(template: str, mask: Any = None, min_score: Any = 0.9,
3233+
region: Any = None) -> Dict[str, Any]:
3234+
"""Adapter: best masked template match (alpha / mask ignores background)."""
3235+
import json
3236+
from je_auto_control.utils.visual_match import match_masked
3237+
if isinstance(region, str):
3238+
region = json.loads(region) if region.strip() else None
3239+
match = match_masked(template, mask=mask, region=region,
3240+
min_score=float(min_score))
3241+
return {"found": match is not None,
3242+
"match": match.to_dict() if match else None}
3243+
3244+
3245+
def _match_masked_all(template: str, mask: Any = None, min_score: Any = 0.9,
3246+
max_results: Any = 20, nms_iou: Any = 0.3,
3247+
region: Any = None) -> Dict[str, Any]:
3248+
"""Adapter: every masked template match on the screen (NMS)."""
3249+
import json
3250+
from je_auto_control.utils.visual_match import match_masked_all
3251+
if isinstance(region, str):
3252+
region = json.loads(region) if region.strip() else None
3253+
matches = match_masked_all(template, mask=mask, region=region,
3254+
min_score=float(min_score),
3255+
max_results=int(max_results),
3256+
nms_iou=float(nms_iou))
3257+
return {"count": len(matches), "matches": [m.to_dict() for m in matches]}
3258+
3259+
32323260
def _find_color_region(rgb: Any, tolerance: Any = 20, min_area: Any = 50,
32333261
region: Any = None) -> Dict[str, Any]:
32343262
"""Adapter: locate coloured regions on the screen, largest first."""
@@ -4964,6 +4992,8 @@ def __init__(self):
49644992
"AC_grid_cell": _grid_cell,
49654993
"AC_match_template": _match_template,
49664994
"AC_match_template_all": _match_template_all,
4995+
"AC_match_masked": _match_masked,
4996+
"AC_match_masked_all": _match_masked_all,
49674997
"AC_find_color_region": _find_color_region,
49684998
"AC_detect_drift": _detect_drift,
49694999
"AC_categorical_drift": _categorical_drift,

0 commit comments

Comments
 (0)