Skip to content

Commit e23ca66

Browse files
committed
Add perceptual-hash image dedupe (Pillow aHash/dHash)
1 parent 3f0fc5f commit e23ca66

15 files changed

Lines changed: 361 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

16+
- [What's new (2026-06-20) — Perceptual-Hash Image Dedupe](#whats-new-2026-06-20--perceptual-hash-image-dedupe)
1617
- [What's new (2026-06-20) — S3-Compatible Artifact Store](#whats-new-2026-06-20--s3-compatible-artifact-store)
1718
- [What's new (2026-06-20) — Fuzzy String Matching & Dedupe](#whats-new-2026-06-20--fuzzy-string-matching--dedupe)
1819
- [What's new (2026-06-19) — Video Step-Overlay Report](#whats-new-2026-06-19--video-step-overlay-report)
@@ -94,6 +95,12 @@
9495

9596
---
9697

98+
## What's new (2026-06-20) — Perceptual-Hash Image Dedupe
99+
100+
Collapse near-identical screenshots. Full reference: [`docs/source/Eng/doc/new_features/v42_features_doc.rst`](docs/source/Eng/doc/new_features/v42_features_doc.rst).
101+
102+
- **`average_hash` / `dhash` / `hamming_distance` / `images_similar` / `dedupe_images`** (`AC_image_hash` / `AC_dedupe_images`, `ac_*`): perceptual hashing maps visually similar images to close fingerprints, so near-duplicate frames in a recording or step report cluster by Hamming distance and collapse to one representative. Uses **Pillow** (already core — no extra dep); the dedupe/compare logic is pure Python with an injectable `hasher`, so clustering is unit-tested without any image and the real Pillow path under `importorskip`.
103+
97104
## What's new (2026-06-20) — S3-Compatible Artifact Store
98105

99106
Push run artifacts to object storage. Full reference: [`docs/source/Eng/doc/new_features/v41_features_doc.rst`](docs/source/Eng/doc/new_features/v41_features_doc.rst).

‎README/README_zh-CN.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

15+
- [本次更新 (2026-06-20) — 感知哈希图像去重](#本次更新-2026-06-20--感知哈希图像去重)
1516
- [本次更新 (2026-06-20) — S3 兼容成品存储](#本次更新-2026-06-20--s3-兼容成品存储)
1617
- [本次更新 (2026-06-20) — 模糊字符串匹配与去重](#本次更新-2026-06-20--模糊字符串匹配与去重)
1718
- [本次更新 (2026-06-19) — 视频步骤叠加报告](#本次更新-2026-06-19--视频步骤叠加报告)
@@ -93,6 +94,12 @@
9394

9495
---
9596

97+
## 本次更新 (2026-06-20) — 感知哈希图像去重
98+
99+
收合近乎相同的屏幕截图。完整参考:[`docs/source/Zh/doc/new_features/v42_features_doc.rst`](../docs/source/Zh/doc/new_features/v42_features_doc.rst)。
100+
101+
- **`average_hash` / `dhash` / `hamming_distance` / `images_similar` / `dedupe_images`**(`AC_image_hash` / `AC_dedupe_images`、`ac_*`):感知哈希将视觉相似的图像映射到接近的指纹,因此录像或步骤报告中的近似重复画面可依汉明距离分群并收合为一个代表。使用 **Pillow**(已是核心 —— 无额外依赖);去重/比较逻辑为纯 Python 且 `hasher` 可注入,因此分群在无任何图像下单元测试,实际 Pillow 路径以 `importorskip` 测试。
102+
96103
## 本次更新 (2026-06-20) — S3 兼容成品存储
97104

98105
将运行成品推送到对象存储。完整参考:[`docs/source/Zh/doc/new_features/v41_features_doc.rst`](../docs/source/Zh/doc/new_features/v41_features_doc.rst)。

‎README/README_zh-TW.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

15+
- [本次更新 (2026-06-20) — 感知雜湊影像去重](#本次更新-2026-06-20--感知雜湊影像去重)
1516
- [本次更新 (2026-06-20) — S3 相容成品儲存](#本次更新-2026-06-20--s3-相容成品儲存)
1617
- [本次更新 (2026-06-20) — 模糊字串比對與去重](#本次更新-2026-06-20--模糊字串比對與去重)
1718
- [本次更新 (2026-06-19) — 影片步驟疊加報告](#本次更新-2026-06-19--影片步驟疊加報告)
@@ -93,6 +94,12 @@
9394

9495
---
9596

97+
## 本次更新 (2026-06-20) — 感知雜湊影像去重
98+
99+
收合近乎相同的螢幕截圖。完整參考:[`docs/source/Zh/doc/new_features/v42_features_doc.rst`](../docs/source/Zh/doc/new_features/v42_features_doc.rst)。
100+
101+
- **`average_hash` / `dhash` / `hamming_distance` / `images_similar` / `dedupe_images`**(`AC_image_hash` / `AC_dedupe_images`、`ac_*`):感知雜湊將視覺相似的影像對應到接近的指紋,因此錄影或步驟報告中的近似重複畫面可依漢明距離分群並收合為一個代表。使用 **Pillow**(已是核心 —— 無額外相依);去重/比較邏輯為純 Python 且 `hasher` 可注入,因此分群在無任何影像下單元測試,實際 Pillow 路徑以 `importorskip` 測試。
102+
96103
## 本次更新 (2026-06-20) — S3 相容成品儲存
97104

98105
將執行成品推送到物件儲存。完整參考:[`docs/source/Zh/doc/new_features/v41_features_doc.rst`](../docs/source/Zh/doc/new_features/v41_features_doc.rst)。
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Perceptual-Hash Image Dedupe
2+
============================
3+
4+
A screen recording or a step report often contains many nearly identical frames.
5+
Perceptual hashes (average-hash and difference-hash) map visually similar images
6+
to numerically close fingerprints, so frames can be clustered by Hamming distance
7+
and collapsed — keeping one representative per distinct view.
8+
9+
The hashing functions use **Pillow** (already a core dependency — no extra
10+
package required); the dedupe/compare logic is pure Python and the ``hasher`` is
11+
injectable, so clustering is unit-testable without any image. Imports no
12+
``PySide6``.
13+
14+
Headless API
15+
------------
16+
17+
.. code-block:: python
18+
19+
from je_auto_control import (
20+
average_hash, dhash, hamming_distance, images_similar, dedupe_images)
21+
22+
h1 = average_hash("frame1.png") # hex fingerprint
23+
h2 = average_hash("frame2.png")
24+
hamming_distance(h1, h2) # bits that differ
25+
images_similar(h1, h2, max_distance=5) # within tolerance?
26+
27+
dedupe_images(["a.png", "b.png", "c.png"], max_distance=5)
28+
# -> keeps one image per near-duplicate cluster (first wins)
29+
30+
``average_hash`` compares each pixel to the mean brightness; ``dhash`` compares
31+
each pixel to its right neighbour (more robust to gamma shifts). ``dedupe_images``
32+
accepts a ``hasher`` hook (defaulting to ``average_hash``) so the clustering can
33+
be tested with precomputed hashes.
34+
35+
Executor commands
36+
-----------------
37+
38+
================================ ===================================================
39+
Command Effect
40+
================================ ===================================================
41+
``AC_image_hash`` ``{hash}`` of an image (``algo``: average/dhash).
42+
``AC_dedupe_images`` ``{unique}`` with near-duplicate images collapsed.
43+
================================ ===================================================
44+
45+
``paths`` accepts a list or a JSON-string list (so the visual builder works). The
46+
same operations are exposed as MCP tools (``ac_image_hash`` / ``ac_dedupe_images``)
47+
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
@@ -64,6 +64,7 @@ Comprehensive guides for all AutoControl features.
6464
doc/new_features/v39_features_doc
6565
doc/new_features/v40_features_doc
6666
doc/new_features/v41_features_doc
67+
doc/new_features/v42_features_doc
6768
doc/ocr_backends/ocr_backends_doc
6869
doc/observability/observability_doc
6970
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+
感知雜湊影像去重
2+
================
3+
4+
螢幕錄影或步驟報告常含有許多近乎相同的畫面。感知雜湊(average-hash 與 difference-hash)
5+
將視覺上相似的影像對應到數值接近的指紋,因此可依漢明距離分群並收合 —— 每個明顯不同的
6+
畫面只保留一個代表。
7+
8+
雜湊函式使用 **Pillow**(已是核心相依 —— 無需額外套件);去重/比較邏輯為純 Python,且
9+
``hasher`` 可注入,因此分群可在無任何影像下單元測試。不匯入 ``PySide6``。
10+
11+
無頭 API
12+
--------
13+
14+
.. code-block:: python
15+
16+
from je_auto_control import (
17+
average_hash, dhash, hamming_distance, images_similar, dedupe_images)
18+
19+
h1 = average_hash("frame1.png") # 十六進位指紋
20+
h2 = average_hash("frame2.png")
21+
hamming_distance(h1, h2) # 相異的位元數
22+
images_similar(h1, h2, max_distance=5) # 是否在容差內?
23+
24+
dedupe_images(["a.png", "b.png", "c.png"], max_distance=5)
25+
# -> 每個近似重複叢集保留一張(保留第一個)
26+
27+
``average_hash`` 將每個像素與平均亮度比較;``dhash`` 將每個像素與其右鄰比較(對 gamma
28+
偏移更穩健)。``dedupe_images`` 接受 ``hasher`` 掛鉤(預設為 ``average_hash``),因此可
29+
用預先計算的雜湊測試分群。
30+
31+
執行器指令
32+
----------
33+
34+
================================ ===================================================
35+
指令 效果
36+
================================ ===================================================
37+
``AC_image_hash`` 影像的 ``{hash}``(``algo``:average/dhash)。
38+
``AC_dedupe_images`` 收合近似重複影像後的 ``{unique}``。
39+
================================ ===================================================
40+
41+
``paths`` 接受清單或 JSON 字串清單(因此視覺化建構器可用)。相同操作亦提供為 MCP 工具
42+
(``ac_image_hash`` / ``ac_dedupe_images``),以及 Script Builder 中 **Image** 分類下的
43+
指令。

‎docs/source/Zh/zh_index.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ AutoControl 所有功能的完整使用指南。
6464
doc/new_features/v39_features_doc
6565
doc/new_features/v40_features_doc
6666
doc/new_features/v41_features_doc
67+
doc/new_features/v42_features_doc
6768
doc/ocr_backends/ocr_backends_doc
6869
doc/observability/observability_doc
6970
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
@@ -239,6 +239,10 @@
239239
S3ArtifactStore, configure_default_store, get_default_store,
240240
set_default_store,
241241
)
242+
# Perceptual-hash image dedupe (Pillow aHash/dHash)
243+
from je_auto_control.utils.image_dedup import (
244+
average_hash, dedupe_images, dhash, hamming_distance, images_similar,
245+
)
242246
# Background popup/interrupt watchdog (unattended automation)
243247
from je_auto_control.utils.watchdog import (
244248
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -688,6 +692,8 @@ def start_autocontrol_gui(*args, **kwargs):
688692
"fuzzy_best_match", "fuzzy_dedupe", "fuzzy_matches", "fuzzy_ratio",
689693
"S3ArtifactStore", "configure_default_store", "get_default_store",
690694
"set_default_store",
695+
"average_hash", "dedupe_images", "dhash", "hamming_distance",
696+
"images_similar",
691697
# MCP server
692698
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
693699
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

‎je_auto_control/gui/script_builder/command_schema.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,24 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
928928
fields=(FieldSpec("key", FieldType.STRING),),
929929
description="Delete an object from the default S3 store.",
930930
))
931+
specs.append(CommandSpec(
932+
"AC_image_hash", "Image", "Perceptual Hash",
933+
fields=(
934+
FieldSpec("path", FieldType.FILE_PATH),
935+
FieldSpec("algo", FieldType.ENUM, optional=True, default="average",
936+
choices=("average", "dhash")),
937+
),
938+
description="Perceptual hash of an image (average or dhash).",
939+
))
940+
specs.append(CommandSpec(
941+
"AC_dedupe_images", "Image", "Dedupe Near-Identical Images",
942+
fields=(
943+
FieldSpec("paths", FieldType.STRING,
944+
placeholder='["a.png", "b.png"]'),
945+
FieldSpec("max_distance", FieldType.INT, optional=True, default=5),
946+
),
947+
description="Collapse near-duplicate images by perceptual hash.",
948+
))
931949
specs.append(CommandSpec(
932950
"AC_generate_sop", "Report", "Generate SOP Document",
933951
fields=(

‎je_auto_control/utils/executor/action_executor.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3122,6 +3122,20 @@ def _s3_delete(key: str) -> Dict[str, Any]:
31223122
return {"deleted": get_default_store().delete(key)}
31233123

31243124

3125+
def _image_hash(path: str, algo: str = "average") -> Dict[str, Any]:
3126+
"""Adapter: perceptual hash of an image (average or dhash)."""
3127+
from je_auto_control.utils.image_dedup import average_hash, dhash
3128+
hasher = dhash if algo == "dhash" else average_hash
3129+
return {"hash": hasher(path)}
3130+
3131+
3132+
def _dedupe_images(paths: Any, max_distance: int = 5) -> Dict[str, Any]:
3133+
"""Adapter: drop near-duplicate images, keeping the first of each cluster."""
3134+
from je_auto_control.utils.image_dedup import dedupe_images
3135+
return {"unique": dedupe_images(_coerce_list(paths),
3136+
max_distance=max_distance)}
3137+
3138+
31253139
class Executor:
31263140
"""
31273141
Executor
@@ -3381,6 +3395,8 @@ def __init__(self):
33813395
"AC_s3_download": _s3_download,
33823396
"AC_s3_list": _s3_list,
33833397
"AC_s3_delete": _s3_delete,
3398+
"AC_image_hash": _image_hash,
3399+
"AC_dedupe_images": _dedupe_images,
33843400
"AC_a11y_record_start": _a11y_record_start,
33853401
"AC_a11y_record_stop": _a11y_record_stop,
33863402
"AC_a11y_record_events": _a11y_record_events,

0 commit comments

Comments
 (0)