Torisugariの日記: 10で割るか11で割るか 3
私はプログラミングの例文集、いわゆるCookbookの類にはあまり馴染みがないので、もしかしたら、有名な話かもしれませんけれど、最近、なるほどな、と感銘を受けたので、ひとつ。
単位を変換する、というのは、アナログ世界の理学・工学でしばしば問題になりますが、デジタルの世界では、そういった物差しの変更に加えて、型変換の問題があります。
例えば、あるカメラが明るさを0〜1.0の範囲で計測したとして、その値を整数値で持っておきたいことがあります。というか、これは、特異な状況ではありませんよね。CSS2のcolorでは、真っ白を表す時に、
rgb(FF, FF, FF)
rgb(100%, 100%, 100%)
の両方が使えます。まあ、
#FFFFFF
でもいいですが、とにかく、自分でウェブページを作成して、色を指定したことがあるなら、どれくらいの値をいれるか、だれしも頭の中で計算したことがあるでしょう。
余談ですが、CSSの規格では、パーセント表示が整数値なのか、浮動小数表示なのかは曖昧ですね。CSS2やCSS3ではコメント欄に
/* float range 0.0% - 100.0% */
と書いてあるので、元々はrgb(99.9999%, 0%, 0%)みたいな書き方を想定していたはずですが、CSS2の色準拠を名乗るsvg1.1では、
"rgb(" wsp* integer "%" comma integer "%" comma integer "%" wsp* ")"
と書いてあるので、99.9999%は考えていないようです。需要もなかったんでしょうが。まあ、0〜100への写像でも以下の議論は成り立ちますが、ここでは浮動小数が許されている、とします。そして、実際の浮動小数は連続どころか稠密でもないので、おこがましいですが、ここでは表記上の簡便のために、floatが無限の桁数を持つと仮定して、実数と呼ぶことにします。
とにかく、ここではっきりとわかっているのは、「0xFFは100%に対応している」ということと「0は0%に対応している」ということだけです。この二つが確かだとして、その間の値、例えば、1%は整数にしたとき、どんな値になって、整数値の1はパーセント表示ではどれくらいの値にすればよいのでしょうか?
そもそも、人間本位に考えると、今の社会の基本は10進法ですから、0%を0に割り当てて、100%を100に割り当てておけば、悩む必要はなかったのかもしれません。実際、そういう仕様も多いかと思いますが、世の中の大勢としては、そのような考えは受け入れられておらず、0〜255にすることを要求されます。これもひとえに、我々の貧乏性のせいであって、正確に色を指定するには、なるべく細かい方がいいと思っているからです。16色や8ビットカラーの時代を経て、今の世の中になっているわけですから、100までしか使わないのは富豪的すぎるのです。
厚みを持たせたり、片対数にしたり、写像の方法はいろいろあるでしょうが、偏りなどの前情報なく、前述の条件(FF⇄1.0、0⇄0.0)で[0,1.0]の実数(浮動小数)を[0, 255]にマッピングするなら、次のように考えるのが素直なはずです。
unsigned char mapping(float aF) {
return (unsigned char) ::floor(255.0 * aF);
}
しかし、ちょっと待ってください。これだと、255になるのは、aFが1.0の時だけですから、不公平極まりない割り振りです。均等に割り振るには、256段階あるわけですから、
unsigned char mapping(float aF) {
if (aF == 1.0) {
return 255;
}
return (unsigned char) ::floor(256.0 * aF);
}
のようになっていないといけません。部分点が1点ずつで10点満点のテストは11段階評価なのです。
公平である、というのはこの上ない美徳です。何がいいって、表だって反対するものが現れにくいのが素晴らしいです。他にも案はあるかもしれません。でも、これも悪くはないでしょう?なんたって、公平なんですから。均等に割り振るというのは、そういうことです。
----
もし、与えられた課題が「0〜255の値をランダムに作りなさい」で、閉区間[0, 1.0]の乱数が与えられているなら、上記のような考え方でいいと思います。でも、ことは色の話です。だから、やっぱり、文句の付けようが出てきてしまうんですね。
そもそも、上のmappingと逆の演算、つまり、[0, 255]の整数があったとして、それを[1, 0]の実数に引き戻す時、どうすればよいでしょうか?256段階評価なんだから、
float reverse_mapping(unsigned char aI) {
return (aI/256.0);
}
でしょうか?いやいや、違いますよね?aIは[0, 255]なんですから、整数255が実数255/256に対応することになってしまいます。これはいけません。整数255が実数1.0に対応するのは、絶対的な要請ですから、正しくは、
float reverse_mapping(unsigned char aI) {
return (aI/255.0);
}
です。mappingの時は、256を掛け算しておいて、reverse_mappingの時に、255で割るのは、なんだか奇妙じゃないですか?
もっと切実な問題もあります。それは、例えば、整数1をreverse_mappingで実数に変換すると、その値は1/255です。すると、これは1/256より大きい値ですから、mappingで変換すると2になります。実数を整数に変換するときに、誤差が丸められるのはさけられません。しかし、元々整数だったものを実数に変換して、それを整数に戻した時に、元の整数にならない、これはいけません。
もう、うすうす結論が見えてきた方もあると思いますが、望ましい解決方法は、mappingを以下のように実装することです。
unsigned char mapping(float aF) {
return (unsigned char) ::round(255.0 * aF);
}
「四捨五入」は「切り捨て」に比べて丸め誤差の最大値が半分になる、というメリットがありますが、ここではそれは消極的な効果にすぎません。真に偉大な点は、mappingで丸める範囲の正にど真ん中にreverse_mappingで作り出される代表値が位置していることです。
10点満点は11段階評価だから、階段は11段必要ですが、階段の幅は1/11にするよりも、1/10にして始端と終端だけ1/20にした方が美しく話が纏まるのです。
----
この話の元ネタは、cairoメーリングリストの「RFC: More accurate color conversion」なので、私の説明で納得のいかない方は、そちらを参照してください。議論に参加している優秀な人たちが、必ずしも、最初からこの主張に納得しているわけではない、という点もなかなか興味深く、一読の価値はあると思います。
おそらく、端数計算に慣れ親しんだ10進数なら、もっと早く結論に達していたと思いますが、255や65535といったギミックがあると、話が回りくどくなってしまう、という点もポイントだと思います。
---
追記:取り消し線部分で計算ミスしていました。
結局、この話は254がreverse_mappingで(254.9960.../256)にマッピングされる、というような言い方しかできないような気がします。
そんな難しい話? (スコア:0)
unsigned char mapping(float aF) {
return (unsigned char)(255.0f * aF + 0.5f)
}
で良いんじゃ?
aF が [0,1]の範囲ならちゃんと動きます
それ二十ウン年前にPC98のパレットで見たわー (スコア:0)
MAG(鮪)のドキュメントでは早くもその真理にたどり着いて、16階調から256階調への変換では*17を推奨していたのに対して、Piのドキュメントでは「素直」に考えたとおり16を掛けていたのが面白いですね。
段階数は丸めるときにのみ考慮する必要がある (スコア:0)
なぜ、最初の式でfloorが使われているのかが意味不明。最初からroundを使っておけば何も問題はない。自然科学でも日常でも当たり前の処理。
次に、256段階あるからと言って、256をかけるのも意味不明。段階数をかけるのなら0,127,255の3段階の場合は3をかけるのか?段階数は値を丸めるときに必要なだけで、丸める前においては段階数を考慮する必要はない。
段階数に固執するということは、分かっていないということ?