第7講 画像操作のサブセクション
画像の書き込み
画像生成
例題1 グラデーション画像
次のプログラムを作成して実行してみましょう.
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import java.io.File;
import java.io.IOException;;
public class Gradiation {
void run() throws IOException {
// 横幅 255,高さ 255 の画像を生成する.
BufferedImage image = new BufferedImage(255, 255, BufferedImage.TYPE_INT_RGB);
for(Integer x = 0; x < 255; x++) {
for(Integer y = 0; y < 255; y++) {
image.setRGB(x, y, x);
}
}
ImageIO.write(image, "png", new File("dest.png"));
}
public static void main(String[] args) throws IOException {
Gradiation g = new Gradiation();
g.run();
}
}実行結果は dest.png というファイルに png フォーマットで出力されます.
具体的な内容は上の折り畳みを開いて確認してください.
Java で画像を扱うには,BufferedImage という型を用います.
この画像は,上記の10行目のように,BufferedImage の実体を作成しています.
この結果,横幅255,高さ255の画像が生成されます.
また,この画像の各画素(1つのピクセル)は int 型で表され,その値は RGB(Red, Green, Blue)を表すことを示しています.
13 行目の image.setRGB で指定されたピクセルの値を設定しています.
setRGB の引数は前から順に x座標,y座標,ピクセルの値を表しています.
13 行目を image.setRGB(i, j, j) や image.setRGB(i, j, i << 8 | j) のように変更して結果がどのように変わるか確認してみましょう.
画像の書き出し
runメソッドの最後の行(16行目)では,画像ファイルを dest.png というファイルに PNG フォーマットで出力しています.
ImageIO(イメージアイオーと呼ぶ) は画像の入出力を扱うクラスです.
第2引数の "png" を "jpeg" や "gif" などに変えると指定されたフォーマットで出力されるようになります.
以上で画像の生成から出力まで行えるようになりましたが,入出力エラーに対応する必要があります.
それが,8行目,17行目の runメソッド,mainメソッドの定義の後ろについている throws IOException というものです.
もし,この throws IOException がなければコンパイラは次のメッセージを表示し,コンパイルに失敗します.
Gradiation.java:15: エラー: 例外IOExceptionは報告されません。スローするには、捕捉または宣言する必要があります
ImageIO.write(image, "png", new File("dest.png"));
^
エラー1個サポートされている画像フォーマット
ImageIOのAPIドキュメント に記載されている通り,以下のフォーマットの読み書きがサポートされています.
- BMP
- GIF
- JPEG
- PNG
- TIFF
- WBMP
例外機構(Exception Architecture)
例外(Exception)は,近年のプログラミング言語で採用されている実行時エラーの通知機構です.
従来のプログラミング言語,例えば,C 言語の場合,fopenで開くファイルが見つからなかった場合,
返り値をNULL にすることでエラーを通知していました. この場合,プログラマが責任を持って,
返り値の値を確認して,正常か異常かを判断しなければいけませんでした.
一方の例外機構は,異常処理を行うための別の処理経路を作るものです. もし,プログラムの実行途中で何らかの異常が発生した時,それまでに行なっていた処理を中断し, 別の処理を行うようにする機構です.
graph LR;
A[ファイルを開く] --> B{存在確認}
B -->|存在する| C[正常処理]
B -->|存在しない| D[異常処理]
C 言語のようなエラー処理は上記のフローチャートのように,
プログラマ自身がfopenの返り値を元に分岐処理によって,正常処理,異常処理を振り分ける必要があります.
graph LR; A[ファイルを開く] A --> C[正常処理] A -->|存在しない| D[異常処理]
対して,例外機構がサポートされている言語の場合,プログラマが異常,正常の分岐を明示的に書く必要はありません. 異常が起こった場合の処理の経路が決まっており,その経路に処理を書いておくことが異常処理を行うことになります. 正常処理の場合は,それまでの処理の続きにそのまま処理を書いていきます. これにより正常処理の見通しがよくなります.
そして,例外が投げられれば,何らかの異常が発生したと判断できるようになります. 逆に,例外が投げられなければ,データが変であろうが,正常な処理であると判断できます (もちろん,開発途中で変な場合はバグの可能性はありますが).
プログラマが明示的に投げる例外も存在しますが,多くの場合,システムが異常を検知し例外を投げます. 例えば,ファイルが見つからない場合,スリープ中に割り込みが入った場合などです. そのような例外が発生した時に,どのような対応をするのかをあらかじめ決めておく必要があります.
次のプログラムが,例外処理のイメージです.
void exceptionalMethod() throws Exception {
// 例外が発生する可能性のある処理
someMethodCall();
// 例外が起こらなかった場合の処理
process();
}someMethod の実行中に何らかの例外が起こった場合,まずsomeMethodの呼び出し元であるexceptionalMethodにどのように処理するかが問い合わされます.
そして,exceptionalMethodは例外は処理せず,呼び出し元に処理を任せるよう設定している(メソッドにthrows Exceptionと宣言しているため)ため,exceptionalMethod の呼び出し元に通知されます.
このthrows節は後ほど説明します.
検査例外と非検査例外
Java の例外は,検査例外と非検査例外の2つに分類できます. 違いは次に挙げる通りです.
検査例外
例外が発生した時の処理をプログラム中に明示的に書いておかなければコンパイルエラーになる例外. 完全に防ぐことが不可能な例外(実行時の状態やユーザの操作によって発生しうる例外).
例えば,実行時エラーは,プログラムでどれほど厳密にチェックしたとしても,完全に回避できません.
- 代表的な検査例外
IOException- 入出力時にエラーが発生した時.
InterruptedException- 割り込みが発生した時に発生する例外.
非検査例外
一方,非検査例外はプログラム中で十分にチェックすることで避けることが可能です. そのため,例外が発生した時の処理は書かなくてもコンパイルが通るようになっています.
- 代表的な非検査例外
NullPointerException- 初期化されていない変数に対する処理を行なった場合に発生する例外.
ArrayIndexOutOfBoundsException- 配列の範囲を超えてアクセスしようとした時に発生する例外.
NumberFormatException- 数値の変換に失敗した時に投げられる例外.
非検査例外は,事前にチェックすることで,例外の発生を抑えられます. 逆に言えば,実行時に非検査例外が投げられた場合は,事前のチェックが不十分であるとも言えます. 例えば,配列の範囲を超えてアクセスすることは,事前に配列の範囲を超えないようにプログラム中で確認することで避けられます.
例外の責任転嫁
さて,例外が発生した時の対処法をプログラム中に書いておく必要があります. 取れる対処法は2つです.
- 例外が投げられたら,その場で例外に対応する.
- 例外が発生する可能性のあるメソッドを呼び出している元に対応を任せる.
どちらがふさわしいかは場合により異なります. ここでは,呼び出し元に対応を任せましょう. そのために,呼び出し元に責任を丸投げするようにプログラム中に明示しておきましょう.
こうすることで,例外が発生した時はそのメソッドの呼び出し元に責任を転嫁し,正常処理のみに集中して処理を書くことができます.
本講義では,例外に対応する処理については省略します.
詳細を知りたい場合は,try-catch について調べてみてください.
IOException の責任転嫁
さて,コンパイルエラーで,IOExceptionが投げられる可能性があると述べられています.
この例外は,ファイル書き込みに何らかの問題があったときに発生する例外です.
ここでは,呼び出し元に責任を転嫁しましょう.
以下のような対応になります.
ImageIO.writeが例外を発生させた時,ImageIO.writeの呼び出し元であるrunに対応が任されます. これが例外が投げられた,ということです.- そこで,
runの呼び出し元であるmainに対応を任せましょう. - さらに,
mainも呼び出し元に対応を任せましょう. - すると,実行環境が対応されなかった例外をスタックトレースという形で出力し,プログラムが終了するようになります.
throws 節
呼び出し元に対応を任せるには,メソッドのシグネチャに throws節を追加します.
throws 節は以下のように指定します.
以下のように書くことで,例外クラスが発生した時,methodNameの呼び出し元に責任を転嫁することができるようになります.
public class ClassName{
void methodName() throws 例外クラス {
// 検査例外が発生する可能性のある処理
}
}なお,複数の例外が発生する可能性のある場合,throws節に例外クラスの名前をコンマ区切りで指定できます.
例題2. 画像の読み込み/書き込み
ImageIOは画像の書き込みだけではなく,readメソッドで読み込みも可能です.
以下のようなプログラムで画像のフォーマット変換が可能になります.
args[0]から画像を読み出し,出力先(args[1])の拡張子からフォーマットを取り出し,画像を出力します.
import java.io.IOException;
import java.io.File;
import java.util.Objects;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
public class ToJpegConverter {
void run(String[] args) throws IOException {
// 画像ファイルを読み込む.
BufferedImage image = ImageIO.read(new File(args[0]));
String destName = findDestName(args[1])
ImageIO.write(image, "jpg", new File(destName)); // 画像を書き出す.
}
String findDestName(String fileName) {
// ファイル名から最後のドット(.)の位置を取得する.
Integer index = fileName.lastIndexOf(".");
// 取得した位置から後ろの文字列を取得する.拡張子に相当する.
String extension = fileName.substring(index + 1).toLowerCase();
// 拡張子が jpg,もしくは jpeg ならそのまま返す.
if(Objects.equals(extension, "jpg") || Objects.equals(extension, "jpeg"))
return fileName;
// そうでなければ拡張子の前の部分を取り出して,".jpg" を追加して返す.
return fileName.substring(0, index) + ".jpg";
}
public static void main(String[] args) throws IOException {
ToJpegConverter tjc = new ToJpegConverter();
tjc.run(args);
}
}実行結果
$ ls
Gradiation.class Gradiation.java ToJpegConverter.class ToJpegConverter.java dest.png
$ file dest.png # dest.png のファイルフォーマットを調べる.
dest.png: PNG image data, 255 x 255, 8-bit/color RGB, non-interlaced
$ java ToJpegConverter dest.png dest.jpg # フォーマットを変換し,jpegファイルを出力する.
$ file dest.jpg # 出力された dest.jpg のフォーマットを調べる.
dest.jpg: JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 255x255, components 3
$ java ToJpegConverter dest.png dest2.png # 拡張子が何であっても,jpegファイルを出力する.
$ file dest2.jpg # 出力された dest.jpg のフォーマットを調べる.
dest2.jpg: JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 255x255, components 3画像の変換
例題3. アフィン変換
画像に対して,平面上の変換を適用します.
この変換は,平行移動,拡大・縮小,反転,回転,変形により構成されます.
この変換のことをアフィン変換(Affine Transform)と呼びます.
Java では,AffineTransform 型が変換の内容を表し,
AffineTransformOp型がアフィン変換を適用する変換器を表します.
次の例題プログラムをコンパイル,実行してみましょう.
コマンドライン引数で与えられた画像ファイルを90度回転させた画像が transformed.png に出力されます.
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class ImageTransformer {
void run(String[] args) throws IOException {
BufferedImage image = ImageIO.read(new File(args[0]));
BufferedImage result = doFilter(image);
ImageIO.write(result, "png", new File(args[1])));
}
BufferedImage doFilter(BufferedImage source) {
// 画像の中心座標を中心に π/2 (90 度)回転させるアフィン変換を作成する.
AffineTransform affine = AffineTransform.getRotateInstance(Math.PI / 2,
source.getWidth() / 2, source.getHeight() / 2);
AffineTransformOp transformer = new AffineTransformOp(affine, AffineTransformOp.TYPE_BICUBIC);
return transformer.filter(source, null);
}
// main 省略
}
doFilter メソッドの最初の getRotateInstance が回転を表すアフィン変換の実体を取得しています.
第1引数が回転角度(ラジアン指定),第2引数,第3引数が回転の軸です.
上の例題では,元画像(source)の中心座標を軸として$\frac{\pi}{2}$(90度)回転させています.
第2,第3引数を省略すると左上の原点を軸として回転します.
21行目の transformer.filter メソッドの第2引数は出力先の BufferedImage の実体です.
出力先が null であれば適当な大きさのBufferedImageの実体が作成されますので,ここでは nullにしています.
アフィン変換の詳細については,API ドキュメント や 後述の参考資料,また,様々なドキュメントがWeb上にありますので,そちらを参照してください.
度数法と弧度法の相互変換
度数法と弧度法の変換には,Math.toRadianssやMath.toDegrees が利用できます.
Math.toRadians(90)は $\frac{\pi}{2}$ に変換されます.Math.toDegrees(Math.PI / 2)は90.0という値に変換されます(実際には90.0ではなくそれに近い値になります).
参考資料
- アフィン変換(平行移動、拡大縮小、回転、スキュー行列) (imagingsolution.net)
- 完全に理解するアフィン変換 (qiita.com)
- アフィン写像 (ja.wikipedia.org)
例題4. 画像の上下反転
アフィン変換を用いて,画像の上下や左右を反転させることもできます. 以下のサンプルプログラムを動かしてみましょう.
AffineTransform の getScaleInstance メソッドは,画像をx方向y方向それぞれの倍率を設定できます.
そこで,x方向に1倍,y方向に-1倍する変換を行います.
そのままでは,描画領域外に描画されることになりますので,y軸方向に source.getHeight() だけマイナス方向に移動させます.
このように,アフィン変換の実体に対して,変換を繰り返すことが可能です.
以下の画像をクリックして,どのように変化するかを確認してください.
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class ImageFlipper {
void run(String[] args) throws IOException {
BufferedImage image = ImageIO.read(new File(args[0]));
BufferedImage result = doFilter(image);
ImageIO.write(result, "png", new File(args[1]));
}
BufferedImage doFilter(BufferedImage source) {
// x方向に1倍,y方向に-1倍する.
AffineTransform affine = AffineTransform.getScaleInstance(1, -1);
// 上の変換に加えて,x方向に0,y方向に source の高さ分だけ移動させる.
affine.translate(0, -source.getHeight());
AffineTransformOp transformer = new AffineTransformOp(affine, AffineTransformOp.TYPE_BICUBIC);
return transformer.filter(source, null);
}
// main 省略
}
例題5. 画像への書き込み
画像に様々な図形を描画することも可能です.
画像に図形を描画するには,BufferedImage の実体から createGraphics で Graphics2D 型の実体を取得する必要があります.
Graphics2D とは描画器であり,この実体を用いることで,四角形や楕円,文字などを自由に描くことができるようになります.
以下のサンプルプログラムを実行して実行結果を確認してみてください.
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.Color;
// その他の import は省略
public class ImageDrawer {
void run(String[] args) throws IOException {
BufferedImage image = ImageIO.read(new File(args[0]));
BufferedImage result = doFilter(image);
ImageIO.write(result, "png", new File(args[1]));
}
BufferedImage doFilter(BufferedImage image) {
Graphics2D g = image.createGraphics(); // 描画器を取得する.
g.setStroke(new BasicStroke(5f)); // 線の太さを設定する.
g.setColor(Color.BLUE); // 色を設定する.
g.drawRect(10, 10, image.getWidth() - 20, image.getHeight() - 20); // 四角形を描く
g.setColor(Color.ORANGE);
g.drawOval(20, 20, image.getWidth() - 40, image.getHeight() - 40); // 楕円を描く
return image;
}
// main 省略
}
Graphics2D には線の太さや種類(波線や点線など)を変更できる setStroke や,色を設定する setColor のほか,次のような図形を描くことができます.
- 直線
drawLine(x1, y1, x2, y2)x1,y1(Integer型)からx2,y2(Integer型)まで直線を描画する.
- 四角形
drawRect(x, y, width, height)x,y(Integer型)を起点(左上)として,width,height(Integer型)の大きさの四角形を描画する.
- 楕円
drawOval(x, y, width, height)x,y(Integer型)を起点(左上)として,width,height(Integer型)の大きさの楕円を描画する.
- 文字列
drawString(string, x, y)x,y(Integer型)の位置にstringの文字列を描画する.
- 画像を描く
drawImage(image, x, y, ImageObserver)x,y(Integer型) の位置にimageを描画する.ImageOverserはimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
drawImage(image, x, y, width, height, ImageObserver)x,y(Integer型)の位置にwidth,height(Integer型)の大きさでimageを描画する. 最後の引数のImageOverserはimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
drawImage(image, AffineTransform, ImageOverser)x,y(Integer型)の位置にAffineTransformの実体が表す変換を施した上で,imageを描画する. 最後の引数のImageOverserはimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
練習問題
1. グラデーション2
実行例に示す画像のような255×255の画像を作成してください.
画像ファイル名は gradiation2.png とし,クラス名は Gradiation2 としてください.
実行例
ヒント
- 例題1を元に作成しましょう.
setRGBに指定する色を以下のルールで指定すれば良いです.- 右上が青であるため,
xが大きくなるにつれ,青成分を増加させるようにしましょう. - 左下が赤であるため,
yが大きくなるにつれ,赤成分(y << 16)を増加させましょう. - 2つの成分を論理和で合成すると良いでしょう(
x | y << 16).
- 右上が青であるため,
2. ファイルフォーマット変換
2つのコマンドライン引数を受け取り,第1引数で与えられた画像を読み込み,第2引数で与えられたファイルに画像を出力してください.
ただし,第2引数のファイル名の拡張子で指定されたフォーマットで画像を出力してください.
クラス名は ImageFormatConverter としてください.
実行例
$ java ImageFormatConverter sample1.png a2-1.jpg
$ file a2-1.jpg # 出力された a2-1.jpg のフォーマットを調べる.
a2-1.jpg: JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 255x255, components 3
$ java ImageFormatConverter sample1.png a2-2.bmp
$ file a2-2.bmp # 出力された a2-1.bmp のフォーマットを調べる.
a2-2.bmp: dataヒント
- 拡張子は,例題2の
findDestNameのextensionで取得できています. このextensionをそのまま出力フォーマットの指定で用いると良いです.
3. グレースケール画像への変換
コマンドライン引数で与えられた画像をグレースケール画像に変換してgray.pngに出力してください.
クラス名は GrayScaleFilter としてください.
グレースケールへの変換は, BufferedReader の実体を作成する時に,
BufferedImage.TYPE_BYTE_GRAY を指定するとグレースケール画像に変換できます.
実行例
ヒント
- 読み込んだ画像(
source)とは別のBufferedImageの実体を作成します(grayImage).- 作成の際に
BufferedImage.TYPE_BYTE_GRAYを指定しましょう. - 大きさは元の画像の大きさと同じにしましょう.
BufferedImage grayImage = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_BYTE_GRAY)
- 作成の際に
grayImageからcreateGraphicsメソッドを用いてGraphics2Dの実体(g2)を取得します.Graphics2D g2 = grayImage.createGraphics();
g2.drawImageで元の画像をgrayImageに描画しましょう.g2.drawImage(source, 0, 0, null);
grayImageをファイルに出力して終了です.
4. 画像の回転
コマンドライン引数で与えられた画像を,同じくコマンドライン引数で与えられた角度(度数法)だけ回転させて出力してください.
ただし,画像が切れないように大きさを調整してください.
角度は 0〜90 の範囲とします.
クラス名は ImageRotatorとしてください.
グレースケール画像と同様に,元画像とは別の BufferedImageの実体を作成し,そこに回転した画像を描画しましょう.
実行例
$ java ImageRotator sample1.png 60 dset.pngヒント
- 以下の説明では,元画像(
source)の横幅を $w$,高さを$h$とし,回転角度を $\theta$ とします.
変換後の画像の大きさ
- 以下の画像では$\theta=60$の例を示しています(クリックして確認してください).
-
このように,回転後の画像の横幅は,$w\sin\theta + w\cos\theta$ です.
- 同様に,回転後の画像の高さは $h\sin\theta + h\cos\theta$ です.
処理手順
AffineTransformの実体を作成し, $\theta$だけ回転させた後,$x$軸方向に $w\sin\theta$ だけ移動させます.- 変換を指定した順序とは逆の順序で変換が適用される点に注意してください.
- つまり,プログラムでは,平行移動(
translate)した後,回転(rotate)させてください. 以下のようなプログラムで実現できるでしょう.
- つまり,プログラムでは,平行移動(
- 変換を指定した順序とは逆の順序で変換が適用される点に注意してください.
Dobule angle = Math.toRadians(degree);
AffineTransform affine = new AffineTransform();
affine.translate(width * Math.sin(angle), 0d);
affine.rotate(angle);
// ... 出力先画像の Graphics2D の実体を取得し,g2 に代入する.
g2.drawImage(source, affine, null);5. 自由課題
与えられた画像に対して何らかの変換を行いファイルに出力してください.
クラス名は,ImageEditor としてください.
まとめ
まとめ
- 画像を書き込むには,
ImageIO.writeメソッドを用いる- 対応フォーマットはBMP, GIF, JPEG, PNG, TIFF, WBMP.
- 画像を読み込むには,
ImageIO.readメソッドを用いる - 例外とは,エラー時に異常処理プロセスに自動的に移動できる機構のことである
- この講義で紹介する画像の変換手法は次の通り.
- その他






