Go to Previous Page Go to Contents Go to Java Page Go to Next Page
New Features of Java2 SDK, Standard Edition, v1.4
 
 

Image I/O

 
 

画像ファイルのロードとセーブなら

 
 

Java でのグラフィック環境は AWT の頃はむちゃくちゃ不便というか、全然機能がありませんでした。たとえば、線を書くにしても太さを変えることはできません。しょうがないので、1 dot づつずらして複数回書くなどということを行っていました。

そんな不便さも、Java 2 で導入された Java 2D を使えば過去のものになりました。その当時、Java 2 SDK に付属している Java 2D のデモを見て、驚いたものです。

しかし、それだけでは終わりませんでした。その後、グラフィックを強化させるために Java Adavance Imaging (JAI) が JCP の JSR 34 で策定されています。JAI はプルモデルを採用したグラフィックライブラリで、とても興味深いのですが、書籍や雑誌などで取り上げられたことがほとんどないためマイナーな存在になってしまっています。機会があれば、ぜひ取上げてみたいと思っています。

さて、今回取り上げる Image I/O はもともとは、この JAI の一部だったのですが、それが独立して JSR 15 になったという経緯を持ちます。なぜ、番号が Image I/O の方が若いのかは謎です ^^;;

そして、Image I/O は本家を差し置いて J2SE v1.4 に標準バンドルされてしまったのです。

Image I/O は I/O という言葉が表しているように、イメージのロードとセーブを行うための API です。ロードとセーブを行うには画像ファイルのフォーマットに応じたエンコード、デコード処理も含まれます。

Image I/O の特徴の 1 つにプラガビリティがあります。ある画像ファイルフォーマットを扱うためには、そのフォーマットを扱えるプラグインが必要になります。しかし、ユーザはそれを意識することなく使用することができます。

標準で提供されているプラグインは、イメージのロードが JPEG, PNG, GIF、セーブが JPEG, PNG になっています。GIF のセーブはライセンスの問題があるのでサポートされていないようです。その他のフォーマットを扱う場合にはプラグインを自作する必要があります。

ただ、プラグインを書こうと思われる方は非常に小数だと思うので、今回は扱わないことにします。ということで、イメージのロードとセーブに的を絞って解説していきたいと思います。

 

 
 

なにはともロードとセーブをしてみる

 
 

とりあえず、なにも考えずに画像ファイルを読みこんでみましょう。

ImageIOTest1 では、ある画像ファイルを読みこんで、それをフレームで表示するというサンプルアプリケーションです。

アプリケーションのソース ImageIOTest1.java

Image I/O で中心になるのは javax.imageio.ImageIO クラスです。このクラスだけで画像ファイルのロード/セーブが行えます。ImageIOTest1 はこの ImageIO クラスだけを使用して、イメージのロードを行っています。

ImageIOTest1 はたかだか 40 行ぐらいのプログラムなので全文を示しておきます。

 1:import java.awt.Image;
 2:import java.awt.image.BufferedImage;
 3:import java.io.File;
 4:import java.io.IOException;
 5:import javax.imageio.ImageIO;
 6:import javax.swing.ImageIcon;
 7:import javax.swing.JFrame;
 8:import javax.swing.JLabel;
 9:
10:public class ImageIOTest1 {
11:    public ImageIOTest1(String filename) {
12:        File f = new File(filename);
13:
14:        try {
15:            BufferedImage image = ImageIO.read(f);
16:
17:            initFrame(filename, image);
18:        } catch (IOException ex) {
19:            ex.printStackTrace();
20:        }
21:    }
22:    
23:    private void initFrame(String imageName, Image image) {
24:        JFrame frame = new JFrame(imageName);
25:        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
26:
27:        ImageIcon icon = new ImageIcon(image);
28:        JLabel label = new JLabel(icon);
29:	
30:        frame.getContentPane().add(label);
31:        frame.pack();
32:        frame.setVisible(true);
33:    }
34:
35:    public static void main(String[] args){
36:        if (args.length == 1) {
37:            new ImageIOTest1(args[0]);
38:        } else {
39:            System.out.println("Usage: java ImageIOTest1 ");
40:        }
41:    }
42:}

全文は示したのですが、この中で Image I/O を使用しているのは、たったの 1 行です。それは 15 行目の ImageIO.read(f); という部分です。

ImageIO クラスは前述したように Image I/O クラスの中心となるクラスです。Image I/O の static メソッドの read メソッドは引数の違いにより次のように 4 種類あります。戻り値はすべて java.awt.image.BufferedImage オブジェクトとなります。

  1. public static BufferedImage read(java.io.File file)
  2. public static BufferedImage read(javax.imageio.stream.ImageInputStream stream)
  3. public static BufferedImage read(java.io.InputStream stream)
  4. public static BufferedImage read(URL url)

ImageIOTest1 では 1 の引数が File オブジェクトのものを使用しましたが、それ以外にストリームと URL が使用できます。2 番目の javax.imageio.stream.ImageInputStream は Image I/O で提供されている画像用のストリームです。URL が使えるということはネットワーク上にある画像ファイルも扱うことができるということです。

read メソッドはまずファイルの先頭部分を読み込んで、そのファイルがどのような画像フォーマットか調べます。そして、その画像フォーマットに応じたプラグインを利用してイメージをロードします。このため、拡張子が異なっていても、正確にロードを行うことができるのです。

29 行目からの initFrame は読みこんだイメージを表示しているだけです。

ところで、ここまで見てきて、今までの画像ファイルの取り扱いとそんなに変わっていないと思われる方も多いと思います。

