gazou-compressor.jp

画像QAプレイブック(視覚リグレッション・色差・バンディング検出)

画像は圧縮やリサイズで副作用が出ます。文字のにじみ、色ズレ、バンディング—— 人力チェックだけでは取りこぼしがち。ここでは自動検査×手動チェックの併用で 壊れにくい運用を作ります。

先に結論(迷ったら)
  • 視覚リグレッション(pixelmatch/Jest)で差分を自動検出。
  • ΔE色差(CIEDE2000等)で“色だけのズレ”を数値化。
  • 背景トグル(白/黒)とRetina/非Retinaの目視で最終保証。

要点(TL;DR)

  • ゴールデン画像(基準画像)をGitで管理し、差分はPRで可視化。
  • 差分判定は failureThresholdType="percent"0.1–0.3% から開始。
  • UI文字や透過縁は 4:4:4/可逆 を優先(詳細は 文字にじみ対策/ 透過フリンジ対策)。
  • LCP画像はfetchpriority=high+preload。画質は /compare で1:1確認。

1. なぜ“視覚リグレッション”が必要か

  • 圧縮パラメータ変更(Q/サブサンプリング)で微妙なにじみが出る。
  • サイズ/アスペクト比の変更でアンチエイリアスが変化。
  • テーマ(ライト/ダーク)で縁の見えが変わる。
  • CDNやエンコーダ更新で非互換が起きうる。

2. 基準画像の作り方と環境固定

  • ゴールデン画像は同一解像度・同一デバイススケールで取得。
  • 動的領域(日時/乱数)はモック化 or マスク
  • CIで実行する場合は同一ランナー/コンテナで再現。
差分が出やすい領域をマスク
ローディングやアニメ部分は、差分比較から除外(mask)して“意味のある差”だけを検出します。

3. 自動検査:pixelmatch & jest-image-snapshot

// __tests__/image.spec.ts(概略)
import { toMatchImageSnapshot } from "jest-image-snapshot";
expect.extend({ toMatchImageSnapshot });

it("画像差分が閾値以内", async () => {
  const png = await loadPng("./dist/hero.png"); // 取得処理は環境に合わせて
  expect(png).toMatchImageSnapshot({
    failureThresholdType: "percent",
    failureThreshold: 0.2, // 0.2% から調整
    customSnapshotIdentifier: "hero",
    customDiffConfig: { threshold: 0.1 }, // アンチエイリアスのにじみ吸収
  });
});

UI文字のにじみは 文字にじみ対策 を参照。UIは4:4:4(または可逆)が安全です。

4. 色差(ΔE)とバンディング検出

  • 平均/最大ΔE(CIEDE2000)で“色だけのズレ”を数値化。
  • グラデーション帯は局所コントラストをヒートマップ化してバンディング検出。
// 疑似コード:ΔE集計(サンプル)
for (const pixel of pixels) {
  const dE = deltaE(lab(src[pixel]), lab(dst[pixel]));
  max = Math.max(max, dE);
  sum += dE;
}
const avg = sum / pixels.length;
if (max > 2.0) { /* 警告: 目立ち得る色差 */ }
グラデ破綻の対処
グラデの段差は AVIF10bit や微小ノイズ付加で抑制(詳細は 破綻対策ガイド)。

5. 背景トグル(白/黒)と透過縁の確認

透過PNG/WebPは背景色で見えが変わります。白/黒の背景トグルで縁のにじみを確認しましょう(対策は 透過フリンジ徹底対策)。

// HTML(例)
<button id="bg">背景切替</button>
<div id="stage" class="bg-white">
  <img src="/img/cutout.png" width="480" height="360" alt="人物の切り抜き" />
</div>

// JS(例)
document.getElementById("bg")!.onclick = () => {
  document.getElementById("stage")!.classList.toggle("bg-black");
  document.getElementById("stage")!.classList.toggle("bg-white");
};

6. デバイス倍率(Retina/非Retina)比較

文字/細線の見えはデバイススケールで変わります。スクリーンショット取得時にdeviceScaleFactorを切り替え、両方で差分を取ります。

// Playwright(例)
import { test, expect } from "@playwright/test";

for (const dpr of [1, 2]) {
  test(`kv visual @dpr=${dpr}`, async ({ browser }) => {
    const ctx = await browser.newContext({ deviceScaleFactor: dpr, viewport: { width: 1280, height: 800 } });
    const page = await ctx.newPage();
    await page.goto("https://example.com");
    const shot = await page.screenshot({ fullPage: false });
    expect(shot).toMatchSnapshot([`kv-${dpr}x.png`], { maxDiffPixelRatio: 0.002 });
  });
}

7. 運用フロー(チェックリスト)

  • ゴールデン画像をGitで管理(差分はPRに出す)。
  • 動的領域はマスク or モックで安定化。
  • しきい値は0.1–0.3%から調整、失敗時はスクショ添付でレビュー。
  • Retina/非Retina、白/黒背景の両方でOKを取る。
  • 画質破綻は 破綻対策 文字にじみ対策 を参照。
  • 透過縁は 透過フリンジ対策 を適用。
  • 最終は /compare /compressor で容量/見えの落とし所を確認。

8. まとめ:自動×人力で“壊さない”

自動検査で取りこぼしを無くし、手動で体験の質を保証する。画像運用はこの二段構えで安定します。 仕組みを一度整えれば、日々の差し替えや大量更新でも品質を守れます。

gazou-compressor.jp 編集部
画像圧縮・変換・背景除去などの実践テクニックと、Webで“速く・軽く・崩さない”ためのノウハウを発信しています。

関連記事