HSV を RGB に変換しよう
2012/04/05 コメントを残す
さて、ちょっとした用件で色空間の変換をする事になりました。
個人的にカラーピッカーのようなユーザインタフェースは HSV が分かりやすいかなと思っているので、HSV でカラータイルを作成するプログラムを作成しました。
大昔、それも学生の頃、何度か色空間の変換をするプログラムは書きました。 C++ や Pascal、処理系も様々でしたが、一応動いていました。 しかし、あまりに古いため手元にソースコードがあるか定かではありません。 どこかにあったはず… で探して見つかるような生易しい環境ではないのです。
仕方ないので、手元の書籍やネットで検索してアルゴリズムを拾ってきました。 が、ネット上のほとんどの資料は色相 (Hue) を 360 度として扱っているものがほとんどで、1.0 に正規化されているものすら見当たりません。 探し方に問題があったのかも知れませんが。
今回のターゲットプラットフォームは結構チープですので、1.0 に正規化して float や double で計算すると言うのは正直無し線です。 適当な整数でデータを保持して扱うように仕様を定めました。 32ビット整数の中に HSV の三つのフィールドを保持できると言う事で、下位から VSH の順に 10ビットずつ使う事にしました。 つまり、それぞれの値が 0 から 1023 までの範囲を持てるわけです。
さて、ここから実際のプログラムを書くわけですが、チープな環境で掛け算、割り算、余りの計算を繰り返すとかなり処理が遅くなってしまうため、ビットシフトを使って掛け算や割り算を代替する手法をメインにしました。 以下のようになります。
HSV::operator Color( ) const { if( Sat( ) <= 0 ) { return Color( 0 ); } const int P = 6; const int R = FIELD_BITS; // 10 const int S = FIELD_BITS + RGB_MULTIPLY; // 10 + 2 const int M = FIELD_MAX; // 1024 const int h = Hue( ); // 0-1024 const int s = Sat( ); // 0-1024 const int v = Val( ); // 0-1024 const int d = (h * P) % (P << R) >> R; // 0-5 const int n = (h * P - (d << R)); // 0-1024 (!!!!) const int x = (v * ((M ) - (s ))) >> ( S); const int y = (v * ((M << R) - (s * (0 + n)))) >> (R + S); const int z = (v * ((M << R) - (s * (M - n)))) >> (R + S); const int w = v >> RGB_MULTIPLY; switch( d ) { case 0: return Color( w, z, x ); case 1: return Color( y, w, x ); case 2: return Color( x, w, z ); case 3: return Color( x, y, w ); case 4: return Color( z, x, w ); case 5: return Color( w, x, y ); default: __assume( 0 ); } }
処理系が Visual C++ ですので、少しだけ環境依存のコードがありますが気にしないで下さい。
HSV クラスの Color 型への変換オペレータとして実装しました。 Color 型は Windows SDK の COLORREF 構造体(DWORD の別名ですが)の派生クラスのようなものです。 R、G、B の三つの引数からインスタンスを生成するコンストラクタがあります。 Hue( )、Sat( )、Val( ) はそれぞれ HSV のコンポーネントを取得するメンバ関数です。
上のサンプルプログラムは可読性のために若干冗長になっていますが、コンパイラが最適化するので問題ありません。
今回ハマったのは、(!!!!) の部分です。 一つ上の d を取得する処理は、h が 6 の倍数ではないため一旦 6 を掛けたうえで 6の余りを取得し、それを 1024、つまり 10ビットシフトして 0 から 5の値を取得しています。 この値は最後の switch( ) で使用しますが、すぐ次の行でも使用しています。
h、つまり色相に 6 を掛け、先ほど取った余りに 1024 をかけ、お互いに 0 から 6113(1024 の 6倍)の範囲で引き算を行っています。 理屈上、値の範囲は少なくとも 0から 6113 です。 つまり、有効桁 10ビット + 6倍と言う訳ですね。
そして問題が次の次の行。 s、つまり彩度に n を足しています。 そのあと他の数値と掛けたり割ったりしていますが、全て同じ次元、有効桁が 10なら 10同士で計算を行っています。 しかし、M << R と s * n を引き算する式には疑問があります。 式を書き換えると、M * 1024 – s * n となりますが、前半は 10ビットの有効桁をさらに 10ビット追加し、後半は 10ビットの値に 10ビット + 6の有効桁を持つ数値を持ってきています。 普通に考えると、後ろの式は一旦 6倍の有効桁で保持しているため、6で割ってからでなければ計算には使えない…。 と思い込みました。
これが敗因です。
実は、 n を計算する行のコメントにあるように、n の値範囲は 0 から 1023 です。 事前に 6倍していても、範囲は変わりません。 なぜか。 それは元の値から、割り算の商に右辺値を掛けた値を引く事によって、割り算の剰余を取得しているだけだからです。 元の値が何であれ、その値を 1024で割った余りが n となるので、この値は後で使用する時に、6倍されていると考えてはいけないのでした。
さて、私はこれに気付くのに何分かかったでしょうか。 大体 60分前後かかりました…。 残念な事にがっかりな子です。 しょんぼり…。