주요글: 도커 시작하기
반응형
자바에서 이미지의 크기를 변환할 때 품질을 유지하는 방법을 살펴본다.

이미지 크기 변환시 품질 유지 방법

필자가 쓴 'JSP 2.0 프로그래밍' 책에서 자바 1.4부터 추가된 ImageIO 클래스를 사용해서 썸네일 이미지를 작성하는 방법을 소개한 바 있다. 이 때 소개한 코드는 다음과 같다.

    public static void resize(File src, File dest, 
                              int width, int height) throws IOException {
        BufferedImage srcImg = ImageIO.read(src);
        
        int srcWidth = srcImg.getWidth();
        int srcHeight = srcImg.getHeight();
        ...
        BufferedImage destImg = new BufferedImage(
             destWidth, destHeight, BufferedImage.TYPE_3BYTE_BGR);
        Graphics2D g = destImg.createGraphics();
        g.drawImage(srcImg, 0, 0, destWidth, destHeight, null);
        
        ImageIO.write(destImg, "jpg", dest);
    }

필자는 최근에 이미지 관련 서비스를 개발하고 있는데, 썸네일 이미지를 비롯하여 원본 이미지보다 작은 크기의 이미지를 몇 개 생성할 필요가 있어서 위와 같은 코드를 사용하였다. 위 코드는 비교적 빠른 속도로 이미지 크기 변환을 처리했지만, 새롭게 생성된 이미지의 품질이 떨어진다는 문제점을 발견하게 되었다.

이미지 품질을 높이기 위해 찾아낸 해결책은 Image.getScaledInstance(int width, int height, int hints) 메소드를 사용하는 것이었다. 로딩한 이미지를 getScaledInstance() 함수를 통해서 다음과 같이 크기 변환을 하면 변환된 이미지의 품질이 떨어지지 않게 되었다.

    BufferedImage srcImg = ImageIO.read(src);
    Image imgTarget = srcImg.getScaledInstance(destWidth, destHeight, Image.SCALE_SMOOTH);
    int pixels[] = new int[destWidth * destHeight]; 
    PixelGrabber pg = new PixelGrabber(imgTarget, 0, 0, destWidth, destHeight, pixels, 0, destWidth); 
    try {
        pg.grabPixels(); // JEPG 포맷의 경우 오랜 시간이 걸린다.
    } catch (InterruptedException e) {
        throw new IOException(e.getMessage());
    } 
    BufferedImage destImg = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_INT_RGB); 
    destImg.setRGB(0, 0, destWidth, destHeight, pixels, 0, destWidth); 
    

위 코드와 같이 getScaledInstance() 메소드의 세번째 파라미터로 Image.SCALE_SMOOTH를 사용하면 새롭게 생성된 이미지의 품질이 떨어지지 않게 된다. getScaledInstance() 메소드가 생성한 Image 객체로부터 픽셀 정보를 읽어온 뒤, 새롭게 생성한 BufferedImage에 채워 넣어주면 이미지 크기 변환이 마무리 된다.

그런데, 이미지 변환 과정에서 한가지 문제가 발생하였다. 그것은 바로 PNG, GIF, BMP와 달리 JPEG 포맷을 변환할 때는 PixelGrabber.granPixels() 함수를 실행할 때 시간이 오래 걸린다는 것이었다. (이미지 크기가 3000*2000인 경우 3분 이상 소요되기도 했다.) 그래서 몇가지 테스트 끝에 다음과 같이 ImageIcon 클래스를 사용해서 JPEG 이미지를 로딩할 경우 오랜 처리 시간 문제를 해결할 수 있다는 걸 알게 되었다. (왜 JPEG을 사용할 때 시간이 오래 걸리는 지의 문제는 아직 정확하게 파악하지 못했으나, BufferedImage가 JEPG 이미지를 저장할 때 사용되는 방식 때문인 것으로 판단된다.)

    Image srcImg = new ImageIcon(src.toURL()).getImage(); // JPEG 포맷인 경우

그런데, ImageIcon의 경우는 GIF와 JPEG 포맷의 이미지만 사용할 수 있기 때문에, BMP나 PNG 같은 파일은 ImageIcon으로 읽어올 수가 없다. 따라서, 다음과 같이 이미지 포맷에 따라서 서로 다른 방식으로 이미지를 로딩하도록 하였다.

    Image srcImg = null;
    String suffix = src.getName().substring(src.getName().lastIndexOf('.')+1).toLowerCase();
    if (suffix.equals("bmp") || suffix.equals("png") || suffix.equals("gif")) {
        srcImg = ImageIO.read(src);
    } else {
        // JPEG 포맷
        srcImg = new ImageIcon(src.toURL()).getImage();
    }

위와 같은 코드를 사용하면, JPEG인 경우에는 ImageIcon을 사용해서 Image를 생성하고 그 외에 경우에는 ImageIO.read()를 사용해서 Image를 읽어오게 된다. 이제 JPEG 포맷에 대해서 PixelGrabber.granPixels() 메소드를 사용하더라도 빠르게 이미지 크기 변환을 수행할 수 있게 되었다.

이미지 변환과 관련된 완전한 코드는 다음과 같다.

    public class ImageUtil {
        public static final int RATIO = 0;
        public static final int SAME = -1;
        
        public static void resize(File src, File dest, int width, int height) throws IOException {
            Image srcImg = null;
            String suffix = src.getName().substring(src.getName().lastIndexOf('.')+1).toLowerCase();
            if (suffix.equals("bmp") || suffix.equals("png") || suffix.equals("gif")) {
                srcImg = ImageIO.read(src);
            } else {
                // BMP가 아닌 경우 ImageIcon을 활용해서 Image 생성
                // 이렇게 하는 이유는 getScaledInstance를 통해 구한 이미지를
                // PixelGrabber.grabPixels로 리사이즈 할때
                // 빠르게 처리하기 위함이다.
                srcImg = new ImageIcon(src.toURL()).getImage();
            }
            
            int srcWidth = srcImg.getWidth(null);
            int srcHeight = srcImg.getHeight(null);
            
            int destWidth = -1, destHeight = -1;
            
            if (width == SAME) {
                destWidth = srcWidth;
            } else if (width > 0) {
                destWidth = width;
            }
            
            if (height == SAME) {
                destHeight = srcHeight;
            } else if (height > 0) {
                destHeight = height;
            }
            
            if (width == RATIO && height == RATIO) {
                destWidth = srcWidth;
                destHeight = srcHeight;
            } else if (width == RATIO) {
                double ratio = ((double)destHeight) / ((double)srcHeight);
                destWidth = (int)((double)srcWidth * ratio);
            } else if (height == RATIO) {
                double ratio = ((double)destWidth) / ((double)srcWidth);
                destHeight = (int)((double)srcHeight * ratio);
            }
            
            Image imgTarget = srcImg.getScaledInstance(destWidth, destHeight, Image.SCALE_SMOOTH); 
            int pixels[] = new int[destWidth * destHeight]; 
            PixelGrabber pg = new PixelGrabber(imgTarget, 0, 0, destWidth, destHeight, pixels, 0, destWidth); 
            try {
                pg.grabPixels();
            } catch (InterruptedException e) {
                throw new IOException(e.getMessage());
            } 
            BufferedImage destImg = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_INT_RGB); 
            destImg.setRGB(0, 0, destWidth, destHeight, pixels, 0, destWidth); 
            
            ImageIO.write(destImg, "jpg", dest);
        }
    }

관련링크:

+ Recent posts