logo

기하학적 변환

실습 준비

import cv2 as cv
import numpy as np
from yeom import to_pil

파일 불러오기:

balls = cv.imread("balls.png")
chessboard = cv.imread("chessboard.jpg")

balls_height, balls_width = balls.shape[:2]
balls.shape, chessboard.shape
실행 결과
((816, 1456, 3), (1200, 1806, 3))

기하학적 변환

기하학적 변환(geometric transformation)은 입력 이미지의 좌표를 출력 이미지의 새로운 좌표로 옮기는 처리. 이동한 좌표가 정수가 아니면 주변 픽셀을 보간하여 출력 픽셀값을 계산.

입력 좌표 (x, y) → 변환 행렬 → 출력 좌표 (x', y')

1교시의 cv.resize(), cv.flip(), cv.rotate()는 자주 사용하는 변환을 함수로 간단히 수행했음. 이번 시간에는 좌표 변환 행렬을 직접 만들거나 대응점으로 계산.

아핀 변환

아핀 변환(affine transformation)은 선형 변환과 평행 이동을 결합한 좌표 변환.

  • 변환 행렬 사용
  • 평행 이동, 회전, 크기 변경, 전단 표현 가능
  • 직선은 변환 후에도 직선으로 유지
  • 평행한 직선은 변환 후에도 평행
  • 한 직선 위에 있는 점 사이의 비율 보존
  • 서로 일직선 위에 있지 않은 세 대응점으로 행렬 결정 가능

평행 이동, 전단, 크기 변경과 회전을 포함하는 아핀 변환

평행 이동

다음 행렬은 모든 픽셀을 오른쪽으로 80픽셀, 아래로 40픽셀 이동.

translation_matrix = np.array(
    [
        [1, 0, 80],  # x 방향으로 80픽셀 이동
        [0, 1, 40],  # y 방향으로 40픽셀 이동
    ],
    dtype=np.float32,  # 변환 행렬은 실수형이어야 함
)

translated = cv.warpAffine(
    balls,
    translation_matrix,
    (balls_width, balls_height),  # 출력 크기는 (너비, 높이)
)

to_pil(translated)

오른쪽 아래로 평행 이동한 이미지

cv.warpAffine()의 세 번째 인자는 출력 이미지의 (너비, 높이). 출력 영역 밖으로 이동한 픽셀은 잘리고 새로 생긴 영역은 기본적으로 검은색으로 채워짐.

임의 각도 회전

cv.getRotationMatrix2D()는 중심점, 회전각, 크기 배율로 아핀 행렬을 계산. 양의 각도는 반시계 방향.

center = (balls_width / 2, balls_height / 2)

rotation_matrix = cv.getRotationMatrix2D(
    center,  # 회전 중심
    30,      # 회전각(양수는 반시계 방향)
    1.0,     # 크기 배율
)

rotation_matrix.shape
실행 결과
(2, 3)

계산한 행렬을 cv.warpAffine()으로 이미지에 적용.

rotated = cv.warpAffine(
    balls,
    rotation_matrix,
    (balls_width, balls_height),
    borderMode=cv.BORDER_CONSTANT,
    borderValue=(255, 255, 255),
)

to_pil(rotated)

중심을 기준으로 30도 회전한 이미지

출력 영역 밖의 픽셀은 잘림

회전 행렬은 좌표를 옮기지만 출력 이미지의 크기를 자동으로 늘리지 않음. 원본과 같은 (balls_width, balls_height)를 사용하면 회전한 이미지의 모서리가 출력 영역 밖으로 나가 잘릴 수 있음.

잘리지 않게 하려면 회전한 네 모서리를 모두 담는 크기를 출력 크기로 지정하고, 그만큼 행렬의 이동 성분(rotation_matrix[0, 2], rotation_matrix[1, 2])을 더해 중심을 옮김.

체스보드의 대응점 지정

비스듬히 촬영한 체스보드의 네 꼭짓점을 출력 이미지의 네 꼭짓점에 대응시킴. 입력점과 출력점은 같은 인덱스끼리 대응하므로 배열의 순서를 맞춰야.

source_points = np.array(
    [
        [60, 500],
        [900, 100],
        [900, 1100],
        [1700, 500],
    ],
    dtype=np.float32,
)

destination_points = np.array(
    [
        [0, 0],
        [1000, 0],
        [0, 1000],
        [1000, 1000],
    ],
    dtype=np.float32,
)

