본문 바로가기
  • ANALOG CODE
  • AnalogCode
개발

Kivy MatrixInstruction을 이용한 원근 변환 문제 및 해결법

by 아날로그코더 2024. 8. 19.
반응형

Kivy를 사용하여 이미지를 원근변환 하려고 한다. 이떄 MatrixInstruction 을 사용하여 원근변환 행렬(Matrix)를 적용하였더니 변환 행렬이 정상적으로 작동하지 않는 문제가 있다. 테스트 코드를 통해 문제를 알아보고 원인 및 해결법을 정리하였다.

 

 

목표: 이미지 원근변환

아래 그림과 같이 왼쪽의 이미지를 원근변환하는 행렬을 적용하여 오른쪽과 같이 보이도록 만드려고 한다.

원근 변환

 

Perspective Waping을 알고리즘을 사용하여 원근 변환 행렬을 구한 다음에 kivy.graphics.MatrixInstruction 을 사용하여 canvas drawing에 적용해보았다.

 

 

MatrixInstruction 를 이용한 변환 테스트 및 결과

1. MatrixInstruction 적용 및 테스트

가장 먼저 기본적으로 Kivy에서 Canvas에 MatrixInstruction을 적용하여 Matrix 연산을 적용해보자. 코드는 아래와 같다.

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics.transformation import Matrix
from kivy.lang import Builder
from kivy.properties import ObjectProperty


Builder.load_string("""
<ShaderView>:
  canvas:
    PushMatrix
    MatrixInstruction:
      stack: 'modelview_mat'
      matrix: self.matrix
    Rectangle:
      size: (700, 700)
      source: "res/laptop.jpg"
    PopMatrix
""")


class ShaderView(Widget):
    matrix = ObjectProperty(Matrix())


class RendererApp(App):
    def build(self):
        return ShaderView()


if __name__ == "__main__":
    RendererApp().run()

 

kv lang에서 MatrixInstruction을 지정하여 대상(stack)과 대상에 적용될 행렬(matrix)를 지정하여 주면 된다. stack의 'modelview_mat'는 모델뷰 행렬을 나타낸다.

 

프로그램 실행 결과는 아래와 같다.

700, 700 크기의 정사각형 영역에 이미지를 그려준다.

 

 

2. MatrixInstruction에 이동변환 행렬 적용

MatrixInstruction이 정상적으로 적용 되는지를 테스트해보기 위해 이동변환 행렬 (Translate Matrix) 을 적용하여 보자.

위의 코드에서 ShaderView의 생성자인 __init__ 함수를 정의하여 아래와 같이 matrix를 지정하는 코드를 넣어보자.

class ShaderView(Widget):
    matrix = ObjectProperty(Matrix())

    def __init__(self, **kwargs):
        super(ShaderView, self).__init__(**kwargs)

        self.matrix.set(array=[
          [1, 0, 0, 0],
          [0, 1, 0, 0],
          [0, 0, 1, 0],
          [100, 100, 0, 1]
        ])

 

아래 행렬은 x축으로 100, y축으로 100만큼 이동시키는 변환 행렬이다.

[
	[1, 0, 0, 0],
	[0, 1, 0, 0],
	[0, 0, 1, 0],
	[100, 100, 0, 1]
]

 

참고로 kivy에서는 모든 그래픽 연산은 OpenGL기반으로 이루어지고 OpenGL Shader를 통해 그려진다. 그리고 OpenGL의 행렬은 Column-major 포맷으로 처리된다.

그래서 위의 행렬에서 1행과 2행의 끝에 100이 들어간게 아니라, 마지막행에 100, 100을 넣어줘야 되는것에서 가장 의문이고 헤맸던 시간이었다.

 

* 참고자료: Row-major and Column-major 

 

그래서 위의 이동 변환 행렬을 적용하면 아래와 같이 결과가 보여지게 된다.

이동변환 행렬 적용

 

