귀퉁이 서재

OpenCV - 13. 이미지 이동(Translation), 확대/축소(Scaling), 회전(Rotation) 본문

OpenCV

OpenCV - 13. 이미지 이동(Translation), 확대/축소(Scaling), 회전(Rotation)

Baek Kyun Shin 2020. 9. 28. 21:54

이번 포스팅에서는 이미지를 이동, 확대, 축소, 회전하는 방법에 대해 알아보겠습니다. 이번 포스팅 역시 '파이썬으로 만드는 OpenCV 프로젝트(이세우 저)'를 정리한 것임을 밝힙니다.

코드: github.com/BaekKyunShin/OpenCV_Project_Python/tree/master/05.geometric_transform

이미지 이동

이미지를 이동하는 방법은 간단합니다. 원래 있던 좌표에 이동시키려는 거리만큼 더하면 됩니다. 

x_new = x_old + d₁
y_new = y_old + d₂

위 방정식을 행렬식으로 표현하면 아래와 같습니다.

행렬식을 다시 풀어서 써보겠습니다.

x_new = x_old + d₁ = 1*x_old + 0*y_old + d₁
y_new = y_old + d₂ = 0*x_old + 1*y_old + d₂

보시는 바와 같이 이미지의 좌표를 이동하는 변환 행렬은 2 x 3 행렬입니다. 변환 행렬이란 어떤 좌표를 선형 변환(linear tranformations) 해주는 행렬을 뜻합니다. 쉽게 말해서 어떤 좌표를 다른 좌표로 이동시켜주는 행렬이라는 뜻입니다. 즉, 어떤 좌표에 변환 행렬을 곱해주면 다른 좌표가 구해지는 행렬입니다. 아래는 이미지 이동에 대한 변환 행렬입니다.

OpenCV에서는 변환행렬을 이용해서 이미지를 변환하는 기능을 하는 cv2.warpAffine() 함수를 제공합니다.

  • dst = cv2.warpAffine(src, matrix, dsize, dst, flags, borderMode, borderValue)
    src: 원본 이미지, numpy 배열
    matrix: 2 x 3 변환행렬, dtype=float32
    dsize: 결과 이미지의 크기, (width, height)
    flags(optional): 보간법 알고리즘 플래그
    borderMode(optional): 외곽 영역 보정 플래그
    borderValue(optional): cv2.BORDER_CONSTANT 외곽 영역 보정 플래그일 경우 사용할 색상 값 (default=0)
    dst: 결과 이미지

flags의 값은 아래와 같습니다.
cv2.INTER_LINEAR: default 값, 인접한 4개 픽셀 값에 거리 가중치 사용
cv2.INTER_NEAREST: 가장 가까운 픽셀 값 사용
cv2.INTER_AREA: 픽셀 영역 관계를 이용한 재샘플링
cv2.INTER_CUBIC: 인정합 16개 픽셀 값에 거리 가중치 사용

borderMode의 값은 아래와 같습니다.
cv2.BORDER_CONSTANT: 고정 색상 값
cv2.BORDER_REPLICATE: 가장자리 복제
cv2.BORDER_WRAP: 반복
cv2.BORDER_REFLECT: 반사

파라미터가 많아서 헷갈리실 수도 있겠습니다. 원본 이미지인 src를 변환 행렬 matrix에 따라 변환하는 함수라고 보시면 됩니다. 이때 결과 이미지의 크기를 나타내는 파라미터는 dsize입니다. 

아래 코드는 이미지를 평행 이동하는 예제입니다.

# 평행 이동 (translate.py)

import cv2
import numpy as np

img = cv2.imread('../img/fish.jpg')
rows,cols = img.shape[0:2]  # 영상의 크기

dx, dy = 100, 50            # 이동할 픽셀 거리

# ---① 변환 행렬 생성 
mtrx = np.float32([[1, 0, dx],
                   [0, 1, dy]])  
# ---② 단순 이동
dst = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy))   

# ---③ 탈락된 외곽 픽셀을 파랑색으로 보정
dst2 = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy), None, \
                        cv2.INTER_LINEAR, cv2.BORDER_CONSTANT, (255,0,0) )

# ---④ 탈락된 외곽 픽셀을 원본을 반사 시켜서 보정
dst3 = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy), None, \
                                cv2.INTER_LINEAR, cv2.BORDER_REFLECT)

cv2.imshow('original', img)
cv2.imshow('trans',dst)
cv2.imshow('BORDER_CONSTATNT', dst2)
cv2.imshow('BORDER_FEFLECT', dst3)
cv2.waitKey(0)
cv2.destroyAllWindows()

