이미지 I/O
이미지 입출력(I/O)은 모든 CV 작업의 출발점입니다. 이 문서에서는 OpenCV와 PIL(Pillow)을 사용하여 이미지를 읽고, 변환하고, 저장하는 기본 작업을 실습합니다.
환경 준비
pip install opencv-python-headless pillow numpy matplotlib
import cv2
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
OpenCV 기본 사용법
이미지 읽기와 쓰기
# 이미지 읽기 (BGR 형식)
img = cv2.imread('image.jpg')
print(f"형태: {img.shape}") # (높이, 너비, 채널)
print(f"타입: {img.dtype}") # uint8
# 그레이스케일로 읽기
gray = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
print(f"형태: {gray.shape}") # (높이, 너비)
# 이미지 저장
cv2.imwrite('output.jpg', img)
cv2.imwrite('output.png', img) # PNG 형식
OpenCV는 이미지를 BGR 순서로 읽습니다. matplotlib이나 PIL은 RGB 순서를 사용하므로, 시각화할 때 반드시 색상 변환이 필요합니다.
색상 변환
# BGR → RGB 변환 (matplotlib 시각화용)
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# BGR → 그레이스케일
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# BGR → HSV (색상 기반 필터링에 유용)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 시각화
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
plt.imshow(rgb)
plt.title('RGB')
plt.subplot(1, 3, 2)
plt.imshow(gray, cmap='gray')
plt.title('Grayscale')
plt.subplot(1, 3, 3)
plt.imshow(hsv)
plt.title('HSV')
plt.tight_layout()
plt.show()
리사이즈
# 고정 크기 리사이즈
resized = cv2.resize(img, (224, 224))
# 비율 유지 리사이즈
h, w = img.shape[:2]
scale = 224 / max(h, w)
new_w, new_h = int(w * scale), int(h * scale)
resized = cv2.resize(img, (new_w, new_h))
# 보간법 선택
# INTER_AREA: 축소 시 권장
# INTER_LINEAR: 기본값, 확대 시 적합
# INTER_CUBIC: 고품질 확대
resized = cv2.resize(img, (224, 224), interpolation=cv2.INTER_AREA)
자르기와 패딩
# 중앙 크롭 (Center Crop)
h, w = img.shape[:2]
crop_size = 224
start_x = (w - crop_size) // 2
start_y = (h - crop_size) // 2
cropped = img[start_y:start_y+crop_size, start_x:start_x+crop_size]
# 패딩 (비율 유지 + 정사각형 만들기)
def resize_with_pad(image, target_size=224):
"""비율을 유지하면서 패딩으로 정사각형을 만듭니다."""
h, w = image.shape[:2]
scale = target_size / max(h, w)
new_w, new_h = int(w * scale), int(h * scale)
resized = cv2.resize(image, (new_w, new_h))
# 패딩 추가
pad_w = target_size - new_w
pad_h = target_size - new_h
top = pad_h // 2
bottom = pad_h - top
left = pad_w // 2
right = pad_w - left
padded = cv2.copyMakeBorder(
resized, top, bottom, left, right,
cv2.BORDER_CONSTANT, value=(114, 114, 114) # 회색 패딩
)
return padded
result = resize_with_pad(img, 640)
PIL(Pillow) 기본 사용법
이미지 읽기와 쓰기
from PIL import Image
# 이미지 열기 (RGB 형식)
img = Image.open('image.jpg')
print(f"크기: {img.size}") # (너비, 높이) - OpenCV와 순서 다름
print(f"모드: {img.mode}") # RGB
# NumPy 배열로 변환
arr = np.array(img)
print(f"형태: {arr.shape}") # (높이, 너비, 채널)
# 이미지 저장
img.save('output.jpg', quality=95)
img.save('output.png')
변환 작업
# 리사이즈
resized = img.resize((224, 224), Image.LANCZOS)
# 썸네일 (비율 유지, 원본 수정)
thumb = img.copy()
thumb.thumbnail((224, 224), Image.LANCZOS)
# 회전
rotated = img.rotate(45, expand=True)
# 좌우 반전
flipped = img.transpose(Image.FLIP_LEFT_RIGHT)
# 그레이스케일
gray = img.convert('L')
OpenCV vs PIL 비교
| 비교 항목 | OpenCV | PIL (Pillow) |
|---|
| 색상 순서 | BGR | RGB |
| 크기 표현 | (높이, 너비, 채널) | (너비, 높이) |
| 데이터 타입 | NumPy ndarray | PIL.Image |
| 영상 처리 | 강력 (필터, 변환) | 기본 |
| 딥러닝 연동 | torchvision과 사용 | torchvision의 기본 |
| 속도 | 빠름 | 보통 |
| 추천 용도 | 전처리, 영상 처리 | 데이터 로딩, 단순 변환 |
PyTorch 텐서 변환
딥러닝 학습을 위해 이미지를 PyTorch 텐서(Tensor)로 변환하는 과정입니다.
import torch
from torchvision import transforms
# torchvision 변환 파이프라인
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(), # [0, 255] → [0, 1], HWC → CHW
transforms.Normalize(
mean=[0.485, 0.456, 0.406], # ImageNet 평균
std=[0.229, 0.224, 0.225] # ImageNet 표준편차
),
])
# PIL 이미지 → 텐서
img = Image.open('image.jpg')
tensor = transform(img)
print(f"형태: {tensor.shape}") # [3, 224, 224]
print(f"범위: {tensor.min():.2f} ~ {tensor.max():.2f}")
transforms.ToTensor()는 두 가지 변환을 동시에 수행합니다. 값 범위를 [0, 255]에서 [0.0, 1.0]으로 변환하고, 차원 순서를 HWC(높이, 너비, 채널)에서 CHW(채널, 높이, 너비)로 변경합니다.
OpenCV 이미지를 텐서로 변환
def cv2_to_tensor(image, size=(224, 224)):
"""OpenCV BGR 이미지를 정규화된 PyTorch 텐서로 변환합니다."""
# BGR → RGB
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 리사이즈
resized = cv2.resize(rgb, size)
# numpy → tensor, HWC → CHW, uint8 → float32
tensor = torch.from_numpy(resized).permute(2, 0, 1).float() / 255.0
# ImageNet 정규화
mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
tensor = (tensor - mean) / std
return tensor
img = cv2.imread('image.jpg')
tensor = cv2_to_tensor(img)
배치 이미지 처리
import glob
from pathlib import Path
def load_images_from_folder(folder, size=(224, 224)):
"""폴더 내 모든 이미지를 로드하여 텐서 배치로 반환합니다."""
images = []
paths = sorted(glob.glob(f"{folder}/*.jpg") + glob.glob(f"{folder}/*.png"))
for path in paths:
img = Image.open(path).convert('RGB')
tensor = transform(img)
images.append(tensor)
if images:
batch = torch.stack(images) # [N, 3, 224, 224]
return batch, paths
return None, []
batch, paths = load_images_from_folder('images/')
print(f"배치 크기: {batch.shape}") # [N, 3, 224, 224]
대량의 이미지를 처리할 때는 위처럼 직접 로드하기보다 PyTorch의 Dataset과 DataLoader를 사용하는 것이 메모리 효율과 학습 속도 면에서 유리합니다. 이는 분류 튜토리얼에서 다룹니다.
트러블슈팅
cv2.imread()가 None을 반환합니다
파일 경로에 한글이 포함되어 있거나, 파일이 존재하지 않을 때 발생합니다. os.path.exists()로 경로를 확인하고, 한글 경로는 cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)로 읽으세요.
OpenCV(BGR)와 matplotlib(RGB)의 색상 순서 차이 때문입니다. cv2.cvtColor(img, cv2.COLOR_BGR2RGB)로 변환 후 시각화하세요.
PIL의 ImageOps.exif_transpose(img)를 사용하면 EXIF 메타데이터에 따라 이미지를 올바른 방향으로 회전시킵니다.