Google
 

'Java 2D'에 해당되는 글 1건

  1. 2008/01/29 바이웅 Java 2D - CONVOLVEOP로 이미지에 효과주기 (1)

개발자를 위한 자바뉴스레터 - 저자 Daniel H. Steinberg

디지털 카메라로 찍은 이미지와 실제 찍고 싶었던 이미지가 다른 경우가 꽤 있다. 예를 들어, 번짐 효과나 선명한 효과, 혹은 이미지를 밝게 만들거나 톤을 낮추는 등의 효과를 넣고 싶을 수 있다. java.awt.image패키지는 디지털 이미지에 이와 같은 변화를 줄 수 있는 유틸리티를 포함한다. 이러한 유틸리티를 사용하면, 하이 엔드 상용 이미징 패키지(high-end commercial imaging packages)가 제공하는 동일한 효과를 줄 수 있다.

이 글에서는 디지털 이미지에 이러한 효과를 주기 위해서 java.awt.image 패키지 내의 ConvolveOp클래스를 사용한다. ConvolveOp는 소스로부터 목적에 이르는 convolution를 구현한다. . 여기서 convolution은 각각의 픽셀을 본래 픽셀과 그 주변 픽셀의 조합으로 대체하는 동작으로 생각해 볼 수 있겠다. float 배열을 이용해서 위에서 설명한 바와 같이 본래 이미지의 픽셀을 조절하여 이에 적절한 효과를 줄 수가 있다. 여기서 float배열의 각각의 요소들은 본래 이미지 픽셀에 곱해지는 수들이다. 이 float 배열로부터 convolution에서 가장 핵심이 되는 계수배열(곱해지는 배열)의 인스턴스를 생성하고 이를 이용해서 convolution을 실행한다.

예를 들어, 다음과 같은 3*3 배열(이것이 바로 float 배열이다.)이 있다면

   a b c
   d e f
   g h i

결과 convolution는 다음 각 사항의 합으로 가운데 픽셀을 대체한다.

  • a와 본래 이미지의 중심 픽셀에서 왼쪽 위의 픽셀값의 곱
  • b와 본래 이미지의 중심 픽셀의 머리위의 픽셀값의 곱
  • c와 본래 이미지의 중심 픽셀에서 오른쪽 위의 픽셀값의 곱
  • 기타

바꾸어 말하자면, 계수배열에서 해당 포지션의 값을 픽셀값에 곱하고 이를 합친다. 배열의 엔트리 합계가 1이라면, 결과 이미지는 본래 이미지와 동일한 다이나믹 범위(range)를 갖을 것이다.

좀 더 구체적인 예를 보자. 다음 3*3 배열을 살펴보자.

     0   0.2   0
    0.2  0.2  0.2
     0   0.2   0

이러한 배열은 각 픽셀값들을 그 값의 전후좌우의 값들을 평균한 값으로 대체하는 것과 같다. 이는 사진을 약간 번지게 하는 효과가 있다.

그렇다면 다음 3*3배열을 보자. 이것은 일치연산(identity operation)에 해당한다. 이를 convolution의 한 부분으로 적용하면 변하는 것은 아무것도 없다. 즉, 이는 각각의 픽셀을 그것의 이전 값으로 대체한다는 것이다.

   0 0 0
   0 1 0
   0 0 0

이전 배열의 1이 2로 대체되면 결과 이미지는 더 밝아지고 약간 색이 바랜 듯 보인다. 1을 0.5로 대체하면 이미지는 더 어두워진다.

3*3 배열만 특별히 이와 같은 동작을 구현하는 것은 아니다. 중심에 대해 대칭이면서 중심 픽셀에 의해 좌우되는 convolution을 실행하고자 한다면, 홀수로 구성된 정방형 배열이 필요하다. 5*5, 7*7 과 같은 홀수형 배열은 3*3 배열과 동일하게 작동한다. 심지어는 1*1배열을 사용해도 상관없다. 예를 들어서, 다음 메소드에서와 같이 단일 요소를 갖는 float 배열을 생성하고 이 요소로부터 새로운 1*1 Kernel 객체를 생성한 후 이를 ConvolveOp 생성자로 넘긴다. 마지막으로 변경된 이미지를 얻어내기 위해 BufferedImageOp를 본래 BufferedImage에 적용한다.

   private void setBrightnessFactor(float multiple) {
     float[] brightKernel = {multiple};
     BufferedImageOp bright
       = new ConvolveOp(new Kernel(1, 1, brightKernel));
     convolvedImage 
       = bright.filter(originalImage, null);
     repaint();

   }

