Groovy で画像処理、普段使いのスクリプト その2( 回転 )
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軸の増分方向が教科書のそれと逆 だからだと思います。 なので、これで問題ないはずです(たぶん)。
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度 回転させます。
うまくいきました。
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 を指定して変換すれば以下の画像が得られます。
補足説明
回転の基準を キャンバスの原点ではなく、画像の中心に変えるために
- (1)画像の (-width/2, -height/2) 平行移動
- (2)回転
- (3)画像の (width/2, height/2) 平行移動
この (1)-(3)を順に適用したものに相当する 3x3行列を作り出し、それを AffineTransform にセットして使えばよい。
まとめ
Photoshop で回転する場合、角度を指定→プレビュー確認を繰り返すわけですが、 微妙に意図通りではなく、角度を1度ごと打ちかえて…なんてことがあるのですが、 スクリプトなら複数の角度を指定した画像を一気にに生成して 一番気に入った角度の画像を使えばよいので、うれしいかもしれません。 こんな風に…