Groovy で画像処理、普段使いのスクリプト その1 に続き その2 画像回転をやってみます。
Java2D では 画像を回転させるには AffineTransform を使います。 さらに AffineTransform に与える行列を計算するために、3x3行列の積の計算が必要です。 そのまま地道に計算してもたいしたことはないのですが、 ここでは Apache Commons Math を使います。 Commons Math にはさまざまな機能がありますが、 ここで必要な行列の積の計算には MatrixUtils と RealMatrix を理解しておけば十分のようです。
このページにあるスクリプトの作動確認環境は以下の通りです。
groovy -version
Groovy Version: 2.4.6 JVM: 1.8.0_121 Vendor: Oracle Corporation OS: Mac OS X
まず小手試しに 45度回転させてみます。
おっとこれは 意図した結果ではない です。 キャンバスの原点(左上)を中心に 45度回転しただけなので、このようになってしまいました。 あとから、画像の中心を基準にして 回転する例を書きます。
それから 数学の教科書で45度回転したときと 逆方向に回転 しています。
つまり、教科書では プラスの角度で回転させると、反時計回りに回転すると説明されているはずですが、 ここでは、時計回りに回転している。
これは Java の座標系は Y軸の増分方向が教科書のそれと逆 だからだと思います。 なので、これで問題ないはずです(たぶん)。
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度 回転させます。
うまくいきました。
@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 を指定して変換すれば以下の画像が得られます。
回転の基準を キャンバスの原点ではなく、画像の中心に変えるために
この (1)-(3)を順に適用したものに相当する 3x3行列を作り出し、それを AffineTransform にセットして使えばよい。
Photoshop で回転する場合、角度を指定→プレビュー確認を繰り返すわけですが、 微妙に意図通りではなく、角度を1度ごと打ちかえて…なんてことがあるのですが、 スクリプトなら複数の角度を指定した画像を一気にに生成して 一番気に入った角度の画像を使えばよいので、うれしいかもしれません。 こんな風に…