기하학적 변환
실습 준비
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)

출력 영역 밖의 픽셀은 잘림
회전 행렬은 좌표를 옮기지만 출력 이미지의 크기를 자동으로 늘리지 않음. 원본과 같은 (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() |