Skip to content

Commit b4d8a90

Browse files
committed
Add localized motion / activity detection (absdiff)
1 parent 082632d commit b4d8a90

15 files changed

Lines changed: 349 additions & 1 deletion

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/v146_features_doc.rst`](../docs/source/Zh/doc/new_features/v146_features_doc.rst)
6+
7+
- **`changed_regions` / `has_motion` / `activity_score`**(`AC_changed_regions``AC_has_motion`):`wait_until_screen_stable` 是布尔轮询、`ssim_changed_regions` 是结构性(忽略快速动态)、`diff_screenshots` 非活动区块。本功能是便宜的 absdiff 路径——对逐像素差做门槛、膨胀,返回移动区域方框(由大到小)、布尔值,以及移动像素比例。挑选安静区域或定位转圈动画。两个可注入帧 → 无头可测;沿用共用连通元件辅助;执行器中 `after` 默认为即时屏幕截取。
8+
39
## 本次更新 (2026-06-23) — 色彩直方图指纹与变化检测
410

511
判断画面在光照 / 缩放下是否仍是「同一个」。完整参考:[`docs/source/Zh/doc/new_features/v145_features_doc.rst`](../docs/source/Zh/doc/new_features/v145_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/v146_features_doc.rst`](../docs/source/Zh/doc/new_features/v146_features_doc.rst)
6+
7+
- **`changed_regions` / `has_motion` / `activity_score`**(`AC_changed_regions``AC_has_motion`):`wait_until_screen_stable` 是布林輪詢、`ssim_changed_regions` 是結構性(忽略快速動態)、`diff_screenshots` 非活動區塊。本功能是便宜的 absdiff 路徑——對逐像素差做門檻、膨脹,回傳移動區域方框(由大到小)、布林值,以及移動像素比例。挑選安靜區域或定位轉圈動畫。兩個可注入幀 → 無頭可測;沿用共用連通元件輔助;執行器中 `after` 預設為即時螢幕擷取。
8+
39
## 本次更新 (2026-06-23) — 色彩直方圖指紋與變化偵測
410

511
判斷畫面在光照 / 縮放下是否仍是「同一個」。完整參考:[`docs/source/Zh/doc/new_features/v145_features_doc.rst`](../docs/source/Zh/doc/new_features/v145_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) — Localized Motion / Activity Detection
4+
5+
Find which sub-regions are animating between two frames. Full reference: [`docs/source/Eng/doc/new_features/v146_features_doc.rst`](docs/source/Eng/doc/new_features/v146_features_doc.rst).
6+
7+
- **`changed_regions` / `has_motion` / `activity_score`** (`AC_changed_regions`, `AC_has_motion`): `wait_until_screen_stable` is a boolean poll, `ssim_changed_regions` is structural (ignores fast motion), `diff_screenshots` isn't activity blobs. This is the cheap absdiff path — threshold the per-pixel difference, dilate, and return the moved-region boxes (largest first), a boolean, and the fraction of pixels that moved. Pick a quiet area or locate a spinner. Two injectable frames → headless-testable; reuses the shared connected-components helper; `after` defaults to a live screen grab in the executor.
8+
39
## What's new (2026-06-23) — Colour-Histogram Fingerprint & Change Detection
410

