기하학적(Geometric) 변환
기하학적(Geometric) 변환에서 어파인(Affine) 변환과 원근(or 투시, Perspective) 변환 그리고 리매핑(Remapping)이 있다. 여기서 어파인 변환과 투시 변환은 아래와 같은 차이가 있다.
- 어파인 변환
- 변환 결과가 평행사변형 형태
- 이미지의 끝 점 3개의 이전 위치와 변환 후의 위치를 알면 이동 관계를 알 수 있음
=> 평행 사변형에서 점 3개를 알면 나머지 점의 위치 또한 알 수 있기 때문에 점 3개 - 투시 변환 (Perspective Transformation)
- 어파인 변환보다 자유도가 높은 변환
- 이미지의 끝 점 4개의 이전위치와 변환 후의 위치를 알면 이동 관계를 알 수 있음
=> 평행 사변형이 아닌 좀 더 자유로운 사각형이므로 점 4개 모두 알아야함
이 중 Affine 변환을 살펴본다.
어파인(Affine) 변환
어파인(Affine) 변환은 우선 원본 이미지에 3개의 점을 정하고, 이 3개 점을 기준으로 얼마나 뒤틀리게 할것인지 정한다. 이후 세 점에 대해서 뒤틀린 만큼의 3개의 점을 배열로 지정하면, 다른 모든 픽셀들도 그들을 기준으로 바꿀 수 있다.
위 그림처럼 이미지 회전 변환을 수행하기 위해선 특정 각도를 지정해주어야 한다. 반시계(Counter Clockwise) 방향으로 이 각도는 적용되는데 아래처럼 sin, cos 함수로 표현할 수 있다.
$$ x'=cos\theta x + sin\theta y + \alpha = ax + by + c $$
$$ y'=- sin\theta x + cos\theta y + \beta = dx + ey +f $$
$$ \begin{bmatrix}x'\\y'\end{bmatrix}= \begin{bmatrix}cos\theta & sin\theta & \alpha\\-sin\theta & cos\theta & \beta \end{bmatrix} \begin{bmatrix}x\\y\\1\end{bmatrix} = \begin{bmatrix} a & b & c \\d & e & f \end{bmatrix} \begin{bmatrix}x\\y\\1\end{bmatrix}$$
$$ \begin{bmatrix} a & b & c \\d & e & f \end{bmatrix} $$
위의 수식에서 여섯 개의 파라미터로 구성된 부분인 2x3행렬을 Affine Translation Matrix(어파인 변환 행렬)라고 한다. input data인 입력영상과 어파인 변환 결과로부터 어파인 변환 행렬을 구하기 위해 최소 세 점의 이동 관계를 알아야 한다. 미지수가 각각 3개이기 때문이다.
Affine Translation은 직선, 길이(거리)의 비, 평행성(parallelism)을 보존하는 변환이다. 즉, 행렬과 벡터는 바꿔서 표현이 가능하고 선형 변환해도 성질은 유지된다. 하지만 벡터는 크기와 방향만 가지므로 정적인 위치를 표현하기에는 무리가 있어 점(point)을 이용해 위치를 표현해야 한다. 따라서 점(Point)을 찍어 위치를 표현하는 공간을 의미하며 좌표로 그 해당 지점을 말할 필요가 있고 이것이 Affine Space(어파인 공간)이다.
정리하자면 어파인 공간으로 벡터 공간을 형성할 수 있고 행렬의 곱이 곧 dot이고, 이것이 Affine이다. Affine으로 서로 다른 차원의 배열이 곱해져 다른 형태의 배열을 반환하게 할 수 있다. 말로 풀어쓰면 어렵지만 결국 아래와 같은 식으로 볼 수 있다.
$$ y = f(x) = wx+b $$
식에서 보이듯 Affine Translation은 Linear Transformation(선형 변환)을 한 후 시프팅(Translation)한 것이다.
선형 변환은 스칼라 a와 벡터 u, v에 대해 두 벡터 공간(U, V) 사이에서 다음 조건을 만족하는 변환 f:U->V을 의미하며 아래와 같이 표현할 수 있다.
$$ f(au+v)af(u)+f(v) $$
이 조건은 f(0) = 0를 내포한다.
이처럼 Affine 변환은 Translation term이 있기 때문에 기본적으로 비선형(non-linear) 변환이다.
python에서 이 Affine Translation(어파인 변환)을 통해 회전 변환 및 크기 조정을 수행하는 방법과 openCV의 rotate 함수를 이용해 변환하는 방법은 대략 3가지가 있고 아래와 같다.
영상의 좌측 상단 기준 회전
이번엔 이미지의 좌 상단을 기준으로 회전시켜본다.
import cv2
import math
import numpy as np
img = cv2.imread('brokenEgg.jpg')
rad = 20 * math.pi / 180 # 각도 설정
# np.array로 Affine 행렬 생성
affne = np.array([[math.cos(rad), math.sin(rad), 0],
[-math.sin(rad), math.cos(rad), 0]], dtype=np.float32)
result = cv2.warpAffine(img, affne, (0, 0))
cv2.imshow('original', img)
cv2.imshow('result', result)
cv2.waitKey(0)
좌측 상단을 기준으로 이미지를 회전하였지만 출력 영상의 크기를 입력 영상의 크기와 동일하므로 이미지가 잘리는 부분이 생겨 원본이 훼손되는 것을 볼 수 있다.
중앙 기준 회전
여기선 openCV의 함수인 getRotationMatrix2D를 사용한다.
cv2.getRotationMatrix2D(center, angle, scale)
- center: 회전 중심 좌표. (x, y) 튜플
- angle: 반시계 방향 회전 각도(degree). 음수는 시계 방향
- scale: 추가적인 확대 비율
- retval: 2x3 Affine 변환 matrix(float)
수식은 아래와 같다.
회전 중심 좌표는 영상의 가로, 세로 1/2 값을 넣어주면 영상의 중앙 좌표로 설정할 수 있다.
import cv2
import math
def RotateImage(img, angle, scale=1):
if img.ndim > 2:
height, width, channel = img.shape
else:
height, width = img.shape
matrix = cv2.getRotationMatrix2D((width / 2, height / 2), angle, scale)
result = cv2.warpAffine(img, matrix, (width, height))
return result
img = cv2.imread('brokenEgg.jpg')
rad = 20 * math.pi / 180 # 각도 설정
res = RotateImage(img, 30, 0.6)
cv2.imshow('original', img)
cv2.imshow('result', res)
cv2.waitKey(0)
getRotationMatrix2D 함수에서 scale을 조정하여 사이즈를 줄이고 중심을 기준으로 회전 변환을 수행하여 이미지에서 잘리는 부분은 이제 사라지게 할 수 있다.
사실 영상 분야 이외에 기계 학습 분야에서도 Affine 변환이 사용된다. 기계 학습에서 Affine은 기존에 단일 값, 혹은 단일 차원 배열로 넘겨주던 입력값을 행렬로서 받아들여 한번에 처리하는 것을 말하며 Affine 구조는 그러한 행렬을 입력값으로 받아와 순전파와 역전파를 구현할 수 있는 구조를 말한다.
신경망의 순전파 때 수행하는 행렬의 곱을 기하학에서는 Affine 변환이라고 부른다. 기하학적 관점에서의 Affine 변환은 영상을 평행 이동시키거나 회전, 크기 변환 등을 통해 만들 수 있는 변환을 통칭한다.
Rotate 함수
openCV는 rotate 함수도 지원한다.
import cv2
img = cv2.imread('brokenEgg.jpg')
img90 = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) # 시계방향으로 90도 회전
img180 = cv2.rotate(img, cv2.ROTATE_180) # 180도 회전
img270 = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) # 반시계 방향으로 90도 회전
cv2.imshow('original', img)
cv2.imshow('clockwise 90', img90)
cv2.imshow('clockwise 180', img180)
cv2.imshow('counter clockwise 270', img270)
cv2.waitKey(0)
아래는 전체 python 소스 코드이다.
import cv2
import math
import numpy as np
img = cv2.imread('brokenEgg.jpg')
rad = 20 * math.pi / 180 # 각도 설정
def normalRotate(img, angle):
# np.array로 Affine 행렬 생성
affne = np.array([[math.cos(angle), math.sin(angle), 0],
[-math.sin(angle), math.cos(angle), 0]], dtype=np.float32)
result = cv2.warpAffine(img, affne, (0, 0))
return result
def RotateImage(img, angle, scale=1):
if img.ndim > 2:
height, width, channel = img.shape
else:
height, width = img.shape
matrix = cv2.getRotationMatrix2D((width / 2, height / 2), angle, scale)
result = cv2.warpAffine(img, matrix, (width, height))
return result
result = RotateImage(img, rad, scale=0.6)
cv2.imshow('original', img)
cv2.imshow('result', result)
cv2.waitKey(0)
참고 자료
https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&blogId=mycpp&logNo=120104673760
https://luv-n-interest.tistory.com/806
https://velog.io/@nayeon_p00/OpenCV-%EC%96%B4%ED%8C%8C%EC%9D%B8-%EB%B3%80%ED%99%98
https://hooni-playground.com/1271/
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=jang_hwan_im&logNo=221289668071
https://wiserloner.tistory.com/455
https://deep-learning-study.tistory.com/199
소스 코드
https://github.com/sehoon787/Personal_myBlog/blob/main/Data%20Science/CV/Rotate.py