Sunday, October 1, 2017 / Java2D, Groovy

Groovy で画像処理、普段使いのスクリプト その2( 回転 )

Groovy で画像処理、普段使いのスクリプト その1 に続き その2 画像回転をやってみます。

Java2D では 画像を回転させるには AffineTransform を使います。 さらに AffineTransform に与える行列を計算するために、3x3行列の積の計算が必要です。 そのまま地道に計算してもたいしたことはないのですが、 ここでは Apache Commons Math を使います。 Commons Math にはさまざまな機能がありますが、 ここで必要な行列の積の計算には MatrixUtils と RealMatrix を理解しておけば十分のようです。

many-angles-white-donuts-360

このページにあるスクリプトの作動確認環境は以下の通りです。

groovy -version
Groovy Version: 2.4.6 JVM: 1.8.0_121 Vendor: Oracle Corporation OS: Mac OS X

単純な回転

まず小手試しに 45度回転させてみます。

white-donuts-degree45-as-naive

おっとこれは 意図した結果ではない です。 キャンバスの原点(左上)を中心に 45度回転しただけなので、このようになってしまいました。 あとから、画像の中心を基準にして 回転する例を書きます。

それから 数学の教科書で45度回転したときと 逆方向に回転 しています。
つまり、教科書では プラスの角度で回転させると、反時計回りに回転すると説明されているはずですが、 ここでは、時計回りに回転している。
これは Java の座標系は Y軸の増分方向が教科書のそれと逆 だからだと思います。 なので、これで問題ないはずです(たぶん)。

image-rotation-as-naive.groovy

import java.awt.Image
import java.awt.image.BufferedImage
import java.awt.geom.AffineTransform
import javax.imageio.ImageIO

System.setProperty("java.awt.headless", "true")

def toRadian = { degree-> degree * Math.PI/180f } // ラジアンに変換

def toBufferedImage = { Image image, int degree->
    def radian  = toRadian(degree)
    def rotationMatrix = [
        Math.cos(radian), -Math.sin(radian), 0d,
        Math.sin(radian),  Math.cos(radian), 0d,
        0d, 0d, 1d]

    // ------------
    // m00 m01 m02
    // m10 m11 m12
    // m20 m21 m22
    // ------------
    def m00 = rotationMatrix[0]
    def m01 = rotationMatrix[1]
    def m02 = rotationMatrix[2]

    def m10 = rotationMatrix[3]
    def m11 = rotationMatrix[4]
    def m12 = rotationMatrix[5]

    //def m20 = rotationMatrix[6]
    //def m21 = rotationMatrix[7]
    //def m22 = rotationMatrix[8]

    def transform = new AffineTransform(m00, m10, m01, m11, m02, m12)

    def bufferedImage = new BufferedImage(image.width, image.height, BufferedImage.TYPE_4BYTE_ABGR)
    def g = bufferedImage.graphics
    g.drawImage(image, transform, null)
    g.dispose()
    return bufferedImage
}

def doRotate = { inputStream, outputStream, degree->
    def inputBufferedImage = ImageIO.read(inputStream)
    ImageIO.write(toBufferedImage(inputBufferedImage, degree), 'PNG', outputStream)
}


def inputPngFile  = new File(args[0])
def outputPngFile = new File(args[1])
int degree        = args[2] as int

def input = new FileInputStream( inputPngFile )
def output = new FileOutputStream( outputPngFile )

doRotate(input, output, degree)

input.close()
output.close()

使い方

groovy image-rotation-as-naive white-donuts.png white-donuts-degree45-as-naive.png 45

※引数に 45 以外の数値を与えれば、その角度で回転させることができます。

補足説明

回転した状態で イメージ をキャンバスに配置するに graphics.drawImage するときに イメージとともに AffineTransform のインスタンスを与えます。 つまり AffineTransform さえ適切に設定しておけば、自在に画像を変換(トランスフォーム)できます。

ここでは、45度回転させるために θ = 45度として…

| cosθ  -sinθ  0 |
| sinθ   cosθ  0 |
| 0      0     1 |

の 3x3 の行列を AffineTransform にセットしています。

画像の中心で回転

次に画像の中心で 45度 回転させます。

white-donuts-degree45

うまくいきました。

image-rotation.groovy

@Grab(group='org.apache.commons', module='commons-math3', version='3.6.1')

