글 개발자를 위한 자바뉴스레터 - 저자 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이 아닌 이상, 사용자는 이 계수배열을 요소의 합으로 나누어 사용하게 된다. 그런 식으로 이미지의 원본과 같은 휘도의 이미지를 만들 수 있다. 예를 들어, EDGE 와 IDENTITY를 더하면, 다음과 같다.
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"을 참고하기 바란다.


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