Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README/WHATS_NEW_zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 本次更新 — AutoControl

## 本次更新 (2026-06-23) — 色彩直方图指纹与变化检测

判断画面在光照 / 缩放下是否仍是「同一个」。完整参考:[`docs/source/Zh/doc/new_features/v145_features_doc.rst`](../docs/source/Zh/doc/new_features/v145_features_doc.rst)。

- **`image_histogram` / `compare_histograms` / `histogram_changed`**(`AC_image_histogram`、`AC_histogram_changed`):`image_dedup` 的感知哈希是空间性的(对颜色/主题脆弱)、`color_stats` 只有单一颜色。归一化色彩直方图是耐光照/缩放的「同一画面、还是调色板变了?」信号(主题切换、重载、旋转横幅)。`image_histogram` 返回逐通道直方图(`hsv`/`rgb`/`gray`);`compare_histograms` 提供 correlation/chisqr/intersection/bhattacharyya;`histogram_changed` 比较参考与实际屏幕。可注入图像 → 无头可测;OpenCV 核心(`cv2.calcHist`/`compareHist`)。

## 本次更新 (2026-06-23) — 丰富剪贴板(HTML / CF_HTML)

把*格式化*的 HTML 复制粘贴到 Word / Outlook。完整参考:[`docs/source/Zh/doc/new_features/v144_features_doc.rst`](../docs/source/Zh/doc/new_features/v144_features_doc.rst)。
Expand Down
6 changes: 6 additions & 0 deletions README/WHATS_NEW_zh-TW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 本次更新 — AutoControl

## 本次更新 (2026-06-23) — 色彩直方圖指紋與變化偵測

判斷畫面在光照 / 縮放下是否仍是「同一個」。完整參考:[`docs/source/Zh/doc/new_features/v145_features_doc.rst`](../docs/source/Zh/doc/new_features/v145_features_doc.rst)。

- **`image_histogram` / `compare_histograms` / `histogram_changed`**(`AC_image_histogram`、`AC_histogram_changed`):`image_dedup` 的感知雜湊是空間性的(對顏色/主題脆弱)、`color_stats` 只有單一顏色。正規化色彩直方圖是耐光照/縮放的「同一畫面、還是調色盤變了?」訊號(主題切換、重載、旋轉橫幅)。`image_histogram` 回傳逐通道直方圖(`hsv`/`rgb`/`gray`);`compare_histograms` 提供 correlation/chisqr/intersection/bhattacharyya;`histogram_changed` 比較參考與實際螢幕。可注入影像 → 無頭可測;OpenCV 核心(`cv2.calcHist`/`compareHist`)。

## 本次更新 (2026-06-23) — 豐富剪貼簿(HTML / CF_HTML)

把*格式化*的 HTML 複製貼上到 Word / Outlook。完整參考:[`docs/source/Zh/doc/new_features/v144_features_doc.rst`](../docs/source/Zh/doc/new_features/v144_features_doc.rst)。
Expand Down
6 changes: 6 additions & 0 deletions WHATS_NEW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# What's New — AutoControl

## What's new (2026-06-23) — Colour-Histogram Fingerprint & Change Detection

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).

- **`image_histogram` / `compare_histograms` / `histogram_changed`** (`AC_image_histogram`, `AC_histogram_changed`): `image_dedup`'s perceptual hash is spatial (brittle to colour/theme) and `color_stats` is one colour. A normalized colour histogram is the illumination/scale-robust "same view, or palette shifted?" signal (theme switch, reload, rotated banner). `image_histogram` returns a per-channel histogram (`hsv`/`rgb`/`gray`); `compare_histograms` does correlation/chisqr/intersection/bhattacharyya; `histogram_changed` compares a reference vs the live screen. Injectable image → headless-testable; base OpenCV (`cv2.calcHist`/`compareHist`).

## What's new (2026-06-23) — Rich Clipboard (HTML / CF_HTML)

Copy and paste *formatted* HTML into Word / Outlook. Full reference: [`docs/source/Eng/doc/new_features/v144_features_doc.rst`](docs/source/Eng/doc/new_features/v144_features_doc.rst).
Expand Down
44 changes: 44 additions & 0 deletions docs/source/Eng/doc/new_features/v145_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Colour-Histogram Fingerprint & Change Detection
===============================================

