필립 클라인의 저서 코딩 더 매트릭스 5장 행렬
- 여러 가지 선형변환을 이미지 변환에 적용해 봅니다.
선형변환과 이미지 변환
위와 같은 2 X 2 이미지의 위치 행렬은 모서리의 픽셀 좌표쌍과 [x,y,u]의 열벡터로 만들 수 있습니다. 여기서 u는 평행이동 변환에 사용됩니다.
|
(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 파일을 행렬로 변환하고 그 행렬을 브라우저에 그리는 flie2mat
과 mat2display
프로시저를 제공합니다. 간단하게 앵무새 그림의 png 파일 이미지를 띄워보겠습니다.
from img_mat_util import file2mat, mat2display
img_path = '../bird.png'
(M, C) = file2mat(img_path)
mat2display(M, C)
모든 선형변환은 모서리의 열벡터 [x,y,u]에 행렬을 곱함으로서 적용될 수 있습니다. x를 두 배로 스케일링 하는 행렬은 아래와 같습니다.
⎣⎢⎡200010001⎦⎥⎤⎣⎢⎡xyu⎦⎥⎤=⎣⎢⎡2xyu⎦⎥⎤
첫번째로 단위행렬을 곱해 원본이미지 행렬을 그대로 유지하는 프로시저 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
를 이용하면 앞으로 만들 여러 선형변환 프로시저들을 조금 더 수월하게 만들 수 있습니다.
평행이동
(브라우저에서 x축은 왼쪽에서 오른쪽, y축은 위에서 아래로 값이 커집니다. 좌표 (0, 0)은 왼쪽 상단 모서리입니다.)
⎣⎢⎡100010ab1⎦⎥⎤⎣⎢⎡xy1⎦⎥⎤=⎣⎢⎡x+ay+b1⎦⎥⎤
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)
스케일링
⎣⎢⎡α000β0001⎦⎥⎤⎣⎢⎡xy1⎦⎥⎤=⎣⎢⎡αxβy1⎦⎥⎤
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)
회전이동
⎣⎢⎡cosθsinθ0−sinθcosθ0001⎦⎥⎤⎣⎢⎡xy1⎦⎥⎤=⎣⎢⎡xcosθ−ysinθxsinθ+ycosθ1⎦⎥⎤
브라우저 상에서는 y축이 아래로 갈수록 커지므로 일반적으로 알고있는 좌표계와 회전 방향이 반대입니다. 따라서 회전이동 변환의 인수 θ에 대해 시계방향으로 회전합니다.
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)→(cos6πx−sin6πy,sin6πx+cos6πy)
원점이 아닌 점을 중심으로 한 회전이동
평행이동과 회전이동을 통해 구현할 수 있습니다. 아래 그림의 동그란 점을 X 표시된 점을 중심으로 회전시키려 합니다.
기존의 회전이동 프로시저는 원점중심 회전이므로 기준점을 원점으로 평행이동 시키는 벡터를 회전시키려는 점에 적용합니다. 그리고 회전이동 변환을 적용한 뒤 이전에 적용했던 평행이동을 역으로 취해줍니다.
⎣⎢⎡100010ab1⎦⎥⎤⎣⎢⎡cosθsinθ0−sinθcosθ0001⎦⎥⎤⎣⎢⎡100010−a−b1⎦⎥⎤⎣⎢⎡xy1⎦⎥⎤
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π회전
x, y축 대칭이동
⎣⎢⎡1000−10001⎦⎥⎤⎣⎢⎡xy1⎦⎥⎤=⎣⎢⎡x−y1⎦⎥⎤
⎣⎢⎡−100010001⎦⎥⎤⎣⎢⎡xy1⎦⎥⎤=⎣⎢⎡−xy1⎦⎥⎤
def reflect_x():
A = identity()
A['y', 'y'] = -1
return A
def reflect_y():
A = identity()
A['x', 'x'] = -1
return A
A = reflect_y()
T = translation(113, 0)
mat2display(T * A * M, C)
A = reflect_x()
T = translation(0, 99)
mat2display(T * A * M, C)
y축 대칭이동
x축 대칭이동
컬러변환
RGB 3개 컬러 채널의 값을 모두 25677r+256151g+25628b로 만들어 컬러 이미지를 흑백이미지로 변경해 보겠습니다.
2561⎣⎢⎡777777151151151282828⎦⎥⎤⎣⎢⎡rgb⎦⎥⎤=2561⎣⎢⎡77r+151g+28b77r+151g+28b77r+151g+28b⎦⎥⎤
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=mx에 대한 대칭이동 선형변환은 널리 알려져 있습니다.
m2+11[1−m22m2mm2−1][xy]
위 식에 이용되는 행렬을 간단히 유도해 보겠습니다. 원점을 지나는 한 직선 y=mx는 점 (1,m)을 포함합니다. 따라서 점 (1,m)의 직선에 대한 대칭은 (1,m)입니다. 선형변환에 대응하는 행렬을 A라 할때 다음과 같이 쓸 수 있습니다.
A[1m]=[1m]
직선 y=mx와 수직인 직선은 y=−m1x입니다. 점 (−m,1)의 원점에 대한 대칭 (m,−1)은 직선 y=mx에 대한 대칭이기도 합니다. 따라서 다음과 같이 쓸 수 있습니다.
A[−m1]=[m−1]
두 점 (1,m), (−m,1)에 대한 행렬 A의 매핑을 합치면 아래와 같은 행렬방정식이 만들어집니다.
A[1m−m1]=[1mm−1]
역행렬을 이용해 A를 구할 수 있습니다.
A=m2+11[1mm−1][1−mm1]=m2+11[1−m22m2mm2−1]
이제 원점을 지나는 직선에 대한 대칭이동을 구현할 수 있습니다. 한편 만들어야 할 프로시저는 평면상의 임의의 두 점을 지나는 직선 y=mx+n에 대한 대칭이동입니다.
그림의 직선 y=mx와 y=mx+n은 평행이고 분홍색 선의 y=mx와 y=mx+n에 대한 대칭 역시 평행입니다. 평행한 두 대칭선들을 l1, l2라 할때 점들의 집합 l1, l2사이의 거리는 모두 직선 y=mx와 y=mx+n 사이 거리의 두 배입니다. 따라서 y=mx 에 수직이면서 y=mx+n 로의 평행이동인 벡터를 알면 이를 평행이동 선형변환에 이용할 수 있습니다.
직선 y=mx의 점 (0,0)을 지나고 y=mx 에 대해 수직인 직선이 y=mx+n 과 만날 때 그 점을 (a,b)라 하겠습니다. 점 (a,b)는 y=mx+n 위의 점이므로 b=am+n이 성립합니다. 점 (a,b)와 (0,0)을 지나는 직선이 y=mx 와 수직이므로 ab=−m1 입니다.
b=am+nab=−m1
연립방정식을 풀면 a=−m2+1mn, b=m2+1n 입니다. a와 b는 각각 x방향, y방향으로의 벡터와도 같으므로 평행이동 프로시저를 적용할 수 있습니다.
원점을 지나는 직선에 대한 대칭이동, 직선 y=mx+n, y=mx 사이의 평행이동을 이용한 선형변환은 다음과 같습니다.
m2+11⎣⎢⎡100010−m2+12mnm2+12n1⎦⎥⎤⎣⎢⎡1−m22m02mm2−10001⎦⎥⎤⎣⎢⎡xy1⎦⎥⎤
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=−21x에 대한 대칭