RGB値を正規化するなら255と256、どっちが正解?
Should you normalize RGB values by 255 or 256?
Should you normalize RGB values by 255 or 256?
画像処理や機械学習でRGB値を0から1の範囲に正規化する際、255で割るべきか、それとも256で割るべきかという議論をよく見かけます。結論から言うと、一般的には255で割るのが正解です。これは8ビットの色深度(0-255)において、最大値である255を1.0にマッピングしたいという意図があるためです。もし256で割ってしまうと、最大値が約0.996になり、1.0に到達しなくなってしまいます。ただし、特定のグラフィックスAPIや特殊なデータ変換の文脈では256が使われるケースもあるため、目的に応じて使い分けましょう。
もし君が12インチまである定規を持っているなら、定規にある目盛の数である13ではなく、長さLで正規化すべきだよ。
+0.5を使う解決策を支持するよ。まず、端にある半分のサイズのインターバルが気に入らないし、次に、255ベースの表現は通常SDR(HDRではない)画像だからね。RGB値はある適応状態に対する輝度を表しているし、日光の下のシーンでの「ゼロ」は「輝度ゼロ」ではなく、最も明るい地点の約0.001倍の明るさ、つまり何百万個もの光子が存在していて、ゼロとは程遠い。ある意味、人間の目はスライド式のスケールでコントラストを感じているわけで、システム内に絶対零度は存在しないんだ。例えば、放送システムでは歴史的にSDRの輝度範囲として16-235が使われてきた。「ゼロが必須だ」という主張には何らかのバイアスがあると思うけど、ほとんどの用途でゼロが必要だとは思わないな。
どちらも線形伝達関数を前提にしているけど、実際にはそんなことはほとんどないよ。
スマホで見ている人へのアドバイス:冒頭の「256で割る」バージョンのコード例が見たいなら、ランドスケープモード(横向き)で読んで。HTML/CSSがひどくて、改行されずにページの右端から完全に溢れちゃってるんだ。一番重要な部分が画面外に完全に消えていることに気づくまでに、混乱しながら3回も読み直してしまったよ。
255.0を掛けて、オプションでディザリング(三角波ディザでOK)を加えてから、FPUにデフォルトのIEEE 754「偶数丸め(round-to-nearest-ties-to-nearest-even)」で処理させるのがいいよ。変な0.5とか混ぜる必要はないね。 :-)
しばらく考える必要のなかったことについて書かれた、読んでいて楽しい記事だった。ゲーム開発で、ロジックには浮動小数点演算を使っているのに、ピクセルアートは整数値で描画しなければならなかった時のことを思い出したよ。見栄えを少しでも良くするために、+0.5と似たようなことを試したこともある(特にカメラが動く場合、切り捨てが必要になるからね…)。最後にリンクされているJonathan Blowによる2002年の記事[1]も面白かった。最初の記事の可視化のおかげで、より深く理解できたよ。 [1] https://web.archive.org/web/20240706043551/https://number-none.com/product/Scalar%20Quantization/index.html
著者はビン(階級)とビンの境界を混同しているね。最初のプロットでは、0-7がビンの中央値ではなく境界であるべきなのに、そう描かれていないから標準的なアプローチがおかしく見えているんだ。ヒストグラムの例でもその混乱が見て取れる。ビンは256個ではなく255個しかないんだ。そのミスを修正して0.5のオフセットを取り除けば、ヒストグラムは両端で正しく分布するよ。
色値が「正確に」何を意味するかという問題は、1コンポーネントあたり8ビットの場合、ほとんどどうでもいいことだよ。分母が255か256かで生じる誤差なんて微々たるものだし、よほど色覚が鋭くて画面に近づかない限り違いなんて分からない。それに、どのみちモニターやスマホの画面なんて校正されていないだろうから、誰が気にするって話だ。ただ、マイコンを使って8ピン(赤3、緑3、青2)のカラー出力でVGA信号を生成する時には厄介な問題になる。この構成では、色値の意味が「非常に」現実的で、VGAモニターに送るべき電圧レベル(0V〜0.7V)に直結するからね。青チャンネルは(0→0V, 1→0.23V, 2→0.47V, 3→0.7V)のようにマッピングされる。すると、純粋なグレーは見られず、最も近い色でも青や黄色の色味が出てしまう。さらに、赤から白へのグラデーションなども少しオレンジや紫っぽくなってしまうんだ。Raspberry Pi Pico 2用のVGA出力コード(ダブルバッファ320x240)はこちら:https://github.com/moefh/pico-vga-8bit-demo
いや、「代替」アプローチも7ビットの例で見るとおかしく見えるよ。1.0はビン7の右側にあるけど、0.0はビン0の左側にあるんだ。標準アプローチは「中央にサンプルがある」と仮定している。つまり、ゼロは完全な黒であり、そこから(プラスマイナス方向に)多少の不確かさがあるという考え方だ。もし輝度サンプリングに歪みがなければ(露出過多によるクリッピングがないなら)、ビン7は1.0を中心とした値の範囲を表していることになる。これは半分のサイズのインターバルじゃない。…[中略]…要するに、端のビンにはバイアスが存在する可能性があるんだ。解決策としては、最大値を「クリップされた値」を表すカナリア値にすることだね。つまり255は「クリップデータ」を意味するようにして、254以下だけがクリップされていない信号のサンプリング値になるようにする。CGレンダリングなら255を避けるし、カメラセンサーも技術的な撮影時にはクリップが起きないよう調整されているんだ。
結局この問題ってすべて「切り捨て(truncation)」を使っているから発生するんじゃないの?切り捨てを使うと、正確な1.0しか255のビンに入らないことになって、結局256個のビンが255個に減らされてしまう。それなら、利用可能なビンを満たすようにスケーリングすればいいのでは?例えば trunc(result * 255.999) とかさ。