이미지위 위치가 x,y축 기준으로 (100, 100)만큼 이동한 것을 확인할 수 있다.

 

 

3. MatrixInstruction에 원근변환 행렬(Matrix) 적용

이번에는 우리가 목표로하는 원근 변환 행렬을 적용해보도록 하자.

 

적용할 원근 변환 행렬은 아래와 같다. 아래 행렬은 원근변환 행렬을 구하는 알고리즘을 이용하여 계산한 값이다.

[
	[1.3, 0, 0, 0.0003],
	[0.5, 2.1, 0, 0.0014],
	[0, 0, 0, 0],
	[0, 0, 0, 1]
]

 

이것을 아래와 같이 소스코드에 적용해보자.

class ShaderView(Widget):
    matrix = ObjectProperty(Matrix())

    def __init__(self, **kwargs):
        super(ShaderView, self).__init__(**kwargs)

        self.matrix.set(array=[
          [1.3, 0, 0, 0.0003],
          [0.5, 2.1, 0, 0.0014],
          [0, 0, 0, 0],
          [0, 0, 0, 1]
        ])

 

 

하지만 우리가 원하는 결과가 아닌 아래와 같은 결과가 출력되게 된다.

위의 이미지는 우리가 원하는 결과가 아니다. 

 

왜 이런 결과가 나오는건지 알아보자.

 

 

 

원인

Kivy 소스에서 MatrixInstruction 내부 코드를 보면 아래와 같다.

cdef class MatrixInstruction(ContextInstruction):
    '''Base class for Matrix Instruction on the canvas.
    '''

    def __init__(self, *args, **kwargs):
        ContextInstruction.__init__(self, **kwargs)
        self.stack = kwargs.get('stack', 'modelview_mat')
        self._matrix = None

    cdef int apply(self) except -1:
        '''Apply the matrix of this instance to the
        context model view matrix.
        '''
        cdef RenderContext context = self.get_context()
        cdef Matrix mvm
        mvm = context.get_state(self._stack)
        context.set_state(self._stack, mvm.multiply(self.matrix))

 

마지막 줄에 현재 context의 Matrix에  MatrixInstruction에서 지정된 Matrix를 multiply 해주는 코드가 있다. 문제는 Matrix multiply에 있다.

 

kivy.graphics.transformation에 Matrix 객체 정의 코드를 살펴보니 multiply 함수가 아래와 같이 되어 있다.

 

cdef class Matrix:
    '''
    Optimized matrix class for OpenGL::
    '''
    
    ...
    
    cpdef Matrix multiply(Matrix mb, Matrix ma):
        '''Multiply the given matrix with self (from the left)
        i.e. we premultiply the given matrix by the current matrix and return
        the result (not inplace)::

            m.multiply(n) -> n * m

        :Parameters:
            `ma`: Matrix
                The matrix to multiply by
        '''
        cdef Matrix mr = Matrix()
        cdef double *a = <double *>ma.mat
        cdef double *b = <double *>mb.mat
        cdef double *r = <double *>mr.mat
        with nogil:
            r[ 0] = a[ 0] * b[0] + a[ 1] * b[4] + a[ 2] * b[ 8]
            r[ 4] = a[ 4] * b[0] + a[ 5] * b[4] + a[ 6] * b[ 8]
            r[ 8] = a[ 8] * b[0] + a[ 9] * b[4] + a[10] * b[ 8]
            r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[ 8] + b[12]
            r[ 1] = a[ 0] * b[1] + a[ 1] * b[5] + a[ 2] * b[ 9]
            r[ 5] = a[ 4] * b[1] + a[ 5] * b[5] + a[ 6] * b[ 9]
            r[ 9] = a[ 8] * b[1] + a[ 9] * b[5] + a[10] * b[ 9]
            r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[ 9] + b[13]
            r[ 2] = a[ 0] * b[2] + a[ 1] * b[6] + a[ 2] * b[10]
            r[ 6] = a[ 4] * b[2] + a[ 5] * b[6] + a[ 6] * b[10]
            r[10] = a[ 8] * b[2] + a[ 9] * b[6] + a[10] * b[10]
            r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + b[14]
            r[ 3] = 0
            r[ 7] = 0
            r[11] = 0
            r[15] = 1
        return mr

 

multiply 결과의 마지막 컬럼은 무조건 0 0 0 1 을 넣어준다.

 

2개의 Matrix를 완전히 곱셈 연산을 해주지 않는다.

 

원근 변환을 위해서는 (x, y, z, w) 값에서 w값까지 계산이 되어야 한다. 하지만 kivy의 transformation Matrix에서는 affine 변환만을 위해 만들어져 있는 것 같다. 원근 변환은 affine 변환에 속하지 않기 때문에 kivy transformation Matrix를 이용하면 정확한 연산이 될 수 없는 것이다.

이 부분은 굳이 왜 이렇게 만들어져 있는지 의문이다. kivy github에서도 이러한 이슈가 올라와 있고 누간가가 수정된 코드로 merge request도 요청해 놓은 상태이지만 아직 검토되지는 않고 있다.

 

어쨌든 이것이 수정되지 않는한 kivy의 transformation Matrix를  사용하여 원근 변환 행렬 적용이 불가능하다.

 

 

 

해결방법: RenderContext

이것을 해결하기 위해서는 MatrixInstruction을 사용할 수가 없다. 직접 Shader에 모델뷰 행렬(Modelview Matrix)를 전달해줘야 된다.

 

그러기 위해서는 아래와 같이 코드를 수정해주면 된다.

from kivy.app import App

from kivy.uix.widget import Widget
from kivy.graphics.transformation import Matrix

from kivy.graphics import RenderContext
from kivy.lang import Builder
from kivy.properties import ObjectProperty


Builder.load_string("""
<ShaderView>:
  canvas:
    Rectangle:
      size: (700, 700)
      source: "res/laptop.jpg"

<MainView>:
  ShaderView
""")


class ShaderView(Widget):
    matrix = ObjectProperty(Matrix())

    def __init__(self, **kwargs):
        self.canvas = RenderContext(use_parent_projection=True)

        super(ShaderView, self).__init__(**kwargs)

        self.matrix.set(array=[
          [1.3, 0, 0, 0.0003],
          [0.5, 2.1, 0, 0.0014],
          [0, 0, 0, 0],
          [0, 0, 0, 1]
        ])

        self.canvas['modelview_mat'] = self.matrix

class MainView(Widget):
  pass

class RendererApp(App):
    def build(self):
        return MainView()


if __name__ == "__main__":
    RendererApp().run()

 

OpenGL Shader에 전달되는 modelview_mat 파라미터에 원하는 Matrix를 직접 전달해주는 것이다.

 

RenderContext()를 사용하면 이렇게 Shader를 직접 조작할 수 있다. vertex shader와 fragment shader를 프로그래밍할 수도 있다. 여기까지는 여기서 다루는 영역은 아니지만, 어쨌든 shader로 가는 모델뷰 행렬을 직접 지정하는 것이라고 보면 되겠다.

 

그리고 canvas['modelview_mat']를 이용하여 모델뷰 행렬을 전달하여 주기만 하면 된다.

 

 

여기에서 위의 예제와는 조금 다르게 App의 build 함수에서 ShaderView를 Root로 지정하면 프로그램이 실행되지 않는다. RenderContext()와 뭔가 연관이 있는것 같지만 정확한 이유를 아직 못찾았다. 그래서 상단에 MainView라는 Root 위젯을 하나 만들고 그 밑에 ShaderView 위젯을 넣어주도록 바꾸었다.

 

위의 코드를 실행하면 원근변환 행렬이 정상적으로 적용되서 아래와 같이 보이는 것을 확인할 수 있다.

 

원근변환 행렬 적용

 

 

반응형

댓글