이 메소드는 2가지 점을 강조하고 있다. 하나는 이미지가 바뀔 때마다 그것을 새로 그리기 위해 paintComponent() 메소드를 오버라이드해야 한다는 것이다.

   public void paintComponent(Graphics g) {
     g.drawImage(convolvedImage, 0, 0, this);
   }

두번째는 이러한 효과를 적용할 수 있는 BufferedImage 를 먼저 생성해야 한다는 것이다. 이 경우에 test.jpg를 읽고 그곳으로부터 Image를 생성한다. 그리고는 원본 이미지와 같은 크기로 BufferedImage을 생성한다. 마지막 단계로, BufferedImage 에 그 이미지를 그려넣어야 한다.

   Image image = new ImageIcon("test.jpg").getImage();
   originalImage 
     = new BufferedImage(image.getWidth(null),
       image.getHeight(null), 
       BufferedImage.TYPE_INT_RGB);
   Graphics g = originalImage.createGraphics();
   g.drawImage(image, 0, 0, null);
   g.dispose();

1차원 계수배열을 이용하는 대신에, setBrightnessFactor의 다음 버전을 통해 3*3 계수배열을 이용할 수 있다.

   private void setBrightnessFactor(float multiple) {
      float[] brightKernel = {0,    0,     0,
                              0, multiple, 0,
                              0,    0,     0};
      BufferedImageOp bright
        = new ConvolveOp(new Kernel(3, 3, brightKernel));
      convolvedImage 
        = bright.filter(originalImage, null);
      repaint();
   }

이미지의 뷰어를 생성하는 BrightnessChanger프로그램을 보자. 이 뷰어는 사용되는 배수의 값을 변경해서 밝기를 조절할 때 쓰는 슬라이더를 포함한다. 테스트 이미지가 필요할 것이다. 이 때, 이미지의 이름은 test.jpg로 하자. 이 이미지는 BrightnessChanger 프로그램을 실행하는 디렉토리에 위치해 있어야 한다. 테스트 이미지로 사용할 수 있는 재미난 그림을 다운로드 받자.

    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.ImageIcon;
    import javax.swing.JSlider;
    import javax.swing.event.ChangeListener;
    import javax.swing.event.ChangeEvent;
    import java.awt.Graphics;
    import java.awt.Image;
    import java.awt.BorderLayout;
    import java.awt.image.Kernel;
    import java.awt.image.ConvolveOp;
    import java.awt.image.BufferedImageOp;
    import java.awt.image.BufferedImage;

    public class BrightnessChanger extends JPanel {

       private BufferedImage originalImage;
       private BufferedImage convolvedImage;
       private JSlider slide = new JSlider(1,50,10);

       BrightnessChanger() {
         createBufferedImages();
         setUpJFrame();
       }

       private void createBufferedImages() {
         Image image
           = new ImageIcon("test.jpg").getImage();
         originalImage
           = new BufferedImage(image.getWidth(null),
             image.getHeight(null),
             BufferedImage.TYPE_INT_RGB);
         convolvedImage
           = new BufferedImage(image.getWidth(null),
             image.getHeight(null),
             BufferedImage.TYPE_INT_RGB);
         Graphics g = originalImage.createGraphics();
         g.drawImage(image, 0, 0, null);
         g.dispose();
         setBrightnessFactor(1);
       }

       private void setUpJFrame() {
         JFrame myFrame = new JFrame("Image Brightness");
         myFrame.setSize(convolvedImage.getWidth(),
                         convolvedImage.getHeight());
         myFrame.getContentPane().setLayout(
           new BorderLayout());
         myFrame.getContentPane().add(
           this, BorderLayout.CENTER);
         slide.addChangeListener(
           new BrightnessListener());
         myFrame.getContentPane().add(
           slide,BorderLayout.SOUTH);
         myFrame.setDefaultCloseOperation(
           JFrame.EXIT_ON_CLOSE);
         myFrame.setVisible(true);
       }

       private void setBrightnessFactor(float multiple) {
         float[] brightKernel = {multiple};
         BufferedImageOp bright
           = new ConvolveOp(
               new Kernel(1, 1, brightKernel));
         bright.filter(originalImage, convolvedImage);
         repaint();

       }

        public void paintComponent(Graphics g) {
         g.drawImage(convolvedImage, 0, 0, this);
       }

       class BrightnessListener implements ChangeListener{
         public void stateChanged(
           ChangeEvent changeEvent) {
             setBrightnessFactor(
               (float)(slide.getValue())/10);
         }
       }

       public static void main(String[] args) {
           new BrightnessChanger();
         }

    }

