14장. 영상 기하학 변환과 Affine, Perspective
실습 3 — 뒤집기, 90도 돌리기, 평행 이동
모눈 그림을 cv2.flip 과 cv2.rotate 로 뒤집고 돌려, 빨간 점이 어디로 가는지 계산과 실제 값으로 확인합니다. 이어서 2×3 표를 직접 만들어 평행 이동하고, 점 하나의 새 자리를 손계산한 값과 견줍니다.
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_grid.png")
h, w = img.shape[:2]
x, y = 40, 40 # 왼쪽 위 빨간 점의 자리 (x, y)
print("원본 빨간 점:", [int(v) for v in img[y, x]])
# flipCode 1: 좌우, 0: 위아래, -1: 둘 다. 뒤집은 뒤 빨간 점이 갈 자리를 계산해 둔다
cases = {1: (w - 1 - x, y), 0: (x, h - 1 - y), -1: (w - 1 - x, h - 1 - y)}
flips = {}
for code, (nx, ny) in cases.items():
flips[code] = cv2.flip(img, code)
print(f"flipCode {code:2}: 계산한 자리 ({nx}, {ny}) 의 색 {[int(v) for v in flips[code][ny, nx]]}")
# NumPy 의 거꾸로 슬라이싱과 숫자가 같은지 본다
print("좌우: flip 과 [:, ::-1] 이 같은가:", np.array_equal(flips[1], img[:, ::-1]))
print("둘 다: flip 과 [::-1, ::-1] 이 같은가:", np.array_equal(flips[-1], img[::-1, ::-1]))원본 빨간 점: [40, 40, 220]
flipCode 1: 계산한 자리 (279, 40) 의 색 [40, 40, 220]
flipCode 0: 계산한 자리 (40, 199) 의 색 [40, 40, 220]
flipCode -1: 계산한 자리 (279, 199) 의 색 [40, 40, 220]
좌우: flip 과 [:, ::-1] 이 같은가: True
둘 다: flip 과 [::-1, ::-1] 이 같은가: True2단계. 90도 돌리기
# ROTATE_90_CLOCKWISE: 시계 방향 90도, ROTATE_90_COUNTERCLOCKWISE: 반시계 방향 90도
cw = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
ccw = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
print("원본 shape:", img.shape, "/ 돌린 shape:", cw.shape)
# 시계 방향으로 돌리면 (x, y) 가 (세로 칸 수 - 1 - y, x) 로 간다
nx, ny = h - 1 - y, x
print(f"시계 방향: 계산한 자리 ({nx}, {ny}) 의 색 {[int(v) for v in cw[ny, nx]]}")
# np.rot90(그림, k): 반시계 방향으로 k 번 90도. k=-1 은 시계 방향 한 번
print("rotate 와 np.rot90(k=-1) 이 같은가:", np.array_equal(cw, np.rot90(img, k=-1)))
# 시계 방향 네 번이면 제자리
four = img
for _ in range(4):
four = cv2.rotate(four, cv2.ROTATE_90_CLOCKWISE)
print("네 번 돌리면 원본과 같은가:", np.array_equal(four, img))원본 shape: (240, 320, 3) / 돌린 shape: (320, 240, 3)
시계 방향: 계산한 자리 (199, 40) 의 색 [40, 40, 220]
rotate 와 np.rot90(k=-1) 이 같은가: True
네 번 돌리면 원본과 같은가: True3단계. 2×3 표로 평행 이동하고 손계산과 견주기
# 오른쪽으로 60, 아래로 20 옮기는 2×3 표
tx, ty = 60, 20
M = np.float32([[1, 0, tx],
[0, 1, ty]])
# 손계산: 줄마다 "첫째 × x + 둘째 × y + 셋째"
new_x = M[0, 0] * x + M[0, 1] * y + M[0, 2]
new_y = M[1, 0] * x + M[1, 1] * y + M[1, 2]
print(f"새 x = 1×{x} + 0×{y} + {tx} = {int(new_x)}")
print(f"새 y = 0×{x} + 1×{y} + {ty} = {int(new_y)}")
# cv2.warpAffine(그림, 2×3 표, 결과 크기 (가로, 세로))
moved = cv2.warpAffine(img, M, (w, h))
print("옮긴 그림의 그 자리 색:", [int(v) for v in moved[int(new_y), int(new_x)]])
# 빈 곳(원본이 닿지 않는 왼쪽 60줄, 위쪽 20줄)이 몇 칸인지 센다
empty = int(np.all(moved == 0, axis=2).sum())
print(f"검게 빈 칸: {empty:,}칸 (계산: 320×240 - 260×220 = {w * h - (w - tx) * (h - ty):,})")새 x = 1×40 + 0×40 + 60 = 100
새 y = 0×40 + 1×40 + 20 = 60
옮긴 그림의 그 자리 색: [40, 40, 220]
검게 빈 칸: 19,600칸 (계산: 320×240 - 260×220 = 19,600)4단계. 빈 곳을 흰색으로 채우고 한눈에 보기
# borderValue: 빈 곳에 칠할 색(B, G, R)
moved_white = cv2.warpAffine(img, M, (w, h), borderValue=(255, 255, 255))
fig, axes = plt.subplots(2, 3, figsize=(11, 5.6))
panels = [("원본", img), ("좌우 (flipCode 1)", flips[1]), ("위아래 (flipCode 0)", flips[0]),
("시계 방향 90도", cw), ("옮기기 (60, 20)", moved), ("옮기기, 빈 곳 흰색", moved_white)]
for ax, (title, arr) in zip(axes.flat, panels):
ax.imshow(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB))
ax.set_title(title)
plt.tight_layout()
plt.show()
코드 설명
- 빨간 점 (40, 40) 은
flipCode 1에서 (279, 40),0에서 (40, 199),-1에서 (279, 199) 로 갑니다. 계산한 자리의 색이 모두[40, 40, 220]이어서, 뒤집기는 값은 그대로 두고 자리만 바꾼다는 것을 확인했습니다.cv2.flip과 NumPy 의[:, ::-1],[::-1, ::-1]은 숫자가 똑같습니다. cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)는(240, 320, 3)을(320, 240, 3)으로 바꿉니다. 시계 방향으로 돌리면 (x, y) 가 (세로 칸 수 − 1 − y, x) 로 가므로 빨간 점은 (199, 40) 에 있습니다.np.rot90(img, k=-1)과 같고, 네 번 돌리면 원본과 같습니다.- 2×3 표
[[1, 0, 60], [0, 1, 20]]로 손계산한 새 자리 (100, 60) 에 실제로 빨간 점이 있습니다. 표의 셋째 칸이 더할 거리이고, 곱해서 더하는 계산이cv2.warpAffine안에서 모든 칸에 일어납니다. - 옮긴 그림의 검은 칸은 19,600칸으로, 320×240 − 260×220 과 같습니다. 원본이 닿지 않는 왼쪽 60줄과 위쪽 20줄입니다.
borderValue=(255, 255, 255)를 주면 이 빈 곳이 흰색으로 칠해집니다.