물고기 이미지를 가로(x) 방향으로 100픽셀, 세로(y) 방향으로 50픽셀 평행 이동시켰습니다. 가로의 양의 방향은 오른쪽이고, 세로의 양의 방향은 아래쪽입니다. 그리고 평행 이동을 시키면 기존 영역은 이미지가 잘립니다. 잘리는 영역을 어떻게 보정할지를 borderMode 파라미터로 조정할 수 있습니다. 사실 이 코드에서 외곽 영역 이외에는 픽셀의 탈락이 발생하지 않으므로 flags 파라미터는 무의미합니다.

이미지 확대 및 축소

이미지를 일정 비율로 확대 및 축소하는 방법은 아래와 같습니다. 기존의 좌표에 특정한 값을 곱하면 됩니다.

x_new = a₁ * x_old
y_new = a * y_old

이를 다시 풀어쓰면 아래와 같습니다.

x_new = a₁ * x_old = a * x_old + 0 * y_old + 0 * 1
y_new = a * y_old = 0 * x_old + a* y_old + 0 * 1

변환 행렬은 평행 이동할 때와 마찬가지로 2 x 3 행렬입니다. 2 x 2 행렬로 나타낼 수 있는데 굳이 2 x 3 행렬로 표현한 이유는 cv2.warpAffine() 함수는 변환 행렬이 2 x 3 행렬이 아니면 오류를 내기 때문입니다. 기하학적 변환은 이미지 확대 및 축소뿐만 아니라 평행 이동도 있습니다. 두 변환을 같이 하기 위해 2 x 3 행렬로 맞춘 것입니다.

아래는 변환 행렬을 이용해서 이미지를 확대하고 축소하는 예제 코드입니다. 이미지 평행 이동과 마찬가지로 cv2.warpAffine() 함수를 사용했습니다.

# 행렬을 이용한 이미지 확대 및 축소 (scale_matrix.py)

import cv2
import numpy as np

img = cv2.imread('../img/fish.jpg')
height, width = img.shape[:2]

# --① 0.5배 축소 변환 행렬
m_small = np.float32([[0.5, 0, 0],
                       [0, 0.5,0]])  
# --② 2배 확대 변환 행렬
m_big = np.float32([[2, 0, 0],
                     [0, 2, 0]])  

# --③ 보간법 적용 없이 확대 축소
dst1 = cv2.warpAffine(img, m_small, (int(height*0.5), int(width*0.5)))
dst2 = cv2.warpAffine(img, m_big, (int(height*2), int(width*2)))

# --④ 보간법 적용한 확대 축소
dst3 = cv2.warpAffine(img, m_small, (int(height*0.5), int(width*0.5)), \
                        None, cv2.INTER_AREA)
dst4 = cv2.warpAffine(img, m_big, (int(height*2), int(width*2)), \
                        None, cv2.INTER_CUBIC)

# 결과 출력
cv2.imshow("original", img)
cv2.imshow("small", dst1)
cv2.imshow("big", dst2)
cv2.imshow("small INTER_AREA", dst3)
cv2.imshow("big INTER_CUBIC", dst4)
cv2.waitKey(0)
cv2.destroyAllWindows()

실행을 하면 원본 이미지가 2배 확대된 이미지와 1/2로 축소된 이미지를 보여줍니다. 일반적으로 보간법 파라미터로는 축소에는 cv2.INTER_AREA를 쓰고, 확대에는 cv2.INTER_CUBIC, cv2.INTER_LINEAR를 씁니다. 

변환 행렬을 쓰지 않고도 확대 및 축소를 할 수 있습니다. cv2.resize() 함수를 사용하면 됩니다.

  • cv2.resize(src, dsize, dst, fx, fy, interpolation)
    src: 입력 원본 이미지
    dsize: 출력 영상 크기(확대/축소 목표 크기, (width, height)형식), 생략하면 fx, fy 배율을 적용
    fx, fy: 크기 배율, dsize가 주어지면 dsize를 적용함
    interpolation: 보간법 알고리즘 선택 플래그 (cv2.warpAffine()과 동일)
    dst: 결과 이미지

cv2.resize() 함수를 사용하면 확대/축소를 몇 픽셀로 할지 혹은 어떤 배율로 할지 선택할 수 있습니다. dsize는 확대/축소를 원하는 목표 이미지의 크기이며, fx, fy는 변경할 배율입니다. 예를 들어 fx = 2, fy = 0.5이면 x축으로 2배, y축으로 0.5배로 스케일링한다는 뜻입니다. 아래는 cv2.resize()를 적용하여 이미지를 확대 및 축소하는 코드입니다.

# cv2.reize()로 이미지 확대 및 축소 (scale_resize.py)

import cv2
import numpy as np

img = cv2.imread('../img/fish.jpg')
height, width = img.shape[:2]

