Skip to content

Commit c97a23b

Browse files
authored
Merge pull request #376 from Integration-Automation/feat/match-autothresh-batch
Add auto-thresholded template matching (Otsu on score map)
2 parents e28028d + 29d440d commit c97a23b

15 files changed

Lines changed: 385 additions & 0 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-24) — 自动门槛模板匹配(对分数图做 Otsu)
4+
5+
不再手调 `min_score`——由分数图推导匹配门槛。完整参考:[`docs/source/Zh/doc/new_features/v164_features_doc.rst`](../docs/source/Zh/doc/new_features/v164_features_doc.rst)
6+
7+
- **`match_auto` / `auto_threshold`**(`AC_match_auto``AC_auto_threshold`):每次 `match_template_all` 都迫使你猜 `min_score`(太低充满 NMS 噪声、太高漏掉换肤目标,且因素材而异)。本功能对*相关性分数直方图*套用 Otsu,找出背景相关与真正匹配之间的谷,返回该门槛加上 *separability* 分离度(接近 0 = 单峰、无明确匹配 → 不要信任)。`match_auto` 每个过门槛区域只返回单一峰(通过 `connected_boxes`,避免宽峰上的重复命中),并以 `floor` 夹住。重用新增的 `visual_match._score_map`;`haystack` 可注入;不导入 `PySide6`
8+
39
## 本次更新 (2026-06-24) — 令牌预算化的观测增量(变更了什么)
410

511
告诉代理*变更了什么*,而非再次整个屏幕。完整参考:[`docs/source/Zh/doc/new_features/v163_features_doc.rst`](../docs/source/Zh/doc/new_features/v163_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-24) — 自動門檻樣板比對(對分數圖做 Otsu)
4+
5+
不再手調 `min_score`——由分數圖推導比對門檻。完整參考:[`docs/source/Zh/doc/new_features/v164_features_doc.rst`](../docs/source/Zh/doc/new_features/v164_features_doc.rst)
6+
7+
- **`match_auto` / `auto_threshold`**(`AC_match_auto``AC_auto_threshold`):每次 `match_template_all` 都迫使你猜 `min_score`(太低充滿 NMS 雜訊、太高漏掉換膚目標,且因素材而異)。本功能對*相關性分數直方圖*套用 Otsu,找出背景相關與真正匹配之間的谷,回傳該門檻加上 *separability* 分離度(接近 0 = 單峰、無明確匹配 → 不要信任)。`match_auto` 每個過門檻區域只回傳單一峰(透過 `connected_boxes`,避免寬峰上的重複命中),並以 `floor` 夾住。重用新增的 `visual_match._score_map`;`haystack` 可注入;不匯入 `PySide6`
8+
39
## 本次更新 (2026-06-24) — 權杖預算化的觀測差異(變更了什麼)
410