今まではアプレットであれば java.applet.Applet#loadImage メソッド、それ以外なら java.awt.Toolkit#getImage メソッド、もしくは javax.swing.ImageIcon クラスを使用されると思います。

さて、何がちがうのでしょう。

たいして変わりません ^^;;

あえて違いをあげるとすれば、Applet#loadImage メソッドや Toolkit#getImage メソッドはいつ画像のロードが終わったのかは java.awt.MediaTracker を使用しないと分からないという点ぐらいです。ImageIcon クラスは表面上は MediaTracker を使用していないようですが、内部で MediaTracker を使用しています。

Image I/O の read メソッドは画像ファイルを最後まで読みこむまでブロッキングされます。ですから、read メソッドの次の行からロードしたイメージを使用することができます。

ここまでではありがたみが分からないのでイメージのセーブをしてみましょう。

今まで、イメージをセーブするには com.sun.image.codec.jpeg パッケージのクラス群を使用することができましたが、パッケージ名が表すように Java の標準的な API ではありませんでした。また、JPEG 以外の画像ファイルフォーマットも扱うこともできませんでした。

そこで、Image I/O の登場です。

アプリケーションのソース ImageIOTest2.java

ImageIOTest2 は引数を 2 つとり、最初の引数で示された画像ファイルを第 2 引数で示されたファイルにセーブするアプリケーションです。ImageIOTest2 でイメージのセーブを行っているコンストラクタとユーティリティメソッドの getExtension メソッドを次に示します。

 1:    public ImageIOTest2(String sourceFile, String destinationFile) {
 2:        File source = new File(sourceFile);
 3:        File dest = new File(destinationFile);
 4:        String extension = getExtension(destinationFile);
 5:
 6:        try {
 7:            BufferedImage image = ImageIO.read(source);
 8:            ImageIO.write(image, extension, dest);
 9:        } catch (IOException ex) {
10:            ex.printStackTrace();
11:        }
12:    }
13:
14:    private String getExtension(String filename) {
15:        int i = filename.lastIndexOf('.');
16:        if(i > && i < filename.length()-1) {
17:            return filename.substring(i+1).toLowerCase();
18:        };
19:
20:        return "";
21:    }

7 行目でイメージのロードを行ったら、それを 8 行目でセーブしています。write メソッドの第 1 引数は java.awt.image.RenderedImage オブジェクト、第 2 引数が画像ファイルフォーマット、第 3 引数がセーブするファイルになります。read メソッドと同様に、write メソッドも出力先として java.io.File オブジェクト、java.io.OutputStream オブジェクト、javax.imageio.stream.ImageOutputStream オブジェクトを指定することができます。

第 1 引数の RenderedImage インタフェースはあまりなじみがないかもしれませんが、ビットマップイメージを扱うためのインタフェースです。BufferedImage クラスは RenderedImage インタフェースを実装しているので、ImageIO クラスの write メソッドで使用することができます。

第 2 引数の画像ファイルフォーマットは JPEG であれば "JPG"、"JPEG", "jpg", "jpeg" のどれかになります。同じように PNG であれば "PNG" もしくは "png" です。

ImageIOTest2 クラスではフォーマットにはファイルの拡張子を利用しています。ファイルの拡張子を抽出しているのが getExtension メソッドです。ただ、単に拡張子を使っているだけで、その拡張子に応じた画像ファイルフォーマットを使用できるかどうかは調べていません。

どのような画像ファイルフォーマットを扱うことができるかを調べるためのメソッドも ImageIO クラスには用意されています。それを使ってみたのが ImageIOTest3 です。

アプリケーションのソース ImageIOTest3.java

 

 1:    public ImageIOTest3() {
 2:        String[] readerNames = ImageIO.getReaderFormatNames();
 3:        System.out.println("ImageIO Readable Image Type:");
 4:        for (int i = 0 ; i < readerNames.length ; i++) {
 5:            System.out.println(readerNames[i]);
 6:        }
 7:
 8:        String[] readerMimes = ImageIO.getReaderMIMETypes();
 9:        System.out.println("\nImageIO Readable MIME Type:");
10:        for (int i = 0 ; i < readerMimes.length ; i++) {
11:            System.out.println(readerMimes[i]);
12:        }
13:
14:        String[] writerNames = ImageIO.getWriterFormatNames();
15:        System.out.println("\nImageIO Writable Image Type:");
16:        for (int i = 0 ; i < writerNames.length ; i++) {
17:            System.out.println(writerNames[i]);
18:        }
19:
20:        String[] writerMimes = ImageIO.getWriterMIMETypes();
21:        System.out.println("\nImageIO Writable MIME Type:");
22:        for (int i = 0 ; i < writerMimes.length ; i++) {
23:            System.out.println(writerMimes[i]);
24:        }
25:    }

ここでは扱うことができる画像ファイルフォーマットを調べるために 4 種類のメソッドを使用しています。

public String[] getReaderFormatNames() ロードできる画像ファイルのフォーマット名の一覧
public String[] getReaderMimeNames() ロードできる画像ファイルの MIME タイプの一覧
public String[] getWriterFormatNames() セーブできる画像ファイルフォーマット名の一覧
public String[] getWriterMimeNames() セーブできる画像ファイルの MIME タイプの一覧

これを実行してみましょう。

C:\temp>java ImageIOTest3
ImageIO Readable Image Type:
jpeg
gif
JPG
png
jpg
JPEG

ImageIO Readable MIME Type:
image/png
image/jpeg
image/x-png
image/gif

ImageIO Writable Image Type:
jpeg
png
JPG
PNG
jpg
JPEG

ImageIO Writable MIME Type:
image/png
image/jpeg
image/x-png
 
