본문 바로가기
AI/딥러닝 프로젝트

[01 - ImageNet Sketch 데이터] - Torchvision 와 Albumentations Trasnformer 비교

by AI-BT 2024. 9. 13.
728x90
반응형

최근에 ImageNet Sketch 데이터셋을 사용하여 딥러닝 모델을 학습하던 중,
두 가지 이미지 변환 라이브러리인 TorchvisionAlbumentations에서 미묘한 차이점을 발견하게 되었습니다.
표면적으로는 동일한 이미지 전처리 작업을 적용했지만, 내부 구현 방식의 차이로 인해 모델 학습 과정에서 로스(loss)와 성능에 차이가 생겼습니다. 이 글에서는 두 라이브러리 간의 차이점을 명확히 비교하고, 실제로 어떻게 다른지 설명드리겠습니다.


1. 변환 라이브러리의 내부 구현 차이

  • 회전의 중심이 어디로 설정되는지,
  • 이미지 크기 조정 방식,
  • 픽셀 처리 방식 등이 다를 수 있다.

이러한 미세한 차이들이, 특히 모델이 민감하게 반응할 수 있는 스케치 이미지와 같은 데이터에서 큰 영향을 미칠 수 있습니다. 스케치 이미지에서는 선과 디테일이 중요한데, 이러한 디테일 처리 방식에서 차이가 발생해 모델의 학습 성능에 영향을 미칩니다.


2. ToTensor 변환 방식의 차이

두 라이브러리에서 텐서로 변환하는 방식에도 차이가 있습니다.

  • Torchvision의 transforms.ToTensor()는 PIL 이미지를 PyTorch 텐서로 변환하는데,
    이 과정에서 이미지 픽셀 값을 [0, 1] 범위로 정규화합니다.
    즉, PIL.Image에서 텐서로 변환되면서 이미지가 부드럽게 스케일링됩니다.
  • Albumentations의 ToTensorV2()는 OpenCV 방식의 NumPy 배열을 텐서로 변환합니다. 이때, 이미지의 픽셀 값은 처음에 [0, 255] 범위를 유지하다가 변환 후에 [0, 1] 범위로 스케일링됩니다.

결과적으로, 이미지 데이터의 처리 방식이 다르며, 이는 특히 이미지의 픽셀 값 표현 방식에서 차이가 나게 됩니다. 이 차이는 모델 학습 과정에서 손실 값과 예측 정확도에 미세한 영향을 줄 수 있습니다.


3. 정규화 적용 순서의 차이

  • Torchvision에서는 transforms.ToTensor() 이후에 transforms.Normalize()가 적용됩니다.
    즉, 텐서로 변환된 후에 평균과 표준 편차로 정규화하는 방식입니다.
  • Albumentations에서는 A.Normalize()가 ToTensorV2() 이전에 적용됩니다.
    즉, NumPy 배열 상태에서 정규화가 먼저 이루어지고, 그 후 텐서로 변환됩니다.

이 차이로 인해 이미지 데이터가 정규화되는 시점이 달라지고, 이는 모델이 학습하는 데이터의 분포에 영향을 미칠 수 있습니다. 정규화 적용 시점의 미묘한 차이가 손실 값의 변화로 이어질 수 있습니다.

직접 코드를 작성해서 확인 했을때는 차이는 없었다.

# %%
import os
import numpy as np
import torch
from torchvision import transforms
from albumentations import Compose, Normalize
from albumentations.pytorch import ToTensorV2
from PIL import Image
import matplotlib.pyplot as plt

# 이미지 파일을 ./test_image 폴더에서 로드하는 함수
def load_images_from_folder(folder, num_images=10):
    images = []
    filenames = os.listdir(folder)
    for i, filename in enumerate(filenames):
        if i >= num_images:
            break
        img_path = os.path.join(folder, filename)
        img = np.array(Image.open(img_path).convert("RGB"))  # 이미지 파일을 NumPy 배열로 변환
        images.append((img, filename))
    return images

# Torchvision 변환 적용 함수
def apply_torchvision(image: np.ndarray):
    transform = transforms.Compose([
        transforms.ToTensor(),  # Torchvision의 ToTensor
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 정규화
    ])
    pil_image = Image.fromarray(image)  # Torchvision은 PIL 이미지를 다루므로 변환
    tensor = transform(pil_image)
    return tensor

# Albumentations 변환 적용 함수
def apply_albumentations(image: np.ndarray):
    transform = Compose([
        Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),  # 정규화
        ToTensorV2()  # Albumentations의 ToTensorV2
    ])
    augmented = transform(image=image)
    tensor = augmented['image']
    return tensor

