スクリーンショットの色はsRGBではなく、あなたのUIがそれを明らかにします
macOSのスクリーンショットから取得した16進数カラーコードは、sRGBではなくディスプレイプロファイルの色空間にあります。それをコードに貼り付けると、色がずれます。以下にその往復証明を示します。
スクリーンショットからスポイトでサンプリングした色を16進数カラーコードとしてスタイルシートに貼り付けて色を合わせようとすると、結果が目に見えてずれることがあります。数値が間違っているわけではありません。間違った色空間で測定されているのです。広色域ディスプレイでは、スクリーンショットはディスプレイプロファイルでエンコードされますが、CSS、テーママニフェスト、デザイントークンはsRGBとして解釈されます。同じ数値でも、色が異なるのです。
私は、あるUI要素を、すでに正しく表示されている別のUI要素に合わせようとしたときにこの問題に直面しました。私がサンプリングした値は#6DCEBDでした。実際にそのピクセルを生成する値は#28D0BCです。これらはかけ離れています。
スポイトが実際に返す値
広色域ディスプレイ上のスクリーンショットは、sRGB値のダンプではありません。コンポジターはディスプレイの色空間にレンダリングし、スクリーンショットファイルはそれを記述したICCプロファイルを保持します。そのファイルを開いてピクセルを読み取ると、そのプロファイルで表現された数値が得られます。
あなたのコードは異なって読み取られます。CSSの16進数カラーコード、テーママニフェスト、そしてほとんどのデザイン トークン フォーマットは、定義上sRGBです。パイプラインのどの部分も、あなたに代わって変換を行うことはありません。そのため、このラウンドトリップには2つの変換が含まれており、そのうちの1つしかあなたには見えません。
それを証明するラウンドトリップ
理論を鵜呑みにしないでください。数行でパイプライン全体を検証できます。あなたには、グラウンドトゥルースがすでにわかっているからです。つまり、あなた自身のコードで宣言した任意の色は、定義上、sRGB値です。スクリーンショットでそれを見つけて、逆変換してください。
from PIL import Image, ImageCms
import io
im = Image.open("screenshot.png")
profile = ImageCms.ImageCmsProfile(io.BytesIO(im.info["icc_profile"]))
to_srgb = ImageCms.buildTransformFromOpenProfiles(
profile, ImageCms.createProfile("sRGB"), "RGB", "RGB", renderingIntent=0
)
converted = ImageCms.applyTransform(im.convert("RGB"), to_srgb)
宣言された3つの色を、未加工でサンプリングし、変換した結果です。
| 宣言値 (sRGB) | ファイル内の未加工ピクセル | 変換後 |
|---|---|---|
10, 8, 23 |
10, 8, 22 |
10, 8, 23 |
25, 24, 36 |
25, 24, 35 |
25, 24, 36 |
163, 18, 98 |
147, 33, 96 |
164, 18, 99 |
すべての宣言値が1単位以内で復元されます。パイプラインは確認されました。次に、一致させたい色で順方向に実行します。
sRGB 40, 208, 188 becomes on screen 109, 206, 189
sRGB 109, 206, 189 becomes on screen 138, 204, 190
最初の行が答えです。2行目は、サンプリングした16進数値をコードに直接貼り付けた場合に起こることです。3番目の色が得られますが、それは意図的に見えるほど近く、合わせようとしていた要素の隣では濁って見えるほど間違っています。
なぜ暗い色が問題を隠すのか
もう一度、表を見てください。最初の2行は1単位だけずれています。3行目は赤で16、緑で15移動します。その非対称性こそが、このバグが簡単なチェックをすり抜ける理由です。
色空間間の変換は、無彩色軸の近くや黒の近くでは、恒等変換に近くなります。暗い背景、境界線、影はすべて、ほぼ完璧にラウンドトリップします。スクリーンショットを背景色と見比べて一致を確認した場合、そのスクリーンショットはsRGBであると結論付けて次に進んでしまいます。
乖離は、彩度が存在するところに生じます。アクセントカラー、ブランドの色相、ステータスインジケーター、シンタックスハイライトは、まさに最もずれが大きくなる値であり、かつ、目視で一致させようとする可能性が最も高い値でもあります。あなたが行ったチェックが通ったのは、バグが存在しない色域の部分で実行したからです。
同じ形をした2つ目の罠があります。ツールが保存時にICCプロファイルを削除または無視した場合、数値はそのまま残りますが、その意味は失われます。チャットアプリに貼り付けられたり、リサイズされたり、再エンコードされたりしたスクリーンショットは、プロファイルがまったくない状態で届くことがあります。その時点では、ファイル内の何も、どの色空間にいるのかを教えてくれません。そして、唯一正直な答えは、ファイルだけでは知ることができない、ということです。
2つの修正方法
明示的に変換する。 絶対値が必要な場合は、上記で示した変換を実行し、変換された数値を使用します。これは、スタイルシート、マニフェスト、または他の人が読むトークンファイルに色を書き込む必要がある場合に正しい選択です。後で導出を繰り返せるように、元のスクリーンショットとそのプロファイルをそのまま保持してください。
または、1つの画像内で比較する。 実際の目標が「要素Aを要素Bと同じ色にする」ことである場合、絶対値はまったく必要ありません。両方を含むスクリーンショットを1つ撮り、同じツールと同じコードパスで両方のピクセルを抽出し、互いに比較します。ファイルがどの色空間にあっても、両方のサンプルはその色空間内にあります。sRGB値がわからなくても、比較は有効です。
2番目のアプローチはより堅牢で、私は今ではまずそちらを試します。これを始めたきっかけのケースでは、最終的なチェックはまさにそれでした。つまり、1つのキャプチャから参照要素とターゲット要素をサンプリングし、色相と彩度を比較したのです。色相の差は0.0度、彩度の差は0.003でした。一致を確認するために変換は必要ありませんでした。
2つの詳細が、画像内比較を信頼できるものにします。
- グリフの内部をサンプリングし、エッジはサンプリングしない。 アンチエイリアス処理されたテキストは、境界で背景に向かってブレンドされます。平均値ではなく、領域内で最も彩度の高いピクセルを取得してください。そうしないと、色ではなくブレンドを測定することになります。
- 無効状態に注意する。 ツールバーのグレー表示されたアイコンは、アクティブな色からかけ離れていても、彩度フィルターを通過することがあります。領域全体をスキャンして最も一般的な値を信頼するのではなく、参照要素を意図的に選択してください。
これが現れる場所
このパターンは、特定のプラットフォームやファイル形式に限定されるものではありません。これは、レンダリングされた画像が、後で固定空間で解釈される値の信頼できる情報源 (source of truth) として扱われるあらゆる場所に現れます。
- 広色域のマシンでエクスポートされたデザインモックアップの色に、UI要素を一致させる。
- 直接読み取ることができない既存のものをサンプリングして、テーマやスキンファイルを構築する。
- キャプチャしたピクセルをハードコードされた期待値と比較する、ビジュアルリグレッションテストを作成する。
- CSSで使用するために、写真やビデオフレームからパレットを抽出する。
- スクリーンショットで色のバグを報告する。その際、読み手はあなたの画像をサンプリングし、どちらもレンダリングしていない数値を取得する。
ビジュアルリグレッションテストについては、特筆すべき警告があります。ラップトップでキャプチャを行い、sRGBの定数に対してアサーションを行うスイートは、どのマシンで実行されたかによって成功したり失敗したりします。CIランナーが開発者のマシンと異なるプロファイルを持っている場合、失敗はフレーク(不安定な失敗)のように見え、リトライによって見過ごされてしまいます。
よくある質問
これはWindowsやLinuxにも影響しますか? このメカニズムは一般的ですが、影響を受けるかどうかはディスプレイとキャプチャツールに依存します。この問題は、キャプチャがsRGBではなく広色域空間でエンコードされた場合に発生します。標準色域ディスプレイでsRGBにキャプチャした場合は問題なくラウンドトリップするため、広色域パネルがノートPCで一般的になる前は、この問題は稀でした。
プロファイルを削除するだけでよいですか? いいえ。プロファイルを削除しても何も変換されません。変換に必要な情報が削除されるだけで、その意味が文書化されていない数値が残ります。ファイルにプロファイルがない場合、ピクセルだけから元の空間を復元することはできません。
デザインツールに表示される16進数コードがスクリーンショットと異なります。どちらが正しいですか? 空間が明記されていなければ、どちらも正しくありません。それぞれの数値がどの空間にあるのかを尋ねてください。デザインツールは、広色域ディスプレイを通してプレビューしながらsRGB値を表示することがよくあります。これこそが、画面上のピクセルと報告される16進数コードが一致しない理由です。
これはガンマやビット深度と同じものですか? いいえ。ガンマエンコーディングとビット深度は別の問題であり、これらも単純なピクセル計算を歪める可能性があります。この特定の問題は、数値が表現されている色空間に関するものであり、ガンマ処理が正しく、全体がチャネルあたり8ビットであっても発生し続けます。
どうすれば再発を防げますか?
値の横に空間を記述してください。16進数定数の横にsRGBとコメントを書いたり、サンプルの出所を記録したファイル名にしたりすることは、何もコストがかからず、バグを可能にする曖昧さを取り除きます。元のキャプチャをそのプロファイルと一緒に保持してください。そうすれば、そこから派生した値を再推測するのではなく、再計算できます。