SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Javaで学ぶグラフィックス処理

コンボリューションを用いた画像の平滑化、鮮鋭化とエッジ検出

Java 2D APIを利用して画像処理を行う際のテクニック


  • X ポスト
  • このエントリーをはてなブックマークに追加

コンボリューション(畳み込み演算)を画像処理に使うと、画像を滑らかにしたり、シャープにしたりできます。本稿では、Java 2D APIを利用して、画像処理の基本となる平滑化、鮮鋭化、およびエッジ検出を実装する方法を解説します。

  • X ポスト
  • このエントリーをはてなブックマークに追加

はじめに

 コンボリューション(Convolution)は数学用語で、日本語では「畳み込み」といいます。掛け算の結果を足し集める演算からなり、コンピュータ処理の得意とするところです。コンボリューションを画像処理に使うと、画像を滑らかにしたり、シャープにしたりできます。掛け算の係数は、3×3などのサイズのマトリックスで指定します。これをオペレータ、フィルタ、マスク、カーネルなどと呼びます。

 Java 2D APIには、画像処理でコンボリューションを行うのに便利なConvolveOpKernelのクラスがあります。しかし、これらだけに頼ると不便な点もあるため、これらを利用しながら、従来と変わらぬ画像処理ができるように工夫しました。

対象読者

 画像処理の基本を学び、ペイント系画像ソフトの一部の機能を自作したい人。

必要な環境

 J2SE 5.0を使っていますが、J2SE 1.4.2でも大丈夫です。

コンボリューションに使用するオペレータのいろいろ

 コンボリューションは、3×3(一例)のオペレータを原画像に沿って移動させ、オペレータの各要素の値と対応する原画像のピクセル値とを掛け合わせて合計して行います。ここでは、画像処理の目的に応じた各種のオペレータを紹介します。

平滑化オペレータ

 一番単純なのは平滑化オペレータで、3×3の範囲の原画像のピクセル値を平均して、中心のピクセル値として出力します。一種の移動平均を取る操作で、画像は「ぼけ」た状態になります。

 図1は、オペレータの要素と効果を示したもので、中央が平均化(平滑化)用です。左は、説明のためのもので、実際に使用されることはありませんが、これを使用すると原画像と同じ出力が得られます。右は、「ぼけ」の程度を弱めたもので、中心のピクセルの値に大きな比重を与えています。いずれも、各要素を合計すると1になることに注目してください。

図1 平滑化オペレータの原理
図1 平滑化オペレータの原理

 平滑化オペレータには、もっと複雑なものもあります。ガウス分布を用い、「ぼけ」の範囲をσ(シグマ)で調整できる7×7や15×15の大きなものです。

鮮鋭化オペレータ

 鮮鋭化オペレータには、原画像に二次微分空間フィルタLaplacian(ラプラシアン)の結果を加算する方式と、アンシャープと呼ばれる、原画像から平滑化(ぼけ)の結果を減算する方法とがあります。アンシャープ方式の方が、ガウス分布による調節ができるので、多く使われています。

 アンシャープ(Unsharp)は、文字通り訳すと「シャープにしない」ですが、実際は「シャープにする」オペレータです。図2はアンシャープオペレータの原理を示したもので、原画像と平均(平滑化)画像との差分を原画像に上乗せすることにより、変化分を強調します。

 プログラムでは、アンシャープ方式の最も簡単なものを採用しています。

図2 先鋭化オペレータの一つであるアンシャープオペレータの原理
図2 先鋭化オペレータの一つであるアンシャープオペレータの原理

エッジ検出オペレータ

 エッジ検出オペレータには、一次微分を用いるものと二次微分を用いるものがあります。一次微分は方向性があり、垂直方向、水平方向などに分けて検出する必要があります。一次微分方式で有名なものには、Prewitt(プレビィット)とSobel(ゾーベル)がありますが、差はほとんどありません。二次微分はLaplacianを用い、方向性がありませんが感度が高いために、雑音に弱い欠点があります。したがって、これを単独で用いることは少なく、ガウスぼかし(平滑化)と併用します。

 プログラムでは、一次微分の中では一般的なSobelオペレータを用いています。

 図3はSobelオペレータの原理をイメージ的に示したもので、右側に黒い部分がある画像部分をコンボリューションすると「4」が得られ、左側に黒い部分があると「-4」が得られます。上下にある場合や、変化がない場合は、コンボリューション結果は「0」になります。つまり、このオペレータは、垂直方向のエッジを検出します。右が黒か、左が黒かは、出力の符号で分かります。ただし、Java 2D APIのConvolveOpの場合は、負の出力はカットされますので、オペレータの各要素を反転した(-1を掛けた)、負出力用のオペレータを併用する必要があります。

図3 Sobelオペレータの原理
図3 Sobelオペレータの原理

