SHOEISHA iD

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

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

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

写真を油彩画のように変換する簡易フィルタの実装

減色と最頻値による写真画像の簡単な油彩画調化


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

カメラなどで撮った自然画像を絵画のように変えてみたいことがあります。著名な市販レタッチソフトや、他の有償無償の画像ソフトにもこの種の機能が含まれています。本稿では比較的単純な手法で、油彩画に似た絵画調の画像を作成する方法を紹介します。

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

はじめに

 カメラなどで撮った自然画像を絵画のように変えてみたいことがあります。著名な市販レタッチソフトや、他の有償無償の画像ソフトにもこの種の機能が含まれています。絵画調化専門のソフトもあります。ここでは比較的単純な手法で、油彩画に似た絵画調の画像を作ってみました。

対象読者

 レタッチソフトで写真を絵画風に加工する仕組みに興味を持ち、自分で絵画調化ソフトを作ってみたい人。

必要な環境

 J2SE 5.0を使っていますが、それ以前のバージョンでも大丈夫です。

絵画調化とは

 フォトレタッチソフトには、アーティスティック・フィルタ(Artistic Filter)と呼ぶ、写真を芸術的な感じに変換する機能があります。一般的には、このような処理を「絵画調化」といいます。

 絵画調化には、ソフトを使いながらインタラクティブに、制作者の意図に沿った完成度の高い作品を生成する方法(参考資料1. 2. 3. など)と、パラメータを与えるだけのソフトによる完全な自動生成があります。

 絵画調化ソフトは奥が深く、SIGGRAPH(米国情報処理学会Association for Computing MachineryのSpecial Interest Group on Computer Graphics )などに多くの論文が発表されています。これらのソフトは、原画像を解析し、絵画調化の際のストローク・タッチなどを最適化し、原画像を加工します。目的とする絵画には、油彩画、水彩画、ペン画などがあり、それぞれ異なった手法が用いられます。

簡単な方法は「減色」と「非線形平滑化」

 水彩画や油彩画は、絵の具を用いて筆で描きます。絵の具の本数は限られていますので、それらを混ぜ合わせたとしても、使われる色の種類はあまり多くありません。この理由で、まず、減色することが考えられます。rgbで言えば、階調を減らすことです。

 筆には、ある程度の大きさがあり、あまり細かい部分は忠実に描写できません。絵は省略することでもあり、ボカしのテクニックも使われています。そこで、平滑化することが考えられます。しかし、単なる平滑化ですと、原画像の僅かな変化部分も、それなりに結果に反映される欠点がありますので、非線形の平滑化手法(最頻値フィルタやメディアンフィルタなど)を用いてその感じを出します。

プログラムの構成

 プログラムは、次の部分からなっています。

  1. 原画像を読み込み、画面の左に表示します。
  2. 原画像を拡張します。
  3. 着目するピクセルの周囲のピクセルを使用しますので、原画像の四辺、四隅をコピーして広げておきます。最初に上下を拡張し、それを左右に拡張します。拡張した結果は、図1のようになります。これにより、5 x 5までの範囲の最頻値取得が可能になります。
    図1 原画像と拡張後の画像との関係
    図1 原画像と拡張後の画像との関係
  4. 絵画調化を行います。
    • ヒストグラムを採るためのカウンタをクリアします。
    • 配列counter[k][l][m]の定義で兼用しています。添字Klmの大きさは、それぞれ減色化の階調stepsにします。
    • 1階調当りのrgb値の幅をbaseと呼び、stepsから計算します。
    • base=256/stepsのように整数間除算を行うと、端数が切り捨てになるので、base=(int)Math.ceil(256.0/steps)で切り上げています。切り捨てをすると、red1/baseなどが4になる恐れがあり、実行時エラーが発生します。
      このようにして得られたstepsbaseの関係は下表の通りです。
      減色化の階調(steps)2345678
      階調当りの幅(base)128866452433732
    • 原画像の階調を減らしながらヒストグラムを採ります。
    • x方向(横方向)とy方向(縦方向)を、それぞれ-region/2からregion/2まで変化させて(region=3のときは、-1から1まで、region=5のときは、-2から2まで)、注目するピクセルの周辺のrgb値に対応するカウンタをインクリメントします。
      減色処理は、原画像のrgb値をbaseで整数間除算し、カウンタの添字klm値に用いることにより行います。図2に、その関係を示します。図ではsteps=4を用い(base=64となります)、例としてredを取り上げていますが、green、blueについても、同様に行います。
      図2 原画像のrgb値とカウンタの添え字klmの関係
      図2 原画像のrgb値とカウンタの添え字klmの関係
    • 頻度が最大となるrgb値を求める。
    • 配列counter[k][l][m]に対して、klm値を、それぞれ0からsteps-1の間でスキャンし、その最大値を求めます。そのときのklm値をk_maxl_maxm_maxとします。
      このままでは、まだrgb値になっていませんので、
      red1=k_max*base+base/2
      
      などによって、rgb値に変換します。図3は、steps=4(base=64)の場合について、k_maxからred1が求まり、減色化が行われる例を示しています。l_maxからgreen1m_maxからblue1への変換も同様に行います。
      図3 カウンタの添え字klmと絵画調画像のrgb値との関係
      図3 カウンタの添え字klmと絵画調画像のrgb値との関係
    • 上記rgb値で描画します。

