gazou-compressor.jp

色差(ΔE00)とガマット検証パイプライン

広色域(P3/AdobeRGB/HDR) 素材を 事故なく sRGB 配信 へ収束させるには “変換した” だけでは不十分です。変換結果を ΔE00 / ガマット外率 / 肌色個別指標 で継続監視し、閾値逸脱を CI で即座に検知する 色品質ガバナンス が必要です。

先に結論
① sRGB固定 ② ゴールデン画像基準化 ③ ΔE00 平均/最大/95pct 閾値 ④ 肌色/ブランドマスク差分 ⑤ CI + ダッシュボード履歴 ⑥ 四半期で閾値リベース。これで“いつの間にか彩度が落ちてた”事故を防げます。

要点(TL;DR)

背景:色品質は“後戻りしがちな非機能”

圧縮や再生成のたびに彩度/局所コントラストが僅かに失われ、ローンチ直後と半年後で“なんとなくくすんだ”状態になることがあります。sRGB固定パイプラインP3→sRGB肌色チューニング は単発施策。持続的には差分指標の継続記録が要件です。

また HDR / 10bit ソースは LDR 8bit 化でトーン圧縮が入るため、トーンマッピング 後の ΔE 分布監視が不可欠です。

設計:インベントリ → 変換 → 差分 → CI → 可視化 → レビュー

  1. インベントリ — ICC/色域/ビット深度/アルファ有無を機械抽出。
  2. 正規化 — ICC→sRGB, Orientation適用, メタ最小化。
  3. 差分計測 — ゴールデンと Lab 変換し ΔE00 分布算出。
  4. マスク特化 — 肌色/ブランド領域 ΔE 集計 (平均/最大)。
  5. CIゲート — 閾値逸脱で失敗。ログに上位Nピクセルを出力。
  6. ダッシュボード — 週次トレンド/閾値境界/最新逸脱一覧。
HowTo JSON-LD
本ページは手順を HowTo 構造化し検索での理解促進を図っています。

実装:Sharp × ΔE00 計測スクリプト

libvips(Sharp) は高速に sRGB 変換でき、raw() でピクセルへアクセス可能。そこで WASM ΔE 実装と組み合わせ高スループット比較を行います。

// libvips (sharp) を使った sRGB 正規化 + ΔE 計測準備 (Node.js)
import sharp from 'sharp';
import { readFileSync, writeFileSync } from 'node:fs';

const src = 'input-p3.png';
const ref = 'golden-srgb.png';
const out = 'out-srgb.png';

// 1) sRGB へ変換 (ICC 埋め込み→変換→ICCタグ最小化 or strip)
await sharp(src)
  .withMetadata({})
  .toColorspace('srgb')
  .png({ compressionLevel: 9 })
  .toFile(out);

// 2) ΔE00 計測 (簡略概念: 実際はWASMで高速化)
import { deltaE00 } from './lib/deltae-wasm';
const r1 = sharp(ref); const r2 = sharp(out);
const [i1, i2] = await Promise.all([r1.raw().ensureAlpha().toBuffer({ resolveWithObject:true }), r2.raw().ensureAlpha().toBuffer({ resolveWithObject:true })]);
if(i1.info.width!==i2.info.width||i1.info.height!==i2.info.height) throw new Error('size mismatch');
// RGB→Lab 変換 & ΔE00 集計 (擬似コード)
let sum=0, max=0; const arr=[];
for(let p=0; p<i1.data.length; p+=4){
  const lab1 = rgbToLab(i1.data[p], i1.data[p+1], i1.data[p+2]);
  const lab2 = rgbToLab(i2.data[p], i2.data[p+1], i2.data[p+2]);
  const d = deltaE00(lab1, lab2); sum+=d; if(d>max) max=d; arr.push(d);
}
arr.sort((a,b)=>a-b);
const mean = sum / (arr.length);
const p95 = arr[Math.floor(arr.length*0.95)];
console.log({ mean: mean.toFixed(2), max: max.toFixed(2), p95: p95.toFixed(2) });

CI は “変換 → 色差計測 → 閾値判定” を直列に。閾値は初期広め→運用データで徐々に引き締める段階導入が安全です。

# GitHub Actions (抜粋) - ΔE00 閾値逸脱で失敗
jobs:
  color-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: node scripts/generate-images.js  # 変換
      - run: node scripts/deltae-check.mjs --threshold-mean 1.0 --threshold-max 2.5

肌色/ブランドカラーの局所ΔE監視

全画素平均 ΔE が 0.6 でも、肌色領域だけ 1.8 など局所劣化が潜むことがあります。マスクJSON(ポリゴン)を適用し独立指標を保持します。

// 肌色/ブランドカラー領域マスク JSON 例 (ポリゴン座標)
{
  "skin": [ { "file": "model.png", "polygon": [[120,88],[180,90],[178,140],[118,138]] } ],
  "brand": [ { "file": "hero.png", "polygon": [[40,40],[300,40],[300,140],[40,140]] } ]
}

このマスクは OGP や OGP安全域 のテンプレ生成時に併用し、ブランドカラー領域の不意なトリミングも検知可能です。

ガマット外率とクリッピング戦略

P3→sRGB 変換で彩度の高い赤/緑領域がクリッピングする場合、軽度なら ΔE 閾値に吸収されますが、ロゴ等で集中する場合は perceptual 変換や彩度圧縮前処理を検討します。

閾値設定とガバナンス

初期は mean<1.5 / p95<2.5 / max<4.0 の緩め設定で誤検知を避け、履歴が安定したら 1.0 / 2.0 / 3.0 へ段階的に絞ります。肌色/ブランドは独自に mean<0.8 を目安。閾値逸脱 PR には ΔE 上位 N ピクセルの座標 (x,y,ΔE) をJSONで添付し、再現性を確保します。

関連ツール

NG/落とし穴

公開前チェックリスト

まとめ

色品質を“属人ノウハウ”から外し、ΔE00 + ガマット外率 + マスク領域の 3層モニタリング で再現性を獲得します。これによりデザイン刷新・コーデック変更・圧縮再調整の各フェーズでブランド/肌色の破綻を早期検知し、品質ガバナンス の成熟度を高められます。

FAQ

FAQ(よくある質問)

1肌色領域を分離する理由

全画素平均では隠れる“局所的彩度低下”を顕在化するため。ブランド/肌色は視認感度が高く ΔE=1.0 未満でも主観差異が拾われるケースがあります。

2HDRトーンマッピングで注意すべき点

ハイライト圧縮で局所コントラストが失われ ΔE が上がる。HDR→LDRトーンマッピング の手順を参照しガンマ再調整を挟む。

3既存の sRGB 変換記事との差

sRGB変換とICCプロファイル は“変換手順”中心。本稿はその後の“継続検証/閾値/通知”という運用レイヤに焦点。

gazou-compressor.jp 編集部

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

関連記事

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