1. ホーム
  2. graphics

[解決済み] リニアとノンリニアのRGB空間で色を扱う場合の実用的な違いとは?

2022-10-08 07:12:49

質問

線形RGB空間の基本的な性質と、非線形の基本的な性質とは何でしょうか。それらの 8 ビット (またはそれ以上) の各チャンネル内の値について話すとき、何が変わるのでしょうか?

OpenGLでは、色は3+1の値で、これはRGB+αを意味し、各チャンネルに8ビット予約されており、これは私が明確に理解している部分です。

しかし、ガンマ補正に関しては、非線形の RGB 空間で作業することの効果がわかりません。

私は写真編集用のグラフィックソフトウェアで曲線を使用する方法を知っているので、私の説明は、線形RGB空間では、操作や数学関数が付属していない値をそのまま取得し、代わりに非線形である場合、各チャンネルは通常古典的な力関数動作に従って進化する、ということです。

なぜなら、計算の後、すべての非線形の RGB 空間は線形になり、最も重要なことは、非線形の色空間が人間の目により適しているという部分がわからないからです。

どのように解決するのですか?

RGBカラーを扱っているとします。各色は3つの 強さ または輝度で表されます。このとき、リニア RGB と sRGB のいずれかを選択する必要があります。今のところ、3 つの異なる強度を無視して物事を単純化し、強度は 1 つだけであると仮定します。

線形色空間では、保存する数値とそれらが表す強度の関係は線形です。つまり、数値が2倍になれば、強度(グレーの明るさ)も2倍になります。2 つの強度を足したい場合(2 つの光源の寄与に基づいて強度を計算しているため、または不透明なオブジェクトの上に透明なオブジェクトを追加しているため)、2 つの数値を足すだけでこれを行うことができます。 2Dブレンディングや3Dシェーディング、あるいはほとんどすべての画像処理をしている場合、線形色空間での強度が必要です。 そのため、数値の加算、減算、乗算、および除算を行うだけで、強度に同じ効果を与えることができます。ほとんどの色処理およびレンダリング アルゴリズムは、すべてに余分な重みを加えない限り、リニア RGB でしか正しい結果を得ることができません。

これは本当に簡単なことのように聞こえますが、問題があります。人間の目の光に対する感度は、高輝度よりも低輝度の方が細かいのです。つまり、見分けられる強度をすべてリストアップすると、明るいものよりも暗いものの方が多いのです。言い方を変えると、明るいグレーよりも暗いグレーの方が見分けがつくということです。特に、8ビットで強度を表現する場合、これを線形色空間で行うと、明るいシェードが多すぎて、暗いシェードが少なくなってしまいます。暗い領域ではバンディングが発生し、明るい領域では、ユーザーが区別できない白に近いさまざまなシェードにビットを浪費していることになります。

この問題を回避し、8ビットを最大限に活用するために、私たちはしばしば sRGB . sRGB 標準は、色を非線形にするために使用するカーブを示しています。 曲線は、下部が浅いので暗いグレーが多くなり、上部は急なので明るいグレーが少なくなります。数値が2倍になれば、強さも2倍以上になります。つまり、sRGBの色を足し合わせると、本来よりも明るい結果になってしまうのです。最近のモニターは、入力された色をsRGBとして解釈するものがほとんどです。ですから 画面に色を表示するとき、または 8 ビット/チャネルのテクスチャに保存するときは、sRGB として保存してください。 そうすれば、その8ビットを最大限に活用できます。

リニアスペースで処理された色を sRGB で保存する場合、問題が発生することにお気づきでしょう。これは、読み込み時に sRGB からリニアへの変換を行い、書き込み時にリニアから sRGB への変換を行うことになることを意味します。リニア8ビットのインテンシティは十分なダークを持たないことは既に述べたとおりですが、これでは問題が発生するので、もう一つ実用的なルールがあります。 8ビットリニアカラーは使用しないでください を避けることができます。8ビットカラーは必ずsRGBになる」というルールが定着しつつあるので、8ビットから16ビットへ、つまり整数から浮動小数点へ輝度を広げると同時にsRGBから線形変換し、同様に浮動小数点処理が終わったらsRGBへの変換と同時に8ビットへ狭めるのです。このルールを守れば、ガンマ補正を気にする必要はありません。

sRGBの画像を読み込むときに、直線的な強度が欲しい場合は、各強度にこの式を適用します。

float s = read_channel();
float linear;
if (s <= 0.04045) linear = s / 12.92;
else linear = pow((s + 0.055) / 1.055, 2.4);

逆に、画像をsRGBで書きたいときは、各直線輝度に対してこの式を適用してください。

float linear = do_processing();
float s;
if (linear <= 0.0031308) s = linear * 12.92;
else s = 1.055 * pow(linear, 1.0/2.4) - 0.055; ( Edited: The previous version is -0.55 )

どちらの場合も、浮動小数点数の s 値は 0 から 1 までの範囲なので、8ビット整数を読む場合は最初に 255 で割り、8ビット整数を書く場合は通常と同じように最後に 255 を掛けるようにします。sRGBを扱うために知っておくべきことはこれだけです。

ここまで、1つの強さだけを扱ってきましたが、色にはもっと賢いやり方があります。人間の目は、異なる色合いよりも異なる明るさを見分けることができます (より技術的には、クロミナンスよりもルミナンスの解像度が優れています)。したがって、明るさを色合いとは別に保存することで、24ビットをさらに有効に活用することができます。これがYUVやYCrCbなどの表現がやろうとしていることです。Yチャンネルは色の全体的な明るさであり、他の2つのチャンネルよりも多くのビットを使用します(またはより多くの空間解像度を持ちます)。このため、RGBの強度のようにカーブを適用する必要は(常に)ありません。YUV は線形色空間であるため、Y チャンネルの数値を 2 倍にすると色の明度が 2 倍になりますが、RGB 色のように YUV 色を足したり掛けたりすることはできないため、画像処理には使用せず、保存と伝送にのみ使用されます。

質問の答えになったと思いますので、最後に簡単な歴史的なメモをします。sRGB以前の古いCRTは、非線形性を内蔵していました。ある画素の電圧を2倍にすると、強度が2倍以上になるのです。どれくらい多くなるかはモニターごとに異なり、このパラメーターを ガンマ . この動作は、明るい色よりも暗い色を多く得られるという意味で便利でしたが、最初にキャリブレーションしない限り、ユーザーの CRT でどの程度明るい色が表示されるかがわからないということでもありました。 ガンマ補正 とは、最初に指定した色 (おそらく線形) を、ユーザーの CRT のガンマに合わせて変換することです。OpenGLはこの時代から来ており、そのためsRGBの動作は時々少し混乱します。しかし、GPUベンダーは現在、上で述べたような慣習で動く傾向があります。テクスチャやフレームバッファに8ビットの強度を保存するときはRGBで、色を処理するときはリニアである、ということです。例えば、OpenGL ES 3.0では、各フレームバッファとテクスチャに"sRGB flag"があり、これをオンにすると読み書きの際に自動変換されるようになっています。明示的にsRGB変換やガンマ補正を行う必要は全くありません。