[OpenCV] 기하학적 변환

메르센고수·2023년 12월 27일
0

OpenCV

목록 보기
5/9


Latte는 수능에 무조건 기하와벡터가 출제되서 수학의 정석으로 공부를 했었었었던 기억이 있는데, 요즘은 기벡이 선택과목인 것도 모자라 앞으로 안나올 수도 있다니...기벡 참 재미있었는데, 그 재미를 알게 될 기회조차 없는 요즘 고등학생들이 안타깝다

사실 재미있었다고만 했지 잘했다고는 한적이 없다ㅋㅋ;

앞으로 배울 컴퓨터그래픽스와 컴퓨터비전 과목에 기하학적 변환 기법이 많이 사용될 것 같으니 많이 연습해 놓으면 분명 도움이 될 것 같다.

영상의 이동 변환과 전단 변환

이동 변환 (Translation transformation)

: 가로 또는 세로 방향으로 영상을 특정 크기만큼 이동시키는 변환

영상의 어파인 변환 함수

cv2.warpAffine(src, M, dsize, dst=None, flags=None, orderMode=None, borderValue=None) -> dst

1) src : 입력 영상
2) M : 2x3 어파인 변환 행렬 (실수형)
3) dsize : 결과 영상 크기 (w,h) 튜플, (0,0)이면 src와 같은 크기로 설정
4) dst : 출력 영상
5) flags : 보간법, 기본값은 cv2.INTER_LINEAR
6) borderMode : 가장자리 픽셀 확장 방식. 기본값은 cv2.BORDER_CONSTANT
7) borderValue : cv2.BORDER_CONSTANT일 때 사용할 상수값. 기본값은 0

import sys
import numpy as np
import cv2


src = cv2.imread('tekapo.bmp')

if src is None:
    print('Image load failed!')
    sys.exit()

# Affine 변환 행렬
aff = np.array([[1, 0, 200],
                [0, 1, 100]], dtype=np.float32)

dst = cv2.warpAffine(src, aff, (0, 0))

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()


결과를 보면 이미지가 창에서 우측 하단으로 affine 변환행렬에 의해 이동한 것을 알 수 있다.

전단 변환 (Shear translation)

: 층 밀림 변환. x축과 y축의 방향에 대해 따로 정의한다.

import sys
import numpy as np
import cv2


src = cv2.imread('tekapo.bmp')

if src is None:
    print('Image load failed!')
    sys.exit()

aff = np.array([[1, 0.5, 0],
                [0, 1, 0]], dtype=np.float32)

h, w = src.shape[:2]
dst = cv2.warpAffine(src, aff, (w + int(h * 1.5), h))

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()


여기까지가 영상의 위치나 모양을 변환시키는 작업이고, 지금부터는 크기를 변환시키는 작업이다.

영상의 확대와 축소

크기 변환 (Scale transformation)

  • x축과 y축 방향으로의 스케일 비율을 지정한다.

영상의 크기 변환

cv2.resize(src, dsize, fx=None, fy=None, interpolation=None) -> dst

1) src : 입력 영상
2) dsize : 결과 영상 크기 (w,h) 튜플. (0,0) 이면 fx와 fy 값을 이용하여 결정
3) dst : 출력 영상
4) fx, fy : x와 y방향 스케일 비율 (dsize 값이 0일 때 유효)
5) interpolation : 보간법 지정. 기본값은 cv2.INTER_LINEAR

-> n x n 행렬에서 n이 커질수록 정확도가 높아지지만, 연산량의 증가율이 훨씬 더 크기 때문에 시간도 오래걸리게 된다.

import sys
import numpy as np
import cv2


src = cv2.imread('rose.bmp') # src.shape=(320, 480)

if src is None:
    print('Image load failed!')
    sys.exit()

dst1 = cv2.resize(src, (0, 0), fx=4, fy=4, interpolation=cv2.INTER_NEAREST)
dst2 = cv2.resize(src, (1920, 1280))  # cv2.INTER_LINEAR
dst3 = cv2.resize(src, (1920, 1280), interpolation=cv2.INTER_CUBIC)
dst4 = cv2.resize(src, (1920, 1280), interpolation=cv2.INTER_LANCZOS4)

