3장. 디지털 영상과 색공간 변환
실습 2 — 해상도와 데이터형 살펴보기
같은 바구니 그림의 칸 수를 줄였다 늘려 모자이크를 보고, 해상도에 따른 메모리 크기를 계산합니다. 이어서 숫자를 float32 의 0.0~1.0 으로 바꿨다가 uint8 로 되돌리며, 되돌릴 때 clip 이 왜 필요한지 확인합니다. 크기를 바꾸는 cv2.resize 는 여기서는 맛보기로만 쓰고, 자세한 옵션은 14장에서 배웁니다.
1단계. 해상도와 메모리 크기 계산하기
import os
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("fruit_basket.png")
h, w, c = img.shape
print(f"해상도: {w}×{h}, 화소 수: {w * h:,}칸")
# 메모리 크기 = 가로 × 세로 × 채널 × 한 칸의 바이트(uint8 은 1)
print(f"계산한 메모리: {w} × {h} × {c} × 1 = {w * h * c:,}바이트")
# nbytes: 배열이 실제로 차지하는 바이트 수
print(f"nbytes: {img.nbytes:,}바이트")
# 파일은 png 가 압축해 저장하므로 훨씬 작다
print(f"파일 크기: {os.path.getsize('fruit_basket.png'):,}바이트")해상도: 320×240, 화소 수: 76,800칸
계산한 메모리: 320 × 240 × 3 × 1 = 230,400바이트
nbytes: 230,400바이트
파일 크기: 23,384바이트2단계. 칸 수를 줄였다 늘려 모자이크 보기
panels = [("원본 320×240", img)]
for sw, sh in [(80, 60), (32, 24)]:
# cv2.resize(그림, (새 가로, 새 세로), interpolation=방법): 크기를 바꾼다. 크기는 (가로, 세로) 순서
# INTER_AREA(area, 넓이): 줄일 때 칸 여러 개의 평균을 낸다
small = cv2.resize(img, (sw, sh), interpolation=cv2.INTER_AREA)
# INTER_NEAREST(nearest, 가장 가까운): 키울 때 가까운 칸을 그대로 늘려 네모 모자이크가 보이게 한다
big = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
print(f"{sw}×{sh}: 모양 {small.shape}, 메모리 {small.nbytes:,}바이트")
panels.append((f"{sw}×{sh} 로 줄였다 키움", big))
fig, axes = plt.subplots(1, 3, figsize=(10, 2.9))
for ax, (title, im) in zip(axes, panels):
ax.imshow(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()80×60: 모양 (60, 80, 3), 메모리 14,400바이트
32×24: 모양 (24, 32, 3), 메모리 2,304바이트
3단계. float32 의 0.0~1.0 으로 바꾸기
# astype(np.float32): 그릇을 소수용으로 바꾼다. 그다음 255 로 나눠 0.0~1.0 으로(정규화)
f = img.astype(np.float32) / 255
print("데이터형:", img.dtype, "→", f.dtype)
print("가장 작은 값:", round(float(f.min()), 3), "/ 가장 큰 값:", round(float(f.max()), 3))
# 숫자 하나가 4바이트이므로 메모리가 4배가 된다
print(f"메모리: {img.nbytes:,} → {f.nbytes:,}바이트 ({f.nbytes // img.nbytes}배)")
# 사과 한 칸: 0~255 의 숫자가 255 로 나눈 비율이 되었다
print("사과 칸, uint8:", [int(v) for v in img[135, 140]])
print("사과 칸, float32:", [round(float(v), 3) for v in f[135, 140]])데이터형: uint8 → float32
가장 작은 값: 0.098 / 가장 큰 값: 1.0
메모리: 230,400 → 921,600바이트 (4배)
사과 칸, uint8: [34, 37, 206]
사과 칸, float32: [0.133, 0.145, 0.808]4단계. 되돌릴 때 clip 이 필요한 까닭
# 1.5배 밝게 한다. 1.0 을 넘는 칸이 생긴다
brighter = f * 1.5
print("1.0 을 넘는 숫자의 비율:", round(float((brighter > 1).mean()), 3))
# 잘못된 방법: 범위를 넘은 소수를 그대로 uint8 에 담는다
wrong = (brighter * 255).astype(np.uint8)
# 바른 방법: np.clip 으로 0~1 밖을 잘라 낸 뒤 255 를 곱하고 반올림해 담는다
right = (np.clip(brighter, 0, 1) * 255).round().astype(np.uint8)
# 바나나 한 칸(x=80, y=92)에서 견준다. 원래 G=212, R=243 이라 1.5배면 255 를 넘는다
print("바나나 원본:", [int(v) for v in img[92, 80]])
print("clip 없이: ", [int(v) for v in wrong[92, 80]])
print("clip 하고: ", [int(v) for v in right[92, 80]])1.0 을 넘는 숫자의 비율: 0.562
바나나 원본: [41, 212, 243]
clip 없이: [61, 62, 108]
clip 하고: [62, 255, 255]코드 설명
img.nbytes는 가로 × 세로 × 채널 × 한 칸 바이트로 계산한 값과 같습니다. png 파일은 이것을 압축해서 메모리 크기보다 훨씬 작습니다.cv2.resize로 80×60 으로 줄이면 칸 수가 원본의 1/16 이 되어 메모리도 1/16 입니다. 다시 키워도 잃은 칸은 돌아오지 않아 네모 모자이크로 보입니다.img.astype(np.float32) / 255는 0~255 를 0.0~1.0 으로 바꿉니다. 숫자 하나가 4바이트라 메모리가 4배가 됩니다.- 1.0 을 넘는 소수를
clip없이astype(np.uint8)에 담으면 255 에서 멈추지 않고 엉뚱한 값이 됩니다. 그 값은 컴퓨터마다 다를 수 있으므로, 되돌릴 때는 언제나np.clip을 먼저 합니다.