画像のビジュアルリグレッション:SSIM/PixelmatchをCIに組み込む最短手順
画像圧縮・LQIP・Progressive……設定を変えるほど見た目の破綻リスクは増えます。手元の視認では限界。 そこで自動の目をCIに入れ、差分が出たら即座に検知します。ここではpixelmatchとSSIMを併用する安定構成を紹介します。
先に結論
- Docker+フォント固定で再現性を上げる。
- 差分指標はpixelmatch(2%) + SSIM(≥0.985)の両輪。
- 失敗時は差分PNGをArtifactで保存、原因特定を即時化。
要点(TL;DR)
- “はみ出し/にじみ/色ズレ”は人間より機械が得意。
- SSIMは知覚的、pixelmatchはピクセル差。併用が安定。
- 対象はヒーロー/OGP/比較ページ。静的HTMLの差分も同時にチェック。
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. 公開前チェック
- ベースラインが最新(必要時は
vr:update
)。 - 差分PNGのアップロードが有効。
- Docker/フォント固定で再現性を確保。
6. まとめ
画像の品質は自動テストで守れます。CIにSSIMとpixelmatchを入れて、破綻を本番前に止めましょう。