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");
        }
      }
   }

AffineCheckermatrixMultiply() 메소드는 표준 행렬의 곱(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)

그리고 나서 NothingCheckerAffineCheckerperform() 메소드를 통해 transform()을 리턴된 AffineTransform에 호출한다. 여기서 주목할 만한 것은 점(2,3)을 변환하고 affineResult에 결과점(resulting point)을 저장하기 위해 startingPoint affineResulttransform()메소드에 넣어주고 있다는 것이다. 다음으로 NothingCheckerAffine 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"에서 찾을 수 있다.