OpenCV 비전 실무14장 · 영상 기하학 변환과 Affine, Perspective15 / 21← 이전목차다음 →MetaContents

14장. 영상 기하학 변환과 Affine, Perspective

실습 2 — 가운데 자르기와 레터박스로 128×128 맞추기

크기와 비율이 서로 다른 가상의 사진 세 장(ch14_photos/)을 학습용 크기 128×128 로 맞춥니다. 늘리기, 가운데 자르기, 레터박스를 함수로 만들어 견주고, 레터박스 결과를 out_ch14_letterbox.png 로 저장합니다.

1단계. 사진 세 장의 크기와 비율 보기

Python
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.100

2단계. 가운데 자르기 함수

Python
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단계. 레터박스 함수

Python
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.094

4단계. 세 방법 견주고 저장하기

Python
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
세 사진의 원본과 늘리기, 가운데 자르기, 레터박스로 128×128 에 맞춘 결과를 3행 4열로 놓은 그림

코드 설명

  • 세 사진의 가로÷세로는 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 로 작습니다.
  • 늘리기는 아무것도 버리지 않지만 시계는 위아래로 길쭉한 타원이, 귤은 옆으로 넓적한 타원이 되었습니다. 모양이 중요한 데이터라면 늘리기보다 가운데 자르기나 레터박스를 고릅니다.
← 이전 절14장 · 실습 1 — resize 와 보간법 비교다음 절 →14장 · 실습 3 — 뒤집기, 90도 돌리기, 평행 이동