プログラム

import java.applet.Applet;
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;

public class Artistic extends Applet
   implements ItemListener,ActionListener{

   static final int WIDTH=320;
   static final int HEIGHT=240;
   static final int SIZE=76800;

   static final int X0=10;
   static final int Y0=50;

   Choice choice1,choice2,choice3;
   Button button1,button2,button3,button4;
 
   //デフォルト値の設定
   String filename="gent.jpg";   //原画像のファイル名
   int steps=5;                  //減色化の階調
   int region=3;                 //最頻値取得の範囲
   int brightness=0;             //明るさの補正値

   public void init(){

      //-------------------- チョイス関係準備 ----------------------

      choice1=new Choice();
      choice1.add("(原画像の指定)");
      choice1.add("biei.jpg");
      choice1.add("cheskykrumlov.jpg");
      choice1.add("gent.jpg");
      choice1.add("kanazawa.jpg");
      choice1.add("praha.jpg");
      choice1.add("xprovence.jpg");
      choice1.addItemListener(this);
      add(choice1);

      choice2=new Choice();
      choice2.add("(減色化の階調)");
      choice2.add("2階調");
      choice2.add("3階調");
      choice2.add("4階調");
      choice2.add("5階調");
      choice2.add("6階調");
      choice2.add("7階調");
      choice2.add("8階調");
      choice2.addItemListener(this);
      add(choice2);

      choice3=new Choice();
      choice3.add("(最頻値取得の範囲)");
      choice3.add("3x3");
      choice3.add("5x5");
      choice3.addItemListener(this);
      add(choice3);

      //--------------------- ボタン関係準備 -----------------------

      button1=new Button("実  行");
      button1.addActionListener(this);
      add(button1);

      button2=new Button("明るく");
      button2.addActionListener(this);
      button2.setBackground(new Color(240,240,240));
      add(button2);

      button3=new Button("暗 く");
      button3.addActionListener(this);
      button3.setBackground(new Color(92,92,92));
      button3.setForeground(Color.white);
      add(button3);

      button4=new Button("標 準");
      button4.addActionListener(this);
      add(button4);
   }

   //------------------- チョイス関係メソッド ----------------------

   public void itemStateChanged(ItemEvent ie){

      if(choice1.getSelectedIndex()!=0)
         filename=choice1.getSelectedItem();

      if(choice2.getSelectedIndex()!=0)
         steps=choice2.getSelectedIndex()+1;

      if(choice3.getSelectedIndex()==1)  region=3;   //3x3
      if(choice3.getSelectedIndex()==2)  region=5;   //5x5

   }

   //-------------------- ボタン関係メソッド -----------------------

    public void actionPerformed(ActionEvent ae){
      if(ae.getSource()==button1) repaint();
      if(ae.getSource()==button2){
         brightness+=20;   //明るくする
         repaint();
      }
      if(ae.getSource()==button3){
         brightness-=20;   //暗くする
         repaint();
      }
      if(ae.getSource()==button4){
         brightness=0;     //標準に戻す
         repaint();
      }

   }

   
   public void paint(Graphics g){

      Image img_src;

      int[][] pixels=new int[HEIGHT+4][WIDTH+4];
 
      //画像ファイルを読み込み、Image画像img_srcにする
      img_src=readImageFile(filename);
      //画像img_srcを左に表示する
      g.drawImage(img_src,X0,Y0,this);    
      //Image画像img_srcの四辺を拡張し、
      //二次元ピクセルデータに変換する
      create2DPixelsDataExpanded(img_src,pixels);        
      //二次元ピクセルデータを用いて絵画調化し、右に描画する
      drawArtistic(pixels,steps,region,brightness,X0+WIDTH+20,Y0);
      //明るさ補正brightnessを右下に表示する
      g.drawString("明るさ補正= "+String.valueOf(brightness),
                   X0+WIDTH+20,Y0+HEIGHT+30);

   }

   //絵画調化するメソッド
   private void drawArtistic(int[][] _pixels, int _steps,
                 int _region, int _brightness, int x0, int y0){

      int i,j,k,l,m;
      int red,green,blue,red1,green1,blue1;

      Color color;

      int base=(int)Math.ceil(256.0/_steps);

      Graphics g=getGraphics();

      for(j=2;j<HEIGHT+2;j++)
         for(i=2;i<WIDTH+2;i++){
            
            //counter[][][]を設定し、ゼロにクリアする
            int[][][] counter=new int[_steps][_steps][_steps];

            //counterによりヒストグラムを取得する
            for(k=-_region/2;k<=_region/2;k++)
               for(l=-_region/2;l<=_region/2;l++){
                  color=new Color(_pixels[j+l][i+k]);
                  red=color.getRed()+_brightness;
                  green=color.getGreen()+_brightness;
                  blue=color.getBlue()+_brightness;

                  if(red>255)   red=255;
                  if(green>255) green=255;
                  if(blue>255)  blue=255;
                  if(red<0)    red=0;
                  if(green<0)  green=0;
                  if(blue<0)   blue=0;

                  counter[red/base][green/base][blue/base]++;
               }

            //最大頻度を求めて、そのrgb値を求める
            int counter_max=0;
            int k_max=0,l_max=0,m_max=0;

            for(k=0;k<_steps;k++)
               for(l=0;l<_steps;l++)
                  for(m=0;m<_steps;m++){
                     if(counter[k][l][m]>counter_max){
                        counter_max=counter[k][l][m];
                        k_max=k;
                        l_max=l;
                        m_max=m;
                     }
                   }

            red1=k_max*base+base/2;
            green1=l_max*base+base/2;
            blue1=m_max*base+base/2;

            g.setColor(new Color(red1,green1,blue1));
            g.drawRect(x0+i-2,y0+j-2,0,0);

      }

   }

   //Image画像imgの四辺を拡張し、
   //二次元ピクセルデータに変換するメソッド
   public void create2DPixelsDataExpanded
                       (Image img, int[][] pixels){

      int i,j;

      int[] rgb=new int[SIZE];

      //画像imgを一次元RGBデータrgb[]にする
      PixelGrabber grabber=new 
         PixelGrabber(img,0,0,WIDTH,HEIGHT,rgb,0,WIDTH);
      try{
         grabber.grabPixels();
      }catch(InterruptedException e){}

      //一次元RGBデータrgb[]を二次元ピクセルデータ
      //pixels[][]の中心に設定する
      int n=0;
      for(j=0;j<HEIGHT;j++)
         for(i=0;i<WIDTH;i++)
            pixels[j+2][i+2]=rgb[n++];

      //上辺をコピーする
      for(j=2;j<=3;j++)
         for(i=2;i<WIDTH+2;i++)
            pixels[3-j][i]=pixels[j][i];

      //下辺をコピーする
      for(j=HEIGHT+1;j>=HEIGHT;j--)
         for(i=2;i<WIDTH+2;i++)
            pixels[2*HEIGHT+3-j][i]=pixels[j][i];

      //左辺をコピーする
      for(i=2;i<=3;i++)
         for(j=0;j<HEIGHT+4;j++)
            pixels[j][3-i]=pixels[j][i];

      //右辺をコピーする
      for(i=WIDTH+1;i>=WIDTH;i--)
         for(j=0;j<HEIGHT+4;j++)
            pixels[j][2*WIDTH+3-i]=pixels[j][i];
 
   }

   //画像ファイルを読み込み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;

   }

}