C:\temp>

この結果から標準でロードできるのは JPEG, PNG, GIF の 3 種類のフォーマット、セーブできるのは JPEG と PNG だということがお分かりになると思います。

このように ImageIO クラスを使えば、ほとんど何も考えずにイメージのロード・セーブができるのですが、簡単にできるということは逆にいえば痒いところには手が届かないわけです。そこで、次からはもう少し痒いところに手が届くような使い方をみていきましょう。

 

 
 

イメージのロード

 
 

イメージをロードするには javax.imageio.ImageReader クラスを使用します。さっそく、このクラスを使用したサンプルを見てみましょう。

アプリケーションのソース ImageReaderTest1.java

イメージをロードしている部分を次に示します。

 1:    protected Image readImage(String filename) {
 2:        Iterator readers = ImageIO.getImageReadersBySuffix(getSuffix(filename));
 3:  
 4:        while (readers.hasNext()) {
 5:            ImageReader reader = (ImageReader)readers.next();
 6: 
 7:            try {
 8:                ImageInputStream stream
 9:                    = ImageIO.createImageInputStream(new File(filename));
10:                reader.setInput(stream);
11:
12:                Image image = reader.read(0);
13:                reader.dispose();
14:          
15:                return image;
16:            } catch (IOException ex) {
17:                ex.printStackTrace();
18:            }
19:        }
20:
21:        return null;
22:    }

ImageReader オブジェクトを生成するには ImageIO クラスのファクトリメソッドを使用します。ImageIO クラスの read メソッドでは画像のフォーマットを自動的に調べてくれましたが、ImageReader は画像ファイルフォーマットに 1 対 1 に対応しているクラス (実際には ImageReader クラスの派生クラスが画像フォーマットと対応します) なので明示的に画像ファイルのフォーマットを指定しなくてはなりません。フォーマットを指定するにはフォーマットの名前か、MIME タイプで行います。

フォーマット名を使用するには

public Iterator getImageReadersByFormatName(String type)

MIME で指定する場合は

public Iterator getImageReadersByFormatName(String mimeType)

を使用します。どちらのメソッドも戻り値は Iterator オブジェクトになります。これはその画像フォーマットを扱うことのできるプラグインが複数あるかもしれないからです。

そこで 4 行目からの while ループで Iterator オブジェクトから 1 つづつ ImageReader オブジェクトを取り出して使用するようにします。

もし、画像フォーマットを扱えない場合は、空の Iterator オブジェクトが返されます (null が返されるわけではありません)。

ImageReader オブジェクトを取得できたら、次に ImageReader オブジェクトへの入力を指定します。入力には ImageIO クラスの read でも使用できた javax.imageio.stream.ImageInputStream クラスを使用します。ImageInputStream オブジェクトは 8 行目のように ImageIO クラスの createImageInputStream メソッドを使用して生成します。このメソッドは引数が Object オブジェクトなのですが、通常は java.io.File, java.io.RandomAcessFile, java.io.InputStream などのオブジェクトを使用することができます。

ImageInputStream は実際にはインタフェースで、プラグインを追加することによって他の入力媒体を扱うことができるようになります。Image I/O はこんなところもプラガブルになっているわけです。

生成した ImageInputStream オブジェクトを ImageReader オブジェクトに設定するために 9 行目の setInput メソッドを使用します。そして、11 行目で示したように read メソッドを使用してイメージをロードします。

画像フォーマットによれば、複数の画像を 1 つのファイルに保持することも可能です。例えば、Animated GIF などが複数の画像をまとめることができます。read メソッドの引数は、そんな複数の画像を持つ入力に対して、何枚目の画像をロードするかを指定します。

ImageReader オブジェクトを使用しおえたら、最後に ImageReader#dispose メソッドをコールして ImageReader オブジェクトが使用したリソースを解放させるのを忘れないようにしましょう。

read メソッドの戻り値は先ほどと同じように BufferedImage オブジェクトです。

 

 
 

画像ファイルの情報とサムネイル

 
 

単に画像をロードすることができたのですが、ImageReader クラスではイメージのロードを行わずに画像ファイルの情報を得ることができます。通常、画像ファイルにはヘッダと呼ばれるイメージの情報を保持した部分があります。そこの部分だけを読み込むことで、イメージのロードをせずに情報だけ取得することができるのです。

アプリケーションのソース ImageReaderTest2.java

ImageReader クラスで取得できる画像情報には次のようなものがあります。

情報 使用するメソッド
画像フォーマット String getFormatName()
int getWidth(int imageIndex)
高さ int getHeight(int imageIndex)
アスペクト比 float getAspectRatio(int imageIndex)
画像の枚数 int getNumImages(boolean allowSearch)
サムネイルの有無 boolean hasThumbnails(int imageIndex)
サムネイルの枚数 int getNumThumbnails(int imageIndex)
サムネイルの幅 int getThumbnailWidth(int imageIndex, int thumbnailIndex)
サムネイルの高さ int getThumbnailHeight(int imageIndex, int thumbnailIndex)
タイリングの有無 boolean isImageTiled(int imageIndex)
タイルの幅 int getTileWidth(int imageIndex)
タイルの高さ int getTileHeight(int imageIndex)
タイルの x 軸方向のオフセット int getTileGridXOffset(int imageIndex)
タイルの y 軸方向のオフセット int getTileGridYOffset(int imageIndex)

 

これらの情報の中で画像の枚数だけはヘッダだけだと取得できない場合があります。例えば、Animated GIF はファイルをすべて読み込まないと画像の枚数が分かりません。このため、getNumImages メソッドにはファイルの最後までサーチするかどうかを示すフラグを引数として与えるようにします。

