14장. 영상 기하학 변환과 Affine, Perspective
실습 5 — 원근 변환으로 비스듬한 화이트보드 펴기
비스듬히 찍힌 가상의 화이트보드 그림(ch14_board_photo.png)을 바로 앞에서 본 것처럼 폅니다. 네 귀퉁이 좌표는 그림을 만들 때 정한 값이고, 눈금으로 읽어 확인합니다. 점의 순서를 틀리면 어떻게 되는지도 보고, 편 그림을 out_ch14_board_flat.png 로 저장합니다.
1단계. 네 귀퉁이 확인하기
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_board_photo.png")
print("그림 shape:", img.shape)
# 화이트보드의 네 귀퉁이 (x, y): 왼쪽 위부터 시계 방향
src = np.float32([[62, 40], [268, 22], [296, 214], [34, 198]])
# 귀퉁이에 점과 번호를 그려 눈금과 함께 띄운다. 그림 함수는 (x, y) 순서(2차시)
marked = img.copy()
for k, (x, y) in enumerate(src.astype(int), start=1):
cv2.circle(marked, (int(x), int(y)), 5, (40, 40, 220), -1)
cv2.putText(marked, str(k), (int(x) + 6, int(y) + 16), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (40, 40, 220), 2)
plt.figure(figsize=(6, 4.5))
plt.imshow(cv2.cvtColor(marked, cv2.COLOR_BGR2RGB))
plt.title("네 귀퉁이: 1 왼쪽 위부터 시계 방향")
plt.show()그림 shape: (240, 320, 3)
2단계. 변의 길이로 결과 크기 짐작하기
def length(p, q):
"""두 점 사이의 거리: 피타고라스로 가로 차이와 세로 차이에서 구한다(10차시)."""
return float(np.sqrt((p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2))
tl, tr, br, bl = src
print(f"윗변 {length(tl, tr):.1f}, 아랫변 {length(bl, br):.1f}")
print(f"왼변 {length(tl, bl):.1f}, 오른변 {length(tr, br):.1f}")
# 실제 화이트보드는 가로:세로 = 3:2 라고 알고 있다. 그 비율로 240×160 에 편다
W, H = 240, 160
print(f"결과 크기: {W}×{H}, 가로÷세로 = {W / H}")윗변 206.8, 아랫변 262.5
왼변 160.5, 오른변 194.0
결과 크기: 240×160, 가로÷세로 = 1.53단계. 3×3 표를 만들어 펴기
# 펼 직사각형의 네 귀퉁이: 원본과 같은 순서(왼쪽 위부터 시계 방향)
dst = np.float32([[0, 0], [W - 1, 0], [W - 1, H - 1], [0, H - 1]])
# getPerspectiveTransform(원본 네 점, 옮길 네 점) → 3×3 표
P = cv2.getPerspectiveTransform(src, dst)
print("3×3 표:")
print(P.round(4))
# cv2.perspectiveTransform: 점 목록을 3×3 표로 옮긴다. 모양은 (점 수, 1, 2) 로 준다
moved = cv2.perspectiveTransform(src.reshape(-1, 1, 2), P).reshape(-1, 2)
print("네 귀퉁이가 간 곳:", [[round(float(v), 1) for v in pt] for pt in moved])
# warpPerspective(그림, 3×3 표, 결과 크기 (가로, 세로))
flat = cv2.warpPerspective(img, P, (W, H))
print("편 그림 shape:", flat.shape)3×3 표:
[[ 1.626300e+00 2.882000e-01 -1.123614e+02]
[ 1.288000e-01 1.473600e+00 -6.692810e+01]
[ 1.300000e-03 2.000000e-03 1.000000e+00]]
네 귀퉁이가 간 곳: [[0.0, 0.0], [239.0, 0.0], [239.0, 159.0], [0.0, 159.0]]
편 그림 shape: (160, 240, 3)4단계. 순서를 틀리면 어떻게 되나, 그리고 저장
# 일부러 오른쪽 위(2)와 왼쪽 아래(4)를 바꿔 적는다
wrong_src = np.float32([src[0], src[3], src[2], src[1]])
wrong = cv2.warpPerspective(img, cv2.getPerspectiveTransform(wrong_src, dst), (W, H))
fig, axes = plt.subplots(1, 3, figsize=(12, 3.4), gridspec_kw={"width_ratios": [320, 240, 240]})
for ax, (title, arr) in zip(axes, [("비스듬한 원본", img), ("바르게 편 결과", flat), ("순서를 틀린 결과", wrong)]):
ax.imshow(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB))
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()
print("저장:", cv2.imwrite("out_ch14_board_flat.png", flat))저장: True
코드 설명
- 네 귀퉁이를 왼쪽 위부터 시계 방향으로 적고, 그림 위에 번호를 찍어 눈금과 견주며 확인했습니다. 실제 사진에서는 이렇게 눈금으로 읽거나 12장의 윤곽선으로 찾습니다.
- 변의 길이는 윗변 206.8, 아랫변 262.5, 왼변 160.5, 오른변 194.0 으로 서로 다릅니다. 가까운 쪽(아래, 오른쪽)이 더 길게 찍혔기 때문입니다. 그래서 길이로는 비율을 정확히 알 수 없고, 실제 비율 3:2 를 알고 있어 240×160 으로 정했습니다.
cv2.getPerspectiveTransform은 3×3 표를 만들고,cv2.perspectiveTransform으로 네 귀퉁이를 옮겨 보면 (0, 0), (239, 0), (239, 159), (0, 159) 로 정확히 갑니다.cv2.warpPerspective의 결과는(160, 240, 3)입니다.- 오른쪽 위와 왼쪽 아래를 바꿔 적으면 그림이 대각선으로 접힌 듯 뒤집혀, 글자가 옆으로 누운 거울 글씨가 됩니다. 오류가 나지 않으므로 결과를 눈으로 확인하는 습관이 필요합니다.