画像最適化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. アーキテクチャ
- 代表画像セット収集 (カテゴリ/ケース別)
- main基準のラダー生成 + HEAD生成
- 統合メトリクス比較 (PSNR/SSIM/bytes)
- 閾値逸脱 → 差分PNG/WASMヒートマップ生成
- PRへMarkdownコメント (表+スクリーンショット)
- 週次で閾値再評価 (標準偏差/回帰トレンド)
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. 失敗パターン
- 代表画像偏り → テキスト劣化未検知
- 閾値陳腐化 → 改善後も旧ラインで誤検知
- 全差分保存 → S3コスト肥大
# 失敗パターン検知
node scripts/overfitting-check.js --history metrics/*.json --psnr-variance-th 0.05
10. FAQ
- 代表画像は? → 写真/UI/テキスト/透過/高周波 の5カテゴリ×3枚など。
- 並列数? → メトリクス計算(WASM)のCPUコア数 -1 を上限。
11. まとめ
CI化で “劣化しても気づかない” リスクを排除します。閾値は定期再評価し、Adaptive Q (関連) 等の高度化と併走してください。
公開日: 2025-09-06編集: gazou-compressor.jp