パスワードを忘れた? アカウント作成
10788480 journal
日記

Torisugariの日記: 10で割るか11で割るか 3

日記 by Torisugari

私はプログラミングの例文集、いわゆる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)にマッピングされる、というような言い方しかできないような気がします。

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
  • by Anonymous Coward on 2014年03月25日 20時52分 (#2569160)

    unsigned char mapping(float aF) {
        return (unsigned char)(255.0f * aF + 0.5f)
    }
    で良いんじゃ?

      aF が [0,1]の範囲ならちゃんと動きます

  • by Anonymous Coward on 2014年03月25日 21時16分 (#2569183)

    MAG(鮪)のドキュメントでは早くもその真理にたどり着いて、16階調から256階調への変換では*17を推奨していたのに対して、Piのドキュメントでは「素直」に考えたとおり16を掛けていたのが面白いですね。

  • by Anonymous Coward on 2014年03月26日 0時42分 (#2569305)

    なぜ、最初の式でfloorが使われているのかが意味不明。最初からroundを使っておけば何も問題はない。自然科学でも日常でも当たり前の処理。

    次に、256段階あるからと言って、256をかけるのも意味不明。段階数をかけるのなら0,127,255の3段階の場合は3をかけるのか?段階数は値を丸めるときに必要なだけで、丸める前においては段階数を考慮する必要はない。

    段階数に固執するということは、分かっていないということ?

typodupeerror

最初のバージョンは常に打ち捨てられる。

読み込み中...