511
告訴代理*變更了什麼*,而非再次整個畫面。完整參考:[`docs/source/Zh/doc/new_features/v163_features_doc.rst`](../docs/source/Zh/doc/new_features/v163_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-24) — Auto-Thresholded Template Matching (Otsu on the Score Map)
4+
5+
No more hand-tuned `min_score` — derive the match threshold from the score map. Full reference: [`docs/source/Eng/doc/new_features/v164_features_doc.rst`](docs/source/Eng/doc/new_features/v164_features_doc.rst).
6+
7+
- **`match_auto` / `auto_threshold`** (`AC_match_auto`, `AC_auto_threshold`): every `match_template_all` call forces you to guess `min_score` (too low floods NMS, too high drops re-themed targets, and it differs per asset). This runs Otsu on the *correlation score histogram* to find the valley between background correlation and real matches, returns that cut-off plus a *separability* score (near 0 = unimodal, no clear match → don't trust it). `match_auto` returns one peak per above-threshold region (via `connected_boxes`, avoiding duplicate hits on a wide peak), clamped by a `floor`. Reuses the new `visual_match._score_map`; injectable `haystack`; no `PySide6`.
8+
39
## What's new (2026-06-24) — Token-Budgeted Observation Delta (What Changed)
410

511
Tell an agent *what changed* since the last step, not the whole screen again. Full reference: [`docs/source/Eng/doc/new_features/v163_features_doc.rst`](docs/source/Eng/doc/new_features/v163_features_doc.rst).
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Auto-Thresholded Template Matching (Otsu on the Score Map)
2+
==========================================================
3+
4+
Every call to ``match_template_all`` forces the caller to guess ``min_score``: too low
5+
floods NMS with background noise, too high drops scaled / re-themed targets, and the right
6+
value differs per asset and per screen. ``match_autothresh`` removes the magic number — it
7+
runs Otsu's method on the *correlation score histogram* (not pixel intensities, the way
8+
``preprocess.binarize`` does) to find the valley between the "background correlation" mass
9+
and the "real match" mass, and returns that cut-off plus a *separability* number so the
10+
caller knows when the histogram is unimodal (no clear match → don't trust the threshold).
11+
12+
It reuses ``visual_match._score_map`` (the full ``matchTemplate`` surface the public matchers
13+
discard) and ``cv2_utils.blobs.connected_boxes`` — each above-threshold region yields a single
14+
peak, avoiding the duplicate hits a raw pixel scan + NMS leaves on a wide correlation peak. The
15+
``haystack`` is injectable; the analysis is unit-testable on synthetic arrays. Imports no
16+
``PySide6``.
17+
18+
Headless API
19+
------------
20+
21+
.. code-block:: python
22+
23+
from je_auto_control import match_auto, auto_threshold
24+
25+
# no min_score to tune — the threshold is derived from the score map
26+
for hit in match_auto("save_button.png", floor=0.5):
27+
print(hit.center, hit.score)
28+
29+
info = auto_threshold("save_button.png")
30+
# {"threshold": 0.83, "separability": 0.61, "n_above": 2}
31+
if info["separability"] < 0.3:
32+
print("no clear match — threshold not trustworthy")
33+
34+
``match_auto`` returns one ``Match`` per above-threshold region, ordered by score and capped at
35+
``max_results``; the cut-off is ``max(floor, otsu_threshold)`` so a unimodal / noisy surface
36+
cannot drag it below a sane floor. ``auto_threshold`` returns ``{threshold, separability,
37+
n_above}`` — ``separability`` near 0 means the score histogram is unimodal and the threshold
38+
should be treated as unreliable.
39+
40+
Executor commands
41+
-----------------
42+
43+
``AC_match_auto`` (``template`` / ``floor`` / ``max_results`` / ``region`` / ``method`` →
44+
``{count, matches}``) and ``AC_auto_threshold`` (``template`` / ``region`` / ``method`` →
45+
``{found, info}``). They are exposed as the MCP tools ``ac_match_auto`` / ``ac_auto_threshold``
46+
(read-only) and as the Script Builder commands **Match Template (auto-threshold)** / **Auto
47+
Threshold (Otsu on scores)** under **Image**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ Comprehensive guides for all AutoControl features.
186186
doc/new_features/v161_features_doc
187187
doc/new_features/v162_features_doc
188188
doc/new_features/v163_features_doc
189+
doc/new_features/v164_features_doc
189190
doc/ocr_backends/ocr_backends_doc
190191
doc/observability/observability_doc
191192
doc/operations_layer/operations_layer_doc
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
自動門檻樣板比對(對分數圖做 Otsu)
2+
====================================
3+
4+
每次呼叫 ``match_template_all`` 都迫使呼叫者猜 ``min_score``:太低會讓 NMS 充滿背景雜訊,
5+
太高會漏掉縮放 / 換膚的目標,而正確值因素材與畫面而異。``match_autothresh`` 移除這個魔術
6+
數字——它對*相關性分數直方圖*(而非像素強度,後者是 ``preprocess.binarize`` 的做法)套用
7+
Otsu 法,找出「背景相關」團與「真正匹配」團之間的谷,並回傳該門檻加上一個 *separability*
8+
(分離度)值,讓呼叫端知道何時直方圖為單峰(無明確匹配 → 不要信任該門檻)。
9+
10+
本功能重用 ``visual_match._score_map``(公開比對器丟棄的完整 ``matchTemplate`` 曲面)與
11+
``cv2_utils.blobs.connected_boxes``——每個過門檻區域只回傳單一峰,避免原始像素掃描 + NMS
12+
在寬相關峰上留下的重複命中。``haystack`` 可注入;分析可在合成陣列上單元測試。不匯入
13+
``PySide6``。
14+
15+
無頭 API
16+
--------
17+
18+
.. code-block:: python
19+
20+
from je_auto_control import match_auto, auto_threshold
21+
22+
# 無需手調 min_score——門檻由分數圖推導
23+
for hit in match_auto("save_button.png", floor=0.5):
24+
print(hit.center, hit.score)
25+
26+
info = auto_threshold("save_button.png")
27+
# {"threshold": 0.83, "separability": 0.61, "n_above": 2}
28+
if info["separability"] < 0.3:
29+
print("無明確匹配——門檻不可信")
30+
31+
``match_auto`` 每個過門檻區域回傳一個 ``Match``,依分數排序並以 ``max_results`` 為上限;
32+
門檻為 ``max(floor, otsu_threshold)``,使單峰 / 雜訊曲面無法把它拉到合理下限之下。
33+
``auto_threshold`` 回傳 ``{threshold, separability, n_above}``——``separability`` 接近 0
34+
表示分數直方圖為單峰,該門檻應視為不可靠。
35+
36+
執行器指令
37+
----------
38+
39+
``AC_match_auto``(``template`` / ``floor`` / ``max_results`` / ``region`` / ``method`` →
40+
``{count, matches}``)與 ``AC_auto_threshold``(``template`` / ``region`` / ``method`` →
41+
``{found, info}``)。兩者以 MCP 工具 ``ac_match_auto`` / ``ac_auto_threshold``(唯讀)及
42+
Script Builder 指令 **Match Template (auto-threshold)** / **Auto Threshold (Otsu on scores)**
43+
(位於 **Image** 分類下)形式提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ AutoControl 所有功能的完整使用指南。
186186
doc/new_features/v161_features_doc
187187
doc/new_features/v162_features_doc
188188
doc/new_features/v163_features_doc
189+
doc/new_features/v164_features_doc
189190
doc/ocr_backends/ocr_backends_doc
190191
doc/observability/observability_doc
191192
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@
287287
from je_auto_control.utils.match_trust import (
288288
TrustedMatch, match_with_trust, score_peaks,
289289
)
290+
# Otsu auto-thresholding for template matching (no hand-tuned min_score)
291+
from je_auto_control.utils.match_autothresh import (
292+
auto_threshold, match_auto,
293+
)
290294
# Coarse labelled cell grid for VLM grounding (point <-> cell mapping)
291295
from je_auto_control.utils.screen_grid import (
292296
GridCell, cell_for_point, grid_cells, point_for_cell,
@@ -1213,6 +1217,8 @@ def start_autocontrol_gui(*args, **kwargs):
12131217
"TrustedMatch",
12141218
"match_with_trust",
12151219
"score_peaks",
1220+
"auto_threshold",
1221+
"match_auto",
12161222
"GridCell",
12171223
"grid_cells",
12181224
"cell_for_point",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,27 @@ def _add_image_specs(specs: List[CommandSpec]) -> None:
350350
),
351351
description="Match a template and flag if it is ambiguous (duplicate peak).",
352352
))
353+
specs.append(CommandSpec(
354+
"AC_match_auto", "Image", "Match Template (auto-threshold)",
355+
fields=(
356+
FieldSpec("template", FieldType.FILE_PATH),
357+
FieldSpec("floor", FieldType.FLOAT, optional=True, default=0.5,
358+
min_value=0.0, max_value=1.0),
359+
FieldSpec("max_results", FieldType.INT, optional=True, default=20),
360+
FieldSpec("region", FieldType.STRING, optional=True,
361+
placeholder=_REGION_PLACEHOLDER),
362+
),
363+
description="Find a template with an Otsu auto-threshold (no min_score).",
364+
))
365+
specs.append(CommandSpec(
366+
"AC_auto_threshold", "Image", "Auto Threshold (Otsu on scores)",
367+
fields=(
368+
FieldSpec("template", FieldType.FILE_PATH),
369+
FieldSpec("region", FieldType.STRING, optional=True,
370+
placeholder=_REGION_PLACEHOLDER),
371+
),
372+
description="Derive a match threshold + separability from the score map.",
373+
))
353374
specs.append(CommandSpec(
354375
"AC_grid_cells", "Image", "Grid Cells (coarse grounding)",
355376
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3339,6 +3339,29 @@ def _match_with_trust(template: str, min_score: Any = 0.0, scales: Any = None,
33393339
"match": match.to_dict() if match else None}
33403340

33413341

3342+
def _auto_threshold(template: str, region: Any = None,
3343+
method: str = "ccoeff_normed") -> Dict[str, Any]:
3344+
"""Adapter: Otsu-derived accept threshold for a template (+ separability)."""
3345+
import json
3346+
from je_auto_control.utils.match_autothresh import auto_threshold
3347+
if isinstance(region, str):
3348+
region = json.loads(region) if region.strip() else None
3349+
info = auto_threshold(template, region=region, method=method)
3350+
return {"found": info is not None, "info": info}
3351+
3352+
3353+
def _match_auto(template: str, floor: Any = 0.5, max_results: Any = 20,
3354+
region: Any = None, method: str = "ccoeff_normed") -> Dict[str, Any]:
3355+
"""Adapter: matches above the auto-derived (Otsu) threshold, one per region."""
3356+
import json
3357+
from je_auto_control.utils.match_autothresh import match_auto
3358+
if isinstance(region, str):
3359+
region = json.loads(region) if region.strip() else None
3360+
matches = match_auto(template, region=region, floor=float(floor),
3361+
max_results=int(max_results), method=method)
3362+
return {"count": len(matches), "matches": [m.to_dict() for m in matches]}
3363+
3364+
33423365
def _region_arg(value: Any) -> Optional[List[int]]:
33433366
"""Coerce a JSON-string / list region arg into a list of ints, or None."""
33443367
import json
@@ -5829,6 +5852,8 @@ def __init__(self):
58295852
"AC_match_rotated": _match_rotated,
58305853
"AC_match_rotated_all": _match_rotated_all,
58315854
"AC_match_with_trust": _match_with_trust,
5855+
"AC_auto_threshold": _auto_threshold,
5856+
"AC_match_auto": _match_auto,
58325857
"AC_grid_cells": _grid_cells,
58335858
"AC_cell_for_point": _cell_for_point,
58345859
"AC_point_for_cell": _point_for_cell,

0 commit comments

Comments
 (0)