プログラムの概要

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ですが、次のような欠点もあります。

  1. 周辺処理ができない。
  2. 従来は、原画像を拡張し、あらかじめ周辺に原画像のコピーを配して、コンボリューションによる周辺の欠落を防止できました。しかし、ConvolveOpにはその機能が含まれていないため、周辺は、処理をしないで残すEDGE_NO_OPか、周辺に強制的にゼロを書き込むEDGE_ZERO_FILLかのどちらかしかできません。
    平滑化と鮮鋭化はEDGE_NO_OPでごまかし、エッジ検出はEDGE_ZERO_FILLで対処するしかありません。
  3. 負の演算結果が得られない。
  4. 従来は、コンボリューションに使用する変数をint型で扱い、負出力も認めた上で必要に応じて絶対値を取り、0~255の範囲内に収めて画像化していました。しかし、専用APIのConvolveOpでは、出力は0~255の範囲内に収めてありますが、負出力がカットされています。
    したがって、負出力用に符号を反転したもう一個のオペレータを用意し、コンボリューション結果を合成する必要があります。
  5. インターフェースがBufferedImageのみである。
  6. ConvolveOpへの引数はBufferedImageに限られるので、外部から読み込んだ画像はImageBufferedImageの変換をし、画面表示のためには再びBufferedImageImageの変換が必要になります。

 1.と2.については、これらの欠点を改良したConvolutionOpクラスが英国のUniversity of Leedsから発表されていますが、一般的ではないので、ここでは採用しませんでした。

Java 2D APIの問題点を解決したコンボリューション(エッジ検出)

 エッジ検出に用いるオペレータは、大きな負の出力を発生することがあります。専用APIを用いない場合は、この絶対値を用いていました。しかし、専用APIのConvolveOpクラスは、負の出力はカットしてしまいますので、若干の工夫が必要です。オペレータを二個用いて、これに対処します。つまり、あるオペレータのマトリックスの各要素に-1を掛けた別のオペレータを設け、負の出力用とするのです。詳細は、図4を参照して下さい。

処理全体の構成

 ConvolutionOpKernelを使用し、BufferedImageを対象にコンボリューションを行います。したがって、下記の処理が必要です。

  1. 画像ファイルを読み込み、Image画像を得る。
  2. (→ メソッドreadImageFileを使用)
  3. 読み込んだImage画像をBufferedImage画像に変換する。
  4. (→ メソッドchangeToBufferedImageを使用)
  5. コンボリューションに必要なKernelオペレータ(3×3のマトリックス)を用意する。
  6. Kernelオペレータを用いて、filterメソッドでBufferedImage画像に対してコンボリューションを実行する。
  7. (→ メソッドmakeConvolutionを使用)

平滑化と鮮鋭化の場合

  1. 得られたBufferedImage画像をImage画像に変換する。
  2. (→ メソッドchangeToImageを使用)
  3. Image画像を表示する。

エッジ検出の場合(図4参照)

  1. 得られた二対のBufferedImage画像を合成し、閾値THRESを与えて二個の二値Image画像を作る。
  2. (→ メソッドcreateBrightORedBinaryImageInvertedを使用)
  3. 二個の二値Image画像を合成する。
  4. (→ メソッドcreateBlackORedBinaryImageNonInvertedを使用)
  5. 合成したImage画像を表示する。
図4 エッジ検出処理の流れ
図4 エッジ検出処理の流れ

プログラム

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で、左上は原画像、右上は平滑化画像、左下は鮮鋭化画像、右下はエッジ検出結果の画像です。

図5 コンボリューションを用いた画像処理の例
図5 コンボリューションを用いた画像処理の例

 図6は、文字や図形のエッジ検出を行った結果です。左が原画像、右がエッジ検出結果です。

図6 文字や図形のエッジ検出の結果
図6 文字や図形のエッジ検出の結果

まとめ

 画像の平滑化、鮮鋭化は、画像処理の中で最も一般的なもので、画像ソフトには必ず組み込まれています。ここでは、まだ十分知られていないJava 2D APIのConvolveOpKernelを使ってみました。意外と使い勝手が良く、お薦めできます。

 画像のエッジ検出は、パターン認識の前処理として使われることが多く、これも画像処理の定番ですが、上記のAPIでは不十分な点もありましたので、負出力のためのオペレータを追加し、結果を合成する方法で対処しました。

参考資料

 この記事は、以下に示す筆者のウェブサイトを総合し、Java2D APIを使用して、Java言語に全面的に書き換えたものです。

 Bitmapファイルの読み書きのJava化については、多くのウェブサイト上に参考資料がありますので省略しました。画像処理の一般的な参考図書には、コンボリューション手法が説明されていますが、Java 2D APIの使用については見かけません。

 一般的なものとしては、以下のものがあります。

  1. 「楽しく学ぶJavaではじめる画像処理プログラミング」 杉山三樹雄著 (株)ディー・アート
修正履歴

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
Javaで学ぶグラフィックス処理連載記事一覧

もっと読む

この記事の著者

石立 喬(イシダテ タカシ)

1955年東京工大卒。同年、NECへ入社し、NEC初のコンピュータの開発に参画。磁気メモリ、半導体メモリの開発、LSI設計などを経て、1989年帝京大学理工学部教授。情報、通信、電子関係の教育を担当。2002年定年により退職し現在に至る。2000年より、Webサイト「Visual C++の勉強部屋」を公開。...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/129 2008/03/16 11:05

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング