코딩 더 매트릭스 - 5장 행렬 (3)

선형변환을 이용한 이미지 변환

2018년 12월 10일

필립 클라인의 저서 코딩 더 매트릭스 5장 행렬


  1. 여러 가지 선형변환을 이미지 변환에 적용해 봅니다.





선형변환과 이미지 변환

위와 같은 2 X 2 이미지의 위치 행렬은 모서리의 픽셀 좌표쌍과 [x,y,u][x,y,u]의 열벡터로 만들 수 있습니다. 여기서 uu는 평행이동 변환에 사용됩니다.

(0,0) (0,1) (0,2) (1,2) (1,1) (1,0) (2,2) (2,0) (2,1)
x 0 0 0 1 1 1 2 2 2
y 0 1 2 2 1 0 2 0 1
u 1 1 1 1 1 1 1 1 1

그리고 각각의 픽셀은 RGB 컬러를 가지고 있으므로 컬러 행렬도 만들 수 있습니다.

(0,0) (0,1) (1,0) (1,1)
b 179 245 179 179
g 179 185 245 237
r 245 179 204 245

이미지를 열벡터들로 이루어진 행렬로 바꾸어 몇가지 선형변환을 시도해 보겠습니다. 우선 코딩더매트릭스에서 제공하고 있는 소스파일에서 img_mat_util.py를 다운로드 받습니다. 이 파일은 png 파일을 행렬로 변환하고 그 행렬을 브라우저에 그리는 flie2matmat2display 프로시저를 제공합니다. 간단하게 앵무새 그림의 png 파일 이미지를 띄워보겠습니다.

from img_mat_util import file2mat, mat2display

img_path = '../bird.png'
(M, C) = file2mat(img_path)
mat2display(M, C)

모든 선형변환은 모서리의 열벡터 [x,y,u][x,y,u]에 행렬을 곱함으로서 적용될 수 있습니다. xx를 두 배로 스케일링 하는 행렬은 아래와 같습니다.

[200010001][xyu]=[2xyu]\begin{bmatrix} 2 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ u \end{bmatrix} = \begin{bmatrix} 2x \\ y \\ u \end{bmatrix}

첫번째로 단위행렬을 곱해 원본이미지 행렬을 그대로 유지하는 프로시저 identity를 작성해 보겠습니다.

def identity():
    D = {'x', 'y', 'u'}
    return Mat((D, D), { (r,r): 1 for r in D})

I = identity()
mat2display(I * M, C)

위 코드로 브라우저에 그려지는 이미지는 원본과 같습니다. 프로시저 identity를 이용하면 앞으로 만들 여러 선형변환 프로시저들을 조금 더 수월하게 만들 수 있습니다.





평행이동

(브라우저에서 xx축은 왼쪽에서 오른쪽, yy축은 위에서 아래로 값이 커집니다. 좌표 (0, 0)은 왼쪽 상단 모서리입니다.)

[10a01b001][xy1]=[x+ay+b1]\begin{bmatrix} 1 & 0 & a \\ 0 & 1 & b \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x + a \\ y + b \\ 1 \end{bmatrix}
def translation(alpha, beta):
    A = identity()
    A['x', 'u'] = alpha
    A['y', 'u'] = beta
    return A

A = translation(100, 200)
mat2display(A * M, C)

(x,y)(x+100,y+200)(x,y) \to (x + 100, y + 200)





스케일링

[α000β0001][xy1]=[αxβy1]\begin{bmatrix} \alpha & 0 & 0 \\ 0 & \beta & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} \alpha x \\ \beta y \\ 1 \end{bmatrix}
def scale(alpha, beta):
    A = identity()
    A['x', 'x'] = alpha
    A['y', 'y'] = beta
    return A

A = scale(2,3)
mat2display(A * M, C)

(x,y)(2x,3y)(x,y) \to (2x, 3y)





회전이동

[cosθsinθ0sinθcosθ0001][xy1]=[xcosθysinθxsinθ+ycosθ1]\begin{bmatrix} cos \theta & -sin \theta & 0 \\ sin \theta & cos \theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x cos \theta - y sin \theta \\ x sin \theta + y cos \theta \\ 1 \end{bmatrix}

브라우저 상에서는 yy축이 아래로 갈수록 커지므로 일반적으로 알고있는 좌표계와 회전 방향이 반대입니다. 따라서 회전이동 변환의 인수 θ\theta에 대해 시계방향으로 회전합니다.

from math import sin, cos, pi

def rotate(theta):
    A = identity()
    A['x', 'x'] = cos(theta)
    A['x', 'y'] = -sin(theta)
    A['y', 'x'] = sin(theta)
    A['y', 'y'] = cos(theta)
    return A

A = rotate(pi / 6)
mat2display(A * M, C)

(x,y)(cosπ6xsinπ6y,sinπ6x+cosπ6y)(x,y) \to (cos \frac{\pi}{6}x - sin \frac{\pi}{6}y, sin \frac{\pi}{6}x + cos \frac{\pi}{6}y)





원점이 아닌 점을 중심으로 한 회전이동

평행이동과 회전이동을 통해 구현할 수 있습니다. 아래 그림의 동그란 점을 X 표시된 점을 중심으로 회전시키려 합니다.

기존의 회전이동 프로시저는 원점중심 회전이므로 기준점을 원점으로 평행이동 시키는 벡터를 회전시키려는 점에 적용합니다. 그리고 회전이동 변환을 적용한 뒤 이전에 적용했던 평행이동을 역으로 취해줍니다.

[10a01b001][cosθsinθ0sinθcosθ0001][10a01b001][xy1]\begin{bmatrix} 1 & 0 & a \\ 0 & 1 & b \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} cos \theta & -sin \theta & 0 \\ sin \theta & cos \theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & -a \\ 0 & 1 & -b \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
def rotate_about(theta, x, y):
    A = translation(-x, -y)
    B = rotate(theta)
    C = translation(x, y)
    return C * B * A

A = rotate_about(pi / 3, 113, 99)
mat2display(A * M, C)

이미지의 우측하단 끝 모서리를 중심으로 π3\frac{\pi}{3}회전





x, y축 대칭이동

[100010001][xy1]=[xy1]\begin{bmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x \\ -y \\ 1 \end{bmatrix} [100010001][xy1]=[xy1]\begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} -x \\ y \\ 1 \end{bmatrix}
def reflect_x():
    A = identity()
    A['y', 'y'] = -1
    return A

def reflect_y():
    A = identity()
    A['x', 'x'] = -1
    return A

# y 축 대칭
A = reflect_y()
T = translation(113, 0) # 이미지를 보이기 위해 평행이동
mat2display(T * A * M, C)

# x 축 대칭
A = reflect_x()
T = translation(0, 99) # 이미지를 보이기 위해 평행이동
mat2display(T * A * M, C)

y축 대칭이동


x축 대칭이동





컬러변환

RGB 3개 컬러 채널의 값을 모두 77256r+151256g+28256b\frac{77}{256}r + \frac{151}{256}g + \frac{28}{256}b로 만들어 컬러 이미지를 흑백이미지로 변경해 보겠습니다.

1256[771512877151287715128][rgb]=1256[77r+151g+28b77r+151g+28b77r+151g+28b]\frac{1}{256} \begin{bmatrix} 77 & 151 & 28 \\ 77 & 151 & 28 \\ 77 & 151 & 28 \end{bmatrix} \begin{bmatrix} r \\ g \\ b \end{bmatrix} = \frac{1}{256} \begin{bmatrix} 77r + 151g + 28b \\ 77r + 151g + 28b \\ 77r + 151g + 28b \end{bmatrix}
def scale_rgb(sr, sg, sb):
    D = {'r', 'g', 'b'}
    return Mat((D, D), {
        ('r', 'r'): sr, ('g', 'r'): sr, ('b', 'r'): sr,
        ('r', 'b'): sb, ('g', 'b'): sb, ('b', 'b'): sb,
        ('r', 'g'): sg, ('g', 'g'): sg, ('b', 'g'): sg,
    })

A = scale_rgb(77 / 256 , 151 / 256, 28 / 256)
mat2display(M, A * C)





두개의 점을 지나는 직선에 대한 대칭이동

원점을 지나는 직선 y=mxy = mx에 대한 대칭이동 선형변환은 널리 알려져 있습니다.

1m2+1[1m22m2mm21][xy]\frac{1}{m^2 + 1} \begin{bmatrix} 1 - m^2 & 2m \\ 2m & m^2 - 1 \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix}

위 식에 이용되는 행렬을 간단히 유도해 보겠습니다. 원점을 지나는 한 직선 y=mxy = mx는 점 (1,m)(1, m)을 포함합니다. 따라서 점 (1,m)(1, m)의 직선에 대한 대칭은 (1,m)(1, m)입니다. 선형변환에 대응하는 행렬을 A라 할때 다음과 같이 쓸 수 있습니다.

A[1m]=[1m]A \begin{bmatrix} 1 \\ m \end{bmatrix} = \begin{bmatrix} 1 \\ m \end{bmatrix}

직선 y=mxy = mx와 수직인 직선은 y=1mxy = -\frac{1}{m}x입니다. 점 (m,1)(-m, 1)의 원점에 대한 대칭 (m,1)(m, -1)은 직선 y=mxy = mx에 대한 대칭이기도 합니다. 따라서 다음과 같이 쓸 수 있습니다.

A[m1]=[m1]A \begin{bmatrix} -m \\ 1 \end{bmatrix} = \begin{bmatrix} m \\ -1 \end{bmatrix}

두 점 (1,m)(1, m), (m,1)(-m ,1)에 대한 행렬 AA의 매핑을 합치면 아래와 같은 행렬방정식이 만들어집니다.

A[1mm1]=[1mm1]A \begin{bmatrix} 1 & -m \\ m & 1 \end{bmatrix} = \begin{bmatrix} 1 & m \\ m & -1 \end{bmatrix}

역행렬을 이용해 AA를 구할 수 있습니다.

A=1m2+1[1mm1][1mm1]=1m2+1[1m22m2mm21]A = \frac{1}{m^2 + 1} \begin{bmatrix} 1 & m \\ m & -1 \end{bmatrix} \begin{bmatrix} 1 & m \\ -m & 1 \end{bmatrix} = \frac{1}{m^2 + 1} \begin{bmatrix} 1 - m^2 & 2m \\ 2m & m^2 - 1 \end{bmatrix}



이제 원점을 지나는 직선에 대한 대칭이동을 구현할 수 있습니다. 한편 만들어야 할 프로시저는 평면상의 임의의 두 점을 지나는 직선 y=mx+ny = mx + n에 대한 대칭이동입니다.

그림의 직선 y=mxy = mxy=mx+ny = mx + n은 평행이고 분홍색 선의 y=mxy = mxy=mx+ny = mx + n에 대한 대칭 역시 평행입니다. 평행한 두 대칭선들을 l1l_1, l2l_2라 할때 점들의 집합 l1l_1, l2l_2사이의 거리는 모두 직선 y=mxy = mxy=mx+ny = mx + n 사이 거리의 두 배입니다. 따라서 y=mxy = mx 에 수직이면서 y=mx+ny = mx + n 로의 평행이동인 벡터를 알면 이를 평행이동 선형변환에 이용할 수 있습니다.

직선 y=mxy = mx의 점 (0,0)(0,0)을 지나고 y=mxy = mx 에 대해 수직인 직선이 y=mx+ny = mx + n 과 만날 때 그 점을 (a,b)(a,b)라 하겠습니다. 점 (a,b)(a, b)y=mx+ny = mx + n 위의 점이므로 b=am+nb = am + n이 성립합니다. 점 (a,b)(a,b)(0,0)(0,0)을 지나는 직선이 y=mxy = mx 와 수직이므로 ba=1m\frac{b}{a} = -\frac{1}{m} 입니다.

b=am+nba=1m\begin{aligned} b = am + n \\ \frac{b}{a} = -\frac{1}{m} \end{aligned}

연립방정식을 풀면 a=mnm2+1a = -\frac{mn}{m^2 + 1}, b=nm2+1b = \frac{n}{m^2 + 1} 입니다. aabb는 각각 xx방향, yy방향으로의 벡터와도 같으므로 평행이동 프로시저를 적용할 수 있습니다.

원점을 지나는 직선에 대한 대칭이동, 직선 y=mx+ny = mx + n, y=mxy = mx 사이의 평행이동을 이용한 선형변환은 다음과 같습니다.

1m2+1[102mnm2+1012nm2+1001][1m22m02mm210001][xy1]\frac{1}{m^2 + 1} \begin{bmatrix} 1 & 0 & -\frac{2mn}{m^2 + 1} \\ 0 & 1 & \frac{2n}{m^2 + 1} \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 - m^2 & 2m & 0 \\ 2m & m^2 - 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}
def reflect_by_line(x1, y1, x2, y2):
    D = {'x', 'y', 'u'}
    m = (y2 - y1) / (x2 - x1)
    n = -m * x1 + y1
    R = 1 / (m**2 + 1) * Mat((D, D), {
        ('x','x'): 1-m**2,
        ('x', 'y'): 2 * m,
        ('y', 'x'): 2 * m,
        ('y', 'y'): m**2 - 1,
        ('u', 'u'): m**2 + 1
    })
    T = translation(2 * -m * n / (m**2 + 1), 2 * n / (m**2 + 1))
    return T * R

A = reflect_by_line(0,200, 400,0)
mat2display(A * M, C)

y=12xy = -\frac{1}{2}x에 대한 대칭