| Filter Applet |
|---|
イメージに対するフィルタと一口に言っても、画像にぼかしをかけるような直接的なものから画像の一部を取り出すだけのものや、単に画像の情報をウィンドウで表示するようなものまでさまざまなものが存在します。フィルタとはある画像に対して何らかの作用をするもの (結果的に画像に変更が無くても) と言ったほうが良いでしょう。
左のアプレットでは写真に対してさまざまな効果を行うフィルタを制作してあります。Gray Scale では画像をモノクロに変換しますし、ネガでは画像をネガ反転させます。RGB, CMY カラーモデルで各色のチャンネルを取り出すフィルタも制作してあります。ソースコードはこちらです。
Java の低レベル画像処理では一般的なコンピュータグラフィックと同様に RGB カラーモデルが使用されています。一つの色はアルファカラーを付けた 32 ビット整数 0xAARRGGBB として扱われています。画像のフィルタリング処理を行う場合には、特に理由がなければアルファカラーを変更すべきではありません。ほとんどの処理系では『透明』か『不透明』にしかなり得ないためです。それぞれの基本色は以下のようにして取り出す事ができます。
int alpha = (rgb >> 24) & 0xFF; int red = (rgb >> 16) & 0xFF; int green = (rgb >> 8) & 0xFF; int blue = (rgb >> 0) & 0xFF; |
基本的に低レベルな画像処理を行う場合にはビット演算を使用して処理を行います。もちろんこれは処理効率の理由もあるのですが、色数値は符号ビットまで使用されているという理由もあります。ピクセルごとの画像処理では膨大な処理の繰り返しが行われるため、なるべく効率のよい方法を考えてください。
標準の Java では RGBImageFilter クラスを使用して画像を処理する事ができます。このクラスは画像のピクセルごとに色を変換する処理を行います。このクラスを使用するにはサブクラスで filterRGB() メソッドをオーバーライドします。例として縦方向の奇数スキャンを間引くフィルタ InterlaceFilter を定義してみましょう。
public class InterlaceFilter extends RGBImageFilter{
public InterlaceImageFilter(){
canFilterIndexColorModel = false;
return;
}
public int filterRGB(int x, int y, int rgb){
if(x % 2 == 0) return (rgb & 0xFF000000);
return rgb;
}
} |
コンストラクタで基本クラス RGBImageFilter の protected メンバ canFilterIndexColorModel を設定している点に注意してください。このメンバ変数は画像のフィルタ処理がピクセルの位置に依存するかどうかを表しています。もし位置に依存しないフィルタ処理であれば、インデックスカラーモデルの画像を処理するとき、そのカラーテーブルエントリだけを変更するだけで済むため、この値を true にすると処理効率が上がります。ここでは位置に依存するフィルタ処理を行うため値を false にしています。
filterRGB() メソッドは RGBImageFilter クラスで抽象メソッドとして定義されています。このメソッドは実際にピクセル色をフィルタ処理するために呼び出されます。ここでは渡された x 座標が奇数であれば黒を返すようにしています。アルファカラーを変更しないようにしている点に注意してください。もしコンストラクタで canFilterIndexColorModel の値を true にしていると、filterRGB() に渡される座標は無効なものになります。
さて、ここで制作したインターレースフィルタを使って画像を変換する処理を行ってみます。フィルタクラスは単体では何も行えないため、FilteredImageSource クラスを使用してフィルタリングを行います。
Image original; ... ImageProducer source = original.getSource(); ImageFilter filter = new InterlaceFilter(); ImageProducer producer = new FilteredImageSource(source, filter); Image image = createImage(producer); |
ここでは Component から派生したクラス内で使用されているものとします。Component.createImage() は ImageProducer から新しく画像クラス Image を制作するためのメソッドです。
画像データと言うのはとかく大きくなりがちです。とくに遅い回線を通して画像データの受送信を行う場合やフィルタリング処理を行う場合には結構な時間がかかってしまいます。もし画像データ全体の処理が終了するまで実行を停止してしまうようなプログラムを作った場合、画像をロードするたびに処理が停止してしまうでしょう。
これを避けるため、Java では画像を処理中でも実行を中断しないような (つまり非同期) 設計が行われています。ちょっとこの画像処理モデルについてここで解説しようと思います。
Java で画像を構築したとしましょう。しかし構築が終了した直後では、上記の理由でまだ画像の処理が完全に終わっていないと思われます。つまり『処理中』の画像が構築されてしまうわけです。
しかしプログラムからは画像の処理が完全に終了したかどうかが分からなければいけません。そこで画像クラス Image は ImageObserver に現在の処理状況を伝えてゆきます。したがって、画像をロードするときや処理するときには、画像の処理状況を伝えるための ImageObserver クラスが不可欠になります。
画像についてもっと抽象化して考えてみましょう。画像処理モデルでは画像のデータを供給するものと、そのデータを使用するものが存在します。画像の供給者とはある画像を加工して新しい画像を制作するものであったり、メモリ上の画像ピクセルデータを供給するものだったりします。つまり何らかの過程を経て画像を生成するのが供給者と言う事になります。Java では ImageProducer インターフェースを実装したクラスがこの供給者になる事ができます。
Java で定義されている供給者 (プロデューサ) は FilteredImageSource、ImageFilter、MemoryImageSource の 3 つです。FilteredImageSource は別のプロデューサが生成した画像にフィルタ処理を行って新しい画像を制作するために使用されます。ImageFilter は FilteredImageSource と共に使用されるフィルタクラスで、FilteredImageSource クラスの管理下で画像データの供給を行います。MemoryImageSource はメモリ上に保存されているデータから画像を制作するためのクラスです。
さて、次に画像を使用するクラスですが、Java では ImageConsumer インターフェースを実装したクラスがデータを使用するコンシューマとなります。標準として ImageFilter と PixelGrabber クラスを使用する事ができます。ImageFilter はある画像から別の画像を制作するわけですからコンシューマです。PixelGrabber はある画像からピクセルデータを配列として取り出すときに使用されます。
一般的に Java で画像処理を行う場合、ImageFilter クラスを使用したフィルタクラスを定義して FilteredImageSource 経由で行うか、もっと低レベルな処理が必要であれば PixelGrabber を使用してピクセル配列の処理を行い、MemoryImageSource 経由で画像を生成する方法が取られます。
ピクセル配列から画像を制作する場合、そのピクセル配列のカラーモデルが分かっていなければいけません。Java では ColorModel クラスを使用する事でピクセル配列から画像を生成する方法を指定できます。今の所インデックスカラーモデルから画像を生成する IndexColorModel クラスとダイレクトカラーモデル DirectColorModel クラスが定義されています。
IndexColorModel は構築時に指定されたカラーテーブルのデータにしたがってピクセル配列を RGB 値に変換して行きます。IndexColorModel に適用される配列には RGB 値ではなくカラーテーブルのエントリが入れられていなければいけません。DirectColorModel はピクセル配列内の値をそのまま色情報とみなします。
ColorModel サブクラスはカラーモデルに依存するピクセル値を受け取ってそれを RGB+alpha の値に変換する作業を行います。ピクセル値の幅も独自に設定できるため、ピクセルあたり 32 ビット以上の幅が必要なカラーモデルにも対応できます。
Java では画像の処理が終わらないまま次の処理に入る事ができます。従って、処理中に別の位置のピクセルが未処理のままである可能性があります。
一つの処理を行っている間に次の処理を開始するため、Java の画像処理モデルでは処理が終了した画像の一部分だけを次の処理に渡して行きます。つまり ImageFilter クラスへのピクセルデータの受け渡しは部分的に行われていることに注意してください。RGBImageFilter クラスの filterRGB() メソッドを使用しても別のピクセルの情報を取り出す事ができないと言うのはこのためです。
他のピクセル色に依存するようなフィルタ処理を制作するためには、PixelGrabber でいったん全てのピクセルの配列に変換してから、配列内での処理を行わなければいけません。モザイクやぼかしと言った実用的なフィルタを制作するのに Java 標準の ImageFilter クラスだけでは不十分です。これでできるのはせいぜい色の置き換え程度の事です。
Java では標準で ImageFilter クラスが定義されていますが、これはピクセルごとの処理しか行う事ができません。もっと高度なフィルタリング処理を行うためには、いったん画像をピクセル配列に展開する必要があります。
まず画像クラス Image からピクセルデータの配列を取り出す処理を行ってみましょう。ピクセル配列を取り出すのには PixelGrabber クラスを使用する事ができます。以下に画像 image から全てのピクセルを取り出す例を示します。
int[] pixels = new int[width * height];
PixelGrabber grabber;
grabber = new PixelGrabber(image, 0, 0, width, height, pixels, 0, width);
try{
grabber.grabPixels();
} catch(InterruptedException e){/* */}
if((grabber.status() & ImageObserver.ABORT) != 0){
// image fetch aborted or errored
} |
PixelGrabber 構築時にはピクセルを取り出す Image オブジェクトとその大きさを指定しています。コンストラクタは上記のように Image オブジェクトを指定する事もできますし、ImageProducer を指定する事もできます。
image に続く引数は画像から取り出すピクセルの領域です。この例では画像全体のピクセルを取り出すため、基準点 (0, 0) から画像の幅と高さを指定しています。 pixels とそれに続く値はピクセルをコピーする配列とその開始位置です。ここでは配列の先頭からピクセルを入れて行くように指定しています。最後のパラメータは、ピクセルを取り出すスキャンサイズです。これは配列上でのピクセル 1 行の大きさとなります。上記の例では 1 行ごとに隙間を空けずに取り出すよう指定しています。
オブジェクトの構築が終了すると grabPixels() を呼び出せば pixels 配列にピクセルデータが入れられます。ただし、このメソッドは image に対する直前の処理が完了するまで待機する必要があるため、割り込み発生による処理の中断を処理する必要があります。
上記の例では正常にピクセルデータを取り出せたかどうかを評価するため、処理が終了した後に PixelGrabber.status() を呼び出しています (JDK1.1 では status() ではなく getStatus() を使用してください)。このメソッドで返される値は ImageObserver の public メンバと同じです。
PixelGrabber を使用して取り出したピクセルはデフォルトの RGB カラーモデルが使用されています。従ってピクセル配列の型は常に 32 ビットの int となります。
さて、次にピクセル配列から ImageProducer を制作する方法を解説します。メモリ上のデータから制作するには MemoryImageSource クラスを使用する事ができます。
ImageProducer producer; producer = new MemoryImageSource(width, height, pixels, 0, width); Toolkit tk = Toolkit.getDefaultToolkit(); Image image = tk.createImage(producer); |
MemoryImageSource のコンストラクタは他にもカラーモデルを指定したりバイト配列から構築を行ったりできます。制作された ImageProducer から Image オブジェクトを制作するには Component か Toolkit クラスの createImage() を使用します。
| Filter Applet |
|---|
画像を FAX で送信したり、モノクロプリンタで印刷する場合、全てのピクセルを白と黒に変換する必要が出てきます。最も簡単なアルゴリズムは、ピクセルごとの明るさが 50% のしきい値 (Threshold) を超えるか超えないかで白と黒を決定する方法です。しきい値のパーセンテージを変更すれば明るくしたり暗くしたりと調節が利きます。
しきい値を使用する処理は、アルゴリズムが単純な代わりにあまりきれいな画像になりません。一般的なスキャナや FAX ソフトではパターンディザ (Pattern Dither) や誤差拡散法 (Error Diffusion Method) を使用して画像の 2 値化を行っています。ここではパターンディザについてちょっと解説したいと思います。誤差拡散法はまだ良く分からないので、資料がそろい次第説明します。
パターンディザは元の画像の階調とディザマトリクス (Dither Matrix) と呼ばれるマスクとを比較してピクセルの 2 値を決定します。右の図は 4x4 のディザマトリクスを使用して 2 値化を行っている例です。4x4 ディザマトリクスを使用する場合、元の画像を 4x4=16 階調にしてディザマトリクスの該当する位置の値と比較します。もし元画像の階調のほうが大きければその点を黒とし、小さければ白とします。これを画像の 4x4 セルごとに行えばパターンディザを使用したモノクロ 2 階調に変換する事ができます。
パターンディザはフルカラー画像の色数を落とす場合にも応用する事ができます。簡単な例では、RGB 各カラーチャンネルを 0x00 か 0xFF のどちらかとしてパターンディザをチャンネルに適用します。あとはこの色を合成すれば色数を 8 色に落とした画像となります。
| Filter Applet |
|---|
画像をシャープにするフィルタ処理にアンシャープマスク (Unsharp Mask) という方法があります。名前からするとぼかしをかけるフィルタのようですが、これはぼかしをかけた画像をマスクとして使用すると言う意味です。
画像をシャープにする処理というのは、隣接するピクセルの連続した階調の差分を大きくすると言う事です。逆にぼかしをかけるとは、連続する階調をよりなだらかにする処理になります。なだらかになった階調から元の階調を引いた差分に元の階調を足すと、階調差分がより大きな画像にする事ができます。これがアンシャープマスクのアルゴリズムです。
アンシャープマスクはピンぼけした画像を補正するときや、金属的な部分をより鮮明にしたいときなどによく使用されます。右のアプレットでは光沢のある部分がより鮮明に表示されています。