はじめに
コンボリューション(Convolution)は数学用語で、日本語では「畳み込み」といいます。掛け算の結果を足し集める演算からなり、コンピュータ処理の得意とするところです。コンボリューションを画像処理に使うと、画像を滑らかにしたり、シャープにしたりできます。掛け算の係数は、3×3などのサイズのマトリックスで指定します。これをオペレータ、フィルタ、マスク、カーネルなどと呼びます。
Java 2D APIには、画像処理でコンボリューションを行うのに便利なConvolveOp
やKernel
のクラスがあります。しかし、これらだけに頼ると不便な点もあるため、これらを利用しながら、従来と変わらぬ画像処理ができるように工夫しました。
対象読者
画像処理の基本を学び、ペイント系画像ソフトの一部の機能を自作したい人。
必要な環境
J2SE 5.0を使っていますが、J2SE 1.4.2でも大丈夫です。
コンボリューションに使用するオペレータのいろいろ
コンボリューションは、3×3(一例)のオペレータを原画像に沿って移動させ、オペレータの各要素の値と対応する原画像のピクセル値とを掛け合わせて合計して行います。ここでは、画像処理の目的に応じた各種のオペレータを紹介します。
平滑化オペレータ
一番単純なのは平滑化オペレータで、3×3の範囲の原画像のピクセル値を平均して、中心のピクセル値として出力します。一種の移動平均を取る操作で、画像は「ぼけ」た状態になります。
図1は、オペレータの要素と効果を示したもので、中央が平均化(平滑化)用です。左は、説明のためのもので、実際に使用されることはありませんが、これを使用すると原画像と同じ出力が得られます。右は、「ぼけ」の程度を弱めたもので、中心のピクセルの値に大きな比重を与えています。いずれも、各要素を合計すると1になることに注目してください。
平滑化オペレータには、もっと複雑なものもあります。ガウス分布を用い、「ぼけ」の範囲をσ(シグマ)で調整できる7×7や15×15の大きなものです。
鮮鋭化オペレータ
鮮鋭化オペレータには、原画像に二次微分空間フィルタLaplacian(ラプラシアン)の結果を加算する方式と、アンシャープと呼ばれる、原画像から平滑化(ぼけ)の結果を減算する方法とがあります。アンシャープ方式の方が、ガウス分布による調節ができるので、多く使われています。
アンシャープ(Unsharp)は、文字通り訳すと「シャープにしない」ですが、実際は「シャープにする」オペレータです。図2はアンシャープオペレータの原理を示したもので、原画像と平均(平滑化)画像との差分を原画像に上乗せすることにより、変化分を強調します。
プログラムでは、アンシャープ方式の最も簡単なものを採用しています。
エッジ検出オペレータ
エッジ検出オペレータには、一次微分を用いるものと二次微分を用いるものがあります。一次微分は方向性があり、垂直方向、水平方向などに分けて検出する必要があります。一次微分方式で有名なものには、Prewitt(プレビィット)とSobel(ゾーベル)がありますが、差はほとんどありません。二次微分はLaplacianを用い、方向性がありませんが感度が高いために、雑音に弱い欠点があります。したがって、これを単独で用いることは少なく、ガウスぼかし(平滑化)と併用します。
プログラムでは、一次微分の中では一般的なSobelオペレータを用いています。
図3はSobelオペレータの原理をイメージ的に示したもので、右側に黒い部分がある画像部分をコンボリューションすると「4」が得られ、左側に黒い部分があると「-4」が得られます。上下にある場合や、変化がない場合は、コンボリューション結果は「0」になります。つまり、このオペレータは、垂直方向のエッジを検出します。右が黒か、左が黒かは、出力の符号で分かります。ただし、Java 2D APIのConvolveOp
の場合は、負の出力はカットされますので、オペレータの各要素を反転した(-1を掛けた)、負出力用のオペレータを併用する必要があります。
プログラムの概要
Java 2D APIによるコンボリューション(平滑化と鮮鋭化)
便利な専用APIを使用したコンボリューションは、次のようにして実行します(平滑化の例)。
static final float[] operator={ 0.11f, 0.11f, 0.11f, 0.11f, 0.12f, 0.11f, 0.11f, 0.11f, 0.11f }; Kernel blur=new Kernel(3,3,operator); ConvolveOp convop=new ConvolveOp(blur,ConvolveOp.EDGE_NO_OP,null); BufferedImage bimg_dest=convop.filter(bimg_src,null);
最初に挙げたのは、Kernel
を構成するマトリックスの定義です。一般にfloat
型が用いられ、数値の指定には、最後に「f
」を付けておきます。「f
」がないと、デフォルトでdouble
型とみなされ、float
型との矛盾が指摘されます。この例は、平滑用です。数値は、合計すると1になるようにしておかないと、変化のない部分での明るさが変わってしまいます。
次は、Kernel
クラスのblur
(「ぼけ」の意)を構築しています。3,3
はマトリックスのサイズを表し、operator
は定義済みの配列を指定しています。
ConvolveOp
クラスのconvop
の構築は、Kernel
名、周辺処理方法、RenderingHints
(普通はnull
で良い)を指定します。周辺処理方法は、ConvolveOp.EDGE_NO_OP
(周辺は処理しないで、原画像をそのままコピーする)とConvolveOp.EDGE_ZERO_FILL
(周辺は強制的にゼロ(黒)にする)があります。EDGE_NO_OP
は、平滑化や鮮鋭化など、変換結果が原画像に似ている場合に目立たないので、お勧めです。一方、EDGE_ZERO_FILL
は、エッジ検出など、変換結果が原画像と全く異なり、黒い部分が多くなる場合に有効です。
ここで扱うBufferedImage
クラスの原画像img_src
は、
BufferedImage img_src=
new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
で構築しておく必要があります。誤って、BufferedImage.TYPE_INT_ARGB
を用いると、透明になってしまいます。なお、これらのクラスは「java.awt.image」に含まれており、特にクラスライブラリをインポートする必要はありません。
Java2D APIの問題点
非常に便利なAPIですが、次のような欠点もあります。
- 周辺処理ができない。
- 負の演算結果が得られない。
- インターフェースがBufferedImageのみである。
ConvolveOp
にはその機能が含まれていないため、周辺は、処理をしないで残すEDGE_NO_OP
か、周辺に強制的にゼロを書き込むEDGE_ZERO_FILL
かのどちらかしかできません。EDGE_NO_OP
でごまかし、エッジ検出はEDGE_ZERO_FILL
で対処するしかありません。int
型で扱い、負出力も認めた上で必要に応じて絶対値を取り、0~255の範囲内に収めて画像化していました。しかし、専用APIのConvolveOp
では、出力は0~255の範囲内に収めてありますが、負出力がカットされています。ConvolveOp
への引数はBufferedImage
に限られるので、外部から読み込んだ画像はImage
→ BufferedImage
の変換をし、画面表示のためには再びBufferedImage
→ Image
の変換が必要になります。 1.と2.については、これらの欠点を改良したConvolutionOp
クラスが英国のUniversity of Leedsから発表されていますが、一般的ではないので、ここでは採用しませんでした。
Java 2D APIの問題点を解決したコンボリューション(エッジ検出)
エッジ検出に用いるオペレータは、大きな負の出力を発生することがあります。専用APIを用いない場合は、この絶対値を用いていました。しかし、専用APIのConvolveOp
クラスは、負の出力はカットしてしまいますので、若干の工夫が必要です。オペレータを二個用いて、これに対処します。つまり、あるオペレータのマトリックスの各要素に-1を掛けた別のオペレータを設け、負の出力用とするのです。詳細は、図4を参照して下さい。
処理全体の構成
ConvolutionOp
はKernel
を使用し、BufferedImage
を対象にコンボリューションを行います。したがって、下記の処理が必要です。
- 画像ファイルを読み込み、
Image
画像を得る。 - 読み込んだ
Image
画像をBufferedImage
画像に変換する。 - コンボリューションに必要な
Kernel
オペレータ(3×3のマトリックス)を用意する。 Kernel
オペレータを用いて、filter
メソッドでBufferedImage
画像に対してコンボリューションを実行する。
readImageFile
を使用)changeToBufferedImage
を使用)makeConvolution
を使用)平滑化と鮮鋭化の場合
- 得られた
BufferedImage
画像をImage
画像に変換する。 Image
画像を表示する。
changeToImage
を使用)エッジ検出の場合(図4参照)
- 得られた二対の
BufferedImage
画像を合成し、閾値THRES
を与えて二個の二値Image
画像を作る。 - 二個の二値
Image
画像を合成する。 - 合成した
Image
画像を表示する。
createBrightORedBinaryImageInverted
を使用)createBlackORedBinaryImageNonInverted
を使用)プログラム
import java.awt.*; import java.awt.image.*; import javax.swing.*; public class Convolution extends JApplet{ static final float[][] operator={ { 0.11f, 0.11f, 0.11f, //operator[0] 平滑化 0.11f, 0.12f, 0.11f, 0.11f, 0.11f, 0.11f}, {-0.11f, -0.11f, -0.11f, //operator[1] 鮮鋭化 -0.11f, 1.88f, -0.11f, -0.11f, -0.11f, -0.11f}, {-1.0f, 0.0f, 1.0f, //operator[2] 垂直方向エッジ検出(正) -2.0f, 0.0f, 2.0f, -1.0f, 0.0f, 1.0f}, { 1.0f, 0.0f, -1.0f, //operator[3] 垂直方向エッジ検出(負) 2.0f, 0.0f, -2.0f, 1.0f, 0.0f, -1.0f}, {-1.0f, -2.0f, -1.0f, //operator[4] 水平方向エッジ検出(正) 0.0f, 0.0f, 0.0f, 1.0f, 2.0f, 1.0f}, { 1.0f, 2.0f, 1.0f, //operator[5] 水平方向エッジ検出(負) 0.0f, 0.0f, 0.0f, -1.0f, -2.0f, -1.0f} }; static final int THRES=180; //試行錯誤で決定 Image img_src,img_edge; Image[] img_dest=new Image[2]; public void init(){ BufferedImage[] bimg_dest=new BufferedImage[6]; //画像ファイルを読み込み、Image画像img_srcにする img_src=readImageFile("windmill.jpg"); //Image画像img_srcをBufferedImage画像bimg_srcに変換する BufferedImage bimg_src=changeToBufferedImage(img_src); // ---------------------- 平滑化と鮮鋭化 ---------------------- for(int i=0;i<2;i++){ //bimg_srcをoperator[i]でコンボリューションして、 //bimg_dest[i]を得る //周辺は原画像のまま bimg_dest[i]=makeConvolution(bimg_src,operator[i],0); //bimg_dest[i]をImage画像img_dest[i]にする img_dest[i]=changeToImage(bimg_dest[i]); } // ----------------------- エッジ検出 ------------------------- for(int i=2;i<6;i++){ //縦方向x2、横方向x2 //bimg_srcをoperator[i]でコンボリューションして、 //bimg_dest[i]を得る //周辺はゼロ(黒)で埋める bimg_dest[i]=makeConvolution(bimg_src,operator[i],1); } //bimg_dest[2]とbimg_dest[3]の白方向ORをとり //反転二値画像img_vertを生成する Image img_vert=createBrightORedBinaryImageInverted( bimg_dest[2],bimg_dest[3]); //bimg_dest[4]とbimg_dest[5]の白方向ORをとり //反転二値画像img_horiを生成する Image img_hori=createBrightORedBinaryImageInverted( bimg_dest[4],bimg_dest[5]); //垂直方向と水平方向の合成 //img_vertとimg_horiの黒方向のORをとり //非反転画像img_edgeを生成する img_edge= createBlackORedBinaryImageNonInverted(img_vert,img_hori); } //画像ファイルを読み込みImageクラスの画像にするメソッド public Image readImageFile(String filename){ Image img=getImage(getDocumentBase(),filename); MediaTracker mtracker=new MediaTracker(this); mtracker.addImage(img,0); try{ mtracker.waitForAll(); }catch(Exception e){} return img; } //Imageクラスの画像をBufferedImageクラスの画像に変換するメソッド public BufferedImage changeToBufferedImage(Image img){ int width=img.getWidth(this); int height=img.getHeight(this); BufferedImage bimg= new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); Graphics g=bimg.createGraphics(); g.drawImage(img,0,0,null); return bimg; } //BufferedImageクラスの画像をコンボリューションして //BufferedImageクラスの画像を得るメソッド public BufferedImage makeConvolution( BufferedImage bimg,float[] operator,int type){ Kernel kernel=new Kernel(3,3,operator); ConvolveOp convop; if(type==1) convop=new ConvolveOp(kernel,ConvolveOp.EDGE_ZERO_FILL,null); else convop=new ConvolveOp(kernel,ConvolveOp.EDGE_NO_OP,null); BufferedImage bimg_dest=convop.filter(bimg,null); return bimg_dest; } //二つのBufferedImage画像の白方向ORをとり //反転した二値Image画像を生成するメソッド private Image createBrightORedBinaryImageInverted( BufferedImage bimg1,BufferedImage bimg2){ int width=bimg1.getWidth(); int height=bimg1.getHeight(); int size=width*height; int[] rgb1=new int[size]; int[] rgb2=new int[size]; int[] rgb_dest=new int[size]; bimg1.getRGB(0,0,width,height,rgb1,0,width); bimg2.getRGB(0,0,width,height,rgb2,0,width); Color color1,color2,color_dest; int r1,g1,b1; int r2,g2,b2; int d; for(int i=0;i<size;i++){ color1=new Color(rgb1[i]); r1=color1.getRed(); g1=color1.getGreen(); b1=color1.getBlue(); color2=new Color(rgb2[i]); r2=color2.getRed(); g2=color2.getGreen(); b2=color2.getBlue(); //r1,g1,b1,r2,g2,g2の中の最大値でエッジの強さを判定する int maxd=Math.max(Math.max( Math.max(r1,g1),Math.max(b1,r2)),Math.max(g2,b2)); if(maxd>THRES) d=0; //黒にする else d=255; //白にする color_dest=new Color(d,d,d); rgb_dest[i]=color_dest.getRGB(); } return createImage( new MemoryImageSource(width,height,rgb_dest,0,width)); } //二つのImage画像の黒方向ORをとり //二値非反転Image画像を生成するメソッド private Image createBlackORedBinaryImageNonInverted( Image img1,Image img2){ Color color1,color2,color_dest; int r1,r2,d; int width=img1.getWidth(this); int height=img1.getHeight(this); int size=width*height; int[] rgb1=new int[size]; int[] rgb2=new int[size]; int[] rgb_dest=new int[size]; //img1画像の一次元RGB配列を得る PixelGrabber grabber1= new PixelGrabber(img1,0,0,width,height,rgb1,0,width); try{ grabber1.grabPixels(); }catch(InterruptedException e){} //img2画像の一次元RGB配列を得る PixelGrabber grabber2= new PixelGrabber(img2,0,0,width,height,rgb2,0,width); try{ grabber2.grabPixels(); }catch(InterruptedException e){} for(int i=0;i<size;i++){ color1=new Color(rgb1[i]); r1=color1.getRed(); //二値化されているのでR成分のみで良い color2=new Color(rgb2[i]); r2=color2.getRed(); d=Math.min(r1,r2); //黒方向のORを採るので、値の小さい方にする color_dest=new Color(d,d,d); rgb_dest[i]=color_dest.getRGB(); } return createImage(new MemoryImageSource( width,height,rgb_dest,0,width)); } //BufferedImageクラスの画像をImageクラスの画像に変換するメソッド public Image changeToImage(BufferedImage bimg){ Image img= Toolkit.getDefaultToolkit().createImage(bimg.getSource()); return img; } public void paint(Graphics g){ //画像img_srcを左上に表示する g.drawImage(img_src,10,10,this); //画像img_dest[0]を右上に表示する g.drawImage(img_dest[0],340,10,this); //画像img_dest[1]を左下に表示する g.drawImage(img_dest[1],10,260,this); //画像img_edgeを右下に表示する g.drawImage(img_edge,340,260,this); } }
得られた画面
図5で、左上は原画像、右上は平滑化画像、左下は鮮鋭化画像、右下はエッジ検出結果の画像です。
図6は、文字や図形のエッジ検出を行った結果です。左が原画像、右がエッジ検出結果です。
まとめ
画像の平滑化、鮮鋭化は、画像処理の中で最も一般的なもので、画像ソフトには必ず組み込まれています。ここでは、まだ十分知られていないJava 2D APIのConvolveOp
とKernel
を使ってみました。意外と使い勝手が良く、お薦めできます。
画像のエッジ検出は、パターン認識の前処理として使われることが多く、これも画像処理の定番ですが、上記のAPIでは不十分な点もありましたので、負出力のためのオペレータを追加し、結果を合成する方法で対処しました。
参考資料
この記事は、以下に示す筆者のウェブサイトを総合し、Java2D APIを使用して、Java言語に全面的に書き換えたものです。
- コンボリューション全般と平滑化、鮮鋭化 ----「Visual C++を用いた易しい画像処理(5)- 3×3画素のオペレータを用いた画像の平滑化、鮮鋭化、線画化-」(ただし、現在は Visual C++ 2005 Express Edition 版に改定)
- エッジ検出 ----「Visual C++を用いた易しい画像処理(13)-エッジ検出とBitmapファイルの読み書き-」」(ただし、現在は Visual C++ 2005 Express Edition 版に改定)
- アンシャープによる鮮鋭化 ---- 「Visual C++を用いた易しい画像処理(18) - ガウス分布を計算しアンシャープ処理を行う-」」(ただし、現在は Visual C++ 2005 Express Edition 版に改定)
Bitmapファイルの読み書きのJava化については、多くのウェブサイト上に参考資料がありますので省略しました。画像処理の一般的な参考図書には、コンボリューション手法が説明されていますが、Java 2D APIの使用については見かけません。
一般的なものとしては、以下のものがあります。