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) — 感知式(YIQ)图像比对含反锯齿抑制

会忽略反锯齿边缘的视觉回归比对。完整参考:[`docs/source/Zh/doc/new_features/v149_features_doc.rst`](../docs/source/Zh/doc/new_features/v149_features_doc.rst)。

- **`perceptual_diff` / `assert_perceptual`**(`AC_perceptual_diff`):`image_difference` 计算原始逐通道差、`ssim_compare` 是整体分数——两者都未使用感知式度量也不忽略反锯齿(视觉比对误报的首要来源)。本功能在 YIQ 空间比较(pixelmatch 的色彩度量),并预设以形态学开运算移除单像素反锯齿细边差异,只计算实心变化(`include_aa=True` 保留)。返回 `{diff_pixels, diff_ratio, regions}`;`assert_perceptual` / `max_diff_ratio` 把关回归测试。可注入图像配对 → 无头可测(1px 细边 → 0、实心区块 → 计入)。

## 本次更新 (2026-06-23) — 软性断言(汇整所有失败)

验证很多项,一次报告每一个失败。完整参考:[`docs/source/Zh/doc/new_features/v148_features_doc.rst`](../docs/source/Zh/doc/new_features/v148_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) — 感知式(YIQ)影像比對含反鋸齒抑制

會忽略反鋸齒邊緣的視覺回歸比對。完整參考:[`docs/source/Zh/doc/new_features/v149_features_doc.rst`](../docs/source/Zh/doc/new_features/v149_features_doc.rst)。

- **`perceptual_diff` / `assert_perceptual`**(`AC_perceptual_diff`):`image_difference` 計算原始逐通道差、`ssim_compare` 是整體分數——兩者都未使用感知式度量也不忽略反鋸齒(視覺比對誤報的首要來源)。本功能在 YIQ 空間比較(pixelmatch 的色彩度量),並預設以形態學開運算移除單像素反鋸齒細邊差異,只計算實心變化(`include_aa=True` 保留)。回傳 `{diff_pixels, diff_ratio, regions}`;`assert_perceptual` / `max_diff_ratio` 把關回歸測試。可注入影像配對 → 無頭可測(1px 細邊 → 0、實心區塊 → 計入)。

## 本次更新 (2026-06-23) — 軟性斷言(彙整所有失敗)

驗證很多項,一次回報每一個失敗。完整參考:[`docs/source/Zh/doc/new_features/v148_features_doc.rst`](../docs/source/Zh/doc/new_features/v148_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) — Perceptual (YIQ) Image Diff with Anti-Alias Suppression

Visual-regression diffing that ignores anti-aliased edges. Full reference: [`docs/source/Eng/doc/new_features/v149_features_doc.rst`](docs/source/Eng/doc/new_features/v149_features_doc.rst).

- **`perceptual_diff` / `assert_perceptual`** (`AC_perceptual_diff`): `image_difference` counts raw per-channel deltas and `ssim_compare` is a global score — neither uses a perceptual metric or ignores anti-aliasing, the #1 source of false-positive visual-diff failures. This compares in YIQ space (pixelmatch's colour metric) and, by default, removes thin 1px anti-aliased edge diffs via a morphological open so only solid changes count (`include_aa=True` keeps them). Returns `{diff_pixels, diff_ratio, regions}`; `assert_perceptual` / `max_diff_ratio` gate a regression test. Injectable image pair → headless-testable (a 1px fringe → 0, a solid block → counted).

## What's new (2026-06-23) — Soft Assertions (Aggregate Failures)

Verify many things, report every failure at once. Full reference: [`docs/source/Eng/doc/new_features/v148_features_doc.rst`](docs/source/Eng/doc/new_features/v148_features_doc.rst).
Expand Down
42 changes: 42 additions & 0 deletions docs/source/Eng/doc/new_features/v149_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Perceptual (YIQ) Image Diff with Anti-Alias Suppression
=======================================================

``visual_regression.image_difference`` counts raw per-channel max-delta pixels and
``ssim_compare`` gives a global structural score. Neither uses a *perceptual* colour
metric, and neither ignores **anti-aliased edges** — the #1 source of false-positive
visual-diff failures across DPI and font-hinting. ``perceptual_diff`` compares pixels
in YIQ space (the pixelmatch colour metric, far closer to human perception than RGB)
and, by default, removes the thin one-pixel edge differences that anti-aliasing
produces (a morphological open), so only *solid* changed regions count.

Runs on an injectable image pair (ndarray / path / PIL), so it is headless-testable on
synthetic arrays. OpenCV + NumPy come in via ``je_open_cv``; reuses the shared
connected-component helper and RGB loader. Imports no ``PySide6``.

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