511
Tell whether the view is "the same" despite lighting / scale. Full reference: [`docs/source/Eng/doc/new_features/v145_features_doc.rst`](docs/source/Eng/doc/new_features/v145_features_doc.rst).
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
Localized Motion / Activity Detection
2+
=====================================
3+
4+
Three near-neighbours, all distinct: ``wait_until_screen_stable`` returns a *boolean*
5+
over a live poll loop (not localized boxes on an injectable pair); ``ssim_changed_regions``
6+
is *structural* (Gaussian-windowed SSIM, illumination-tolerant — it deliberately ignores
7+
the fast pixel motion you want for "where is the spinner / video / animation");
8+
``diff_screenshots`` highlights pixel diffs but is not framed as activity blobs with a
9+
score. ``changed_regions`` / ``has_motion`` / ``activity_score`` are the cheap absdiff
10+
path: which sub-regions are *moving* between two frames, so a script can pick a quiet
11+
area or locate a busy spinner.
12+
13+
They operate on two injectable frames (ndarray / path / PIL), so they are headless-
14+
testable on synthetic arrays, and reuse the shared connected-component helper. OpenCV +
15+
NumPy come in via ``je_open_cv``. Imports no ``PySide6``.
16+
17+
Headless API
18+
------------
19+
20+
.. code-block:: python
21+
22+
from je_auto_control import changed_regions, has_motion, activity_score
23+
24+
before = screenshot_to_array()
25+
# ... time passes ...
26+
for box in changed_regions(before, after): # boxes that moved, largest first
27+
print(box["x"], box["y"], box["width"], box["height"])
28+
29+
if has_motion(before, after):
30+
print("still animating, activity =", activity_score(before, after))
31+
32+
``changed_regions`` thresholds the absolute difference (``threshold``), denoises
33+
(``blur``), dilates and returns ``{x, y, width, height, area, center}`` blobs of at
34+
least ``min_area``, largest first. ``has_motion`` is the boolean form; ``activity_score``
35+
is the fraction (0..1) of pixels that moved. Frames of different sizes raise
36+
``ValueError``.
37+
38+
Executor commands
39+
-----------------
40+
41+
``AC_changed_regions`` (``before`` / ``after`` / ``threshold`` / ``min_area`` /
42+
``blur`` → ``{count, regions}``) and ``AC_has_motion`` (``before`` / ``after`` /
43+
``threshold`` / ``min_area`` → ``{moved, activity}``); ``after`` defaults to a live
44+
screen grab. They are exposed as the MCP tools ``ac_changed_regions`` /
45+
``ac_has_motion`` 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
@@ -168,6 +168,7 @@ Comprehensive guides for all AutoControl features.
168168
doc/new_features/v143_features_doc
169169
doc/new_features/v144_features_doc
170170
doc/new_features/v145_features_doc
171+
doc/new_features/v146_features_doc
171172
doc/ocr_backends/ocr_backends_doc
172173
doc/observability/observability_doc
173174
doc/operations_layer/operations_layer_doc
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
局部動態 / 活動偵測
2+
====================
3+
4+
三個相近鄰居,各有區別:``wait_until_screen_stable`` 在即時輪詢迴圈上回傳*布林值*(不是對可注入配對的局部方框);
5+
``ssim_changed_regions`` 是*結構性*的(高斯視窗 SSIM、耐光照——刻意忽略你想抓的快速像素動態);``diff_screenshots``
6+
標示像素差但不以活動區塊 + 分數呈現。``changed_regions`` / ``has_motion`` / ``activity_score`` 是便宜的 absdiff
7+
路徑:兩幀之間哪些子區域在*移動*,讓腳本能挑選安靜區域或定位忙碌的轉圈動畫。
8+
9+
它們在兩個可注入的幀(ndarray / 路徑 / PIL)上運作,因此可對合成陣列做無頭測試,並沿用共用的連通元件輔助函式。
10+
OpenCV + NumPy 透過 ``je_open_cv`` 引入。不匯入 ``PySide6``。
11+
12+
無頭 API
13+
--------
14+
15+
.. code-block:: python
16+
17+
from je_auto_control import changed_regions, has_motion, activity_score
18+
19+
before = screenshot_to_array()
20+
# ... 經過一段時間 ...
21+
for box in changed_regions(before, after): # 移動的方框,由大到小
22+
print(box["x"], box["y"], box["width"], box["height"])
23+
24+
if has_motion(before, after):
25+
print("still animating, activity =", activity_score(before, after))
26+
27+
``changed_regions`` 對絕對差做門檻(``threshold``)、去噪(``blur``)、膨脹,回傳至少 ``min_area`` 的
28+
``{x, y, width, height, area, center}`` 區塊,由大到小。``has_motion`` 是布林形式;``activity_score`` 是移動像素的
29+
比例(0..1)。不同尺寸的幀會丟出 ``ValueError``。
30+
31+
執行器命令
32+
----------
33+
34+
``AC_changed_regions``(``before`` / ``after`` / ``threshold`` / ``min_area`` / ``blur`` → ``{count, regions}``)與
35+
``AC_has_motion``(``before`` / ``after`` / ``threshold`` / ``min_area`` → ``{moved, activity}``);``after`` 預設為
36+
即時螢幕擷取。它們以 MCP 工具 ``ac_changed_regions`` / ``ac_has_motion`` 以及 Script Builder 中 **Image** 分類下的
37+
命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ AutoControl 所有功能的完整使用指南。
168168
doc/new_features/v143_features_doc
169169
doc/new_features/v144_features_doc
170170
doc/new_features/v145_features_doc
171+
doc/new_features/v146_features_doc
171172
doc/ocr_backends/ocr_backends_doc
172173
doc/observability/observability_doc
173174
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,10 @@
343343
from je_auto_control.utils.img_histogram import (
344344
compare_histograms, histogram_changed, image_histogram,
345345
)
346+
# Localized change / activity detection between two frames (absdiff)
347+
from je_auto_control.utils.motion_regions import (
348+
activity_score, changed_regions, has_motion,
349+
)
346350
# CI workflow annotations (GitHub Actions)
347351
from je_auto_control.utils.ci_annotations import (
348352
emit_annotations, format_annotation,
@@ -1202,6 +1206,9 @@ def start_autocontrol_gui(*args, **kwargs):
12021206
"image_histogram",
12031207
"compare_histograms",
12041208
"histogram_changed",
1209+
"changed_regions",
1210+
"has_motion",
1211+
"activity_score",
12051212
"emit_annotations", "format_annotation",
12061213
"ClipboardHistory", "default_clipboard_history",
12071214
"analyze_heal_log", "heal_stats", "scan_secrets",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,27 @@ def _add_image_specs(specs: List[CommandSpec]) -> None:
532532
),
533533
description="Detect a palette/view change vs a reference (illumination-robust).",
534534
))
535+
specs.append(CommandSpec(
536+
"AC_changed_regions", "Image", "Changed Regions (motion)",
537+
fields=(
538+
FieldSpec("before", FieldType.FILE_PATH),
539+
FieldSpec("after", FieldType.FILE_PATH, optional=True),
540+
FieldSpec("threshold", FieldType.INT, optional=True, default=25),
541+
FieldSpec("min_area", FieldType.INT, optional=True, default=80),
542+
FieldSpec("blur", FieldType.INT, optional=True, default=5),
543+
),
544+
description="Boxes of regions that moved between two frames (after=screen).",
545+
))
546+
specs.append(CommandSpec(
547+
"AC_has_motion", "Image", "Has Motion?",
548+
fields=(
549+
FieldSpec("before", FieldType.FILE_PATH),
550+
FieldSpec("after", FieldType.FILE_PATH, optional=True),
551+
FieldSpec("threshold", FieldType.INT, optional=True, default=25),
552+
FieldSpec("min_area", FieldType.INT, optional=True, default=80),
553+
),
554+
description="Whether anything moved between two frames (+ activity score).",
555+
))
535556