# 시각적으로 변환된 이미지를 비교하는 함수
def plot_image_comparison(original_img, torchvision_img, albumentations_img, filename):
    fig, ax = plt.subplots(1, 3, figsize=(15, 5))
    ax[0].imshow(original_img)
    ax[0].set_title(f'Original Image: {filename}')
    
    ax[1].imshow(torchvision_img.permute(1, 2, 0).cpu().numpy())  # Torchvision 텐서 (C, H, W) -> (H, W, C)
    ax[1].set_title('Torchvision Transform')
    
    ax[2].imshow(albumentations_img.permute(1, 2, 0).cpu().numpy())  # Albumentations 텐서 (C, H, W) -> (H, W, C)
    ax[2].set_title('Albumentations Transform')
    
    for a in ax:
        a.axis('off')
    
    plt.show()

# 값 차이를 비교하는 함수
def compare_values(torchvision_img, albumentations_img):
    diff = torch.abs(torchvision_img - albumentations_img)
    mean_diff = torch.mean(diff).item()
    max_diff = torch.max(diff).item()
    
    print(f"Mean Difference: {mean_diff:.6f}")
    print(f"Max Difference: {max_diff:.6f}")

# ./test_image 폴더에서 이미지 로드
image_folder = './test_image'  # 이미지 폴더 경로
num_images = 10
images = load_images_from_folder(image_folder, num_images)

# 10개의 이미지에 대해 변환 및 비교
for idx, (img, filename) in enumerate(images):
    # 원본 이미지
    original_img = img
    
    # Torchvision과 Albumentations 변환 적용
    torchvision_img = apply_torchvision(img)
    albumentations_img = apply_albumentations(img)
    
    # 시각적으로 비교
    print(f"Comparing Image: {filename}")
    plot_image_comparison(original_img, torchvision_img, albumentations_img, filename)
    
    # 값 차이 비교
    print(f"Image {idx+1}:")
    compare_values(torchvision_img, albumentations_img)
    print("\n")

 

결과

 


4. 이미지 처리 방식의 차이

  • TorchvisionPIL.Image 형식을 주로 사용하며, 변환 과정에서 이미지를 PIL 포맷으로 처리합니다.
  • 반면, AlbumentationsNumPy 배열 형식을 다루며, OpenCV를 통해 이미지를 처리합니다.

이로 인해 두 라이브러리는 이미지 데이터 처리 방식에서 차이를 보일 수 있습니다. 예를 들어, PIL과 OpenCV는 색상 공간 처리 방식이나 픽셀 값의 표현 방식에서 차이가 있을 수 있습니다. 특히 스케치 이미지처럼 미세한 픽셀 표현이 중요한 데이터에서는 이러한 차이가 모델 학습에 영향을 미칠 수 있습니다.


5. Resize 처리 방식의 차이

마지막으로, 이미지 크기 조정 방식에서 큰 차이를 발견했습니다. 스케치 이미지처럼 미세한 디테일이 중요한 데이터에서는 리사이즈 처리 방식의 차이가 더욱 두드러집니다.

  • Torchvision의 transforms.Resize()는 PIL 기반으로 작동하며, 기본적으로 bilinear 또는 nearest 인터폴레이션 방식을 사용합니다. 기본 설정은 bilinear이지만, 사용자가 다른 방식으로 명시할 수 있습니다.
  • Albumentations의 A.Resize()는 OpenCV 기반으로 작동하며, 기본적으로 INTER_LINEAR 인터폴레이션 방식을 사용합니다. OpenCV는 이미지 처리에 최적화된 고급 알고리즘을 제공하기 때문에, 특히 선명한 디테일을 유지하는 데 유리할 수 있습니다.

왜 차이가 발생할까?

  1. 인터폴레이션 방식: 두 라이브러리가 사용하는 기본적인 인터폴레이션 방식이 다릅니다. OpenCV의 INTER_LINEAR는 미세한 디테일을 잘 표현할 수 있도록 최적화된 방식이어서, 스케치 이미지의 선명함을 더 잘 보존할 수 있습니다.
  2. 알고리즘 차이: Albumentations는 OpenCV의 고급 이미지 처리 기능을 사용하므로, Torchvision의 PIL 기반 처리보다 더 정밀한 처리가 가능할 수 있습니다. 스케치 이미지처럼 픽셀 기반의 섬세한 이미지에서는 이러한 미세한 차이가 눈에 띌 수 있습니다.