``image_dedup`` fingerprints with a perceptual aHash / dHash — a *spatial* 64-bit
hash that is brittle to colour and theme shifts — and ``color_stats`` reports a single
average / dominant colour. A normalized colour *histogram* is the standard
illumination- and scale-robust signal for "is this the same view, or has the palette
shifted?": a theme switch, a content reload, a rotated banner — which neither hashing
nor one dominant colour captures.

Every function runs on an injectable image (ndarray / path / PIL, RGB), so it is
headless-testable on synthetic arrays. ``cv2.calcHist`` / ``cv2.compareHist`` are base
OpenCV; OpenCV + NumPy come in via ``je_open_cv``. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import (image_histogram, compare_histograms,
histogram_changed)

baseline = image_histogram("golden.png") # 3 * bins floats (HSV)
if histogram_changed("golden.png"): # current = live screen
print("the view changed")

score = compare_histograms(baseline, image_histogram()) # 1.0 == identical

``image_histogram`` returns a per-channel normalized histogram as a flat list
(``space`` = ``hsv`` / ``rgb`` / ``gray``; each channel adds ``bins`` values).
``compare_histograms`` supports ``correlation`` / ``chisqr`` / ``intersection`` /
``bhattacharyya`` (for correlation / intersection higher is more similar; for the
distance methods higher is more different). ``histogram_changed`` compares a
``reference`` against ``current`` (default: the screen) and returns a bool, flipping
the threshold comparison automatically for similarity vs distance methods.

Executor commands
-----------------

``AC_image_histogram`` (``source`` / ``bins`` / ``space`` / ``region`` →
``{bins, space, histogram}``) and ``AC_histogram_changed`` (``reference`` /
``current`` / ``method`` / ``threshold`` / ``space`` / ``region`` →
``{changed, score}``). They are exposed as the MCP tools ``ac_image_histogram`` /
``ac_histogram_changed`` and as Script Builder commands under **Image**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v142_features_doc
doc/new_features/v143_features_doc
doc/new_features/v144_features_doc
doc/new_features/v145_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
36 changes: 36 additions & 0 deletions docs/source/Zh/doc/new_features/v145_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
色彩直方圖指紋與變化偵測
========================

``image_dedup`` 以感知 aHash / dHash 做指紋——那是*空間性*的 64 位元雜湊,對顏色與主題變化很脆弱——而
``color_stats`` 只回傳單一平均 / 主要顏色。正規化的色彩*直方圖*是判斷「這是不是同一個畫面、調色盤是否改變」的標準
耐光照、耐縮放訊號:主題切換、內容重載、旋轉的橫幅——這些雜湊與單一主色都捕捉不到。

每個函式都在可注入的影像(ndarray / 路徑 / PIL,RGB)上執行,因此可對合成陣列做無頭測試。``cv2.calcHist`` /
``cv2.compareHist`` 屬於 OpenCV 核心;OpenCV + NumPy 透過 ``je_open_cv`` 引入。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import (image_histogram, compare_histograms,
histogram_changed)

baseline = image_histogram("golden.png") # 3 * bins 個浮點數(HSV)
if histogram_changed("golden.png"): # current = 實際螢幕
print("the view changed")

score = compare_histograms(baseline, image_histogram()) # 1.0 == 完全相同

``image_histogram`` 回傳逐通道正規化直方圖的平面清單(``space`` = ``hsv`` / ``rgb`` / ``gray``;每通道貢獻
``bins`` 個值)。``compare_histograms`` 支援 ``correlation`` / ``chisqr`` / ``intersection`` /
``bhattacharyya``(correlation / intersection 越高越相似;距離方法越高越不同)。``histogram_changed`` 比較
``reference`` 與 ``current``(預設為螢幕)並回傳布林值,會依相似 vs 距離方法自動翻轉門檻比較方向。

執行器命令
----------

