Skip to main content

이미지 I/O

이미지 입출력(I/O)은 모든 CV 작업의 출발점입니다. 이 문서에서는 OpenCV와 PIL(Pillow)을 사용하여 이미지를 읽고, 변환하고, 저장하는 기본 작업을 실습합니다.

환경 준비

1
라이브러리 설치
2
pip install opencv-python-headless pillow numpy matplotlib
3
라이브러리 임포트
4
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 비교

비교 항목OpenCVPIL (Pillow)
색상 순서BGRRGB
크기 표현(높이, 너비, 채널)(너비, 높이)
데이터 타입NumPy ndarrayPIL.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의 DatasetDataLoader를 사용하는 것이 메모리 효율과 학습 속도 면에서 유리합니다. 이는 분류 튜토리얼에서 다룹니다.

트러블슈팅

파일 경로에 한글이 포함되어 있거나, 파일이 존재하지 않을 때 발생합니다. 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 메타데이터에 따라 이미지를 올바른 방향으로 회전시킵니다.