2010年4月12日月曜日

ImageIOを使って画像操作

Javaのjavax.imageio.ImageIOを使うと、画像操作が簡単に出来ます。

たとえば、gifファイルをjpegファイルに変換するには以下のコードで行えます。
BufferedImage image = ImageIO.read(new File("input.gif"));
ImageIO.write(image, "jpeg", new File("output.jpg"));


簡単ですね。素晴らしい。

カラーのJPEG画像を、グレースケールに変換するには以下で。
BufferedImage image = ImageIO.read(new File("input.jpg"));
BufferedImage newImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
newImage.getGraphics().drawImage(image, 0, 0, null);
ImageIO.write(newImage, "png", new File("output.jpg"));


BufferedImageのタイプには、
public static final int TYPE_INT_RGB = 1;
public static final int TYPE_INT_ARGB = 2;
public static final int TYPE_INT_BGR = 4;
public static final int TYPE_BYTE_GRAY = 10;
public static final int TYPE_BYTE_BINARY = 12;

などなどあり、前記の例では、TYPE_BYTE_GRAYのBufferedImageに書き込んだのでグレースケールにしています。

そんな便利なBufferdImageですが、透過GIFの場合は注意が必要です。
透過GIFを、ImageIO.readで読込みして作成したBufferedImageのタイプは、TYPE_BYTE_BINARY = 12 になります。
TYPE_INT_ARGBではないのですねー。

透過GIFを新しいBufferedImageに書き込みして編集し、再び透過GIFとして出力したい場合にて癖があるみたい。
例えば、単純に以下の操作をしてみる
BufferedImage image = ImageIO.read(new File("alpha.gif"));
BufferedImage newImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
newImage.getGraphics().drawImage(image, 0, 0, null);
ImageIO.write(newImage, "gif", new File("output.gif"));

すると、出力結果は透過GIFではなくなってしまいます。

透過GIFとして出力するためには...
一つ目の方法としては、書き込み先のBufferedImageをインデックスカラーのタイプ(BufferedImage.TYPE_BYTE_INDEXED)にして、カラーパレットを作成し、透過の色を指定してあげると透過GIFとして出力できるようです。以下は、透過gif, 透過pngファイルを一つにして出力した例。

public static void main(String[] args) {
try {
BufferedImage image = ImageIO.read(new File("alpha.png"));
BufferedImage image2 = ImageIO.read(new File("alpha.gif"));
BufferedImage newImage = convertToIndexed(image);
newImage.getGraphics().drawImage(image2, 0, 0, null);
ImageIO.write(newImage, "gif", new File("output.gif"));
} catch (Exception ex) {
ex.printStackTrace();
}
}

public static BufferedImage convertToIndexed(BufferedImage src) {
BufferedImage dest = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_BYTE_INDEXED);
Graphics g = dest.getGraphics();
g.setColor(new Color(231, 20, 189));
g.fillRect(0, 0, dest.getWidth(), dest.getHeight());
dest = makeTransparent(dest, 0, 0);
dest.createGraphics().drawImage(src, 0, 0, null);
return dest;
}

private static BufferedImage makeTransparent(BufferedImage image, int x, int y) {
ColorModel cm = image.getColorModel();
if (!(cm instanceof IndexColorModel))
return image;
IndexColorModel icm = (IndexColorModel) cm;
WritableRaster raster = image.getRaster();
int pixel = raster.getSample(x, y, 0);
int size = icm.getMapSize();
byte[] reds = new byte[size];
byte[] greens = new byte[size];
byte[] blues = new byte[size];
icm.getReds(reds);
icm.getGreens(greens);
icm.getBlues(blues);
IndexColorModel icm2 = new IndexColorModel(8, size, reds, greens, blues, pixel);
return new BufferedImage(icm2, raster, image.isAlphaPremultiplied(), null);
}

透過色をベタっと塗りつぶしてあげる感じ。
だけど、画質が落ちてしまうんですよねー。

もう一つの方法としては、BufferedImage.TYPE_INT_ARGBのBufferedImageに透過の変換をしたイメージ書き込んで透過画像を出力する方法。以下で出来る様子。

public static void main(String[] args) throws IOException {
BufferedImage image1 = ImageIO.read(new File("alpha.png"));
BufferedImage image2 = ImageIO.read(new File("alpha.gif"));

Image transpImg1 = makeColorTransparent(image1, new Color(image1.getRGB(0, 0)));
Image transpImg2 = makeColorTransparent(image2, new Color(image2.getRGB(0, 0)));
BufferedImage dest = new BufferedImage(300,300, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = dest.createGraphics();
g2.drawImage(transpImg1, 0, 0, null);
g2.drawImage(transpImg2, 0, 0, null);
g2.dispose();

File outFile2 = new File("sample2.png");
ImageIO.write(dest, "PNG", outFile2);
}

public static Image makeColorTransparent(BufferedImage im, final Color color) {
ImageFilter filter = new RGBImageFilter() {
public int markerRGB = color.getRGB() | 0xFF000000;
public final int filterRGB(int x, int y, int rgb) {
if ((rgb | 0xFF000000) == markerRGB) {
return 0x00FFFFFF & rgb;
} else {
return rgb;
}
}
};
ImageProducer ip = new FilteredImageSource(im.getSource(), filter);
return Toolkit.getDefaultToolkit().createImage(ip);
}

どっちも、x=0, y=0の位置のものを透過色としているって感じ?あってる?

後者の方が画質が落ちないのですが、Toolkit.getDefaultToolkit().createImageを使うと環境依存がある的な記事もみたので、ちょっと気になったりもする。
• • •