AFFINETRANSFORM 이해하기
사용자가 유사변환(affine transformation)을 수행할 수 있도록 해주는
java.awt.geom.AffineTransform
클래스는 Java 2D 클래스에 속한다. 유사변환은 2차원 이미지의
좌표를 평행선이 평행을 유지하는 것과 같은 방법으로 변환한다. 가령, 사용자가 직사각형으로 시작하더라도, 유사변환을 이용해서 다른 위치에
평행사변형을 생성할 수가 있다. 이때에 라인은 라인을 남기면서 평행관계는 유지되게 된다. 유사변환을 이용하면 변환(translations),
회전(rotations), (플립을 포함한) 크기조정(scaling), 전단(shears)이 가능하다.
AffineTransform
의 documentation을 보면,
그러한 좌표변환은 마지막 행에 [0 0 1]을 수반하는 3행3열 매트릭스로 나타낼 수가 있다. 이 매트릭스는 원시좌표(source coordinates)와 목적좌표(destination coordinates)를 열벡터(column vector)로 만든 후 다음과 같이 매트릭스를 좌표벡터에 곱해가는 방법으로 원시좌표(x,y)를 목적좌표인 (x',y')로 변환한다. [ x'] [ m00 m01 m02 ] [ x ] [ m00x + m01y + m02 ] [ y'] = [ m10 m11 m12 ] [ y ] = [ m10x + m11y + m12 ] [ 1 ] [ 0 0 1 ] [ 1 ] [ 1 ]
이번 테크팁에서는 사용자가 2차원 공간의 유사변환을 표현하기 위해서 3행3열 매트릭스를 이용해야 하는 이유를 설명하면서, 기본변환을 위한
매트릭스를 AffineTransform
클래스에서의 함수호출과 연관시키고자 한다.
이 글에서는 사용자가 AffineChecker
라는 다음의 프로그램을 실행시키면서
AffineTransform
변환을 행렬의 곱(matrix multiplications)과 비교하게 될 것이다. 이
프로그램에서 Point2D.Double
의 3개 인스턴스 변수들을 잘 보아두자. 이 변수들은 (2,3)의 좌표를 갖는
시작점과 각각의 변환으로부터 생긴 점들에 해당한다.
import java.awt.geom.Point2D; import java.awt.geom.AffineTransform; public class AffineChecker { private static Point2D startingPoint = new Point2D.Double(2,3); private static Point2D affineResult = new Point2D.Double(); private static Point2D matrixResult = new Point2D.Double(); public static void matrixMultiply( double m00, double m01, double m10, double m11){ double x = m00 * startingPoint.getX() + m01 * startingPoint.getY(); double y = m10 * startingPoint.getX() + m11 * startingPoint.getY(); matrixResult.setLocation(x,y); } public static void perform(AffineTransform at){ at.transform(startingPoint,affineResult); } public static void report(){ System.out.println("Affine Result = (" + affineResult.getX() + " , " + affineResult.getY() + ")"); System.out.println("Matrix Result = (" + matrixResult.getX() + " , " + matrixResult.getY() + ")"); if (affineResult.distance(matrixResult)<.0001){ System.out.println(" No real difference"); } } }
AffineChecker
의 matrixMultiply()
메소드는 표준 행렬의
곱(standard matrix multiplication)을 보여준다.
[ m00 m01 ] [ x ] [ m00x + m01y] [ m10 m11 ] [ y ] = [ m10x + m11y]
이 프로그램의 report()
메소드는 변환의 결과와 행렬의 곱(matrix multiplication)의 결과값을
출력하고, 결과들이 본질적으로 같다고 볼 수 있을 만큼 충분히 가까운지를 비교한다.
첫번째 테스트로, 시작점에 아무것도 하지말고 비교를 해보자. 이를 위해 다음 프로그램을 돌려본다.
import java.awt.geom.AffineTransform; public class NothingChecker { public static void main(String[] args){ System.out.println( "The result of doing nothing:"); AffineChecker.perform( AffineTransform.getRotateInstance(0)); AffineChecker.matrixMultiply(1,0, 0,1); AffineChecker.report(); } }
NothingChecker
는 새로운 회전변환을 생성하고, 스태틱 메소드 호출을 이용해서 회전되기 위해서
각도(degree)를(이 예제의 경우엔 0) 넘겨준다.
AffineTransform.getRotateInstance(0)
그리고 나서 NothingChecker
는 AffineChecker
의
perform()
메소드를 통해 transform()
을 리턴된
AffineTransform
에 호출한다. 여기서 주목할 만한 것은 점(2,3)을 변환하고
affineResult
에 결과점(resulting point)을 저장하기 위해 startingPoint
과 affineResult
를 transform()
메소드에 넣어주고 있다는 것이다.
다음으로 NothingChecker
는 Affine Result
를 2행2열 항등행렬(identity
matrix)을 곱한 값과 비교한다.
[ 1 0 ] [ 0 1 ]
NothingChecker
을 실행시킬 때, 사용자는 이하의 내용을 보게 되고,
The result of doing nothing: Affine Result = (2.0 , 3.0) Matrix Result = (2.0 , 3.0) No real difference
사용자는 두 변환의 결과가 모두 점(2,3)이라는 것을 발견하게 된다.
x와 y방향으로의 크기조정은 AffineTransform
의 스태틱 메소드인
getScaleInstance()
을 이용한다. X방향으로 4, y 방향으로 5, 이렇게 두 요소를 삽입하여 크기조정을
해보자. 여기 크기조정의 방법을 보여주는 행렬의 곱(matrix multiplication)이 있다.
[ 4 0 ] [ x ] [ 4x ] [ 0 5 ] [ y ] = [ 5y ].
다음의 프로그램은 getScaleInstance()
호출을 보여주는데, 사용자는 변환과 행렬의 곱(matrix
multiplication)을 비교하기 위해 다음 프로그램을 실행할 수 있다.
import java.awt.geom.AffineTransform; public class Scale4Xand5YChecker { public static void main(String[] args) { System.out.println( "The results of scaling four " + "times in the x direction " + "and five times in the y:"); AffineChecker.perform( AffineTransform.getScaleInstance(4,5)); AffineChecker.matrixMultiply(4,0, 0,5); AffineChecker.report(); } }
결과는 다음과 같다.
The results of scaling four times in the x direction and five times in the y: Affine Result = (8.0 , 15.0) Matrix Result = (8.0 , 15.0) No real difference
결과값이 또 다시 같은 것을 볼 수 있다.
사용자는 크기조정 계수 중의 하나에 음수를 넣어봄으로써 x와 y축에 대해서 이해할 수가 있는데, 예를 들면,
[ -1 0 ] [ x ] [ -x ] [ 0 1 ] [ y ] = [ y ]
여기에서 x방향의 값이 바뀐 것을 볼 수 있다. 밑의 프로그램에서는 위의 값을 넣어 수행하고 그것을 행렬의 곱(matrix multiplication)과 비교하고 있다.
import java.awt.geom.AffineTransform; public class FlipChecker { public static void main(String[] args) { System.out.println( "The results of flipping over" + "the y-axis:"); AffineChecker.perform( AffineTransform.getScaleInstance(-1,1)); AffineChecker.matrixMultiply(-1, 0, 0, 1); AffineChecker.report(); } }
결과는,
The results of flipping over the y-axis: Affine Result = (-2.0 , 3.0) Matrix Result = (-2.0 , 3.0) No real difference
회전을 시키기 위해선 디그리(degree)가 아닌 라디안(radian)인 인수(argument)를 넣어 줘야 한다. 물론 이미 알고 있겠지만, 디그리는 pi를 곱하고 이것을 180디그리로 나눔으로써 라디안으로 쉽게 전환할 수가 있다. 예를 들면 30도는 pi/6과 같고, Pi/6라디안만큼 회전시키기 위해서는 다음과 같은 회전행렬 (rotation matrix)을 곱한다.
[ cos pi/6 -sin pi/6 ] [ sin pi/6 cos pi/6 ]
사용자는 회전행렬(rotation matrices)의 결정요소가 1이라는 것을 생각해 낼 수가 있다. 이하와 같은 방법으로 결과를 체크하자.
import java.awt.geom.AffineTransform; public class RotateThirtyChecker { public static void main(String[] args) { System.out.println( "The results of a thirty degree" + " counter clockwise rotation:"); AffineChecker.perform( AffineTransform.getRotateInstance( Math.PI/6)); AffineChecker.matrixMultiply( Math.cos(Math.PI/6), -Math.sin(Math.PI/6), Math.sin(Math.PI/6), Math.cos(Math.PI/6)); AffineChecker.report(); } }
RotateThirtyChecker
을 실행하면 다음과 같은 결과를 얻을 수 있다.
The results of a thirty degree counter clockwise rotation: Affine Result = (0.23205080756887764 , 3.598076211353316) Matrix Result = (0.23205080756887764 , 3.598076211353316) No real difference
전단변환은 변환된 객체의 형태를 바꾼다. 계수 shx는 y좌표의 요소로서 x 방향(direction)의 변화량을, shy는 x좌표의 요소로서 y 방향(direction)의 변화량을 나타내는데, 이것이 바로 직사각형을 평행사변형으로 바꾸는 변환이고 행렬의 형태는 다음과 같다.
[ 1 shx ] [ x ] [ x + y (shx) ] [ shy 1 ] [ y ] = [ y + x (shy) ].
변환을 하고 그것을 행렬의 곱(matrix multiplication)과 비교하는 코드는 다음과 같다.
import java.awt.geom.AffineTransform; public class ShearChecker { public static void main(String[] args) { System.out.println( "The results of shearing:"); AffineChecker.perform( AffineTransform.getShearInstance(5,6)); AffineChecker.matrixMultiply(1, 5, 6, 1); AffineChecker.report(); } }
ShearChecker
을 실행하면,
The results of shearing: Affine Result = (17.0 , 15.0) Matrix Result = (17.0 , 15.0) No real difference
이제 남아있는 것은 고정량에 의해 형태를 옮기는 가장 쉬운 변환이다. x방향의 tx와 y 방향의 ty를 움직이기 위해 다음 덧셈을 수행하자.
[ tx ] [ x ] [ tx + x ] [ ty ] + [ y ] = [ ty + y ].
하지만 문제는 지금까지 모든 변환을 2행2열 매트릭스의 곱셈으로 표현했다는 것이다. 때문에 위의 매트릭스를 이제까지의 아이디어에 충족하도록 만드는 한가지 방법은 모든 변환들을 다음과 같이 적는 것이다.
[ m00 m01 ] [ x ] [ tx ] [ m00x + m01y + tx ] [ m10 m11 ] [ y ] + [ ty ] = [ m10x + m11y + ty ]
여기에서 다음과 같은 3번째 행을 추가 시키면 사용자는 좌측의 내용을 좀 더 간단하게 적을 수 있을 것이다.
[ m00 m01 tx ] [ x ] [ m00x + m01y + tx ] [ m10 m11 ty ] [ y ] = [ m10x + m11y + ty ] [ 0 0 1 ] [ 1 ] [ 1 ]
이상의 모든 구성요소변환(building block transformations)은 6개의 계수 m00, m01, m10, m11, tx,
and ty, 로 환산하여 쓰여질 수 있다. (순서에 주의하자.) 이것은 왼쪽 위의 2행2열 매트릭스를 적용해서 이동을 수행한 것에 해당한다.
다음의 메소드를 AffineChecker
에 추가하자.
private static void translate(double tx, double ty){ matrixResult.setLocation( matrixResult.getX() + tx, matrixResult.getY() + ty); } public static void matrixTransform( double m00, double m01, double m10, double m11, double tx, double ty){ matrixMultiply(m00,m01,m10,m11); translate(tx,ty); }
자, 이제 matrixTransform()
를 사용하면 어떤 변환이라도 호출할 수가 있다. 가령,
ShearChecker
에서 matrixMultiply(1,5,6,1)
호출을 새로운 메소드인
matrixTransform(1,5,6,1,0,0)
호출로 대체할 수가 있다는 것이다. 뿐만 아니라 다음과 같은 방법으로
matrixTransform()
메소드를 호출할 수도 있다.
import java.awt.geom.AffineTransform; public class TranslationChecker { public static void main(String[] args) { System.out.println("The results of translating " + "by (-2,5):"); AffineChecker.perform( AffineTransform.getTranslateInstance(-2,5)); AffineChecker.matrixTransform(1,0,0,1,-2,5); AffineChecker.report(); } }
TranslationChecker
을 실행하면 다음과 같은 결과를 얻게 된다.(위에서 설명한 2개의 함수가
AffineChecker
에 추가되었는지 확인하자.)
The results of translating by (-2,5): Affine Result = (0.0 , 8.0) Matrix Result = (0.0 , 8.0) No real difference
이러한 기본적인 연산을 결합해서 AffineTransform
클래스의 간편한 메소드안에 포착되는 복잡한 결과값을
얻어낼 수 있다.
AffineTransform
에 대한 더 자세한 정보는 자바튜토리얼 "Transforming
Shapes, Text, and Images"에서 찾을 수 있다.