``AC_image_histogram``(``source`` / ``bins`` / ``space`` / ``region`` → ``{bins, space, histogram}``)與
``AC_histogram_changed``(``reference`` / ``current`` / ``method`` / ``threshold`` / ``space`` / ``region`` →
``{changed, score}``)。它們以 MCP 工具 ``ac_image_histogram`` / ``ac_histogram_changed`` 以及 Script Builder 中
**Image** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v142_features_doc
doc/new_features/v143_features_doc
doc/new_features/v144_features_doc
doc/new_features/v145_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
7 changes: 7 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@
from je_auto_control.utils.rich_clipboard import (
build_cf_html, get_clipboard_html, parse_cf_html, set_clipboard_html,
)
# Colour-histogram fingerprint & change detection (illumination-robust)
from je_auto_control.utils.img_histogram import (
compare_histograms, histogram_changed, image_histogram,
)
# CI workflow annotations (GitHub Actions)
from je_auto_control.utils.ci_annotations import (
emit_annotations, format_annotation,
Expand Down Expand Up @@ -1195,6 +1199,9 @@ def start_autocontrol_gui(*args, **kwargs):
"parse_cf_html",
"get_clipboard_html",
"set_clipboard_html",
"image_histogram",
"compare_histograms",
"histogram_changed",
"emit_annotations", "format_annotation",
"ClipboardHistory", "default_clipboard_history",
"analyze_heal_log", "heal_stats", "scan_secrets",
Expand Down
30 changes: 30 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,36 @@ def _add_image_specs(specs: List[CommandSpec]) -> None:
),
description="Refine candidate boxes (within / filter / reading / nth / …).",
))
specs.append(CommandSpec(
"AC_image_histogram", "Image", "Image Histogram",
fields=(
FieldSpec("source", FieldType.FILE_PATH, optional=True),
FieldSpec("bins", FieldType.INT, optional=True, default=32),
FieldSpec("space", FieldType.ENUM, optional=True, default="hsv",
choices=("hsv", "rgb", "gray")),
FieldSpec("region", FieldType.STRING, optional=True,
placeholder=_REGION_PLACEHOLDER),
),
description="Colour-histogram fingerprint of an image / the screen.",
))
specs.append(CommandSpec(
"AC_histogram_changed", "Image", "Histogram Changed?",
fields=(
FieldSpec("reference", FieldType.FILE_PATH),
FieldSpec("current", FieldType.FILE_PATH, optional=True),
FieldSpec("method", FieldType.ENUM, optional=True,
default="correlation",
choices=("correlation", "chisqr", "intersection",
"bhattacharyya")),
FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.9,
min_value=0.0, max_value=1.0),
FieldSpec("space", FieldType.ENUM, optional=True, default="hsv",
choices=("hsv", "rgb", "gray")),
FieldSpec("region", FieldType.STRING, optional=True,
placeholder=_REGION_PLACEHOLDER),
),
description="Detect a palette/view change vs a reference (illumination-robust).",
))


def _add_ocr_specs(specs: List[CommandSpec]) -> None:
Expand Down
33 changes: 33 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3676,6 +3676,37 @@ def _get_clipboard_html() -> Dict[str, Any]:
return {"found": html is not None, "html": html}


def _image_histogram(source: Any = None, bins: Any = 32, space: str = "hsv",
region: Any = None) -> Dict[str, Any]:
"""Adapter: per-channel colour histogram of an image / the screen."""
import json
from je_auto_control.utils.img_histogram import image_histogram
if isinstance(region, str):
region = json.loads(region) if region.strip() else None
hist = image_histogram(source, region=region, bins=int(bins), space=str(space))
return {"bins": int(bins), "space": str(space), "histogram": hist}


def _histogram_changed(reference: str, current: Any = None, method: str =
"correlation", threshold: Any = 0.9, space: str = "hsv",
region: Any = None) -> Dict[str, Any]:
"""Adapter: whether the screen / current image differs from a reference."""
import json
from je_auto_control.utils.img_histogram import (compare_histograms,
histogram_changed,
image_histogram)
if isinstance(region, str):
region = json.loads(region) if region.strip() else None
changed = histogram_changed(reference, current, region=region,
method=str(method), threshold=float(threshold),
space=str(space))
ref_hist = image_histogram(reference, space=str(space))
cur_hist = (image_histogram(current, space=str(space)) if current is not None
else image_histogram(region=region, space=str(space)))
return {"changed": changed,
"score": compare_histograms(ref_hist, cur_hist, method=str(method))}