import numpy as np
import torch
import matplotlib.pyplot as plt
from PIL import Image
from torchvision import transforms
import albumentations as A
from albumentations.pytorch import ToTensorV2
import random
import os

# Torchvision 변환 클래스
class TorchvisionTransform:
    def __init__(self, is_train: bool = True):
        common_transforms = [
            transforms.Resize((224, 224)),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
        ]
        
        if is_train:
            self.transform = transforms.Compose(
                [
                    # transforms.RandomHorizontalFlip(p=0.5),
                    # transforms.RandomRotation(15),
                    # transforms.ColorJitter(brightness=0.2, contrast=0.2),
                ] + common_transforms
            )
        else:
            self.transform = transforms.Compose(common_transforms)

    def __call__(self, image: np.ndarray) -> torch.Tensor:
        image = Image.fromarray(image)
        transformed = self.transform(image)
        return transformed

# Albumentations 변환 클래스
class AlbumentationsTransform:
    def __init__(self, is_train: bool = True):
        common_transforms = [
            A.Resize(224, 224),
            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
            ToTensorV2()
        ]
        
        if is_train:
            self.transform = A.Compose(
                [
                    # A.HorizontalFlip(p=0.5),
                    # A.Rotate(limit=15),
                    # A.RandomBrightnessContrast(p=0.2),
                ] + common_transforms
            )
        else:
            self.transform = A.Compose(common_transforms)

    def __call__(self, image: np.ndarray) -> torch.Tensor:
        transformed = self.transform(image=image)
        return transformed['image']
    
# 시각적으로 변환된 이미지를 비교하는 함수
def plot_image_comparison(original_img, torchvision_img, albumentations_img, idx):
    fig, ax = plt.subplots(1, 3, figsize=(15, 5))
    ax[0].imshow(original_img)
    ax[0].set_title(f'Original Image {idx}')
    
    ax[1].imshow(torchvision_img.permute(1, 2, 0))  # torch tensor는 (C, H, W)이므로 (H, W, C)로 변환
    ax[1].set_title('Torchvision Transform')
    
    ax[2].imshow(albumentations_img.permute(1, 2, 0))
    ax[2].set_title('Albumentations Transform')
    
    for a in ax:
        a.axis('off')
    
    plt.show()
    
# 이미지 파일을 ./test_image 폴더에서 로드하는 함수
def load_images_from_folder(folder, num_images=10):
    images = []
    filenames = os.listdir(folder)
    for i, filename in enumerate(filenames):
        if i >= num_images:
            break
        img_path = os.path.join(folder, filename)
        img = np.array(Image.open(img_path).convert("RGB"))  # 이미지 파일을 NumPy 배열로 변환
        images.append((img, filename))
    return images

# ./test_image 폴더에서 이미지 로드
image_folder = './test_image'
num_images = 10
images = load_images_from_folder(image_folder, num_images)

# Torchvision 및 Albumentations 변환 생성
torchvision_transform = TorchvisionTransform(is_train=True)
albumentations_transform = AlbumentationsTransform(is_train=True)

# 10개의 이미지에 대해 변환 및 비교
for idx, (img, filename) in enumerate(images):
    # 원본 이미지
    original_img = img
    
    # Torchvision과 Albumentations 변환 적용
    torchvision_img = torchvision_transform(img)
    albumentations_img = albumentations_transform(img)
    
    # 시각적으로 비교
    print(f"Comparing Image: {filename}")
    plot_image_comparison(original_img, torchvision_img, albumentations_img, idx)

 

 

결과

 

 


결론

스케치 이미지와 같이 미세한 디테일이 중요한 데이터에서는, 이미지 전처리 라이브러리의 차이가 모델 성능에 영향을 미칠 수 있습니다. 특히 AlbumentationsResize내부 변환 처리에서 더 세밀한 디테일을 유지하는 데 유리합니다.

 

Albumentations가 OpenCV 기반의 고급 이미지 처리 기능을 사용하기 때문에, 스케치 이미지의 선명한 선과 디테일을 잘 보존할 수 있습니다. 반면, Torchvision은 PIL 기반의 처리 방식으로 인해 일부 미세한 정보가 손실될 가능성이 있습니다.

 

따라서, 스케치 이미지처럼 미세한 차이가 중요한 상황에서는 Albumentations를 사용하는 것이 더 나은 선택일 수 있다는 결론에 도달했습니다.

728x90
반응형

댓글