gazou-compressor.jp

画像最適化CIパイプライン構築:Quality Ladder/差分メトリクス/PRコメント自動化

画像最適化は合意された 閾値/指標/失敗時の可視化 が無いと徐々に劣化します。本稿は Quality Ladder (ツール) と差分メトリクスを統合した CI パイプラインの標準テンプレです。

Goal
  • 定量: PSNR / SSIM / bytes / optional VMAF
  • 定性: 差分PNG / ヒートマップ / ラダーJSON
  • PR: 自動コメントで逸脱の即通知

1. なぜCI化が必要か

ローカル目視は再現性が低く、開発速度が上がるほど“いつ劣化したか不明” になります。CIは 逸脱地点の特定差分証跡の保全 を自動化します。

2. 代表画像セット

カテゴリ(写真/UI/透過/高周波/小テキスト) を均衡させバイアスを低減。 diversity スクリプトで 偏りアラート を出します。

# 代表画像セット構築
find public/images -maxdepth 1 -type f | grep -E '\.(jpg|png|webp)$' | shuf -n 40 > samples.txt
node scripts/bucket-diversity.js samples.txt --min-categories 5 --out ladder-set.txt

3. アーキテクチャ

  1. 代表画像セット収集 (カテゴリ/ケース別)
  2. main基準のラダー生成 + HEAD生成
  3. 統合メトリクス比較 (PSNR/SSIM/bytes)
  4. 閾値逸脱 → 差分PNG/WASMヒートマップ生成
  5. PRへMarkdownコメント (表+スクリーンショット)
  6. 週次で閾値再評価 (標準偏差/回帰トレンド)

4. 指標としきい値

指標役割初期閾値例注意
PSNR粗い構造差-0.25dB局所劣化を埋める
SSIM構造保持-0.004明暗分布偏重
VMAF主観近似-2〜-3計算コスト
bytes容量回帰+4%分布ばらつき

5. アーティファクト保管

失敗PRのみ差分PNG/Heatmapを30日保持しコスト最小化。

# 差分アーティファクトS3ライフサイクル
aws s3 cp diff/ s3://image-ci-artifacts/pr-$PR_NUMBER/ --recursive
# 30日後自動削除のLifecycle設定

6. ワークフローYAML

name: image-ci
on: [pull_request]
jobs:
  metrics:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Install deps
        run: npm ci
      - name: Generate ladder JSON (before/after)
        run: |
          node scripts/ladder.js --image sample.png --out base.json --ref origin/main
          node scripts/ladder.js --image sample.png --out head.json --ref ${{ github.sha }}
      - name: Compare metrics
        run: node scripts/metrics-compare.js base.json head.json --psnr-threshold 0.25 --ssim-threshold 0.004
      - name: Visual regression
        run: npm run test:vr
      - name: Post PR comment
        if: always()
        run: node scripts/post-pr-comment.js base.json head.json reports/vr-summary.md

7. スケール/並列

CPU/WASMバウンド。物理コア-1 を上限に 70〜80% 利用で発熱/スループット妥協点。

# 並列ワーカー数 = (物理コア - 1) * 0.75 切り捨て
node scripts/run-metrics.js --workers 6

8. スクリプト抜粋

5.1 メトリクス比較

// scripts/metrics-compare.js (simplified)
import fs from 'node:fs';
const [basePath, headPath] = process.argv.slice(2);
const base = JSON.parse(fs.readFileSync(basePath,'utf8'));
const head = JSON.parse(fs.readFileSync(headPath,'utf8'));
let fail = false;
function check(name, get){
  const b = get(base);
  const h = get(head);
  const delta = h - b;
  if (name==='psnr' && delta < -0.25) fail = true;
  if (name==='ssim' && delta < -0.004) fail = true;
  return { name, base:b, head:h, delta };
}
const rows = [
  check('psnr', x=>x.global.psnr),
  check('ssim', x=>x.global.ssim),
  check('bytes', x=>x.global.bytes)
];
console.table(rows);
if (fail) {
  console.error('Regression detected');
  process.exit(1);
}

5.2 PRコメント投稿

// scripts/post-pr-comment.js (excerpt)
import { Octokit } from 'octokit';
import fs from 'node:fs';
const token = process.env.GITHUB_TOKEN;
const octokit = new Octokit({ auth: token });
// build Markdown table
// ... collect ladder deltas & vr summary
await octokit.rest.issues.createComment({
  owner: 'ORG', repo: 'REPO', issue_number: Number(process.env.PR_NUMBER),
  body: mdReport
});

5.3 Playwright 例

// tests/visual.spec.ts (excerpt)
import { test, expect } from '@playwright/test';
import { compare } from './pixel-compare';
test('hero image stable', async ({ page }) => {
  await page.goto('http://localhost:3000');
  const shot = await page.locator('img.hero').screenshot();
  const base = Buffer.from(process.env.BASE_SHOT!, 'base64');
  const result = compare(base, shot, { threshold: 0.04 });
  expect(result.mismatchPercentage).toBeLessThan(0.8);
});

9. 失敗パターン

# 失敗パターン検知
node scripts/overfitting-check.js --history metrics/*.json --psnr-variance-th 0.05

10. FAQ

11. まとめ

CI化で “劣化しても気づかない” リスクを排除します。閾値は定期再評価し、Adaptive Q (関連) 等の高度化と併走してください。

公開日: 2025-09-06編集: gazou-compressor.jp

gazou-compressor.jp 編集部

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

関連記事

トピック/更新日の近いコンテンツ