cv2.imshow('src', src)
cv2.imshow('dst1', dst1[500:900, 400:800])
cv2.imshow('dst2', dst2[500:900, 400:800])
cv2.imshow('dst3', dst3[500:900, 400:800])
cv2.imshow('dst4', dst4[500:900, 400:800])
cv2.waitKey()
cv2.destroyAllWindows()

영상의 축소 시 고려할 사항

  • 한 픽셀로 구성된 선분 같은 경우 영상 축소 시 디테일이 사라지는 경우가 발생한다
  • 입력 영상을 부드럽게 필터링한 후 축소, 다단계 축소를 진행한다.
  • OpenCV의 cv2.resize() 함수에서는 cv2.INTER_AREA 플래그를 사용한다.

영상의 대칭 변환

cv2.flip(src, flipCode, dst=None) -> dst

1) src : 입력 영상
2) flipcode : 대칭 방향 지정 [+1 : 좌우 대칭, 0 : 상하 대칭, -1 : 상하좌우 대칭]
3) dst : 출력 영상

이미지 피라미드

: 하나의 영상에 대해 다양한 해상도의 영상 세트를 구성한 것

  • 보통 가우시안 블러링 & 다운 샘플링 형태로 축소하여 구성한다.

영상 피라미드 다운샘플링

cv2.pyrDown(src, dst=None, dstsize=None, borderType=None) -> dst

1) src : 입력 영상
2) dst : 출력 영상
3) dstsize : 출력 영상 크기 - 따로 지정하지 않으면 입력 영상의 가로, 세로 크기의 1/2로 설정
4) borderType : 가장 자리 픽셀 확장 방식
5) 참고 사항
- 먼저 5x5 크기의 가우시안 필터를 적용
- 이후 짝수 행과 열을 제거하여 작은 크기의 영상을 생성

영상 피라미드 업샘플링

cv2.pyrup(src, dst=None, dstsize=None, borderType=None) -> dst

1) src : 입력 영상
2) dst : 출력 영상
3) dstsize : 출력 영상 크기 - 따로 지정하지 않으면 입력 영상의 가로, 세로 크기의 2배로 설정
4) borderType : 가장 자리 픽셀 확장 방식

예시 코드

import sys
import numpy as np
import cv2


src = cv2.imread('cat.bmp')

if src is None:
    print('Image load failed!')
    sys.exit()


rc = (250, 120, 200, 200)  # rectangle tuple
# (250, 120) 에서 시작, 가로 & 세로 길이= 200

# 원본 영상에 그리기
cpy = src.copy()
cv2.rectangle(cpy, rc, (0, 0, 255), 2)
cv2.imshow('src', cpy)
cv2.waitKey()

# 피라미드 영상에 그리기
for i in range(1, 4):
    src = cv2.pyrDown(src) # 다운샘플링
    cpy = src.copy()
    cv2.rectangle(cpy, rc, (0, 0, 255), 2, shift=i)
    cv2.imshow('src', cpy)
    cv2.waitKey()
    cv2.destroyWindow('src')

cv2.destroyAllWindows()

영상의 회전

: 영상 관련해서는 회전을 빼놓을 수가 없다. 선형대수에서 배운 회전 행렬을 이용해서 픽셀값을 회전시키면 영상 전체가 회전되는 원리를 이용한다.
math 라이브러리에 있는 math.sin과 math.cos 함수를 이용하면 회전행렬을 쉽게 구할 수 있다.

import sys
import math
import numpy as np
import cv2


src = cv2.imread('tekapo.bmp')

if src is None:
    print('Image load failed!')
    sys.exit()

# 라디안 값 구하기
rad = 20 * math.pi / 180
# Affine 행렬 (회전 행렬) 구하기
aff = np.array([[math.cos(rad), math.sin(rad), 0],
                [-math.sin(rad), math.cos(rad), 0]], dtype=np.float32)

dst = cv2.warpAffine(src, aff, (0, 0))

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()

cv2.destroyAllWindows()

이 코드는 전단변환을 하는 작업을 회전행렬로 해서 영상이 전체적으로 radian 만큼 반시계 방향으로 회전되어 있는 것을 알 수 있다.

영상의 회전 변환 행렬 구하기

cv2.getRotationMatrix2D(center, angle, scale) -> retval

1) center : 회전 중심 좌표 (x,y) 튜플
2) angle : (반시계 방향) 회전 각도 (degree). 음수는 시계 방향
3) scale : 추가적인 확대 비율
4) retval : 2x3 어파인 변환 행렬. 실수형