.. code-block:: python

from je_auto_control import perceptual_diff, assert_perceptual

result = perceptual_diff("actual.png", "golden.png", threshold=0.1)
print(result.diff_pixels, result.diff_ratio, result.regions)

# Gate a visual-regression test (raises if the ratio is exceeded).
assert_perceptual("actual.png", "golden.png", max_diff_ratio=0.01)

``perceptual_diff`` returns a ``PerceptualDiffResult`` (``diff_pixels``,
``total_pixels``, ``diff_ratio``, and the ``regions`` boxes of the changed clusters).
``threshold`` (0..1) is the pixelmatch sensitivity. ``include_aa=True`` keeps the thin
edge differences instead of suppressing them. ``assert_perceptual`` raises
``AutoControlActionException`` when ``diff_ratio`` exceeds ``max_diff_ratio``. Images of
different sizes raise ``ValueError``.

Executor command
----------------

``AC_perceptual_diff`` (``actual`` / ``expected`` / ``threshold`` / ``include_aa`` /
``max_diff_ratio`` → ``{diff_pixels, total_pixels, diff_ratio, regions}``; raises when
``max_diff_ratio`` is given and exceeded). It is exposed as the MCP tool
``ac_perceptual_diff`` and as a Script Builder command 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 @@ -171,6 +171,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v146_features_doc
doc/new_features/v147_features_doc
doc/new_features/v148_features_doc
doc/new_features/v149_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
35 changes: 35 additions & 0 deletions docs/source/Zh/doc/new_features/v149_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
感知式(YIQ)影像比對含反鋸齒抑制
====================================

``visual_regression.image_difference`` 計算原始逐通道最大差像素數,``ssim_compare`` 給出整體結構分數。兩者都未使用
*感知式*色彩度量,也都不忽略**反鋸齒邊緣**——那是跨 DPI 與字體微調時視覺比對誤報的首要來源。``perceptual_diff``
在 YIQ 空間比較像素(pixelmatch 的色彩度量,比 RGB 更接近人眼感知),並預設移除反鋸齒造成的單像素細邊差異
(形態學開運算),因此只計算*實心*變化區域。

在可注入的影像配對(ndarray / 路徑 / PIL)上執行,因此可對合成陣列做無頭測試。OpenCV + NumPy 透過 ``je_open_cv``
引入;沿用共用的連通元件輔助與 RGB 載入器。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import perceptual_diff, assert_perceptual

result = perceptual_diff("actual.png", "golden.png", threshold=0.1)
print(result.diff_pixels, result.diff_ratio, result.regions)

# 把關視覺回歸測試(超出比例則拋例外)。
assert_perceptual("actual.png", "golden.png", max_diff_ratio=0.01)

``perceptual_diff`` 回傳 ``PerceptualDiffResult``(``diff_pixels``、``total_pixels``、``diff_ratio``,以及變化叢集
的 ``regions`` 方框)。``threshold``(0..1)是 pixelmatch 靈敏度。``include_aa=True`` 會保留細邊差異而不抑制。
``assert_perceptual`` 在 ``diff_ratio`` 超過 ``max_diff_ratio`` 時丟出 ``AutoControlActionException``。不同尺寸的
影像會丟出 ``ValueError``。

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

``AC_perceptual_diff``(``actual`` / ``expected`` / ``threshold`` / ``include_aa`` / ``max_diff_ratio`` →
``{diff_pixels, total_pixels, diff_ratio, regions}``;給定 ``max_diff_ratio`` 且超出時拋例外)。它以 MCP 工具
``ac_perceptual_diff`` 以及 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 @@ -171,6 +171,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v146_features_doc
doc/new_features/v147_features_doc
doc/new_features/v148_features_doc
doc/new_features/v149_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 @@ -353,6 +353,10 @@
)
# Soft assertions (accumulate checks, raise the aggregate at block end)
from je_auto_control.utils.soft_assert import SoftAssertions
# Perceptual (YIQ) image diff with anti-alias edge suppression
from je_auto_control.utils.perceptual_diff import (
PerceptualDiffResult, assert_perceptual, perceptual_diff,
)
# CI workflow annotations (GitHub Actions)
from je_auto_control.utils.ci_annotations import (
emit_annotations, format_annotation,
Expand Down Expand Up @@ -1220,6 +1224,9 @@ def start_autocontrol_gui(*args, **kwargs):
"bring_to_front",
"send_to_back",
"SoftAssertions",
"perceptual_diff",
"assert_perceptual",
"PerceptualDiffResult",
"emit_annotations", "format_annotation",
"ClipboardHistory", "default_clipboard_history",
"analyze_heal_log", "heal_stats", "scan_secrets",
Expand Down
13 changes: 13 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,19 @@ def _add_image_specs(specs: List[CommandSpec]) -> None:
),
description="Whether anything moved between two frames (+ activity score).",
))
specs.append(CommandSpec(
"AC_perceptual_diff", "Image", "Perceptual Diff (YIQ)",
fields=(
FieldSpec("actual", FieldType.FILE_PATH),
FieldSpec("expected", FieldType.FILE_PATH),
FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.1,
min_value=0.0, max_value=1.0),
FieldSpec("include_aa", FieldType.BOOL, optional=True, default=False),
FieldSpec("max_diff_ratio", FieldType.FLOAT, optional=True,
min_value=0.0, max_value=1.0),
),
description="Perceptual image diff that ignores anti-aliased edges.",
))