비스듬히 촬영한 보드는 마름모처럼 보이므로 네 꼭짓점을 왼쪽, 위, 아래, 오른쪽 순으로 지정. 출력점도 같은 순서로 좌상, 우상, 좌하, 우하를 지정하여 보드를 정사각형으로 펴면서 약 45도 회전시킴.

순서source_points보드에서의 위치destination_points출력에서의 위치
0[60, 500]왼쪽 꼭짓점[0, 0]좌상
1[900, 100]위쪽 꼭짓점[1000, 0]우상
2[900, 1100]아래쪽 꼭짓점[0, 1000]좌하
3[1700, 500]오른쪽 꼭짓점[1000, 1000]우하

지정한 좌표가 실제로 어디인지 원본 위에 표시하여 확인.

marked_chessboard = chessboard.copy()  # 원본을 그대로 두고 사본에 표시
colors = [
    (0, 0, 255),      # 빨강: 왼쪽 꼭짓점
    (255, 0, 0),      # 파랑: 위쪽 꼭짓점
    (255, 255, 0),    # 하늘: 아래쪽 꼭짓점
    (0, 255, 0),      # 초록: 오른쪽 꼭짓점
]

for point, color in zip(source_points, colors):
    cv.circle(
        marked_chessboard,
        point.astype(int),  # 좌표는 정수여야 함
        16,                 # 반지름
        color,
        -1,                 # 음수는 내부를 채움
    )

to_pil(marked_chessboard)

네 꼭짓점을 색으로 표시한 체스보드

세 대응점으로 아핀 변환

cv.getAffineTransform()은 서로 일직선 위에 있지 않은 세 대응점으로 아핀 행렬을 계산. 앞에서 지정한 네 대응점 중 처음 세 쌍만 사용.

affine_source_points = source_points[:3]  # 네 쌍 중 처음 세 쌍만 사용
affine_destination_points = destination_points[:3]

affine_matrix = cv.getAffineTransform(
    affine_source_points,
    affine_destination_points,
)

affine_matrix.shape
실행 결과
(2, 3)
affine_result = cv.warpAffine(
    chessboard,
    affine_matrix,
    (1000, 1000),
)

to_pil(affine_result)

세 대응점으로 변환한 체스보드

처음 세 대응점은 원하는 위치로 이동하지만 네 번째 점은 변환 계산에 사용되지 않음. 아핀 변환은 평행성을 보존하므로 체스보드에 나타난 원근 왜곡을 완전히 바로잡지 못함.

투시 변환

투시 변환(perspective transformation)은 원근에 따른 형태 변화를 표현할 수 있는 좌표 변환.

마지막 좌표 로 나누어 실제 출력 좌표를 구함.

  • 변환 행렬 사용
  • 직선은 변환 후에도 직선으로 유지
  • 평행선은 평행하지 않게 변할 수 있음
  • 평면을 비스듬히 본 원근 왜곡 표현 가능
  • 네 대응점으로 행렬 결정 가능

사각형을 사다리꼴 형태로 바꾸는 투시 변환

네 대응점으로 투시 변환

이번에는 같은 네 대응점을 모두 사용. cv.getPerspectiveTransform()으로 행렬을 계산하고 cv.warpPerspective()로 이미지에 적용.

perspective_matrix = cv.getPerspectiveTransform(
    source_points,
    destination_points,
)

perspective_result = cv.warpPerspective(
    chessboard,
    perspective_matrix,
    (1000, 1000),
)

perspective_matrix.shape
실행 결과
(3, 3)
to_pil(perspective_result)

네 대응점으로 정면 형태에 가깝게 변환한 체스보드

투시 변환은 네 꼭짓점을 모두 원하는 위치로 옮기며 평행성을 보존할 필요가 없음. 따라서 비스듬히 촬영해 사다리꼴처럼 보이는 체스보드를 직사각형에 가깝게 펼칠 수 있음.

아핀 변환과 투시 변환 비교

구분아핀 변환투시 변환
행렬 크기
필요한 대응점3쌍4쌍
평행성보존보존되지 않을 수 있음
주요 변환이동, 회전, 크기 변경, 전단원근 왜곡을 포함한 평면 변환
행렬 계산cv.getAffineTransform()cv.getPerspectiveTransform()
이미지 적용cv.warpAffine()cv.warpPerspective()
Previous
이미지 변형