AI 生成 UI 画像 (banner / GPT-Image / 概念図) と実装した HTML/React コンポーネントの screenshot を比較するための CLI ツール。LLM judge への evidence として使う。
6 つのアルゴリズムで 2 枚の UI 画像を比較し、それぞれ「何が違うか」を異なる角度から可視化します。
| ツール | 何を見る | 出力 | 使いどころ |
|---|---|---|---|
pixel-diff |
RGB ピクセル絶対差 | 赤ハイライト diff PNG + ratio | HTML→HTML regression。AI 画像比較ではほぼ常に大差で参考程度 |
ssim |
局所構造類似度 (Canny 前処理 + SSIM) | ヒートマップ PNG + global SSIM | 色味やテクスチャを無視して構造だけ比較 |
canny |
エッジ抽出 + IoU | 個別エッジ PNG + 赤青 overlay PNG + IoU 数値 | 「輪郭の重なり具合」で骨格一致度を見る |
clip |
CLIP embedding コサイン類似度 | スカラー値 (0〜1) | 「同じ意図のデザインか」のグローバル一致 |
grid |
8×8 グリッド SSIM | グリッドオーバーレイ PNG + per-cell JSON | 「どのマスがズレてるか」位置情報付き |
region-mapping |
diff cluster + DOM bbox の overlap | クラスタ rect + 対応 data-visual-id + computed style |
LLM judge / repair agent への決定打 |
compare サブコマンドで 6 ツールを順次実行して outputs/report.md に全結果を集約します。
HTML の主要要素に data-visual-id="..." を付け、capture サブコマンドが screenshot と同時に 全 [data-visual-id] の bbox + computed style を inputs/dom-bboxes.json に dump。region-mapping ツールは以下を実行:
- AI 画像 vs HTML render の per-pixel diff intensity (Gaussian blur で隣接 pixel をマージ)
- 閾値 + morphological close → connected components で diff cluster の矩形 を抽出
- 各クラスタを全 DOM bbox と overlap (IoU + coverage) →
coverage_cluster × specificity^0.4で ranking - 出力:
outputs/region-mapping.png(赤=cluster, 緑=最マッチ DOM, 青=他 DOM, 緑線=対応) +outputs/region-mapping.json(各クラスタの top-3 matches w/ computed style)
これにより LLM judge / repair agent は 「diff cluster #3 → [data-visual-id='metric-visitors']、現状 font-size: 22px」 のような具体修正指示を生成できます。React でも JSX に data-visual-id を付ければ同じ仕組みで動きます。
uv / uvx をインストール済みであれば追加セットアップは不要です。
Playwright の Chromium は system-wide cache (macOS: ~/Library/Caches/ms-playwright、Linux: ~/.cache/ms-playwright) に入るため、1 マシンに 1 回だけ ダウンロードすれば venv が違っても共有されます:
# 初回のみ: chromium DL (system cache)
uvx --from /Users/masao/project/ui-visual-diff playwright install chromium
# 以降は普通に呼ぶだけ
uvx --from /Users/masao/project/ui-visual-diff ui-visual-diff capture ...(任意) editable install で開発する場合:
cd /Users/masao/project/ui-visual-diff
uv venv
source .venv/bin/activate
uv pip install -e .
# chromium がまだなら同じく 1 回だけ:
playwright install chromiumuvx --from /Users/masao/project/ui-visual-diff ui-visual-diff --versionuvx --from /Users/masao/project/ui-visual-diff ui-visual-diff capture \
examples/target.html \
--out-png inputs/html-render.png \
--out-bboxes inputs/dom-bboxes.json \
--viewport 1672x941uvx --from /Users/masao/project/ui-visual-diff ui-visual-diff compare \
inputs/ai-image.png inputs/html-render.png \
--bboxes inputs/dom-bboxes.json \
--out outputs
open outputs/report.mdskill / CI から呼ぶときは --json を付けると stdout に整形済み JSON dict (results + errors サマリ) を吐きます。進捗ログは stderr に回るので stdout 側はそのまま jq 等でパース可能。--strict を付けると 1 つでも失敗したときに exit 1。
ui-visual-diff compare ai.png html.png --bboxes bb.json --out /tmp/run --json
# stdout: { "ai_image": "/abs/...", "results": {...}, "errors": {...} }エラー時のスタックトレースを見たいときは -v (or --verbose) を付けるか UVD_DEBUG=1 を立ててください。デフォルトでは error: <type>: <message> の 1 行のみ stderr に出ます。
uvx --from /Users/masao/project/ui-visual-diff ui-visual-diff tool clip \
inputs/ai-image.png inputs/html-render.png --out outputs
uvx --from /Users/masao/project/ui-visual-diff ui-visual-diff tool ssim \
inputs/ai-image.png inputs/html-render.png --out outputs
uvx --from /Users/masao/project/ui-visual-diff ui-visual-diff tool grid \
inputs/ai-image.png inputs/html-render.png --out outputs
uvx --from /Users/masao/project/ui-visual-diff ui-visual-diff tool region-mapping \
inputs/ai-image.png inputs/html-render.png \
--bboxes inputs/dom-bboxes.json --out outputsツール名: pixel-diff / ssim / canny / clip / grid / region-mapping
このパッケージのデフォルト出力パス (outputs/、inputs/html-render.png、inputs/dom-bboxes.json) は cwd 相対 です。複数の caller (Claude Code skill の fork、CI ジョブ、並列ループ等) が同じ cwd を共有すると、PNG / JSON が互いに上書きされて結果が壊れます。
並列で呼ぶ caller は必ず明示的な --out (および capture の --out-png / --out-bboxes) を渡してください。 各 fork ごとに固有のディレクトリ (例: /tmp/ui-diff-<run-id>/) を渡すのが安全です。compare の exit code は 0 = 全成功、2 = 一部成功、1 = 全失敗 で返るので、skill 側はこれを見て継続判断できます (--strict 指定時はどれか 1 つでも失敗したら 1)。
各 pixel の RGB 絶対差が閾値超なら赤くマーク。1px ズレで全境界が真っ赤になるので AI 画像比較では参考程度。HTML→HTML regression test ではデファクト。
11×11 などの小窓を画像上でスライドさせ、各窓で「平均輝度・分散・共分散」を比較。色そのものではなく 明るさ分布のパターン を見るので人間の目に近い。本ツールでは前処理に Canny を入れて 構造のズレだけ 拾う。
明るさが急変する線 (= 輪郭) だけ抽出した白黒線画。テクスチャやグラデを捨てて骨格だけ残すため SSIM や IoU の前処理として有用。
学習済みマルチモーダルモデルに画像を食わせて 512 次元のベクトルを取る。意味的特徴 で比較するので「同じデザイン意図か」のグローバル一致度になる。位置情報は失う。絶対値はノイジーなので「iteration 前 vs 後」の 相対指標 として使うのが堅い。
画像を 8×8 マスに分割して各マスで SSIM。「右上のマスだけスコアが低い」のような 位置付きの差分 が出るので LLM judge が "ヘッダー右側がズレ" と語りやすくなる。
上記 (1) と同じ diff intensity を Gaussian blur → 閾値 → connected components で「ズレている矩形クラスタ」を取り出し、data-visual-id 付き DOM bbox との overlap で「どの React/HTML 要素が原因か」を ranking。LLM judge への evidence を最も具体的に出せる。
[AI 画像] ─┐
├─→ ① CLIP 類似度 → グローバル一致度 (1 数値)
[HTML SS] ─┤
├─→ ② Canny → SSIM → 構造のみのヒートマップ
│
├─→ ⑤ 8×8 グリッド SSIM → 位置付き差分
│
└─→ ⑥ region-mapping → DOM 要素レベルの修正指示
↓
①②⑤⑥ + 元 2 画像 → LLM judge → "ヘッダー右ロゴ位置がズレ" 等
絶対閾値での合否判定ではなく、iteration ごとの相対改善 を測る指標として使う運用を推奨。
このCLIは masao-origin/.claude/skills/run-ui-visual-iter/ から呼ばれる前提で設計されています。skill ペアは:
| skill | 役割 |
|---|---|
run-ui-visual-iter |
orchestrator (context: fork)。Phase 1-4 で CLI を順次呼び、評価器 → generator → 再評価のループを回す |
assign-ui-visual-evaluator |
internal evaluator (ref なし、metrics + 画像 vision で 5 観点採点)。eval-schema.json (kind: metric) 並置 |
5 採点軸:
clip_cosine(CLIP コサイン類似度) — 意味的類似 25%grid_ssim_mean(8×8 SSIM 平均) — 構造類似 20%region_match_specificity(cluster の leaf 一致率) — 修正可能性 15%text_fidelity(LLM vision) — テキスト一致 20%layout_fidelity(LLM vision) — レイアウト一致 20%
Skill 経由起動例:
Skill({skill: "run-ui-visual-iter", args: "<ai_image> <html_target> --max 5 --threshold-clip 0.93"})
skill は CLI を uvx --from /Users/masao/project/ui-visual-diff 経由で叩くので、本パッケージのインストール手順を踏むだけで利用可能。
ui-visual-diff/
├── pyproject.toml
├── README.md
├── ui_visual_diff/
│ ├── __init__.py
│ ├── __main__.py # python -m ui_visual_diff entry
│ ├── _common.py
│ ├── defaults.py # CLI default paths shared by main()s and cli.py
│ ├── pixel_diff.py
│ ├── ssim_heatmap.py
│ ├── canny_edges.py
│ ├── clip_similarity.py
│ ├── grid_heatmap.py
│ ├── region_mapping.py
│ ├── run_all.py
│ ├── capture.py # Playwright screenshot + bbox dump
│ └── cli.py # argparse subcommand entry
├── examples/
│ └── target.html # data-visual-id 付き HTML サンプル
├── inputs/ # 実行時生成 (gitignored)
└── outputs/ # 実行時生成 (gitignored)