14장. 영상 기하학 변환과 Affine, Perspective
실습 2 — 가운데 자르기와 레터박스로 128×128 맞추기
크기와 비율이 서로 다른 가상의 사진 세 장(ch14_photos/)을 학습용 크기 128×128 로 맞춥니다. 늘리기, 가운데 자르기, 레터박스를 함수로 만들어 견주고, 레터박스 결과를 out_ch14_letterbox.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
# 크기가 제각각인 가상의 사진 세 장
names = ["wide.png", "tall.png", "square.png"]
photos = {name: cv2.imread(f"ch14_photos/{name}") for name in names}
for name, img in photos.items():
h, w = img.shape[:2]
# 종횡비: 가로 ÷ 세로. 1 보다 크면 가로로 길다
print(f"{name:11} 가로 {w}, 세로 {h}, 가로÷세로 = {w / h:.3f}")wide.png 가로 320, 세로 180, 가로÷세로 = 1.778
tall.png 가로 160, 세로 240, 가로÷세로 = 0.667
square.png 가로 220, 세로 200, 가로÷세로 = 1.1002단계. 가운데 자르기 함수
SIZE = 128 # 학습용 크기
def center_crop(img, size=SIZE):
"""짧은 변에 맞춰 가운데를 정사각형으로 자르고 size×size 로 줄인다."""
h, w = img.shape[:2]
side = min(h, w) # 정사각형 한 변 = 짧은 변
x0, y0 = (w - side) // 2, (h - side) // 2 # 양쪽에서 버릴 칸의 절반
square = img[y0:y0 + side, x0:x0 + side] # 슬라이싱은 [세로, 가로] 순서(2차시)
# 줄이는 것이므로 INTER_AREA
return cv2.resize(square, (size, size), interpolation=cv2.INTER_AREA), (x0, y0, side)
for name, img in photos.items():
out, (x0, y0, side) = center_crop(img)
h, w = img.shape[:2]
# 버린 칸 수: 가로 또는 세로에서 잘려 나간 칸
print(f"{name:11} 자른 곳 x {x0}~{x0 + side - 1}, y {y0}~{y0 + side - 1} → {out.shape}, 버린 칸 {w * h - side * side:,}칸")wide.png 자른 곳 x 70~249, y 0~179 → (128, 128, 3), 버린 칸 25,200칸
tall.png 자른 곳 x 0~159, y 40~199 → (128, 128, 3), 버린 칸 12,800칸
square.png 자른 곳 x 10~209, y 0~199 → (128, 128, 3), 버린 칸 4,000칸3단계. 레터박스 함수
GRAY = (128, 128, 128) # 여백 색: 모든 사진에 같게 쓴다
def letterbox(img, size=SIZE, color=GRAY):
"""긴 변을 size 에 맞춰 비율대로 줄이고, 모자란 칸을 양쪽에 나눠 덧댄다."""
h, w = img.shape[:2]
scale = size / max(h, w) # 긴 변 기준 배율
nw, nh = round(w * scale), round(h * scale)
small = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_AREA)
top = (size - nh) // 2
left = (size - nw) // 2
# copyMakeBorder = copy and make border: 둘레에 칸을 덧댄다
out = cv2.copyMakeBorder(small,
top, size - nh - top, # 위, 아래 칸 수
left, size - nw - left, # 왼쪽, 오른쪽 칸 수
cv2.BORDER_CONSTANT, # 정한 색 하나로 채운다
value=color) # 채울 색
return out, (nw, nh, top, left)
for name, img in photos.items():
out, (nw, nh, top, left) = letterbox(img)
# 여백 비율: 128×128 가운데 사진이 아닌 칸의 몫
pad_ratio = 1 - (nw * nh) / (SIZE * SIZE)
print(f"{name:11} 줄인 크기 {nw}×{nh}, 위 {top}칸, 왼쪽 {left}칸 → {out.shape}, 여백 비율 {pad_ratio:.3f}")wide.png 줄인 크기 128×72, 위 28칸, 왼쪽 0칸 → (128, 128, 3), 여백 비율 0.438
tall.png 줄인 크기 85×128, 위 0칸, 왼쪽 21칸 → (128, 128, 3), 여백 비율 0.336
square.png 줄인 크기 128×116, 위 6칸, 왼쪽 0칸 → (128, 128, 3), 여백 비율 0.0944단계. 세 방법 견주고 저장하기
fig, axes = plt.subplots(3, 4, figsize=(11, 8.4))
for row, (name, img) in enumerate(photos.items()):
# 늘리기: 비율을 무시하고 128×128 로 바로 바꾼다
stretched = cv2.resize(img, (SIZE, SIZE), interpolation=cv2.INTER_AREA)
panels = [("원본 " + name, img), ("늘리기", stretched),
("가운데 자르기", center_crop(img)[0]), ("레터박스", letterbox(img)[0])]
for col, (title, arr) in enumerate(panels):
axes[row, col].imshow(cv2.cvtColor(arr, cv2.COLOR_BGR2RGB))
axes[row, col].set_title(title)
axes[row, col].axis("off")
plt.tight_layout()
plt.show()
# 레터박스 세 장을 가로로 이어 붙여 저장한다(높이가 모두 128 이라 hconcat 이 된다)
boxes = [letterbox(img)[0] for img in photos.values()]
print("저장:", cv2.imwrite("out_ch14_letterbox.png", cv2.hconcat(boxes)))저장: True
코드 설명
- 세 사진의 가로÷세로는 1.778, 0.667, 1.100 입니다. 1 에서 멀수록 128×128 에 맞출 때 잃는 것이 많습니다.
center_crop은 짧은 변에 맞춰img[y0:y0 + side, x0:x0 + side]로 자릅니다.wide.png는 가로 70~249 만 남아 25,200칸을 버렸고, 양 끝의 빨강·파랑 네모가 모두 잘려 나갔습니다.tall.png도 세로 40~199 만 남아 위아래 네모가 잘렸습니다. 비율이 1 에 가까운square.png는 4,000칸만 버렸습니다.letterbox는 긴 변을 128 에 맞춘 뒤cv2.copyMakeBorder로 모자란 칸을 덧댑니다.wide.png는 128×72 로 줄고 위아래에 28칸씩 붙어 여백 비율이 0.438 입니다. 128×128 의 절반 가까이가 회색 여백입니다.square.png는 여백 비율이 0.094 로 작습니다.- 늘리기는 아무것도 버리지 않지만 시계는 위아래로 길쭉한 타원이, 귤은 옆으로 넓적한 타원이 되었습니다. 모양이 중요한 데이터라면 늘리기보다 가운데 자르기나 레터박스를 고릅니다.