BrightnessChanger예제는 convolution를 소개하는 것이 목적이기 때문에 이미지의 밝기를 조정할 때 추천할만한 방법은 아니다. 이후 테크팁에서 색과 밝기를 조정하는 방법에 대해서 다루게 될 것이다.

convolution의 계수배열을 위한 정방형/홀수형 배열의 이용 외에는 대부분의 경우에 대칭 배열을 사용해야 한다. 다음 예제는 이하의 3개 컴포넌트를 이용해서 구성한 3*3 배열을 이용하고 있다.

             0  0  0         0  1  0              1  0  1
   IDENTITY  0  1  0   EDGE  1  0  1      CORNER  0  0  0
             0  0  0         0  1  0              1  0  1

결과 배열은 이러한 구성 요소(building block)의 연속적인 조합으로 만들어진다. 엔트리의 합이 0이 아닌 이상, 사용자는 이 계수배열을 요소의 합으로 나누어 사용하게 된다. 그런 식으로 이미지의 원본과 같은 휘도의 이미지를 만들 수 있다. 예를 들어, EDGEIDENTITY를 더하면, 다음과 같다.

   0  1  0  
   1  1  1  
   0  1  0

엔트리의 합은 5이다. 이제 각각의 엔트리를 5로 나눠서 '번짐효과'를 주는 배열을 만든다.

    0  0.2  0
   .2  0.2 0.2
    0  0.2  0

3*3 형식 배열의 배수를 위한 파라미터로부터 이러한 계수배열을 생성하기 위해 코드를 만들자.

   private Kernel getKernel(
     int corner, int edge, int identity) {
      float[] kernel = new float[9];
      int sum = corner * 4 + edge * 4 + identity;
      if (sum == 0) sum = 1;
      for (int i = 0; i < 9; i++) {
        kernel[i] = (corner * CORNER[i]
          + edge * EDGE[i]
          + identity * IDENTITY[i]) / sum;
    }
    return new Kernel(3, 3, kernel);
 }

계수배열을 만들었다면 이전과 같은 방법으로 convolution을 실행하자.

   void convolveImage(Kernel kernel) {
      BufferedImageOp convolve
        = new ConvolveOp(kernel);
      buffImage = convolve.filter(buffImage, null);
      repaint();
   }

이제 테스트 프로그램인 Convolve를 실행해 보자. Convolve를 실행할 때 3개의 커맨드 라인 파라미터를 입력해야 한다. 이들은 각각 CORNER, EDGE, IDENTITY 의 배수에 해당하는 int값이다. 가령 다음과 같이 입력하면,

   java Convolve 1 0 0 