ただし、ファイルが順方向にアクセスできないような場合、最後までサーチするように指定しても IllegalStateException が発生します。順方向にアクセスしかできないかどうかは isSeekForwardOnly メソッドを使用して調べることができます。また、入力を設定する setInput メソッドで順方向アクセスのみかどうかを指定することができます。

さて、これらのメソッドを使用してみたのが ImageReaderTest2 です。

    protected Image readImage(String filename) { 
        Iterator readers = ImageIO.getImageReadersBySuffix(getSuffix(filename));
        
        if (readers.hasNext()) {
            ImageReader reader = (ImageReader)readers.next();
            
            try {
                ImageInputStream stream
                    = ImageIO.createImageInputStream(new File(filename));
                reader.setInput(stream);
 
                System.out.println("Format: " + reader.getFormatName());
                System.out.println("Aspect Ratio: " + reader.getAspectRatio(0));
 
                System.out.println("Number of Images [Seek File]: "
                                       + reader.getNumImages(true));
 
                System.out.println("Thumbnails: " + reader.hasThumbnails(0));
                System.out.println("Number of Thumbnails: " + reader.getNumThumbnails(0));
 
                System.out.println("Width: " + reader.getWidth(0));
                System.out.println("Height: " + reader.getHeight(0));
 
                if (reader.hasThumbnails(0)) {
                    showThumbnails(reader);
                }
 
                Image image = reader.read(0);
                reader.dispose();
 
                return image;
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
 
        return null;
    }

画像フォーマットによってはサムネイルをつけられるものがありますが、ImageReader クラスではサムネイルを扱うこともできます。サムネイルは readThumbnail メソッドを使用してロードできます。また、同じようにタイルも扱うことができ、こちらは readTile メソッドを使用してロードできます。

ImageReaderTest2 ではサムネイルがある場合にはサムネイルのロードも行っています。

 1:    private void showThumbnails(ImageReader reader) {
 2:        try {
 3:            JLabel[] thumbnails = new JLabel[reader.getNumThumbnails(0)];
 4:            for (int i = 0 ; i < reader.getNumThumbnails(0) ; i++) {
 5:                BufferedImage thumbnail = reader.readThumbnail(0, i);
 6:                thumbnails[i] = new JLabel(new ImageIcon(thumbnail));
 7:            }
 8:            
 9:            JPanel panel = new JPanel();
10:            for (int i = 0 ; i < thumbnails.length ; i++) {
11:                panel.add(thumbnails[i]);
12:            }
13:
14:            JOptionPane.showMessageDialog(new JFrame(), panel,
15:                                          "Thumbnails", JOptionPane.PLAIN_MESSAGE);
16:        } catch (IOException ex) {
17:            ex.printStackTrace();
18:        }
19:    }

1 枚の画像に複数のサムネイルが含まれることがあるので、サムネイルの表示には 3 行目に示すように JLabel の配列を使用してみました。その後、枚数分だけループを繰り返し、5 行目の readThumbnail メソッドでサムネイルのロードを行っています。readThumbnail メソッドの第 1 引数がイメージのインデックス、第 2 引数が第 1 引数で示されるイメージのサムネイルのインデックスになります。

読み込んだサムネイルはパネルに並べて (9 〜 12 行目)、ダイアログで表示しています (14, 15 行目)。

実際に複数のイメージやサムネイルを扱えるかどうかはプラグイン次第です。標準で提供されている GIF のプラグインは複数イメージが扱えるようです。

 

 
  条件付きのイメージのロード  
 

普通のイメージのロードではなく、イメージの一部分だけロードしたり、縮小イメージのロードを行ったりすることも ImageReader クラスを使えば可能です。

画像ビューアなどのアプリケーションでは、普通はサムネイルだけ表示されており、イメージを指定するとオリジナルのイメージを表示するものが多くあります。このときにサムネイルだけ表示するのに、イメージをロードしてから縮小イメージを作成し、ロードしたオリジナルイメージは保持しておくにはメモリの消費が大きいので捨ててしまうということがよく行われています。そして、オリジナルの表示の時には画像を再ロードします。

しかし、この方法だとロードと縮小イメージを作るコストはバカになりません。それだったらはじめから縮小イメージをロードするようにすればメモリの使用量も減ります。

そこで登場するのが ImageReadParam クラスです。これを利用してイメージのロードを行ったのが ImageReaderTest3.java になります。

アプリケーションのソース ImageReaderTest3.java

ImageReadParam クラスはイメージのロードのいろいろなパラメータを設定するために使用します。

 1:    protected Image readImage(String filename) { 
 2:        Iterator readers = ImageIO.getImageReadersBySuffix(getSuffix(filename));
 3:        
 4:        if (readers.hasNext()) {
 5:            ImageReader reader = (ImageReader)readers.next();
 6:             
 7:            try {
 8:                ImageInputStream stream
 9:                    = ImageIO.createImageInputStream(new File(filename));
10:                reader.setInput(stream);
11: 
12:                ImageReadParam param = reader.getDefaultReadParam();
13: 
14:                param.setSourceRegion(new Rectangle(200, 200, 400, 400));
15:                param.setDestinationOffset(new Point(50, 0));
16:                param.setSourceSubsampling(4, 2, 2, 1);
17:                
18:                Image image = reader.read(0, param);
19:                reader.dispose();
20:               
21:                return image;
22:            } catch (IOException ex) {
23:                ex.printStackTrace();
24:            }
25:        }
26: 
27:        return null;
28:    }

ImageReadParam クラスのインスタンス化は new でできますが、実際には画像フォーマットに応じた ImageReadParam クラスの派生クラスを使用するために ImageReader クラスの getDefaultReadParam メソッドを使用するほうが望ましいようです。11 行目で ImageReadParam オブジェクトをこの方法で取得しています。

今回は、パラメータとして読み込む範囲 (13 行目)、生成するイメージのオフセット (14 行目)、間引きロードの条件 (15 行目) を使用してみました。

SourceRegion はイメージの一部だけを読み込むときに使用します。13 行目では座標でいうと (200, 200) から (400, 400) の縦 200 pixel 横 200 pixel の部分を読み込むように設定しています。

DestinationOffset は生成するイメージのオフセットです。14 行目は (50, 0) から画像の表示を行うようにオフセットを使用しています。オフセットされた部分は黒になります。

15 行目が縮小イメージをロードするための、間引きロードです。setSourceSubsampling メソッドの第 1 引数が x 軸方向の間引き量、第 2 引数が y 軸方向の間引き量、第 3 引数が x 軸方向の間引きのオフセット、第 4 引数が y 軸方向のオフセットになります。15 行目では x 軸方向には 4 pixel に 1 pixel だけロードを行い、そのときのオフセットは 2 になります。y 軸方向は 2 pixel に 1 pixel 間引きし、そのオフセットは 1です。

ようするに、ロードする pixel は x 軸方向には (2, 1), (6, 1), (10, 1) となり、y 軸方向では (2, 1), (2, 3), (2, 5) のようになり、縦長のイメージになってしまいます。

ちなみに setSourceSubsampling の引数が 1, 1, 0, 0 であれば、間引きを行わないオリジナルの画像をロードできます。

パラメータを設定したら 17 行目のように、read メソッドに引数として渡すことで、パラメータに応じたロードを行います。

さて、条件付でロードができるようになったので、イメージをサブサンプリングでロードするのと、通常のイメージをロードして縮小するのではどのくらいパフォーマンスが異なるか測ってみましょう。

アプリケーションのソース ImageReaderTest4.java

イメージの縮小にはいろいろな方法があるのですが、ImageReaderTest4 では BufferedImage オブジェクトに対するイメージ処理を行う java.awt.image.BufferedImageOp インタフェースを使用してみました。BufferedImageOp はインタフェースなので、実際にはイメージ処理に応じた実装クラスを使用します。イメージのスケーリングを行うには java.awt.image.AffineTransformOp クラスを使用します (RescaleOp というクラスもありますが、こちらは色のスケーリングです)。

 1:    private Image readShrinkImage(ImageReader reader) {
 2:        BufferedImage shrinkImage = null;
 3:
 4:        try {
 5:            long start = System.currentTimeMillis();
 6:            BufferedImage image = reader.read(0);
 7:            
 8:            shrinkImage = new BufferedImage(image.getWidth()/10, image.getHeight()/10,
 9:                                            image.getType());
10:            AffineTransformOp atOp = new AffineTransformOp(
11:                                   AffineTransform.getScaleInstance(0.1, 0.1), null);
12:            atOp.filter(image, shrinkImage);
13:
14:            long end = System.currentTimeMillis();
15:
16:            System.out.println("Shrink Image Load Time: " + (end - start));
17:        } catch (IOException ex) {
18:            ex.printStackTrace();
19:        }
20:
21:        return shrinkImage;
22:    }

6 行目でイメージのロードを行っているのは今までと同じです。その後、ロードしたイメージの縮小サイズの BufferedImage オブジェクトを生成します。イメージのタイプ (BufferedImage.TYPE_INT_RBG など) が異なっているとスケーリングができないので注意が必要です。

次に 10 行目で AffineTransformOp オブジェクトを生成しています。AffineTransformOp オブジェクトを生成するには java.awt.geom.AffineTransform オブジェクトが必要なので、AffineTransform クラスの static メソッドでスケーリングを行うオブジェクトを生成できる getScaleInstance メソッドを使用しました。このメソッドは第 1 引数が x 軸方向のスケーリングファクター、第 2 引数が y 軸方向のスケーリングファクターになります。11 行目では x 軸、y 軸とも 1/10 にするようにしました。AffineTransformOp のコンストラクタの最後の引数はスケーリングの方式を指定するものです。null の場合はニアレストネイバーという補間アルゴリズムが使用されます。

そして、12 行目でロードしたイメージから縮小イメージを生成しています。filter メソッドの第 1 引数がオリジナルのイメージ、第 2 引数に処理が施されたイメージになります。

さて、実行してみましょう。筆者の環境は CPU が Athron 1.2 GHz, メモリが 512MB で、OS が Windows 2000 です。使用した画像ファイルは 600 万画素のデジカメの画像ファイルを使用してみました。

C:\temp>
java ImageReaderTest4 sample.jpg
Shrink Image Load Time: 1843
Subsampling Image Load Time: 1031
 
C:\temp>

結果的にはサブサンプリングでロードしたものの方が半分ぐらいの時間で済んでいます。

どちらか一方だけを使用するようにしてメモリ使用量も調べてみました。メモリ使用量は Windows のタスクマネージャを使用して最大メモリ使用量を調べました。画像ファイルは先ほどのものと同じもので、画像ファイルのサイズは 2.52 MB です。

手法 最大メモリ使用量 [KB]
通常ロード & 縮小
33,580
サブサンプリングロード
15,264

 

メモリ使用量も時間と同じように、サブサンプリングの方が約半分のメモリ量で済んでいます。

とはいうものの、縮小イメージを得るためのアルゴリズムにはいろいろあります。この中でも、サブサンプリングで行っている間引きは一番単純な方法になります。そのため、サムネイルに使用するためのイメージを生成するぐらいであればいいのですが、高度なイメージ処理を行うには適さないと思います。

実際、両方の方法で表示されたイメージを見ると少し色が異なっていたり、コントラストが違っているのが分かると思います。どちらがいいかは一概にはいえないので、適材適所で使う手法を変えるのがいいのでしょう。

 

 
  画像のロードに関するイベント  
 

イメージのロードを行っているとき、今どこまでロードできているかが知りたくなることはないですか。

従来であれば MediaTracker クラスを使って、ロードが完了したかどうかは知ることができました。しかし、今どのくらいまでロードが終わっているかを調べることはできませんでした。

しかし、例えばプログレスバーでどの程度までロードされているか表示できれば、ロード中であることだけ表示するよりも格段にユーザフレンドリーになると思いませんか。

こう書いてくれば Image I/O ではこれができると言っているも同じですね ^^;; 確かにできます。

どうやるかというと、Image I/O では画像のロードに関するイベントがあるのでこれを利用して行います。とはいうものの、実際にはリスナーはあるのですが、イベントクラスは定義されていません。なぜなんでしょう。とても不思議です。

アプリケーションのソース

ImageReaderTest5.java
ReadProgressDialog.java

ImageReader クラスでは使用できるのは

  • ロードの進行度合い IIOReadProgressListener インタフェース
  • イメージの更新 IIOReadUpdateListener インタフェース
  • ワーニング IIOReadWarningListener インタフェース

の 3 種類があります。ImageReaderTest4 では進行度合いを利用して、ロードがどのくらい進んだかをプログレスバーで表示するようにしてみました。

リスナーの登録は addIIOReadXXXXXListener メソッドを使用します。進行度合いが addIIOReadProgressListener メソッド、アップデートが addIIOReadUpdateListener メソッド、ワーニングが addIIOReadWarningListener になります。

ImageReaderTest4 では IIOReadProgressListener メソッドを使用しています。

    protected Image readImage(String filename) {
        Iterator readers = ImageIO.getImageReadersBySuffix(getSuffix(filename));
        
        if (readers.hasNext()) {
            ImageReader reader = (ImageReader)readers.next();
            reader.addIIOReadProgressListener(new ReadProgressDialog());
 
            try {
                ImageInputStream stream
                    = ImageIO.createImageInputStream(new File(filename));
                reader.setInput(stream);
 
                Image image = reader.read(0);
                reader.dispose();
 
                return image;
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
 
        return null;
    }

ReadProgressDialog クラスが IIOReadProgressListener インタフェースを実装しているクラスです。IIOReadProgressListener インタフェースには 9 種類のメソッドが定義されています。例えば、進行度合いが進むとコールされるのが imageProgress メソッドで、第 2 引数がパーセンテージになります。

ReadProgressDialog クラスは状態を表示する JTextField オブジェクトと進行度合いを表示する JProgressBar オブジェクトがダイアログに配置されています。先ほどの imageProgress メソッドは ReadProgressDialog クラスでは次のようになっています。

    public void imageProgress(ImageReader source, float percentageDone) {
        progressBar.setValue((int)percentageDone);
    }

パーセンテージをそのままプログレスバーに設定しているだけです ^^;

ImageReaderTest4 を実行すると下図のようなダイアログが表示されるはずです。あまり小さい画像ファイルをロードしてもあっという間に終わってしまうので、なるべく大きいイメージのロードをしてみてください。

 
 

イメージのセーブ

 
 

ロードの次はセーブです。セーブもロードと同じように行うことができます。

イメージのセーブは Reader が Writer になっただけで、javax.imageio.ImageWriter クラスを使用します。使い方も ImageReader に似ています。

アプリケーションのソース ImageWriterTest1.java

イメージのセーブは writeImage メソッドで行っています。

 1:    protected void writeImage(BufferedImage image, String filename) {
 2:        Iterator writers = ImageIO.getImageWritersByFormatName(getSuffix(filename));
 3:        
 4:        if (writers.hasNext()) {
 5:            ImageWriter writer = (ImageWriter)writers.next();
 6:
 7:            try {
 8:                ImageOutputStream stream 
 9:                    = ImageIO.createImageOutputStream(new File(filename));
10:                writer.setOutput(stream);
11:
12:                writer.write(image);
13:                writer.dispose();
14:                
15:                return;
16:            } catch (IOException ex) {
17:                ex.printStackTrace();
18:            }
19:        }
20:    }

ImageWriter オブジェクトの生成も ImageReader と同じように ImageIO クラスのファクトリメソッドを使用します。やはり、画像ファイルフォーマットを指定して行います。フォーマットのしても ImageReader の時と同じでフォーマットの名前か、MIME タイプです。

フォーマット名 : public Iterator getImageWritersByFormatName(String type)

MIME : public Iterator getImageWritersByFormatName(String mimeType)

これらの戻り値は Iterator オブジェクトなので、4 行目からの while ループで ImageWriter オブジェクトを取り出します。もし、画像フォーマットを扱えない時は、ImageReader クラスの時と同様に空の Iterator オブジェクトになります。ImageWriter オブジェクトを取得できたら、出力を指定します。出力は javax.imageio.stream.ImageOutputStream クラスを使用します。

ImageOutputStream オブジェクトを ImageWriter オブジェクトに設定しているのが 9 行目の setOutput メソッドです。最後に、11 行目の write メソッドを使用して引数で指定されたイメージのセーブを行います。

ImageWriter クラスは複数のイメージをセーブする機能もあるのですが、標準で提供されている PNG のプラグインは複数イメージを扱えません。JPEG は複数イメージはないのでこれでいいのですが、PNG で複数イメージを扱えないのは痛いです。

結局、サポートされていないのも同然なので、ここでは複数イメージのセーブの説明は割愛させていただきます。

最後に、ImageReader クラスと同様に dispose メソッドを用いて、ImageWriter オブジェクトが使用したリソースを解放させます。

 

 
 

サムネイルも含めたセーブ

 
 

次はサムネイルもセーブしてみましょう。

アプリケーションのソース ImageWriterTest2.java

サムネイルをセーブするには、イメージとサムネイルとイメージの情報 (メタデータ) を一緒に扱うためのクラス IIOImage を使用します。

    protected void writeImage(BufferedImage image,
                              BufferedImage thumbnail,
                              String filename) {
        Iterator writers = ImageIO.getImageWritersBySuffix(getSuffix(filename));
        
        if (writers.hasNext()) {
            ImageWriter writer = (ImageWriter)writers.next();

            try {
                ImageOutputStream stream
                    = ImageIO.createImageOutputStream(new File(filename));
                writer.setOutput(stream);
                
                List thumbnails = new ArrayList();
                thumbnails.add(thumbnail);

                IIOImage iioImage = new IIOImage(image, thumbnails, null);

                writer.write(iioImage);

                writer.dispose();
 
                return;
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

IIOImage クラスのコンストラクタの第 1 引数がイメージ、第 2 引数がサムネイル、第 3 引数がメタデータになります。

ロードのところで説明したように、1 つのイメージに複数のサムネイルをつけることができます。IIOImage クラスのコンストラクタでは複数のサムネイルを List オブジェクトで指定するようにします。

メタデータは今まで説明してきませんでしたが、イメージの情報を表しており、IIOMetadata クラスで表されます。IIOMetadata クラスはプロパティに XML の DOM オブジェクトを持っており、ここにイメージ情報を記述するようになっています。こうなっているのも、画像ファイルフォーマットによってメタデータの記述方が異なるからです。

IIOImage のコンストラクタの第 3 引数に null を指定すると、イメージをセーブするときにデフォルトのメタデータが使用されます。

IIOImage オブジェクトが生成できれば、後は write するだけです。

 

 
 

条件付きセーブ

 
 

ImageReader クラスと同じ流れですが、ImageWriter クラスもセーブのパラメーターを使用することができます。このパラメーターには圧縮に関するパラメータなどが含まれています。どのようなパラメータがあるのか調べてみるのが ImageWriterTest3 です。

アプリケーションのソース ImageWriterTest3.java

パラメータは画像ファイルフォーマットによって異なり、例えば圧縮やタイリングができるかどうかで設定できるものが異なります。いろいろあるので詳しくは ImageWriteParam クラスの JavaDoc を見ていただきたいと思います。

さて、実行してみましょう。ImageWriterTest3 は画像ファイルを指定するのではなく、画像ファイルフォーマットを指定します。

C:\temp>java ImageWriterTest3 JPG
Ability of Progressive: true
Ability of Compress: true
Ability of Tiling: false

Compression Parameter
Initial Mode: COPY FROM METADATA

Mode set MODE_EXPLICIT
Quality: 0.75
Quality Description: Minimum useful, Visually lossless, Maximum useful.
Quality Values: 0.05, 0.75, 0.95.
Type: JPEG
Types: JPEG.
Localized Type Name: JPEG

C:\temp>java ImageWriterTest3 PNG
Ability of Progressive: true
Ability of Compress: false
Ability of Tiling: false

C:\temp>

これを見ると JPEG は圧縮ができ、圧縮率はデフォルトで 0.75 になっていることが分かります。一方、PNG はプログレッシブだけサポートされています。

どのようなパラメータが使えるか分かったところで、実際に設定してみましょう。

アプリケーションのソース ImageWriterTest4.java

ImageWriterTest4 は圧縮率を変更できるようにしてみました。実行時の第 3 引数に圧縮率を指定して実行します。圧縮率を設定している部分を次に示します。

    protected void writeImage(BufferedImage image, String filename, float quality) {
        Iterator writers = ImageIO.getImageWritersBySuffix(getSuffix(filename));
        
        if (writers.hasNext()) {
            ImageWriter writer = (ImageWriter)writers.next();

            try {
                ImageOutputStream stream
                    = ImageIO.createImageOutputStream(new File(filename));
                writer.setOutput(stream);

                ImageWriteParam param = writer.getDefaultWriteParam();
                if (param.canWriteCompressed()) {
                    param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                    param.setCompressionQuality(quality);
                } else {
                    System.out.println("Compression is not supported.");
                }

                writer.write(null, new IIOImage(image, null, null), param);
                writer.dispose();
 
                return;
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

赤字の部分が圧縮率を設定している部分です。そのすぐ前の if 文で圧縮が可能かどうかチェックして、可能であれば圧縮率を設定するようにしています。

まず、setCompressionMode メソッドで圧縮を実行するかどうか、また圧縮を実行するならばその条件をどのように指定するかを設定します。このメソッドでは 4 つのモードを指定することができます。

ImageWriteParam.MODE_DISABLED 圧縮を行わない
ImageWriteParam.MODE_EXPLICIT 圧縮条件を ImageWriteParam で設定する
ImageWriteParam.MODE_COPY_FROM_METADATA 圧縮条件をメタデータで設定する
ImageWriteParam.MODE_DEFAULT デフォルトの圧縮条件を使用する

setCompressionMode メソッドで指定しないと、MODE_COPY_FROM_METADATA がデフォルトで使用されます。

ここでは MODE_EXPLICIT を使用して ImageWriteParam オブジェクトで設定するようにしました。次の行で圧縮率を設定しています。圧縮率の設定には setCompressionQuality メソッドを使用します。引数は float で 0 から 1 の値です。1 に近いほど圧縮率が低くなり、高画質になります。

実際に実行してみましょう。第 3 引数が圧縮率になります。例として、サンフランシスコのアルカトラズ島の写真を示しておきます。圧縮率が 0.8 のものはオリジナルとそれほど変わらないのですが、圧縮率 0.2 だとかなりブロックノイズが出てしまっています。

オリジナル
圧縮率 0.8
圧縮率 0.2
 
 

イメージのセーブに関するイベント

 
 

イメージをセーブするときにも、ロードと同様にイベントを使用することができます。しかし、やはりロードの時と同様にリスナーはありますが、イベントクラスは定義されていません。このイベントを使用してセーブの進行度を表わすようにしたのが ImageWriterTest5 クラスです。

アプリケーションのソース

ImageWriterTest5.java
WriteProgressDialog.java

ImageWriter クラスではロードの時とは異なりイメージの更新に関するリスナはありません。したがって、

  • セーブの進行度合い IIOWriteProgressListener インタフェース
  • ワーニング IIOWriteWarningListener インタフェース

の 2 種類のリスナが使用できます。リスナーの登録はそれぞれ addIIOWriteProgressListener メソッド、addIIOReadWarning メソッドです。

それでは、ロードのときと同じように進行度合いをプログレスバーで表示してみましょう。使用するのは IIOWriteProgressListener インタフェースなので次のようになります。

    protected void writeImage(BufferedImage image, BufferedImage thumbnail, String filename) {
        Iterator writers = ImageIO.getImageWritersByFormatName(getSuffix(filename));
        
        if (writers.hasNext()) {
            ImageWriter writer = (ImageWriter)writers.next();
            writer.addIIOWriteProgressListener(new WriteProgressDialog());

            try {
                ImageOutputStream stream = ImageIO.createImageOutputStream(new File(filename));
                writer.setOutput(stream);
                
                List thumbnails = new ArrayList();
                thumbnails.add(thumbnail);

                IIOImage iioImage = new IIOImage(image, thumbnails, null);

                writer.write(iioImage);
                writer.dispose();
 
                return;
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

赤字の部分でリスナを登録しています。WriteProgressDialog クラスが IIOWriteProgressListener インタフェースを実装しているクラスです。IIOReadProgressListener インタフェースには 7 種類のメソッドが定義されています。

WriteProgressDialog クラスはダイアログの初期化などはロードのイベントのサンプルで使用した ReadProgressDialog クラスと同様で、イベントの処理もほとんど同じです。たとえば、進行度イベントでコールされる imageProgress メソッドは次のようになります。

    public void imageProgress(ImageWriter source, float percentageDone) {
        progressBar.setValue((int)percentageDone);
    }

ロードの時と同様に、プログレスバーに進行度のパーセンテージをセットしています。

ImageWriterTest5 は実行すると ImageReaderTest4 と同様にラベルとプログレスバーが描画されているダイアログが表示されるはずです。プログレスバーが進行度を表わしています。画像本体のセーブだけでなく、サムネールのセーブの進行度も表わせるようになっています。

 

 
 

Applet や Java Web Start で使うには

 
 

そういえば、Applet や Java Web Start で Image I/O を単純に使おうとするとセキュリティで引っかかります。というのも、Image I/O ではテンポラリのキャッシュをローカルファイルに作るからです。もし、Applet などで Image I/O を使う場合は、ImageIO クラスの setUseCache メソッドで false をセットするようにします。

と書きましたが、J2SE v1.4.1 beta ではローカルのキャッシュを使わなくなったようです。v1.4.0 と v1.4.1 の両方を試せる方がいらっしゃったら、次のアプレットを試してみてください。v1.4.1 では両方とも動作することが確認できると思います。

  HTML ソース
キャッシュ使用版 ImageIOApplet1.html ImageIOApplet1.java
キャッシュ非使用版 ImageIOApplet2.html ImageIOApplet2.java

Java Web Start の JNLP ファイルや Java Plug-in の object タグもしくは embed タグだと JRE のバージョンを記述できるのでいいのですが、HTML の applet タグだと JRE のバージョンを記述できないのでキャッシュは使わないようにしておいたほうがいいかもしれません。その前に v1.4 が入れていない人の方が多いかもしれませんが....

 

 
 

おまけ

 
 

Image I/O の登場以前は、イメージのロードは java.awt.Toolkit クラスの createImage メソッドか getImage メソッドを使用するか、com.sun.image.codec.jpeg パッケージのクラスを使用しなければなりませんでした。Toolkit クラスを使用する場合はいいのですが、com.sun.image.codec.jpeg パッケージは com.sun のパッケージなのでいつ消えてなくなるか分かりません (J2SE v1.4.1 ではまだあるようです。イメージのセーブはコアライブラリではない拡張ライブラリの JIMI などを使えば可能でしたが、やはりコアにないと使いにくい場合が多いのも確かです。

Image I/O がコアライブラリに入ったので、心おきなくロードとセーブが行えます。しかし、今のところプラグインが少ない & 提供されているプラグインでも機能が十分でないという気がかりなところもあります。今後、だんだんと充実されていくことを望んでいます。えっ、自分で書け、そりゃごもっともでございます。

今回使用したサンプルはここからダウンロードできます。

参考 URL

(Aug. 2002)
(改訂 Oct. 2005 ImageReader#dispose/ImageWriter#dispose を追加)

 
 
Go to Previous Page Go to Contents Go to Java Page Go to Next Page