画像の変換

例題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 省略
}

元画像(DALL・E2で生成) 元画像(DALL・E2で生成)

変換後の画像) 変換後の画像)

doFilter メソッドの最初の getRotateInstance が回転を表すアフィン変換の実体を取得しています. 第1引数が回転角度(ラジアン指定),第2引数,第3引数が回転の軸です. 上の例題では,元画像(source)の中心座標を軸として$\frac{\pi}{2}$(90度)回転させています. 第2,第3引数を省略すると左上の原点を軸として回転します.

21行目の transformer.filter メソッドの第2引数は出力先の BufferedImage の実体です. 出力先が null であれば適当な大きさのBufferedImageの実体が作成されますので,ここでは nullにしています.

アフィン変換の詳細については,API ドキュメント や 後述の参考資料,また,様々なドキュメントがWeb上にありますので,そちらを参照してください.

度数法と弧度法の相互変換

度数法と弧度法の変換には,Math.toRadianssMath.toDegrees が利用できます.

  • Math.toRadians(90)$\frac{\pi}{2}$ に変換されます.
  • Math.toDegrees(Math.PI / 2)90.0 という値に変換されます(実際には90.0ではなくそれに近い値になります).

参考資料

例題4. 画像の上下反転

アフィン変換を用いて,画像の上下や左右を反転させることもできます. 以下のサンプルプログラムを動かしてみましょう.

AffineTransformgetScaleInstance メソッドは,画像をx方向y方向それぞれの倍率を設定できます. そこで,x方向に1倍,y方向に-1倍する変換を行います. そのままでは,描画領域外に描画されることになりますので,y軸方向に source.getHeight() だけマイナス方向に移動させます. このように,アフィン変換の実体に対して,変換を繰り返すことが可能です. 以下の画像をクリックして,どのように変化するかを確認してください.

Affine変換による画像変換
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 省略
}

元画像(DALL・E2で生成) 元画像(DALL・E2で生成)

変換後の画像) 変換後の画像)

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);
    }
    public static void main(String[] args) throws IOException {
        ImageFlipper flipper = new ImageFlipper();
        flipper.run(args);
    }
}

例題5. 画像への書き込み

画像に様々な図形を描画することも可能です. 画像に図形を描画するには,BufferedImage の実体から createGraphicsGraphics2D 型の実体を取得する必要があります. 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 省略
}

元画像(DALL・E2で生成) 元画像(DALL・E2で生成)

変換後の画像) 変換後の画像)

Graphics2D には線の太さや種類(波線や点線など)を変更できる setStroke や,色を設定する setColor のほか,次のような図形を描くことができます.

  • 直線
    • drawLine(x1, y1, x2, y2)
      • x1, y1Integer型)からx2, y2Integer型)まで直線を描画する.
  • 四角形
    • drawRect(x, y, width, height)
      • x, yInteger型)を起点(左上)として,width, heightInteger型)の大きさの四角形を描画する.
  • 楕円
    • drawOval(x, y, width, height)
      • x, yInteger型)を起点(左上)として,width, heightInteger型)の大きさの楕円を描画する.
  • 文字列
    • drawString(string, x, y)
      • x, yInteger型)の位置に string の文字列を描画する.
  • 画像を描く
    • drawImage(image, x, y, ImageObserver)
      • x, yInteger型) の位置に image を描画する.ImageOverserimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
    • drawImage(image, x, y, width, height, ImageObserver)
      • x, yInteger型)の位置に width, heightInteger型)の大きさで imageを描画する. 最後の引数のImageOverserimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
    • drawImage(image, AffineTransform, ImageOverser)
      • x, yInteger型)の位置に AffineTransformの実体が表す変換を施した上で,imageを描画する. 最後の引数のImageOverserimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

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;
    }
    public static void main(String[] args) throws IOException {
        ImageDrawer drawer = new ImageDrawer();
        drawer.run(args);
    }
}