プログラムの使い方

 アプレットを実行すると、上部に左から[原画像のファイル名][減色化の階調][最頻値取得の範囲]の各ドロップダウン選択ボックスと、[実行][明るく][暗く][標準]ボタンが表示され、下には左に原画像、右に絵画調化した画像が表示されます。

 絵画調化した画像の下には、その時の明るさ補正量が表示されます。

 最初は、デフォルトの原画像とデフォルトの条件(階調は[5階調]、最頻値取得範囲は[3 x 3])での絵画調化画像が表示されますので、選択ボックスで任意の原画像を選び、階調や範囲を変えて[実行]ボタンをクリックして下さい。[明るく][暗く]などの変更は、選択ボックスの条件を変えても設定が残っています。明るさをデフォルト状態に戻したい場合は、[標準]ボタンをクリックして下さい。

 原画像によって、最適の階調があります。一般に多階調の方がきれいに見えますが、明るさを調整しながら階調を下げると、面白い効果が得られる場合があります。最頻値を求める範囲を大きく([5 x 5])取ると、表現が荒っぽくなり、絵画らしく見えますが、細かい描写は失われます。

プログラムの実行結果

 下図で左上から、「原画像ファイル名」「減色のための階層数」「最頻値を求める周辺データの範囲」が示されています。[実行]ボタンをクリックすると、以上の選択された条件で絵画調化が行われますが、暗いと思われたので、[明るく]を一回クリックしてあります。右下の表示で、明るさ補正がされていることが分かります。