#--① 크기 지정으로 축소
#dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)),\
#                        None, 0, 0, cv2.INTER_AREA)
dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)), \
                         interpolation=cv2.INTER_AREA)

#--② 배율 지정으로 확대
dst2 = cv2.resize(img, None,  None, 2, 2, cv2.INTER_CUBIC)
#--③ 결과 출력
cv2.imshow("original", img)
cv2.imshow("small", dst1)
cv2.imshow("big", dst2)
cv2.waitKey(0)
cv2.destroyAllWindows()

위 코드는 원본 이미지를 2배, 1/2배로 확대 및 축소합니다. dst1은 원하는 결과의 크기를 픽셀 값으로 직접 지정해준 것이고, dst2는 배율을 지정하여 이미지를 스케일링한 것입니다. cv2.resize() 함수는 변환 행렬을 이용하여 이미지 스케일링을 하는 것보다 더 간결하고 쉽습니다.

이미지 회전

이미지 회전을 위한 변환 행렬식은 아래와 같습니다.

출처: https://slidesplayer.org/slide/14096571/

변환 행렬을 이용하여 이미지를 회전해보겠습니다.

# 변환행렬을 이용한 이미지 회전 (rotate_martix.py)

import cv2
import numpy as np

img = cv2.imread('../img/fish.jpg')
rows,cols = img.shape[0:2]

# ---① 라디안 각도 계산(60진법을 호도법으로 변경)
d45 = 45.0 * np.pi / 180    # 45도
d90 = 90.0 * np.pi / 180    # 90도

# ---② 회전을 위한 변환 행렬 생성
m45 = np.float32( [[ np.cos(d45), -1* np.sin(d45), rows//2],
                    [np.sin(d45), np.cos(d45), -1*cols//4]])
m90 = np.float32( [[ np.cos(d90), -1* np.sin(d90), rows],
                    [np.sin(d90), np.cos(d90), 0]])

# ---③ 회전 변환 행렬 적용
r45 = cv2.warpAffine(img,m45,(cols,rows))
r90 = cv2.warpAffine(img,m90,(rows,cols))

# ---④ 결과 출력
cv2.imshow("origin", img)
cv2.imshow("45", r45)
cv2.imshow("90", r90)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

변환 행렬에 사용할 회전 각은 60진법에서 라디안(radian)으로 변경해야 합니다. 1 라디안(radian) = (180/π)도 입니다.  따라서 1도 = (π/180) 라디안입니다. 따라서 45도 = (45 * π)/180라는 것을 알 수 있습니다. 또한, 변환 행렬의 마지막에 0이 아닌 rows//2, -1*cols//4, rows를 사용했습니다. 영상의 회전 기준 축이 좌측 상단이므로 회전한 영상은 프레임 바깥으로 벗어나게 됩니다. 프레임 바깥으로 벗어난 이미지를 프레임 안쪽으로 이동시키기 위해  rows//2, -1*cols//4, rows를 사용한 것입니다. 회전을 한 뒤 평행 이동을 한 것인데, 이를 하나의 변환 행렬로 처리한 것입니다. 

하지만 이렇게 변환 행렬을 직접 구하는 것은 복잡한 작업입니다. 이동시킬 좌표의 크기까지 고려해야 하기 때문입니다. OpenCV는 간단하게 변환행렬을 생성할 수 있게 아래와 같은 함수를 제공합니다.

이 함수를 쓰면 손쉽게 회전을 위한 변환 행렬을 구할 수 있습니다.

# OpenCv로 회전 변환행렬 구하기 (rotate_getmatrix.py)

import cv2

img = cv2.imread('../img/fish.jpg')
rows,cols = img.shape[0:2]

#---① 회전을 위한 변환 행렬 구하기
# 회전축:중앙, 각도:45, 배율:0.5
m45 = cv2.getRotationMatrix2D((cols/2,rows/2),45,0.5) 
# 회전축:중앙, 각도:90, 배율:1.5
m90 = cv2.getRotationMatrix2D((cols/2,rows/2),90,1.5) 

#---② 변환 행렬 적용
img45 = cv2.warpAffine(img, m45,(cols, rows))
img90 = cv2.warpAffine(img, m90,(cols, rows))

#---③ 결과 출력
cv2.imshow('origin',img)
cv2.imshow("45", img45)
cv2.imshow("90", img90)
cv2.waitKey(0)
cv2.destroyAllWindows()

cv2.getRotationMatrix2D() 함수를 이용해서 이미지를 회전해봤습니다. 

 

이상으로 OpenCV 함수를 이용해서 이미지를 이동하고 스케일링하고 회전하는 방법에 대해 알아봤습니다.

Comments