src=cv2.imread('tekapo.bmp')

cp=(src.shape[1]/2, src.shape[0]/2)
rot=cv2.getRotationMatrix2D(cp, 20, 1)

dst=cv2.warpAffine(src, rot, (0,0))

이런식으로 코드를 바꾸면 똑같이 회전을 하더라도 이미지의 중심을 축으로 두고 회전을 하게 되기 때문에 잘리는 부분이 적어지게 된다.

어파인 변환과 투시 변환

어파인 변환 행렬

cv2.getAffineTransform(src, dst) -> retval

1) src : 3개의 원본 좌표점. numpy.ndarray.shape=(3,2)
e.g) np.array([[x_1, y_1], [x_2, y_2], [x_3, y_3]], np.float32)
2) dst : 3개의 결과 좌표점. numpy.ndarray.shape=(3,2)
3) retval : 2x3 투시 변환 행렬

투시 변환 행렬

cv2.getPerspectiveTransform(src, dst, solveMethod=None) 
-> retval

1) src : 4개의 원본 좌표점. numpy.ndarray.shape=(4,2)
e.g) np.array([[x_1, y_1], [x_2, y_2], [x_3, y_3], [x_4, y_4]], np.float32)
2) dst : 4개의 결과 좌표점. numpy.ndarray.shape=(4,2)
3) retval : 3x3 투시 변환 행렬

영상의 어파인 변환 함수

cv2.warpAffine(src, M, dsize, dst=None, flags=None, borderMode=None, borderValue=None) -> dst

1) src : 입력 영상
2) M : 2x3 어파인 변환 행렬. 실수형
3) dsize : 결과 영상 크기. (w,h) 튜플. (0,0)이면 src와 같은 크기로 설정
4) dst : 출력 영상
5) flags : 보간법. 기본값은 cv2.INTER_LINEAR
6) borderMode : 가장자리 픽셀 확장 방식. 기본값은 cv2.BORDER_CONSTANT
7) borderValue : cv2.BORDER_CONSTANT일 때 사용할 상수 값. 기본 값은 0

영상의 투시 변환 함수

cv2.warpPerspective(src, M, dsize, dst=None, flags=None, borderMode=None, borderValue=None) -> dst

1) src : 입력 영상
2) M : 3x3 투시 변환 행렬. 실수형
3) dsize : 결과 영상 크기. (w,h) 튜플. (0,0)이면 src와 같은 크기로 설정
4) dst : 출력 영상
5) flags : 보간법. 기본값은 cv2.INTER_LINEAR
6) borderMode : 가장자리 픽셀 확장 방식. 기본값은 cv2.BORDER_CONSTANT
7) borderValue : cv2.BORDER_CONSTANT일 때 사용할 상수 값. 기본 값은 0

예시 코드

import sys
import numpy as np
import cv2


src = cv2.imread('namecard.jpg')

if src is None:
    print('Image load failed!')
    sys.exit()

w, h = 720, 400
# 기울어져 있는 명함의 edge 좌표
srcQuad = np.array([[325, 307], [760, 369], [718, 611], [231, 515]], np.float32)
# 펴진 명함의 edge 좌표
dstQuad = np.array([[0, 0], [w-1, 0], [w-1, h-1], [0, h-1]], np.float32)

pers = cv2.getPerspectiveTransform(srcQuad, dstQuad)
dst = cv2.warpPerspective(src, pers, (w, h))

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

리매핑

: 영상의 특정 위치 픽셀을 다른 위치에 재배치하는 일반적인 프로세스

리매핑 함수

cv2.remap(src, map1, map2, interpolation, dst=None, borderMode=None, borderValue=None) -> dst

1) src : 입력 영상
2) map1 : 결과 영상의 (x,y) 좌표가 참조할 입력 영상의 x좌표
입력 영상과 크기는 같고, 타입은 np.float32인 numpy.ndarray
3) map2 : 결과 영상의 (x,y) 좌표가 참조할 입력 영상의 y좌표
4) interpolation : 보간법
5) dst : 출력 영상
6) borderMode : 가장자리 픽셀 확장 방식. 기본값은 cv2.BORDER_CONSTANT
7) borderValue : cv2.BORDER_CONSTANT일 때 사용할 상수 값. 기본 값은 0

profile
블로그 이전했습니다 (https://phj6724.tistory.com/)

0개의 댓글

관련 채용 정보