어느 정도 번짐 효과가 생긴 이미지를 볼 수 있다. 다시 한번, 이미지의 이름은 test.jpg로 한다.

   import javax.swing.ImageIcon;
   import javax.swing.JFrame;
   import javax.swing.JPanel;
   import java.awt.image.BufferedImage;
   import java.awt.image.BufferedImageOp;
   import java.awt.image.ConvolveOp;
   import java.awt.image.Kernel;
   import java.awt.Image;
   import java.awt.Graphics;

   public class Convolve extends JPanel {
      private BufferedImage buffImage;

      private final float[] IDENTITY = {0, 0, 0,
                                        0, 1, 0,
                                        0, 0, 0};

      private final float[] EDGE = {0, 1, 0,
                                    1, 0, 1,
                                    0, 1, 0};

      private final float[] CORNER = {1, 0, 1,
                                      0, 0, 0,
                                      1, 0, 1};


      Convolve(int corner, int edge, int identity) {
        createBufferedImages();
        setUpJFrame();
        convolveImage(getKernel(corner, edge, identity));
      }

      private void createBufferedImages() {
        Image image 
          = new ImageIcon("test.jpg").getImage();
        buffImage 
          = new BufferedImage(image.getWidth(null),
            image.getHeight(null), 
            BufferedImage.TYPE_INT_RGB);
        Graphics g = buffImage.createGraphics();
        g.drawImage(image, 0, 0, null);
        g.dispose();
      }

      private void setUpJFrame() {
        JFrame myFrame = new JFrame("Image Brightness");
        myFrame.setSize(buffImage.getWidth(),
          buffImage.getHeight());
        myFrame.getContentPane().add(this);
        myFrame.setDefaultCloseOperation(
          JFrame.EXIT_ON_CLOSE);
        myFrame.setVisible(true);
      }

      void convolveImage(Kernel kernel) {
        BufferedImageOp convolve
          = new ConvolveOp(kernel);
        buffImage = convolve.filter(buffImage, null);
        repaint();

      }

      private Kernel getKernel(
        int corner, int edge, int identity) {
         float[] kernel = new float[9];
         int sum = corner * 4 + edge * 4 + identity;
         if (sum == 0) sum = 1;
         for (int i = 0; i < 9; i++) {
           kernel[i] = (corner * CORNER[i]
             + edge * EDGE[i]
             + identity * IDENTITY[i]) / sum;
         }
        return new Kernel(3, 3, kernel);
      }

      public void paintComponent(Graphics g) {
        g.drawImage(buffImage, 0, 0, this);
      }


      public static void main(String[] args) {
        if (args.length != 3) {
          System.out.println("Usage: java Convolve" +
                             " corner edge identity");
          System.out.println("where corner, edge, " +
                             "and identity are ints");
          System.exit(0);
        }
        int corner = Integer.parseInt(args[0]);
        int edge = Integer.parseInt(args[1]);
        int identity = Integer.parseInt(args[2]);
        new Convolve(corner, edge, identity);

      }

   }

커맨드 라인 파라미터에 다른 값을 입력해보자. -1 -1 8를 입력하면 객체의 모서리가 강조된 것을 볼 수 있다. 이는 합이 0으로 계산 되지 않는다. 아니면 원본을 보다 뚜렷하게 보기 위해 0 -1 5을 입력해보자. 사용자는 이 예제를 5*5나 그 이상의 배열로 쉽게 확장해 볼 수 있다. ConvoleOp의 사용에 있어 좀 더 자세한 정보를 원하면 Java 2D API 프로그래머 가이드의 5장 "Imaging"을 참고하기 바란다.

2008/01/29 11:15 2008/01/29 11:15
    태그 연관글
      이글의 태그와 관련된 글이 없습니다.
    구글광고
    구독안내 Bywoong Blog는 주 2~3회 새글이 올라옵니다. 블로그 방문없이 업데이트 되는 글을 구독하세요. RSS . E-Mail . HanRSS . WZD . Google Reader . Bloglines

    댓글을 달아 주세요

    1. OpenID Logo바이웅님의 생각 2008/02/26 02:57  댓글주소  수정/삭제  댓글쓰기

      아무리 애를 써도 역시 한계가 있기는 있나봅니다.
      요즘 썸네일때문에 아주 골치가 아파요...@.@
      뭘 잘못사용했는지, JAI로 테스팅을 해봐도 개선이 안되는듯.

    [로그인][오픈아이디란?]



    Loading...