gazou-compressor.jp

画像のビジュアルリグレッション:SSIM/PixelmatchをCIに組み込む最短手順

画像圧縮・LQIP・Progressive……設定を変えるほど見た目の破綻リスクは増えます。手元の視認では限界。 そこで自動の目をCIに入れ、差分が出たら即座に検知します。ここではpixelmatchとSSIMを併用する安定構成を紹介します。

先に結論
  • Docker+フォント固定で再現性を上げる。
  • 差分指標はpixelmatch(2%) + SSIM(≥0.985)の両輪。
  • 失敗時は差分PNGをArtifactで保存、原因特定を即時化。

要点(TL;DR)

1. 依存とスクリプト

// package.json(抜粋)
{
  "devDependencies": {
    "@playwright/test": "^1.47.0",
    "pixelmatch": "^5.3.0",
    "pngjs": "^7.0.0",
    "ssim.js": "^3.6.0",
    "tsx": "^4.0.0"
  },
  "scripts": {
    "vr:test": "playwright test",
    "vr:update": "PLAYWRIGHT_UPDATE_SNAPSHOTS=1 playwright test"
  }
}

2. Playwrightテスト

// tests/visual.spec.ts — Playwright + pixelmatch + SSIM
import { test, expect } from "@playwright/test";
import pixelmatch from "pixelmatch";
import { PNG } from "pngjs";
import { ssim } from "ssim.js";
import fs from "node:fs";

const THRESHOLD_PIXELS = 0.02; // ピクセル差 2% まで許容
const THRESHOLD_SSIM = 0.985;  // SSIM 0.985 未満で失敗

test("homepage visual regression", async ({ page }) => {
  await page.goto("http://localhost:3000/");
  await page.waitForLoadState("networkidle");

  const shot = await page.screenshot({ fullPage: true });
  const baselinePath = "tests/__screenshots__/homepage.png";
  const diffPath = "tests/__screenshots__/homepage.diff.png";

  if (!fs.existsSync(baselinePath)) {
    fs.mkdirSync("tests/__screenshots__", { recursive: true });
    fs.writeFileSync(baselinePath, shot);
    test.info().attach("baseline-created", { body: shot, contentType: "image/png" });
    return; // 初回はベースライン作成
  }

  // pixelmatch
  const img1 = PNG.sync.read(shot);
  const img2 = PNG.sync.read(fs.readFileSync(baselinePath));
  const { width, height } = img1;
  const diff = new PNG({ width, height });
  const diffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, { threshold: 0.1 });
  const rate = diffPixels / (width * height);

  // SSIM
  const ssimRes = ssim({ data: img1.data, width, height }, { data: img2.data, width, height });
  const ok = rate <= THRESHOLD_PIXELS && ssimRes.ssim >= THRESHOLD_SSIM;
  if (!ok) fs.writeFileSync(diffPath, PNG.sync.write(diff));

  expect(ok, `diff=${(rate*100).toFixed(2)}% ssim=${ssimRes.ssim.toFixed(4)}`).toBeTruthy();
});

3. GitHub Actions 連携

# .github/workflows/visual.yml — GitHub Actions
name: visual
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20" }
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run build && npm start & npx wait-on http://localhost:3000
      - run: npm run vr:test
      - name: Upload diffs
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: visual-diffs
          path: tests/__screenshots__/*.diff.png

4. 応用と使いどころ

5. 公開前チェック

6. まとめ

画像の品質は自動テストで守れます。CIにSSIMとpixelmatchを入れて、破綻を本番前に止めましょう。

公開:2025-09-03

gazou-compressor.jp 編集部

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

関連記事