def _add_ocr_specs(specs: List[CommandSpec]) -> None:
Expand Down
15 changes: 15 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3784,6 +3784,20 @@ def _soft_assert(checks: Any, raise_on_fail: Any = False) -> Dict[str, Any]:
"failures": soft.failures}


def _perceptual_diff(actual: str, expected: str, threshold: Any = 0.1,
include_aa: Any = False,
max_diff_ratio: Any = None) -> Dict[str, Any]:
"""Adapter: perceptual (YIQ) image diff with anti-alias suppression."""
from je_auto_control.utils.perceptual_diff import perceptual_diff
result = perceptual_diff(actual, expected, threshold=float(threshold),
include_aa=bool(include_aa))
if max_diff_ratio is not None and result.diff_ratio > float(max_diff_ratio):
raise AutoControlActionException(
f"perceptual diff {result.diff_ratio} exceeds {max_diff_ratio}")
return {"diff_pixels": result.diff_pixels, "total_pixels": result.total_pixels,
"diff_ratio": result.diff_ratio, "regions": result.regions}


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 @@ -5535,6 +5549,7 @@ def __init__(self):
"AC_bring_to_front": _bring_to_front,
"AC_send_to_back": _send_to_back,
"AC_soft_assert": _soft_assert,
"AC_perceptual_diff": _perceptual_diff,
"AC_tile_rect": _tile_rect,
"AC_grid_rects": _grid_rects,
"AC_cascade_rects": _cascade_rects,
Expand Down
24 changes: 23 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3209,6 +3209,28 @@ def soft_assert_tools() -> List[MCPTool]:
]


def perceptual_diff_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_perceptual_diff",
description=("Perceptual (YIQ) diff of 'actual' vs 'expected' image "
"paths. By default suppresses anti-aliased edge diffs "
"(include_aa=true to keep them). Returns {diff_pixels, "
"total_pixels, diff_ratio, regions}; pass 'max_diff_ratio' "
"to raise when exceeded. 'threshold' 0..1 sensitivity."),
input_schema=schema({
"actual": {"type": "string"},
"expected": {"type": "string"},
"threshold": {"type": "number"},
"include_aa": {"type": "boolean"},
"max_diff_ratio": {"type": "number"}},
required=["actual", "expected"]),
handler=h.perceptual_diff,
annotations=READ_ONLY,
),
]


def ssim_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -6716,7 +6738,7 @@ def media_assert_tools() -> List[MCPTool]:
hsv_segment_tools, text_regions_tools, edge_lines_tools, expect_poll_tools,
locator_chain_tools, rich_clipboard_tools, img_histogram_tools,
motion_regions_tools, window_zorder_tools, soft_assert_tools,
plugin_sdk_tools, governance_tools,
perceptual_diff_tools, plugin_sdk_tools, governance_tools,
credential_lease_tools, egress_tools, approval_testing_tools,
trajectory_eval_tools, compliance_tools, agent_trace_tools,
video_report_tools, fuzzy_tools, artifact_store_tools, image_dedup_tools,
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2288,6 +2288,12 @@ def soft_assert(checks, raise_on_fail=False):
return _soft_assert(checks, raise_on_fail)


def perceptual_diff(actual, expected, threshold=0.1, include_aa=False,
max_diff_ratio=None):
from je_auto_control.utils.executor.action_executor import _perceptual_diff
return _perceptual_diff(actual, expected, threshold, include_aa, max_diff_ratio)