import org.apache.commons.math3.linear.MatrixUtils
import org.apache.commons.math3.linear.RealMatrix

import java.awt.Image
import java.awt.image.BufferedImage
import java.awt.geom.AffineTransform
import javax.imageio.ImageIO

System.setProperty("java.awt.headless", "true")

def toRadian = { degree-> degree * Math.PI/180f } // ラジアンに変換

def createRealMatrix = { matrixValues->
    def row0 = [matrixValues[0], matrixValues[1], matrixValues[2]] as double[]
    def row1 = [matrixValues[3], matrixValues[4], matrixValues[5]] as double[]
    def row2 = [matrixValues[6], matrixValues[7], matrixValues[8]] as double[]
    return MatrixUtils.createRealMatrix( [ row0, row1, row2 ] as double[][] )
}

def toBufferedImage = { Image image, int degree->
    int width  = image.width
    int height = image.height

    // 原点を画像の中心に平行移動
    def translationMatrix0 = [
        1d, 0d, -width/2d,
        0d, 1d, -height/2d,
        0d, 0d, 1d]

    // 指定角度分回転
    def radian  = toRadian(degree)
    def rotationMatrix = [
        Math.cos(radian), -Math.sin(radian), 0d,
        Math.sin(radian),  Math.cos(radian), 0d,
        0d, 0d, 1d]

    // 元の位置に平行移動
    def translationMatrix1 = [
        1d, 0d, width/2d,
        0d, 1d, height/2d,
        0d, 0d, 1d]

    def matrixA = createRealMatrix( translationMatrix0 )
    def matrixB = createRealMatrix( rotationMatrix )
    def matrixC = createRealMatrix( translationMatrix1 )

    // matrixA->matrixB->matrixC の順に変換したい
    def resultMatrix = matrixC.multiply(matrixB).multiply(matrixA)

    // ------------
    // m00 m01 m02
    // m10 m11 m12
    // m20 m21 m22
    // ------------
    def m00 = resultMatrix.getRow(0)[0]
    def m01 = resultMatrix.getRow(0)[1]
    def m02 = resultMatrix.getRow(0)[2]

    def m10 = resultMatrix.getRow(1)[0]
    def m11 = resultMatrix.getRow(1)[1]
    def m12 = resultMatrix.getRow(1)[2]

    //def m20 = resultMatrix.getRow(2)[0]
    //def m21 = resultMatrix.getRow(2)[1]
    //def m22 = resultMatrix.getRow(2)[2]

    def transform = new AffineTransform(m00, m10, m01, m11, m02, m12)

    def bufferedImage = new BufferedImage(image.width, image.height, BufferedImage.TYPE_4BYTE_ABGR)
    def g = bufferedImage.graphics
    g.drawImage(image, transform, null)
    g.dispose()
    return bufferedImage
}

def doRotate = { inputStream, outputStream, degree->
    def inputBufferedImage = ImageIO.read(inputStream)
    ImageIO.write(toBufferedImage(inputBufferedImage, degree), 'PNG', outputStream)
}

def inputPngFile  = new File(args[0])
def outputPngFile = new File(args[1])
int degree        = args[2] as int

def input = new FileInputStream( inputPngFile )
def output = new FileOutputStream( outputPngFile )

doRotate(input, output, degree)

input.close()
output.close()

使い方

groovy image-rotation white-donuts.png white-donuts-degree45.png 45

※引数に 45 以外の数値を与えれば、その角度で回転させることができます。

たとえば 90 を指定して変換すれば以下の画像が得られます。

white-donuts-degree90

補足説明

回転の基準を キャンバスの原点ではなく、画像の中心に変えるために

  • (1)画像の (-width/2, -height/2) 平行移動
  • (2)回転
  • (3)画像の (width/2, height/2) 平行移動

この (1)-(3)を順に適用したものに相当する 3x3行列を作り出し、それを AffineTransform にセットして使えばよい。

まとめ

Photoshop で回転する場合、角度を指定→プレビュー確認を繰り返すわけですが、 微妙に意図通りではなく、角度を1度ごと打ちかえて…なんてことがあるのですが、 スクリプトなら複数の角度を指定した画像を一気にに生成して 一番気に入った角度の画像を使えばよいので、うれしいかもしれません。 こんな風に…

many-angles-white-donuts