536557

537558
def _add_ocr_specs(specs: List[CommandSpec]) -> None:

je_auto_control/utils/executor/action_executor.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3707,6 +3707,34 @@ def _histogram_changed(reference: str, current: Any = None, method: str =
37073707
"score": compare_histograms(ref_hist, cur_hist, method=str(method))}
37083708

37093709

3710+
def _changed_regions(before: str, after: Any = None, threshold: Any = 25,
3711+
min_area: Any = 80, blur: Any = 5) -> Dict[str, Any]:
3712+
"""Adapter: boxes of regions that moved between two frames (after=screen)."""
3713+
from je_auto_control.utils.motion_regions import changed_regions
3714+
regions = changed_regions(before, _resolve_after(after), threshold=int(threshold),
3715+
min_area=int(min_area), blur=int(blur))
3716+
return {"count": len(regions), "regions": regions}
3717+
3718+
3719+
def _has_motion(before: str, after: Any = None, threshold: Any = 25,
3720+
min_area: Any = 80) -> Dict[str, Any]:
3721+
"""Adapter: whether anything moved between two frames (after=screen)."""
3722+
from je_auto_control.utils.motion_regions import activity_score, has_motion
3723+
resolved = _resolve_after(after)
3724+
return {"moved": has_motion(before, resolved, threshold=int(threshold),
3725+
min_area=int(min_area)),
3726+
"activity": activity_score(before, resolved, threshold=int(threshold))}
3727+
3728+
3729+
def _resolve_after(after: Any):
3730+
"""Return the 'after' frame, grabbing the screen when it is not given."""
3731+
if after is not None:
3732+
return after
3733+
import numpy as np
3734+
from je_auto_control.utils.cv2_utils.screenshot import pil_screenshot
3735+
return np.asarray(pil_screenshot().convert("RGB"))
3736+
3737+
37103738
def _with_modifiers(modifiers: Any, actions: Any) -> Dict[str, Any]:
37113739
"""Adapter: run nested actions while modifier keys are held down."""
37123740
import json
@@ -5452,6 +5480,8 @@ def __init__(self):
54525480
"AC_get_clipboard_html": _get_clipboard_html,
54535481
"AC_image_histogram": _image_histogram,
54545482
"AC_histogram_changed": _histogram_changed,
5483+
"AC_changed_regions": _changed_regions,
5484+
"AC_has_motion": _has_motion,
54555485
"AC_tile_rect": _tile_rect,
54565486
"AC_grid_rects": _grid_rects,
54575487
"AC_cascade_rects": _cascade_rects,

0 commit comments

Comments
 (0)