def detect_drift(reference, current, threshold=0.25, bins=10):
from je_auto_control.utils.executor.action_executor import _detect_drift
return _detect_drift(reference, current, threshold, bins)
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/perceptual_diff/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Perceptual (YIQ) image diff with anti-alias edge suppression."""
from je_auto_control.utils.perceptual_diff.perceptual_diff import (
PerceptualDiffResult, assert_perceptual, perceptual_diff,
)

__all__ = ["PerceptualDiffResult", "assert_perceptual", "perceptual_diff"]
94 changes: 94 additions & 0 deletions je_auto_control/utils/perceptual_diff/perceptual_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Perceptual (YIQ) image diff with anti-alias edge suppression.

``visual_regression.image_difference`` counts raw per-channel max-delta pixels and
``ssim`` gives a global structural score. Neither uses a *perceptual* colour metric, and
neither ignores **anti-aliased edges** — the #1 source of false-positive visual-diff
failures across DPI / font-hinting. This compares pixels in YIQ space (the pixelmatch
colour metric, far closer to human perception than RGB) and, by default, suppresses the
thin one-pixel edge differences that anti-aliasing produces via a morphological open, so
only *solid* changed regions count.

Runs on an injectable image pair (ndarray / path / PIL), so it is headless-testable on
synthetic arrays. OpenCV + NumPy come in via ``je_open_cv``; reuses the shared
connected-component helper and the RGB loader. Imports no ``PySide6``.
"""
from dataclasses import dataclass
from typing import Any, Dict, List

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

ImageSource = Any
_MAX_YIQ_DELTA = 35215.0 # pixelmatch: max possible YIQ delta for 255 diff


@dataclass(frozen=True)
class PerceptualDiffResult:
"""The outcome of a perceptual diff: changed-pixel count, ratio and regions."""

diff_pixels: int
total_pixels: int
diff_ratio: float
regions: List[Dict[str, Any]]


def _yiq_delta(first, second):
"""Return the per-pixel squared YIQ colour distance between two RGB float images."""
weights_y = (0.29889531, 0.58662247, 0.11448223)
weights_i = (0.59597799, -0.27417610, -0.32180189)
weights_q = (0.21147017, -0.52261711, 0.31114694)

def channel(image, weights):
return (image[..., 0] * weights[0] + image[..., 1] * weights[1]
+ image[..., 2] * weights[2])

delta_y = channel(first, weights_y) - channel(second, weights_y)
delta_i = channel(first, weights_i) - channel(second, weights_i)
delta_q = channel(first, weights_q) - channel(second, weights_q)
return 0.5053 * delta_y ** 2 + 0.299 * delta_i ** 2 + 0.1957 * delta_q ** 2


def perceptual_diff(actual: ImageSource, expected: ImageSource, *,
threshold: float = 0.1, include_aa: bool = False,
min_area: int = 1) -> PerceptualDiffResult:
"""Compare two images perceptually; return the changed pixels, ratio and regions.

``threshold`` (0..1) is the pixelmatch sensitivity — higher tolerates more colour
difference before a pixel counts as changed. When ``include_aa`` is False (default)
a morphological open removes thin anti-aliased edge differences so only solid
changes remain. Different-sized images raise ``ValueError``.
"""
import cv2
import numpy as np
from je_auto_control.utils.cv2_utils.blobs import connected_boxes
first = _to_rgb(actual).astype(np.float64)
second = _to_rgb(expected).astype(np.float64)
if first.shape != second.shape:
raise ValueError(f"images must be the same size: {first.shape} vs "
f"{second.shape}")
max_delta = _MAX_YIQ_DELTA * float(threshold) * float(threshold)
mask = (_yiq_delta(first, second) > max_delta).astype(np.uint8)
if not include_aa:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
diff_pixels = int(np.count_nonzero(mask))
total = int(mask.size)
regions = connected_boxes(mask * 255, int(min_area))
return PerceptualDiffResult(diff_pixels, total,
round(diff_pixels / total, 6) if total else 0.0,
regions)


def assert_perceptual(actual: ImageSource, expected: ImageSource, *,
threshold: float = 0.1, include_aa: bool = False,
max_diff_ratio: float = 0.0) -> PerceptualDiffResult:
"""Like :func:`perceptual_diff` but raise when the diff ratio exceeds ``max_diff_ratio``."""
from je_auto_control.utils.exception.exceptions import (
AutoControlActionException)
result = perceptual_diff(actual, expected, threshold=threshold,
include_aa=include_aa)
if result.diff_ratio > float(max_diff_ratio):
raise AutoControlActionException(
f"perceptual diff {result.diff_ratio} exceeds {max_diff_ratio} "
f"({result.diff_pixels} pixels changed)")
return result
Loading
Loading