def _with_modifiers(modifiers: Any, actions: Any) -> Dict[str, Any]:
"""Adapter: run nested actions while modifier keys are held down."""
import json
Expand Down Expand Up @@ -5419,6 +5450,8 @@ def __init__(self):
"AC_locate_chain": _locate_chain,
"AC_set_clipboard_html": _set_clipboard_html,
"AC_get_clipboard_html": _get_clipboard_html,
"AC_image_histogram": _image_histogram,
"AC_histogram_changed": _histogram_changed,
"AC_tile_rect": _tile_rect,
"AC_grid_rects": _grid_rects,
"AC_cascade_rects": _cascade_rects,
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/img_histogram/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Colour-histogram fingerprint & change detection (illumination-robust)."""
from je_auto_control.utils.img_histogram.img_histogram import (
compare_histograms, histogram_changed, image_histogram,
)

__all__ = ["compare_histograms", "histogram_changed", "image_histogram"]
93 changes: 93 additions & 0 deletions je_auto_control/utils/img_histogram/img_histogram.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Colour-histogram fingerprint & change detection — "is this the same view?".

``image_dedup`` fingerprints with a perceptual aHash/dHash (a *spatial* 64-bit hash,
brittle to colour / theme shifts) and ``color_stats`` reports a single average /
dominant colour. A normalized colour *histogram* is the standard illumination- and
scale-robust signal for "is this the same view, or has the palette shifted" — a theme
switch, a content reload, a rotated ad — which neither hashing nor one dominant colour
captures.

Every function runs on an injectable image (ndarray / path / PIL, RGB) so it is
headless-testable on synthetic arrays. ``cv2.calcHist`` / ``cv2.compareHist`` are base
OpenCV; OpenCV + NumPy come in via ``je_open_cv``. Imports no ``PySide6``.
"""
from typing import Any, List, Optional, Sequence

# Reuse the RGB loader / screen grab (single source of truth, no copy).
from je_auto_control.utils.color_region.color_region import _grab_rgb, _to_rgb

ImageSource = Any
_SIMILARITY_METHODS = ("correlation", "intersection")


def _convert(rgb, space: str):
import cv2
if space == "hsv":
return (cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV),
[[0, 180], [0, 256], [0, 256]])
if space == "rgb":
return rgb, [[0, 256], [0, 256], [0, 256]]
if space == "gray":
return cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY), [[0, 256]]
raise ValueError(f"unknown space: {space!r}")


def image_histogram(haystack: Optional[ImageSource] = None, *,
region: Optional[Sequence[int]] = None, bins: int = 32,
space: str = "hsv") -> List[float]:
"""Return a per-channel normalized colour histogram as a flat list of floats.

``space`` is ``hsv`` / ``rgb`` / ``gray``; each channel contributes ``bins``
values (so HSV/RGB give ``3 * bins``). ``haystack`` defaults to a screen grab of
the optional ``region``.
"""
import cv2
rgb = _to_rgb(haystack) if haystack is not None else _grab_rgb(region)
image, ranges = _convert(rgb, space)
channels = 1 if image.ndim == 2 else image.shape[2]
out: List[float] = []
for channel in range(channels):
hist = cv2.calcHist([image], [channel], None, [int(bins)], ranges[channel])
cv2.normalize(hist, hist, 0.0, 1.0, cv2.NORM_MINMAX)
out.extend(float(value) for value in hist.flatten())
return out


def compare_histograms(hist_a: Sequence[float], hist_b: Sequence[float], *,
method: str = "correlation") -> float:
"""Compare two histograms. ``method``: correlation / chisqr / intersection / bhattacharyya.

For correlation / intersection higher means more similar; for chisqr /
bhattacharyya higher means more different.
"""
import cv2
import numpy as np
methods = {"correlation": cv2.HISTCMP_CORREL,
"chisqr": cv2.HISTCMP_CHISQR,
"intersection": cv2.HISTCMP_INTERSECT,
"bhattacharyya": cv2.HISTCMP_BHATTACHARYYA}
if method not in methods:
raise ValueError(f"unknown method: {method!r}")
array_a = np.asarray(hist_a, dtype=np.float32)
array_b = np.asarray(hist_b, dtype=np.float32)
return round(float(cv2.compareHist(array_a, array_b, methods[method])), 4)


def histogram_changed(reference: ImageSource,
current: Optional[ImageSource] = None, *,
region: Optional[Sequence[int]] = None,
method: str = "correlation", threshold: float = 0.9,
space: str = "hsv") -> bool:
"""Return whether ``current`` (default: screen) differs from ``reference``.

Compares their histograms with ``method``; for similarity methods (correlation /
intersection) it is "changed" when the score drops below ``threshold``, for
distance methods (chisqr / bhattacharyya) when it rises above ``threshold``.
"""
reference_hist = image_histogram(reference, space=space)
current_hist = (image_histogram(current, space=space) if current is not None
else image_histogram(region=region, space=space))
score = compare_histograms(reference_hist, current_hist, method=method)
if method in _SIMILARITY_METHODS:
return score < float(threshold)
return score > float(threshold)
Loading
Loading