図4 実行結果の画面
図4 実行結果の画面

まとめ

 著名な市販ソフトや専用ソフトは、かなりの歴史があり、高度の技術によって、非常に良くできています。素人が簡単に真似できるものではありません。しかし、それらを実際に使ってみると、表現がややオーバーであったり、原画像を軽視していたりして、必ずしも満足な結果が得られません。

 ここで紹介した手法は簡単過ぎて、ひいき目に見ても、ようやく「絵画調かな」という程度ですが、自分で作った喜びが味わえます。これを出発点にして、さらなる改良をされることを望みます。

参考資料

 この記事は、筆者のウエブサイト『Visual C++ 6.0を用いた易しい画像処理(7)-画像の絵画調化(油絵化の場合)-(ただし、現在はVisual C++ 2005 Express Edition 版に改定)』を改良し、Java言語に書き直して、分かりやすく加筆したものです。

 最初のきっかけとなった文献は、

 です。市販ソフトを使ってインタラクティブに絵画調化する手法については、

  1. 『写真を素材にPhotoshopで描くデジタル絵画』 HAL_・桑島幸男・甲田正治・岩渕泰治・酒井和男・かめむらしゅんじ 著、パレット 編、毎日コミュニケーションズ、2003年7月
  2. 『写真を素材に始めるデジタル水彩画入門―PhotoshopやPainterで描く7人の作家による水彩画の世界』 藤田修一・小野寺浩二・細井敏昭・田宮彩・城谷俊也・横井由美子・渡部政人 著、パレット 編、毎日コミュニケーションズ、2004年7月
  3. 『フォトショップ美術部 趣味の絵画』 古岡ひふみ 著、エムディエヌコーポレーション、2004年1月

 などがあります。

修正履歴

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

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

もっと読む

この記事の著者

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

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

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

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

この記事をシェア

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

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング