この章では、色に着目した画像変換を利用した圧縮方法について説明したいと思います。
前の章でも少し説明をしましたが、ディスプレイ上でカラー画像を表す時の色成分としては現在 RGB が一般的に使用されています。RGB は、光の三原色である R(赤) G(緑) B(青) で構成される表色系で、それぞれの色成分が持つ値(強さ)の組み合わせによって様々なカラーを作り出すことが可能になります。他にも表色系の例として、YMC, YOQ, YUV などが挙げられます。これらについては、以前「(3) ペイントルーチンの応用」で紹介してありますので、「表色系について」をご覧下さい。
各色成分の強さは値の強弱で表されます。例えば赤色を表したい場合は、赤成分のみを最大値にして他の成分を 0 にすればいいわけです。HTML ドキュメントでは、背景やフォントの色を RGB 表色系で定義することができるので、参考までに色コードの値により表示される実際の色をいくつか挙げておきます。HTML では色コードを#[赤][緑][青]で表し、各成分は 16 進数 2 桁 ( 00 - FF ) で表すため 256 段階で大きさを指定することが可能です。
Black(#000000) | White(#FFFFFF) | Red(#FF0000) | Green(#00FF00) | Blue(#0000FF) |
Yellow(#FFFF00) | Magenta(#FF00FF) | Cyan(#00FFFF) | Gray(#C0C0C0) | Orange(#FF7F00) |
上の例で、各色成分が同じ値のもの ( Black, White, Gray ) は彩度の全くない色 ( 無彩色 ; 黒〜灰色〜白 ) になることがわかります。
HTML の場合、色コードは各成分ごとに 256 段階で大きさを指定できるため約 1677 万色まで表現することが可能になります ( これを True Color といいます )。しかし、ハードの制限などでディスプレイではそれほどたくさんの色を表現することができないような場合、各成分の端数を切り捨てて表示するような必要が生じます。極端な場合、True Colorの画像をFAXで送るようなときは、白黒の二値にまで切り捨てなければならず、単純に切り捨てただけでは何の画像かわからなくなってしまいます。そこで、画像の劣化を抑えつつ減色を行う方法について、いくつか紹介をしたいと思います。
これから RGB 成分を加工するサンプル・プログラムを作成する必要があるので、RGB 成分を表現するクラスを用意します。今までは必要なメンバ関数だけ定義してその中身までは記載していませんでしたが、ここで実装も含めて示しておきます。
/** RGB成分用パレット構造体 **/ struct RGB { // 型宣言 typedef unsigned int color_type; // 色成分の型 typedef unsigned char primary_type; // RGB各成分の型 typedef size_t size_type; // 添字の型 // 定数宣言 static const primary_type MAX = UCHAR_MAX; // RGB各成分の最大値 static const color_type MASK = UCHAR_MAX; // RGB各成分のマスク static const primary_type BIT = CHAR_BIT; // RGB各成分のビット数 // 添字定義用定数 static const size_type RED = 0; // 赤成分の添字 static const size_type GREEN = 1; // 緑成分の添字 static const size_type BLUE = 2; // 青成分の添字 static const size_type ALPHA = 3; // アルファチャンネル成分の添字 static const size_type RGB_SIZE = 3; // (アルファチャンネルを除いた)RGB成分のサイズ static const size_type RGBA_SIZE = 4; // RGBA成分のサイズ // 公開メンバ変数 primary_type r; // 赤成分 primary_type g; // 緑成分 primary_type b; // 青成分 primary_type a; // アルファチャンネル /* 色コードから各成分を取得する関数 */ /// 色コードから赤成分を抽出する /// /// 赤成分は、color_type の下位より 16 から 23 ビットまでとする /// ... OOOOOOOO xxxxxxxx xxxxxxxx /// /// color 対象の色コード /// 戻り値 抽出した赤成分 static primary_type getRed( color_type color ) { return( ( color >> 16 ) & MASK ); } /// 色コードから緑成分を抽出する /// /// 緑成分は、color_type の下位より 8 から 15 ビットまでとする /// ... OOOOOOOO xxxxxxxx /// /// color 対象の色コード /// 戻り値 抽出した緑成分 static primary_type getGreen( color_type color ) { return( ( color >> 8 ) & MASK ); } /// 色コードから青成分を抽出する /// /// 青成分は、color_type の下位より 0 から 7 ビットまでとする /// ... OOOOOOOO /// /// color 対象の色コード /// 戻り値 抽出した青成分 static primary_type getBlue( color_type color ) { return( color & MASK ); } /// 色コードからアルファチャンネルを抽出する /// /// アルファチャンネルは、color_type の下位より 24 から 31 ビットまでとする /// ... OOOOOOOO xxxxxxxx xxxxxxxx xxxxxxxx /// /// color 対象の色コード /// 戻り値 抽出したアルファチャンネル static primary_type getAlpha( color_type color ) { return( ( color >> 24 ) & MASK ); } /* コンストラクタ */ /// RGB各成分からの構築 /// /// alpha 値を指定しない場合はデフォルト値として RGB::MAX で初期化される /// /// red 赤成分 /// green 緑成分 /// blue 青成分 /// alpha アルファチャンネル RGB( primary_type red, primary_type green, primary_type blue, primary_type alpha = MAX ) : r( red ), g( green ), b( blue ), a( alpha ) {} /// 色コードからの構築 /// /// c 対象の色コード /// hasAlpha アルファチャンネルを持つか ? RGB( color_type c, bool hasAlpha ) { r = getRed( c ); g = getGreen( c ); b = getBlue( c ); a = ( hasAlpha ) ? getAlpha( c ) : MAX; } /// デフォルト・コンストラクタ /// /// RGB 成分は全てゼロ、アルファチャンネルもゼロとする RGB() : r( 0 ), g( 0 ), b( 0 ), a( 0 ) {} /* 各成分の取得 */ /// RGB各成分とアルファチャンネルを引数側に渡す /// /// red 赤成分を渡す変数へのポインタ /// green 緑成分を渡す変数へのポインタ /// blue 青成分を渡す変数へのポインタ /// alpha アルファチャンネルを渡す変数へのポインタ void sendToParam( primary_type* red, primary_type* green, primary_type* blue, primary_type* alpha ) const { if ( red != 0 ) *red = r; if ( green != 0 ) *green = g; if ( blue != 0 ) *blue = b; if ( alpha != 0 ) *alpha = a; } /// RGB各成分を引数側に渡す /// /// red 取得した赤成分 /// green 取得した緑成分 /// blue 取得した青成分 void sendToParam( primary_type* red, primary_type* green, primary_type* blue ) const { if ( red != 0 ) *red = r; if ( green != 0 ) *green = g; if ( blue != 0 ) *blue = b; } /// 引数から渡された RGBA 各成分をセットする /// /// red セットする赤成分 /// green セットする緑成分 /// blue セットする青成分 /// alpha セットするアルファチャンネル void receiveFromParam( primary_type red, primary_type green, primary_type blue, primary_type alpha = RGB::MAX ) { r = red; g = green; b = blue; a = alpha; } /// 添字を利用して各成分への参照を返す /// /// index 対象の成分への添字 /// 戻り値 対象の成分への参照 primary_type& operator[]( size_type index ); }; /* RGB::operator[] : 添字を利用して各成分への参照を返す 未定義の size_type が指定された場合、例外を投げるようにしている。 */ RGB::primary_type& RGB::operator[]( size_type index ) { switch ( index ) { case RED : return( r ); case GREEN : return( g ); case BLUE : return( b ); case ALPHA : return( a ); default : throw std::out_of_range( "Out of range at RGB::operator[]" ); } }
一般的に、True Color における RGB カラーモデルは各成分を 1 バイト ( 8 ビット ) で表現します。
Red | Green | Blue | |||||||||||||||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
各成分を 16 ビットや 32 ビットで表現する 48 ビットカラー・96 ビットカラーというものも存在しますが、利用する機会はほとんどありません。従って、RGB 成分全体は 32 ビット符号なし int 型、各成分は 8 ビット符号なし char 型で表現し、それぞれ color_type, primary_type と型名を定義しています。また、添字での指定ができるよう、size_type 型を定義しています。具体的な使い方についてはこの後詳しく説明します。
実際の各成分は、R・G・B 各成分の順でそれぞれ r, g, b メンバ変数で表します。これらは公開されているので、例えば color.r などと指定すれば赤成分を抽出・代入することができます。全成分は 32 ビット int 型一つで表現することができるので、画像から色を取得したり描画する時にまとめて処理できるように全成分を一つの変数でまとめるというやり方もあります。しかし、各成分を取得するときにはビット操作が必要になるので、どちらも一長一短がありそうです。少々強引なやり方として、型変換を活用する方法もあります。
color_type rgb; primary_type r() { return( ( (primary_type*)&rgb )[RED] ); } :
color_type 型の変数 rgb のアドレスを primary_type 型配列の先頭アドレスと見立てて、添字を使って成分を抽出する方法です。しかし、この場合は変数の中のどの位置に各成分が代入されるかが定まりません。各バイトの配置は機種によって異なり、モトローラの MC68000 シリーズなどに代表される「ビッグエンディアン (Big-endian)」と、インテルの x86 シリーズに代表される「リトルエンディアン (Little-endian)」などがあります。前者はデータの上位バイトをメモリの先頭側から配置する方法、後者は逆に末尾側から配置する方法です。画像データに色コードをセットする場合は配置が統一されているので、機種によって配置を考慮しておく必要があります。このような互換性・移植性の問題が発生するので、今回は素直に各成分用の変数を用意しています。
color_type から各成分を取得するときは、最下位から順に B, G, R と並んでいるものとします。最上位 1 バイト分は「アルファ・チャンネル (Alpha Channel)」と呼ばれる補助データで、通常は不透明度を表すために利用されます。この領域は、画像データによって利用できたり、できなかったりします。
最後に、添字を使って各成分を取得するために operator[] 関数を定義しています。ここでは size_type 型の変数 index を引数に、その値に応じた成分への参照を返します。これによって、例えば
RGB rgb;
rgb[RGB::RED] = 0xFF; // 赤成分を最大値に
のように添字で各成分を指定することができるようになります。特に有用なのがループ処理を行う場合で、
RGB rgb;
for ( RGB::size_type i = RGB::RED ; i < RGB::RGB_SIZE ; ++i )
rgb[i] = 0xFF; // 各成分を最大値に
のように処理することができます。なお、各成分を配列 "primary_type c_[RGBA_SIZE]" という形で表現すれば、operator[] 関数の内容をもう少し簡単にすることができますが、各成分を単独で取得したい場合にメンバ変数に直接アクセスすることができなくなります。この場合はアクセス用のメンバ関数を以下のように用意することになるでしょう。
primary_type& r() { return( c_[RGB::RED] ); }
まずは、単純に各成分の端数を切り捨ててしまう処理 (色の量子化) について説明します。
RGB の各色成分は 1 バイトで表現され、0 から 255 までの 256 階調の値を取り得ることは前に説明した通りですが、この各色成分の下位ビットを単純に切り捨ててしまえば (つまり、微妙な色の変化を切り捨てることになります) 簡単に階調を落とすことができます。
True Color ( 8 ビット ; 256 階調 ) から High Color ( 5 ビット ; 32 階調 ) に階調を落とす場合、下位 3 ビット分をマスクすればいいわけですから、赤・緑・青の成分をそれぞれ r, g, b とすれば
r >>= 3; // 1111 1000 → 1 1111 g >>= 3; // 1111 1000 → 1 1111 b >>= 3; // 1111 1000 → 1 1111
と処理することで実現できます。あとはこれを 2 バイトに圧縮することで、1 ピクセルあたり 2 / 3 のサイズになります。
圧縮したデータを画像に展開するとき、いくつかの表現方法が考えられます。切り捨てた下位ビットはゼロでクリアするか、ビットを立てておくか、もしくは 0 から 255 までの値となるよう全体を等倍するかなど、その方法によって再現される画像は変化します。
色の量子化処理を実装する前に、RGB 成分を加工するための関数やクラスを準備しておきます。まず、論理積 (AND) や論理和 (OR) などのビット操作を行う関数群を用意します。
/* operator&= : RGB どうしの論理積 */ RGB& RGB::operator&=( RGB mask ) { r &= mask.r; g &= mask.g; b &= mask.b; return( *this ); } /* operator&= : 同一成分による RGB との論理積 */ RGB& RGB::operator&=( primary_type mask ) { r &= mask; g &= mask; b &= mask; return( *this ); } : /* operator>>= : RGB 各成分の右ビットシフト */ RGB& RGB::operator>>=( unsigned int shift ) { r >>= shift; g >>= shift; b >>= shift; return( *this ); } :
ここでは論理積 AND と右ビットシフトの代入演算子に対する多重定義関数 operator&= と operator>>= のみを記述していますが、その他の多重定義関数 operator|= ( 論理和 OR )、operator^= ( 排他的論理和 XOR )、operator<<= ( 左ビットシフト ) などは全く同じやり方で実装できるのでここでは省略しています。その内容も、RGB 各成分に対して演算を行うだけの簡単なものです。
論理積の演算対象として、他の RGB 成分と、同一の primary_type 成分の両方を指定することができるようにしてあります。たいていは、後者の同一成分による演算の方が利用率は高いと思います。また、ここでは RGB メンバ関数として実装していますが、通常の関数としても問題はありません。
次に、RGB 成分への処理を行う関数オブジェクトを用意します。
/** RGB 成分への処理用関数インターフェース RGB 成分に対して処理を行う関数オブジェクトを呼び出す場合に利用するインターフェース。 **/ struct RGB_Op_IF { /// RGB 成分への処理 (純粋仮想関数) virtual bool operator()( RGB* rgb ) = 0; /// 仮想デストラクタ (何もしない) virtual ~RGB_Op_IF() {} }; /** RGB 成分への処理用関数 RGB 成分に対する演算子の多重定義関数を呼び出すことができる **/ template<class T> class RGB_Op : public RGB_Op_IF { public: typedef RGB& (RGB::*Op)( T ); // RGB 成分に対する演算子の多重定義関数の型 private: Op f_; // RGB 成分に対する演算子の多重定義関数 T t_; // f_ に渡す第二引数 public: /* コンストラクタ */ /// RGB 成分に対する演算子の多重定義関数とその第二引数の値を指定して構築 /// /// 例えば、以下のコードは白色 (FF,FF,FF) をグレー (F0,F0,F0) に変える /// /// RGB_Op<RGB::primary_type> op( operator&=, 0xF0 ); /// RGB rgb( 0xFF, 0xFF, 0xFF ); /// op( &rgb ); /// /// f RGB 成分に対する演算子の多重定義関数 /// t f に渡す第二引数 RGB_Op( Op f, const T& t ) : f_( f ), t_( t ) {} /// コンストラクタで指定した多重定義関数 f を呼び出す /// /// rgb 処理対象の RGB 成分へのポインタ /// 戻り値 f が未定義だった場合は false virtual bool operator()( RGB* rgb ) { if ( f_ == 0 ) return( false ); ( rgb->*f_ )( t_ ); return( true ); } }; /** 色のマスキング用クラス **/ template<class T> struct MaskColor : public RGB_Op<T> { /* コンストラクタ */ /// マスクパターンを指定して構築 /// /// mask マスクパターン /// andMask 論理積をとるか (true)、論理和をとるか (false) MaskColor( const T& mask, bool andMask ) : RGB_Op<T>( ( ( andMask ) ? static_cast<typename RGB_Op<T>::Op>( &RGB::operator&= ) : static_cast<typename RGB_Op<T>::Op>( &RGB::operator|= ) ), mask ) {} };
RGB_Op_IF は RGB 成分を処理する関数オブジェクトのインターフェースとして利用します。operator() は戻り値が bool 型で、引数は処理される RGB 成分へのポインタとなっています。処理結果を返して引数は変化させないような設計にする手段もありますが、判定処理などで利用することも想定してこのような設計にしました。
RGB_Op は実際に RGB 成分の処理を行うために使うことを目的としたクラスで、RGB_Op_IF クラスからの派生となっています。typedef を利用して、RGB 構造体のメンバ関数のポインタの型 Op を定義し、コンストラクタで Op 型のメンバ関数ポインタ f_ と第二引数 t_ を渡した上で、operator() 関数では
( rgb->*f_ )( t_ );
という形で、引数で渡された RGB 構造体 rgb のメンバ関数 *f_ を使い、コンストラクタでセットした t_ を引数として処理しています。これはちょうど、STL(Standard Template Library) にある mem_fun1_t (メンバ関数ポインタへのアダプタ) と bind2nd (バインダ) を組み合わせたクラスに相当します。このクラスは、先ほど実装した演算子の多重定義関数を呼び出して利用することを想定して作成しています。また、引数はテンプレートを使って任意の型が指定できるようにしてあるので、RGB 型と primary_type 型のどちらも利用できます。例えば、
RGB_Op<RGB::primary_type> op( static_cast<typename RGB_Op<T>::Op>( &RGB::operator&= ), 0xF0 );
とすれば、op( rgb ) で rgb の下位 4 ビットをゼロにすることができます。最後に、色のマスキング用関数オブジェクトとして MaskColor クラスを用意し、マスクパターンと論理積・論理和の選択をすることで任意の色コードをマスキングできるようにしています。
以上の関数を利用して、色の量子化を行うサンプル・プログラムを作成します。
/* Make_RGBMask : 指定した階調(ビット数) bit のマスクパターンを作成して返す bitInv が true なら作成したマスクパターンを反転して返す */ RGB::primary_type Make_RGBMask( unsigned char bit, bool bitInv = false ) { RGB::primary_type mask = RGB::MAX; // マスクパターン(全ビットを 1 に初期化) if ( bit < RGB::BIT ) for ( unsigned char i = 0 ; i < RGB::BIT - bit ; ++i ) mask <<= 1; return( ( bitInv ) ? ~mask : mask ); } /** 色の量子化処理用クラス **/ struct Quantize : public MaskColor<RGB::primary_type> { /* コンストラクタ */ /// 階調(ビット数) bit を指定して構築する /// /// setBit が true ならマスクする下位ビットを 1 に、そうでないならゼロにする /// 例えば、bit = 3, setBit = false ならマスクパターンは 1110_0000 になる。 /// bit = 5, setBit = true とすると、マスクパターンは反転して 0000_0111 になる。 /// /// bit 階調(残すビットの数) /// setBit マスクしたビットを ON にするなら true にする Quantize( unsigned char bit, bool setBit = false ); }; /** 色の量子化処理後にコントラストを調整するクラス **/ class QuantizeAndScaling : public RGB_Op_IF { unsigned char exBit_; // 量子化によって除外するビット数(0-7) unsigned int qMax_; // 量子化後の最大値 public: /* コンストラクタ */ /// 階調(ビット数) bit を指定して構築する /// /// bit 階調(残すビットの数) QuantizeAndScaling( unsigned char bit ); /* メンバ関数 */ /// 色の量子化処理とコントラスト調整を行う /// /// rgb 処理結果を代入する RGB 構造体へのポインタ virtual bool operator()( RGB* rgb ); }; /* Quantize コンストラクタ 階調(ビット数) bit を指定して構築する setBit が true ならマスクする下位ビットを 1 に、そうでないならゼロにする */ Quantize::Quantize( unsigned char bit, bool setBit ) : MaskColor<RGB::primary_type>( Make_RGBMask( bit, setBit ), ! setBit ) {} /* QuantizeAndScaling コンストラクタ 階調(ビット数) bit を指定して構築する */ QuantizeAndScaling::QuantizeAndScaling( unsigned char bit ) : exBit_( ( bit >= RGB::BIT ) ? 0 : RGB::BIT - bit ), qMax_( Make_RGBMask( exBit_, true ) ) {} /* QuantizeAndScaling::operator() : 色の量子化とコントラスト調整を行い、結果を rgb に代入する */ bool QuantizeAndScaling::operator()( RGB* rgb ) { // rgb が未定義な場合 false を返す if ( rgb == 0 ) return( false ); *rgb >>= exBit_; rgb->r = RGB::Check_RGBPrimary<unsigned int> ( static_cast<unsigned int>( rgb->r ) * static_cast<unsigned int>( RGB::MAX ) / qMax_ ); rgb->g = RGB::Check_RGBPrimary<unsigned int> ( static_cast<unsigned int>( rgb->g ) * static_cast<unsigned int>( RGB::MAX ) / qMax_ ); rgb->b = RGB::Check_RGBPrimary<unsigned int> ( static_cast<unsigned int>( rgb->b ) * static_cast<unsigned int>( RGB::MAX ) / qMax_ ); return( true ); }
Make_RGBMask は階調 bit を指定してマスクパターンを作成するための補助関数です。例えば、bit = 3 なら二進数で 1110 0000 という値を返します。bitInv はマスクパターンを反転するかどうかを指定するための引数で、これを true にするとマスクパターンは 0001 1111 となります。これは論理和 (OR) を使う場合に利用します。
Quantize クラスは MaskColor から派生したクラスで、MaskColor のもつ機能をほぼ全て流用しています。コンストラクタは、マスクパターンの階調 bit とマスクしたビットを立てるかを指定する setBit の二つを引数として、Make_RGBMask を利用してマスクパターンを作成しています。
QuantizeAndScaling クラスは、下位ビットを落とした後で最小値 ( 0 ) から最大値 ( 255 ) までの範囲の値になるよう調整する処理を行います。この場合は値の範囲は 0 から 255 までの間のままで、途中の値が抜けたような形になります。下位ビットをゼロクリアした場合、画像は全体的に暗めとなり、逆にビットをセットした場合は明るめの状態になります。QuantizeAndScaling を利用することで、コントラストは保ったまま階調を落とすことができるようになります。
このサンプル・プログラムでは、落としたい階調から作成したビットマスクでマスキングしたり、下位ビットを落とした後でコントラストの補正をしているだけなので、実際の圧縮は行っていません。圧縮する場合はそのデータを保持するテーブルが必要になりますし、圧縮データから画像を描画する場合のルーチンも新たに追加する必要がありますが、対応はそれほど難しくないと思います。
MaskColor によるビットマスクを利用した処理は、今回のような色の量子化の他にもいろいろな使い道があります。たとえば、赤・緑・青いずれかの成分だけを残したい場合は、残したい成分以外のビットを 0 にして論理積 (AND) を取るだけで実現できますし、全ビットを 1 にした状態で排他的論理和 (XOR) を取ると、色の反転 (補色への置換) ができます。
さてこのままでは、あまり階調を落としすぎると、画質の劣化がひどすぎて実用には耐えないでしょう。以前、「グラフィックパターンの扱い (6) スーパーサンプリング」の章で、階調を落とすことによる画質の劣化を防ぐための手段としてオーダードディザ法と誤差拡散法を紹介しましたが、これらの手法はまさにこのような処理に最適です。
これらのアルゴリズムは「(6) スーパーサンプリング」の章に詳しく紹介していますので、そちらをご覧下さい。ここでは、サンプルプログラムを載せておくだけにします。
/// 浮動小数点型のデータを小数点以下四捨五入して整数型として返す /// /// d 対象データ(浮動小数点型) /// 戻り値 四捨五入した整数値 inline int RoundHalfUp( double d ) { if ( d > 0 ) d += 0.5; else d -= 0.5; return( static_cast<int>( d ) ); } /* MakeDitherMatrix : サイズ size のディザ・マトリックスを dither 上に作成する size は 2 のべき数を表すので、実際に作成される行列のサイズは 2 の size 乗になる また、size の有効範囲は 1 から CHAR_BIT までとし、 ゼロなら 1 に、CHAR_BIT より大きければ CHAR_BIT に変換してから処理を行う */ void MakeDitherMatrix( unsigned char size, vector<int>* dither ) { if ( size == 0 ) size = 1; if ( size > CHAR_BIT ) size = CHAR_BIT; unsigned int matSize = ( size > 0 ) ? ( 1 << size ) : 2; dither->resize( matSize * matSize ); // 2 x 2 サイズで初期化 ( *dither )[0] = 0; ( *dither )[1] = 2; ( *dither )[matSize] = 3; ( *dither )[matSize + 1] = 1; // ひとつ前のサイズの行列の要素を元に構築 for ( unsigned int cnt = 2 ; cnt < matSize ; cnt *= 2 ) { for ( unsigned int r = 0 ; r < cnt ; ++r ) { for ( unsigned int c = 0 ; c < cnt ; ++c ) { ( *dither )[r * matSize + c] *= 4; ( *dither )[r * matSize + cnt + c] = ( *dither )[r * matSize + c] + 2; ( *dither )[( r + cnt ) * matSize + c] = ( *dither )[r * matSize + c] + 3; ( *dither )[( r + cnt ) * matSize + cnt + c] = ( *dither )[r * matSize + c] + 1; } } } // 中央値で減算する(平均値をゼロにする) for ( unsigned int r = 0 ; r < matSize ; ++r ) for ( unsigned int c = 0 ; c < matSize ; ++c ) ( *dither )[r * matSize + c] -= matSize * matSize / 2; } /** オーダード・ディザ処理用クラス 量子化等の処理をする前に、オーダード・ディザ処理を行う。 例えば、 Quantize q( 3 ); // 3 ビットを残して量子化 OrderedDither od( 3, 0.5, q ); // ディザ・マトリックスの大きさは 2 の 3 乗 = 8、比率は 0.5 GBox::FillBox( draw, od, Coord<int>( 0, 0 ), Coord<int>( 99, 99 ) ); とすることで、オーダード・ディザ処理をした上で量子化処理を行うことができる。 **/ class OrderedDither : public GPixelOp { unsigned char ditherSize_; // dither_ の行列数 double rate_; // dither_ 成分を加算するときの比率 RGB_Op_IF& op_; // RGB 成分の処理用関数オブジェクト (量子化など) std::vector<int> dither_; // ディザ・マトリックス public: /* コンストラクタ */ /// ディザ・マトリックスのサイズと加算時の比率、後処理用の関数オブジェクトを指定して構築 /// /// sz ディザ・マトリックスの行列数 ( 2 のべき数で表現 ) /// rate ディザ・マトリックスの成分を加算するときの比率 ( 負値も可 ) /// op RGB 成分の処理用関数オブジェクト ( 量子化など ) OrderedDither( unsigned char sz, double rate, RGB_Op_IF& op ); /// オーダード・ディザ処理を行う /// /// draw 描画領域 /// c 変換対象の位置 virtual bool operator()( DrawingArea_IF& draw, Coord<int> c ); }; /* OrderedDither コンストラクタ ディザ・マトリックスの行列数を 2 の sz 乗、 成分を加算するときの比率を rate、 後処理用関数オブジェクトを op として構築する */ OrderedDither::OrderedDither( unsigned char sz, double rate, RGB_Op_IF& op ) : ditherSize_( sz ), rate_( rate ), op_( op ) { MakeDitherMatrix( ditherSize_, &dither_ ); } /* AddDither : ディザ・マトリックスの要素 dither をパレット rgb に加算する rate は加算する比率を表す */ void AddDither( double dither, double rate, RGB* rgb ) { int add = RoundHalfUp( dither * rate ); rgb->r = RGB::Check_RGBPrimary<int>( static_cast<int>( rgb->r ) + add ); rgb->g = RGB::Check_RGBPrimary<int>( static_cast<int>( rgb->g ) + add ); rgb->b = RGB::Check_RGBPrimary<int>( static_cast<int>( rgb->b ) + add ); } /* OrderedDither::operator() : 描画領域 draw の位置 c にオーダード・ディザ処理を行う */ bool OrderedDither::operator()( DrawingArea_IF& draw, Coord<int> c ) { if ( &op_ == 0 || &draw == 0 ) return( false ); RGB rgb; if ( ! draw.point( c, &rgb ) ) return( false ); AddDither( dither_[( c.x % ditherSize_ ) + ( c.y % ditherSize_ ) * ditherSize_], rate_, &rgb ); if ( ! op_( &rgb ) ) return( false ); if ( ! draw.pset( c, rgb ) ) return( false ); return( true ); }
OrderedDither は、ディザマトリックスを作成して画素にその要素を加算するための関数オブジェクト用クラスです。ディザマトリックスの行列数はコンストラクタの中の引数 sz で渡しますが、この値は 2 のべき数を表しているので、実際の行列数は 2 の sz 乗になることに注意して下さい。また、値の範囲は 1 から CHAR_BIT ( 通常 8 ) までとしています。行列の要素は 0 から行列数の二乗までの値を取りますが、このサンプルでは要素の中央値を 0 として、それより低い値を負数、高い値を正数に変換しています。全て正数として加算処理してしまうと、全体的に白っぽい画像になってしまうため、それを防ぐためにこのような処理を行っています。
行列の数を増やすほど振れ幅は大きくなるため、処理による効果は大きくなります。影響する度合いを調整するために、加算する時に乗算する値 rate を設定することができるようにしています。これは 1 より大きい値を設定することで意図的に影響度を強めることもできます。また、負数にした場合は、ディザマトリックスの要素の符号が反転することになります。
コンストラクタにはもうひとつ、RGB 成分の処理用関数オブジェクト op を渡すことができます。これは、operator() 関数の中でディザマトリックスの要素を色コードに加算した後で呼び出されています。op に量子化処理を適用すれば、オーダードディザ処理を適用しながら量子化を行うことができることになります。なお operator() 関数には、対象の画像として draw、処理を行う対象のピクセル位置として c を渡しています。draw の持つメンバ関数 point と pset がそれぞれ呼び出されており、前者は draw からの色コードの抽出、後者は draw への描画を行います。
/* AddError : 誤差成分 error を RGB 成分 src に加算する 但し、補正のため量子化するビットマスク notMask の半分だけ減算する */ void AddError( const RGB& error, RGB::primary_type notMask, RGB* src ) { *src += error; *src -= notMask / 2; } /* ErrorDispersion : 描画領域 draw に対し、誤差分散法による処理を行う errorBit は誤差成分として扱う階調(ビット数)を表す。 op は誤差分散法を適用した後に行う変換処理(量子化処理など)の関数オブジェクトへの参照 errorBit と op は独立しているので、誤差成分として扱うビット数を量子化のそれと一致させる必要はない 量子化をせずに下位ビットを周囲に分散するようなこともできる (cpos) の誤差は以下の形で周囲に分散される。[mod] は分散された後に残った剰余分。 (cpos) [1/2] [mod] [1/8] [1/4] */ void ErrorDispersion( DrawingArea_IF& draw, unsigned char errorBit, RGB_Op_IF& op ) { // 誤差成分を保持するバッファ // 1 本は現在のライン上、もう 1 本はすぐ下側のライン上の誤差成分を保持する vector< vector<RGB> > buffer( 2 ); RGB::primary_type errorMask = Make_RGBMask( errorBit, true ); // 端数計算用ビットパターン Coord<int> sz = draw.size(); // 画像サイズ // ラインの長さ + 2 分だけバッファを確保 // +2 は両端からはみ出した分専用。両端で左下、右下への計算をしても問題ないようにするため。 // 従って、このはみ出した部分はピクセルへの加算はされない。 for ( unsigned int i = 0 ; i < buffer.size() ; ++i ) buffer[i].assign( sz.x + 2, RGB( 0, 0, 0 ) ); Coord<int> c; RGB rgb; for ( c.y = 0 ; c.y < sz.y ; ++( c.y ) ) { for ( c.x = 0 ; c.x < sz.x ; ++( c.x ) ) { draw.point( c, &rgb ); AddError( buffer[c.y % 2][c.x + 1], errorMask, &rgb ); RGB error = ( rgb & errorMask ) >> 1; // 端数の 1 / 2 op( &rgb ); draw.pset( c, rgb ); // 端数の 1 / 2 を右側に加算 buffer[c.y % 2][c.x + 2] += error; // 端数を 1 / 4 にして右下に代入 error >>= 1; buffer[( c.y % 2 ) ^ 1][c.x + 2] = error; // 端数を 1 / 8 にして左下に加算 RGB error_div8 = error >> 1; buffer[( c.y % 2 ) ^ 1][c.x] += error_div8; // x = 0 の場合は下側を初期化 if ( c.x == 0 ) buffer[( c.y % 2 ) ^ 1 ][c.x + 1] = RGB( 0, 0, 0 ); // 端数の残りを計算して下側に加算 error -= error_div8; buffer[( c.y % 2 ) ^ 1][c.x + 1] += error; } } }
オーダードディザ法とは異なり、誤差拡散法では加算する誤差項が動的に変化します。そのため、各ピクセル単位で処理を行う形ではなく、矩形領域を一括で処理する関数として作成しています。処理の内容は、以前紹介したサンプルと全く同じです。errorMask は誤差成分を得るためのビットマスクで、前述の関数 Make_RGBMask を使って引数の errorBit から生成されます。量子化などの処理を行うための関数オブジェクト op は外部から渡されるので、量子化処理時に落とされる階調と誤差項を計算するための階調 ( errorBit ) は必ずしも一致している必要はありません。さらに、量子化処理を行う必要もないため、単純に誤差項を周りに散りばめるノイズ生成処理としても利用することができます。
白や黒、灰色は無彩色と呼ばれ、各色成分は同じ値を取ります。よってカラー画像をモノクロ画像にすることで、データを 1 / 3 に圧縮することができます。
無彩色に変換する方法として、まず考えつくのは各色成分の最大値を採用するやり方です。
RGB::primary_type GetMaxFromRGB( const RGB& rgb )
{
return( std::max( std::max( rgb.r, rgb.g ), rgb.b ) );
}
std::max は二つの引数の大きい方を返す関数なので、上記の処理によって赤・緑・青各成分の中で最大の値を得ることができます。しかしこの場合、画像によっては変換結果がおかしくなることがあります。例えば、黄色と青で構成された画像を変換した場合、どちらも色成分としては最大値をとるため、変換結果は真っ白な画像になってします。
それでは各色成分の平均値を取る方法はどうでしょうか。
RGB::primary_type GetAveFromRGB( const RGB& rgb )
{
unsigned int sum = rgb.r + rgb.g + rgb.b;
return( sum / 3 );
}
この例ならうまく変換できるような気がしますが、赤と青で構成された画像を考えた場合、全て同じ色で塗り潰された画像に変換されてしまいます。しかし実際には、青よりも赤の方が明るく感じられるでしょう。このように人間の視覚は、単なる光の強弱だけではなく色調によっても明るさの判断に影響されてしまうため、それを考慮した変換が必要になります。
モノクロ画像に変換するには、輝度を算出する必要があります。輝度を算出するには、RGB → YIQ 変換、あるいは RGB → YUV 変換を使用しますが、どちらも輝度への変換式は以下のようになります。
上の式から、緑成分は最も輝度への影響が大きく、逆に青成分はほとんど影響しないことがわかります。このことは、経験上納得できるのではないでしょうか。
モノクロ化サンプル/* RGBtoY RGB各成分から Y 成分を求める Y 成分を求める計算式は Y = 0.299R + 0.587G + 0.114B const RGB& rgb : 対象の RGB 成分 戻り値 : 求めた Y 成分 */ double RGBtoY( const RGB& rgb ) { return( ( 0.299 * static_cast<double>( rgb.r ) ) + ( 0.587 * static_cast<double>( rgb.g ) ) + ( 0.114 * static_cast<double>( rgb.b ) ) ); } /** モノクロ変換用関数オブジェクト **/ struct Monochrome : public RGB_Op_IF { /* メンバ関数 */ /// モノクロ変換を行う /// /// rgb 処理結果を代入する RGB 構造体へのポインタ virtual bool operator()( RGB* rgb ); }; /* Monochrome::operator() : rgb にモノクロ化処理を行う */ bool Monochrome::operator()( RGB* rgb ) { if ( rgb == 0 ) return( false ); rgb->r = rgb->g = rgb->b = RGB::Check_RGBPrimary<int>( RoundHalfUp( RGBtoY( *rgb ) ) ); return( true ); }
RGBtoY は RGB 成分から輝度を求めるための関数で、先ほど紹介した変換式をそのまま利用して計算しているだけの単純なものです。モノクロ化の処理自体も非常に単純で、求めた輝度を RGB 各成分に代入するだけです。
前に述べた通り、True Color では一つの画像に一千万以上もの色を利用することが可能ですが、実際にそれほどたくさんのカラーを使った画像というものは数少なく、たいていはずっと少ない色で構成されているのが普通です。
例えば、True Color でただひとつの色によって塗りつぶされた画像があった場合、その一つの色のみを保存しておいて、あとは全て 1 ビットで一つのピクセルを表すようにすれば、一気に 1 / 24 に圧縮することができます。これは極端な例ですが、使用されている色の種類が少なければ、その色をテーブルに保管しておいて、各ピクセルは、テーブル内でその色が定義されているインデックスを使って表わすようにすることで画像を圧縮することができます。このような色の管理を「インデックス・カラー (Indexed Color)」、管理テーブルは「パレット (Palette)」と呼ぶのが一般的なようです。
パレット化を行うためのサンプル・プログラムを以下に示します。
/* WriteCoordToCharArray : int型の座標値 coord をunsigned char型配列 buffer に登録する */ void WriteCoordToCharArray( const Coord<int>& coord, vector<unsigned char>* buffer ) { unsigned int intSz = sizeof( coord.x ) / sizeof( char ); // int 型のサイズ // 上位側から順に書き込む for ( unsigned int i = intSz ; i > 0 ; --i ) { unsigned char c = ( coord.x >> ( i - 1 ) * CHAR_BIT ) & UCHAR_MAX; buffer->push_back( c ); } for ( unsigned int i = intSz ; i > 0 ; --i ) { unsigned char c = ( coord.y >> ( i - 1 ) * CHAR_BIT ) & UCHAR_MAX; buffer->push_back( c ); } } /* ReadCoordFromCharArray : unsigned char型配列の反復子 it から要素を取り出し int 型の座標値に変換する 変換した値は coord に代入される e は配列の終端の反復子を示し、これを超えてアクセスしようとした場合は処理を中断して false を返す */ bool ReadCoordFromCharArray( vector<unsigned char>::const_iterator& it, vector<unsigned char>::const_iterator e, Coord<int>* coord ) { unsigned int intSz = sizeof( coord->x ) / sizeof( char ); // int 型のサイズ // 上位側から順に登録されていることを前提 for ( unsigned int i = intSz ; i > 0 ; --i ) { if ( it == e ) return( false ); coord->x |= ( *it++ << ( i - 1 ) * CHAR_BIT ); } for ( unsigned int i = intSz ; i > 0 ; --i ) { if ( it == e ) return( false ); coord->y |= ( *it++ << ( i - 1 ) * CHAR_BIT ); } return( true ); } /* lessRGB : RGB の大小比較用関数オブジェクト */ struct lessRGB { /* operator() : rgb1 < rgb2 なら true を返す r, g, b の順に大きさを比較し、等しければ次の要素の結果を返すようにする */ bool operator()( const RGB& rgb1, const RGB& rgb2 ) { if ( rgb1.r != rgb2.r ) return( rgb1.r < rgb2.r ); if ( rgb1.g != rgb2.g ) return( rgb1.g < rgb2.g ); if ( rgb1.b != rgb2.b ) return( rgb1.b < rgb2.b ); return( false ); } }; /* PackPalet : 描画領域 draw の内容からパレット paletTable を作成し、packedData 上でデータを圧縮する パレット数が上限 UCHAR_MAX を超えた場合は false を返す */ bool PackPalet( const DrawingArea_IF& draw, vector<RGB>* paletTable, vector<unsigned char>* packedData ) { std::set<RGB,lessRGB> setPalet; // 画素の RGB 成分に対するインデックスを保持するテーブル Coord<int> sz = draw.size(); // 画像サイズ packedData->clear(); WriteCoordToCharArray( sz, packedData ); // 画像サイズの書き込み // パレットの作成 Coord<int> c; RGB rgb; for ( c.y = 0 ; c.y < sz.y ; ++( c.y ) ) { for ( c.x = 0 ; c.x < sz.x ; ++( c.x ) ) { draw.point( c, &rgb ); setPalet.insert( rgb ); if ( setPalet.size() > UCHAR_MAX ) return( false ); } } paletTable->assign( setPalet.begin(), setPalet.end() ); // データの圧縮 for ( c.y = 0 ; c.y < sz.y ; ++( c.y ) ) { for ( c.x = 0 ; c.x < sz.x ; ++( c.x ) ) { draw.point( c, &rgb ); packedData->push_back( static_cast<unsigned char>( std::distance( setPalet.begin(), setPalet.find( rgb ) ) ) ); } } return( true ); } /* UnpackPalet : パレット paletTable を使って packedData のデータを描画領域 draw に展開する 画像展開前にデータを全て読み込んだ場合は false を返す */ bool UnpackPalet( const vector<RGB>& paletTable, const vector<unsigned char>& packedData, DrawingArea_IF* draw ) { Coord<int> sz; // 画像サイズ vector<unsigned char>::const_iterator it = packedData.begin(); if ( ! ReadCoordFromCharArray( it, packedData.end(), &sz ) ) return( false ); // 画像サイズの読み込み draw->resize( sz ); Coord<int> c; for ( c.y = 0 ; c.y < sz.y ; ++( c.y ) ) { for ( c.x = 0 ; c.x < sz.x ; ++( c.x ) ) { if ( it == packedData.end() ) return( false ); draw->pset( c, paletTable[*it++] ); } } return( true ); }
まず、パレット paletTable を作成した後、各ピクセルの色コードをパレットのインデックスに置き換えてバッファに登録する処理を PackPalet 関数で行なっています。setPalet は STL ( Standard Template Library ) のコンテナクラス set のインスタンスで、RGB 成分がキーとして登録されます。set は要素の探索が効率よくできるように構成されており、重複したキーも登録されないため、重複のない要素を抽出するときに最適です。全ての RGB 成分が画像から抽出できたら内容を paletTable にコピーし、そのインデックスを圧縮データ packedData へ順番に書き込みます。圧縮データの展開は UnpackPalet関数が行っていますが、この関数はバッファからインデックスを取り出して、そのインデックスに該当する色コードを paletTable から抽出・描画する処理を行っているだけです。
この方法は処理がシンプルである反面、パレットの大きさに制限があるためそれを超える数の色コードを持った画像には対応できません。よって、どんな画像にも対応するために、もう少し工夫する必要があります。
パレットが満杯になった場合、テーブルの中から適当な場所を選んでそこに登録された色コードを捨てて、代わりに新しい色コードを登録するような処理を行えば、テーブルの大きさによらず、どのような画像も処理できるようになります。このような処理を行うアルゴリズムとして、ほとんどの OS が持っている仮想記憶の管理機能で利用されているものを流用することができます。
仮想記憶を「ページ ( Page Table )」と呼ばれるいくつかのブロックで管理する時、利用できる仮想記憶の領域が不足した場合にどのページをページアウト ( 新たなデータを書き込むためにページのいずれかを空にすること ) するかを決める時の方式として主に二種類あります。一つは「LRU ( Least Recently Used ) 法」で、この方式では「最も長い間参照されていないページ」をページアウトします。他方、「FIFO ( First In First Out ) 法」というのもあり、こちらは「最も古くから存在するページ」をページアウトします。例えば、以下の順番でページアクセスを行ったとき、Page2 の参照時にページフォールト ( 存在しないページにアクセスしようとすること ) が発生した場合、「LRU 法」では最も前に参照されたページの Page1 を、「FIFO 法」では最も前にページイン ( データをページに書き込むこと ) された Page0 をそれぞれページアウトすることになります。
「LRU 法」では、頻繁にアクセスされるページが優先されるため、アクセス頻度に偏りがある場合は効率よく処理を行うことができますが、アクセス頻度の偏りが小さいような場合は、参照されたかをチェックする処理が余分に必要となるため不利になります。逆に「FIFO 法」では、スタックがあふれたら一番先頭をページアウトするだけでよいため、処理は「LRU 法」と比較すると簡潔にまとめることができますが、アクセス頻度に偏りがある場合は効率よく処理を行うことはできません。
画像の場合、一般的には使用される色コードにはある程度の偏りがあることが見込まれる ( パレット化もそれを利用して圧縮を行っています ) ため、アクセス頻度にも偏りがあると推測することができます。前回紹介した「PICフォーマット」では、パレット化を行う際に「LRU 法」を利用しているので、以下のサンプルではそれに従って「LRU 法」を採用しています。
「LRU 法」を利用したパレット化のサンプル・プログラムを以下に示します。
const unsigned int LRU_TABLE_SIZE = UCHAR_MAX - 1; // LRU テーブルのサイズ const unsigned int LRU_IDENT = LRU_TABLE_SIZE + 1; // パレット・色コード判別子 /* LRU_Info : パレットとアクセス場所の記録用構造体 */ struct LRU_Info { RGB rgb; // 色コード unsigned int count; // 最後にアクセスされたときのピクセル番号 // コンストラクタ LRU_Info( const RGB& rgb_, unsigned int count_ ) : rgb( rgb_ ), count( count_ ) {} }; /* パレット LRU_Table から lruInfo と同色のパレットを探し、なければ追加する テーブルが満杯だった場合、一番古くにアクセスされた場所に上書きする(LRU法) */ vector<LRU_Info>::iterator Insert_LRU_Table( const LRU_Info& lruInfo, vector<LRU_Info>* LRU_Table ) { vector<LRU_Info>::iterator it = LRU_Table->begin(); // パレットのアクセス位置を最初の要素に初期化 vector<LRU_Info>::iterator minCntInfo = it; // 最も古くに参照された要素を最初の位置で初期化 for ( ; it != LRU_Table->end() ; ++it ) { // 一致する色コードが見つかったらその場所を返す if ( it->rgb == lruInfo.rgb ) { it->count = lruInfo.count; // FIFO法にする場合は、この行を削除 return( it ); } // 最も古くに参照された場所を保持 if ( minCntInfo->count > it->count ) minCntInfo = it; } // すでにテーブルはいっぱい if ( LRU_Table->size() >= LRU_TABLE_SIZE ) { *minCntInfo = lruInfo; // まだ空きがある } else { LRU_Table->push_back( lruInfo ); } return( LRU_Table->end() ); } /* LRU法を使い、画像 draw を packedData にデータ圧縮する */ void Pack_LRU_Table( const DrawingArea_IF& draw, vector<unsigned char>* packedData ) { Coord<int> sz = draw.size(); vector<LRU_Info> LRU_Table; LRU_Info lru( RGB( 0, 0, 0 ), 0 ); packedData->clear(); WriteCoordToCharArray( sz, packedData ); Coord<int> c; for ( c.y = 0 ; c.y < sz.y ; ++( c.y ) ) { for ( c.x = 0 ; c.x < sz.x ; ++( c.x ) ) { draw.point( c, &( lru.rgb ) ); lru.count = c.y * sz.x + c.x; vector<LRU_Info>::iterator it = Insert_LRU_Table( lru, &LRU_Table ); // パレットに同じ色があったらパレットコードを登録 if ( it != LRU_Table.end() ) { packedData->push_back( static_cast<unsigned char>( std::distance( LRU_Table.begin(), it ) ) ); // パレットに同じ色がなかったら識別子と色コードを登録 } else { packedData->push_back( LRU_IDENT ); packedData->push_back( lru.rgb.r ); packedData->push_back( lru.rgb.g ); packedData->push_back( lru.rgb.b ); } } } } /* LRU法を使った圧縮データ packedData を描画領域 draw に展開する */ bool Unpack_LRU_Table( const vector<unsigned char>& packedData, DrawingArea_IF* draw ) { Coord<int> sz; // 画像サイズ vector<unsigned char>::const_iterator it = packedData.begin(); if ( ! ReadCoordFromCharArray( it, packedData.end(), &sz ) ) return( false ); draw->resize( sz ); vector<LRU_Info> LRU_Table; LRU_Info lru( RGB( 0, 0, 0 ), 0 ); Coord<int> c; for ( c.y = 0 ; c.y < sz.y ; ++( c.y ) ) { for ( c.x = 0 ; c.x < sz.x ; ++( c.x ) ) { unsigned char palet = *it++; if ( palet == LRU_IDENT ) { lru.rgb.r = *it++; lru.rgb.g = *it++; lru.rgb.b = *it++; lru.count = c.y * sz.x + c.x; // パレットには同じ色はないはず if ( Insert_LRU_Table( lru, &LRU_Table ) != LRU_Table.end() ) return( false ); } else { lru.rgb = LRU_Table[palet].rgb; // FIFO法にする場合は、下行を削除 LRU_Table[palet].count = c.y * sz.x + c.x; // ピクセル番号を更新 } draw->pset( c, lru.rgb ); } } return( true ); }
Pack_LRU_Table 関数では、パレットへの登録とデータの圧縮を同時に行っています。ピクセルの色コードと同じものがテーブルに存在しないかをチェックして、もしあればそのインデックスを圧縮データとして登録します。しかし、同色がテーブルになかった場合はその色コードを新たにテーブルへ登録して、圧縮データには色コードをそのまま登録しておきます。このとき色コードの前に、インデックスと色コードを識別するための識別子 ( LRU_IDENT ) をいっしょに登録しておくことで、両者を識別できるようにしておきます。
パレットへの登録時には、現在チェックしているピクセルが何番目であるかをカウンタとしていっしょに記録しておきます。この値は、一致した色コードが見つかった際も更新を行い、これによってどこでアクセスしたかを記憶することができます。一致した色コードが見つからず、またテーブルもいっぱいだった場合は、このカウンタが最も小さな値 ( = 最も前に参照された色コード ) である要素を上書きします。色コードとカウンタをセットで登録しておく必要があるので、そのための構造体として LRU_Info が定義されています。
Unpack_LRU_Table関数は、圧縮データから一つずつ要素を読み込んで、その値が識別子 ( LRU_IDENT ) ならばそのあとに登録されている色コードを読み込んで画像に展開すると同時にパレットへの登録を行い、そうでないならばパレットから該当する色コードを読み込んでピクセルに描画していく処理を行います。つまり圧縮時だけではなく展開時にも、パレットを構築していくことになります。画像に並んだ色コードは圧縮時と展開時で変わることはないので、パレットが作成される過程も全く同じであることに注意して下さい。
以下に、3 x 3 の大きさの画像を、三つの要素を格納することができるパレットを使ってパレット化したときの例を載せておきます。
000000 | FF0000 | 000000 |
00FF00 | 0000FF | FF0000 |
FFFFFF | FFFFFF | 000000 |
PALET0 | PALET1 | PALET2 |
No. | 処理内容 | PALET0 | PALET1 | PALET2 | |||
---|---|---|---|---|---|---|---|
色コード | カウンタ | 色コード | カウンタ | 色コード | カウンタ | ||
1 | 000000 を PALET0 へ登録し、圧縮データに 03000000 を登録 | 000000 | 0 | ||||
2 | FF0000 を PALET1 へ登録し、圧縮データに 03FF0000 を登録 | 000000 | 0 | FF0000 | 1 | ||
3 | 000000 は PALET0 にあるので、圧縮データに 00 を登録 | 000000 | 2 | FF0000 | 1 | ||
4 | 00FF00 を PALET2 へ登録し、圧縮データに 0300FF00 を登録 | 000000 | 2 | FF0000 | 1 | 00FF00 | 3 |
5 | 0000FF を PALET1 へ登録し、圧縮データに 030000FF を登録 | 000000 | 2 | 0000FF | 4 | 00FF00 | 3 |
6 | FF0000 を PALET0 へ登録し、圧縮データに 03FF0000 を登録 | FF0000 | 5 | 0000FF | 4 | 00FF00 | 3 |
7 | FFFFFF を PALET2 へ登録し、圧縮データに 03FFFFFF を登録 | FF0000 | 5 | 0000FF | 4 | FFFFFF | 6 |
8 | FFFFFF は PALET2 にあるので、圧縮データに 02 を登録 | FF0000 | 5 | 0000FF | 4 | FFFFFF | 7 |
9 | 000000 を PALET1 へ登録し、圧縮データに 03000000 を登録 | FF0000 | 5 | 000000 | 8 | FFFFFF | 7 |
No. | 処理内容 | PALET0 | PALET1 | PALET2 | |||
---|---|---|---|---|---|---|---|
色コード | カウンタ | 色コード | カウンタ | 色コード | カウンタ | ||
1 | 03000000 を読み込み、000000 で描画して PALET0 に登録 | 000000 | 0 | ||||
2 | 03FF0000 を読み込み、FF0000 で描画して PALET1 に登録 | 000000 | 0 | FF0000 | 1 | ||
3 | 00 を読み込み、PALET0 ( = 000000 ) で描画 | 000000 | 2 | FF0000 | 1 | ||
4 | 0300FF00 を読み込み、00FF00 で描画して PALET2 に登録 | 000000 | 2 | FF0000 | 1 | 00FF00 | 3 |
5 | 030000FF を読み込み、0000FF で描画して PALET1 に登録 | 000000 | 2 | 0000FF | 4 | 00FF00 | 3 |
6 | 03FF0000 を読み込み、FF0000 で描画して PALET0 に登録 | FF0000 | 5 | 0000FF | 4 | 00FF00 | 3 |
7 | 03FFFFFF を読み込み、FFFFFF で描画して PALET2 に登録 | FF0000 | 5 | 0000FF | 4 | FFFFFF | 6 |
8 | 02 を読み込み、PALET2 ( = FFFFFF ) で描画 | FF0000 | 5 | 0000FF | 4 | FFFFFF | 7 |
9 | 03000000 を読み込み、000000 で描画して PALET1 に登録 | FF0000 | 5 | 000000 | 8 | FFFFFF | 7 |
ところで「FIFO 法」への切り替え方ですが、いたって簡単で、サンプルソースの中でコメントが書かれている二行を削除するだけで対応することができます。要は、アクセスした時のカウンタの更新をしないようにするだけで対応ができます。
サンプルソースを使った画像加工の結果を以下に示しておきます。画像サンプルとして、鳥取に行ったときに撮影した実家の写真を利用しました。
色の量子化テスト結果
色の量子化は選択した階調によって圧縮比が決まるので、選択した階調によって画像がどの程度劣化するかを示してあります。単純に量子化しただけでは微妙な色調の差が失われてしまうので、劣化がかなり激しい反面、ディザ法や誤差拡散法を利用すると劣化を大幅に抑えることができることが上の結果からわかると思います。それにディザ法や誤差拡散法は、圧縮用途のみならず画像への特殊効果としても利用することができます。なお、誤差拡散法として、サンプル・プログラムにはない「Floyd-Steinberg 法」と「Jarvis, Judice & Ninke 法」での処理結果もいっしょに載せてあります。これらは「グラフィックパターンの扱い (6) スーパーサンプリング」の方にサンプル・プログラムを掲載していますが、誤差の周囲への散らし方が異なるだけで基本的な考え方は同じです。
モノクロ化の結果でも、画像の濃淡が破綻することなく変換できていることが上の結果から理解できると思います。
次は、パレット化による圧縮率の検証です。まずは、前章で利用した「風景」画像 10 枚を使って、階調を量子化した後の単純パレット化によるパレットサイズの推移を確認します。
画像 | 階調 | ||
---|---|---|---|
4 ビット | 3 ビット | 2 ビット | |
画像 1 | failed | failed | 59 |
画像 2 | failed | 173 | 33 |
画像 3 | failed | 198 | 37 |
画像 4 | failed | 234 | 41 |
画像 5 | failed | 199 | 39 |
画像 6 | failed | 173 | 36 |
画像 7 | failed | 151 | 36 |
画像 8 | failed | 157 | 34 |
画像 9 | failed | 154 | 30 |
画像 10 | 218 | 57 | 19 |
表の中で、"failed" となっているのはパレットサイズが上限を超えた個所を表します。階調が少なくなれば、パレットの総数は減ることになるので、階調を落とすほどパレットの数も少なくなります。パレットサイズの最大数に応じてさらにビット数を落とすことで、圧縮率を上げることも可能ですが、上限を固定 ( 例えば 255 ) とした場合はいくらパレットの数を少なくしても圧縮率は変わりません。
画像 | 階調 | |||||||
---|---|---|---|---|---|---|---|---|
8 ビット | 4 ビット | 3 ビット | 2 ビット | |||||
データサイズ | 圧縮率 | データサイズ | 圧縮率 | データサイズ | 圧縮率 | データサイズ | 圧縮率 | |
画像 1 | 1840319 | 78.00 | 1023332 | 43.37 | 789506 | 33.46 | 786617 | 33.34 |
画像 2 | 2369204 | 100.42 | 846782 | 35.89 | 786959 | 33.36 | 786539 | 33.34 |
画像 3 | 2298659 | 97.43 | 793784 | 33.64 | 787034 | 33.36 | 786551 | 33.34 |
画像 4 | 2942918 | 124.74 | 973271 | 41.25 | 787142 | 33.36 | 786563 | 33.34 |
画像 5 | 2322497 | 98.44 | 907625 | 38.47 | 787037 | 33.36 | 786557 | 33.34 |
画像 6 | 3032492 | 128.53 | 865097 | 36.67 | 786959 | 33.36 | 786548 | 33.34 |
画像 7 | 2372450 | 100.56 | 790988 | 33.53 | 786893 | 33.35 | 786548 | 33.34 |
画像 8 | 2597312 | 110.09 | 805220 | 34.13 | 786911 | 33.35 | 786542 | 33.34 |
画像 9 | 2783084 | 117.96 | 813974 | 34.50 | 786902 | 33.35 | 786530 | 33.34 |
画像 10 | 1401245 | 59.39 | 787094 | 33.36 | 786611 | 33.34 | 786497 | 33.34 |
テスト用の画像は 1024 x 768 のサイズなので、ピクセル一つで 3 バイトなら合計で 2359296 バイトになります。LRU 法によってデータサイズがどの程度変化したかを表したのが上表で、実際のデータサイズ (バイト) と元の画像との比率 (%) で表してします。階調 8 バイトとは何も加工していない状態を表し、その他は単純な量子化によって階調を小さくした画像を処理しています。加工せずにパレット化をすると、逆にデータサイズが増えてしまうという状態になる場合もあります。また、階調が 2 ビットになると、全ての画像に対してデータサイズはほぼ一定となります。これは、パレットのサイズが足りなくなるという状態がほとんど発生しなくなったことを意味し、色コードを一度書き込んでしまえばほぼ全てがインデックスカラーで登録できてしまうためです。実際、圧縮率はほぼ 1 / 3 になり、これは、ほぼ全ての色コード 3 バイトがインデックスカラーのサイズ 1 バイトに変換されていることを意味します。なお、パレットに登録するインデックスカラーは、階調によらず 1 バイトで処理しているため、これを改良して、階調に応じてビット単位で圧縮するようにすればさらにデータサイズを小さくすることができます。
画像 | 階調 | |||||
---|---|---|---|---|---|---|
単純量子化 | オーダードディザ法 | 誤差拡散法 | ||||
データサイズ | 圧縮率 | データサイズ | 圧縮率 | データサイズ | 圧縮率 | |
画像 1 | 1023332 | 43.37 | 1029149 | 43.62 | 1029497 | 43.64 |
画像 2 | 846782 | 35.89 | 857744 | 36.36 | 861704 | 36.52 |
画像 3 | 793784 | 33.64 | 795761 | 33.73 | 794825 | 33.69 |
画像 4 | 973271 | 41.25 | 988712 | 41.91 | 1009547 | 42.79 |
画像 5 | 907625 | 38.47 | 924170 | 39.17 | 927788 | 39.32 |
画像 6 | 865097 | 36.67 | 874802 | 37.08 | 888854 | 37.67 |
画像 7 | 790988 | 33.53 | 793985 | 33.65 | 792632 | 33.60 |
画像 8 | 805220 | 34.13 | 809408 | 34.31 | 809714 | 34.32 |
画像 9 | 813974 | 34.50 | 831506 | 35.24 | 823517 | 34.91 |
画像 10 | 787094 | 33.36 | 787265 | 33.37 | 787421 | 33.38 |
単純な量子化と、オーダードディザ法、誤差分散法を組み合わせた場合を比較した結果が上表になります。階調は 4 ビットとし、オーダードディザ法では、ディザマトリックスのサイズを 23 = 8、比率を 1.00 としています。
オーダードディザ法、誤差分散法を利用すると、周囲の色コードの差異は大きくなり、その種類も増える傾向にあるため、データサイズは若干増えます。しかし、予想していたよりも増え方は小さいので、画像の劣化が抑えられるのであればむしろ積極的に利用したほうがいいようです。
前に戻る | タイトルに戻る |