14장. 영상 기하학 변환과 Affine, Perspective
실습 4 — 회전과 세 점 아핀 변환
cv2.getRotationMatrix2D 로 모눈 그림을 30도 돌립니다. 원본 크기로 돌리면 모서리가 잘리는 것을 네 구석 점의 새 자리로 확인하고, 결과 크기를 키워 안 잘리게 돌립니다. 마지막으로 세 점을 짝지어 그림을 기울입니다.
1단계. 원본 크기로 30도 돌리기
import cv2
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
img = cv2.imread("ch14_grid.png")
h, w = img.shape[:2]
center = (w / 2, h / 2)
# getRotationMatrix2D(중심 (x, y), 각도(양수는 반시계), 배율)
M = cv2.getRotationMatrix2D(center, 30, 1.0)
print("2×3 표:")
print(M.round(3))
def move_point(M, x, y):
"""2×3 표로 점 하나를 옮긴다: 줄마다 곱해서 더하기."""
return (float(M[0, 0] * x + M[0, 1] * y + M[0, 2]),
float(M[1, 0] * x + M[1, 1] * y + M[1, 2]))
# 그림의 네 모서리가 어디로 가는지 보고, 그림 밖(0 미만 또는 크기 이상)이면 잘린 것이다
corners = {"왼쪽 위": (0, 0), "오른쪽 위": (w - 1, 0), "오른쪽 아래": (w - 1, h - 1), "왼쪽 아래": (0, h - 1)}
for name, (x, y) in corners.items():
nx, ny = move_point(M, x, y)
inside = 0 <= nx < w and 0 <= ny < h
print(f"{name:6} ({x}, {y}) → ({nx:.1f}, {ny:.1f}) {'안' if inside else '밖: 잘림'}")
rot_cut = cv2.warpAffine(img, M, (w, h))2×3 표:
[[ 0.866 0.5 -38.564]
[ -0.5 0.866 96.077]]
왼쪽 위 (0, 0) → (-38.6, 96.1) 밖: 잘림
오른쪽 위 (319, 0) → (237.7, -63.4) 밖: 잘림
오른쪽 아래 (319, 239) → (357.2, 143.6) 밖: 잘림
왼쪽 아래 (0, 239) → (80.9, 303.1) 밖: 잘림2단계. 결과 크기를 키워 안 잘리게 돌리기
# 표의 첫째 칸과 둘째 칸 숫자(0.866, 0.5)로 돌린 그림을 모두 담는 가로·세로를 계산한다
a, b = abs(M[0, 0]), abs(M[0, 1])
new_w = int(round(h * b + w * a))
new_h = int(round(h * a + w * b))
print(f"새 크기: 가로 {new_w}, 세로 {new_h}")
# 표의 셋째 칸(옮길 거리)을 고쳐 그림의 중심이 새 그림의 한가운데로 오게 한다
M2 = M.copy()
M2[0, 2] += new_w / 2 - center[0]
M2[1, 2] += new_h / 2 - center[1]
for name, (x, y) in corners.items():
nx, ny = move_point(M2, x, y)
inside = -0.5 <= nx < new_w and -0.5 <= ny < new_h
print(f"{name:6} → ({nx:.1f}, {ny:.1f}) {'안' if inside else '밖'}")
rot_full = cv2.warpAffine(img, M2, (new_w, new_h), borderValue=(255, 255, 255))
print("돌린 그림 shape:", rot_full.shape)새 크기: 가로 397, 세로 368
왼쪽 위 → (-0.1, 160.1) 안
오른쪽 위 → (276.2, 0.6) 안
오른쪽 아래 → (395.7, 207.6) 안
왼쪽 아래 → (119.4, 367.1) 안
돌린 그림 shape: (368, 397, 3)3단계. 세 점을 짝지어 기울이기
# 원본의 세 점(왼쪽 위 빨강, 오른쪽 위 초록, 왼쪽 아래 주황)과 옮길 세 점
src = np.float32([[40, 40], [280, 40], [40, 200]])
dst = np.float32([[70, 40], [310, 40], [10, 200]])
# getAffineTransform(원본 세 점, 옮길 세 점) → 2×3 표
A = cv2.getAffineTransform(src, dst)
print("기울이기 표:")
print(A.round(3))
# 정하지 않은 넷째 점(오른쪽 아래 파랑, (280, 200))이 가는 곳
nx, ny = move_point(A, 280, 200)
print(f"넷째 점 (280, 200) → ({nx:.1f}, {ny:.1f})")
sheared = cv2.warpAffine(img, A, (w, h), borderValue=(255, 255, 255))
# 파란 점이 실제로 그 자리에 있는지 색으로 확인한다
print("그 자리의 색:", [int(v) for v in sheared[int(round(ny)), int(round(nx))]])기울이기 표:
[[ 1. -0.375 45. ]
[-0. 1. 0. ]]
넷째 점 (280, 200) → (250.0, 200.0)
그 자리의 색: [200, 90, 30]4단계. 한눈에 견주기
fig, axes = plt.subplots(1, 4, figsize=(13, 3.4), gridspec_kw={"width_ratios": [320, 320, new_w, 320]})
panels = [("원본", img), ("30도, 원본 크기 (잘림)", rot_cut),
(f"30도, {new_w}×{new_h} (안 잘림)", rot_full), ("세 점으로 기울이기", sheared)]
for ax, (title, arr) in zip(axes, panels):
ax.imshow(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB))
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()
코드 설명
cv2.getRotationMatrix2D((160.0, 120.0), 30, 1.0)은[[0.866, 0.5, -38.564], [-0.5, 0.866, 96.077]]을 만듭니다. 원본 크기로 돌리면 네 모서리가 모두 그림 밖(예: 왼쪽 위 (0, 0) → (−38.6, 96.1))으로 나가 잘립니다.- 표의 0.866 과 0.5 로 계산한 새 크기는 397×368 입니다. 셋째 칸에
new_w / 2 - 160,new_h / 2 - 120을 더해 그림을 새 한가운데로 옮기면 네 모서리가 모두 안에 들어옵니다. 왼쪽 위의 −0.1 은 반올림에서 생긴 반 칸 안쪽의 차이라 안으로 셌습니다. cv2.getAffineTransform은 세 쌍의 점으로[[1, -0.375, 45], [0, 1, 0]]을 만들었습니다. 정하지 않은 넷째 점 (280, 200) 은 (250.0, 200.0) 으로 가고, 그 자리의 색[200, 90, 30]이 파란 점입니다. 본문에서 평행사변형 규칙으로 짐작한 자리와 같습니다.- 4단계의
gridspec_kw={"width_ratios": ...}는 칸마다 너비 비율을 정합니다. 그림의 가로 칸 수(320, 397)로 주어, 키운 회전 그림이 다른 그림과 같은 배율로 보이게 했습니다.