0. 라이브러리 & 이미지 불러오기

import numpy as np
import torch
import matplotlib.pyplot as plt # plotting
from PIL import Image # Python Image Library
 
img = np.array(Image.open('../../data/IPA.jpg'))

1. 상하 반전 (Vertical Flip)

원리

  • 행의 순서를 역순으로 재배치
  • 첫 번째 행 ↔ 마지막 행, 두 번째 행 ↔ 마지막-1 행…

구현

def vertical_flip(img):
    # img.shape = (높이, 너비, 채널) 또는 (높이, 너비)
    h, w = img.shape[:2]  # shape의 앞 2개 요소만 추출 (높이, 너비)
    
    # len(img.shape) == 3: 3차원 배열 = 컬러 이미지 (h, w, c)
    if len(img.shape) == 3:  
        # np.zeros_like(img): img와 동일한 shape과 dtype을 가진 0으로 채워진 배열 생성
        flipped = np.zeros_like(img)
        
        # 모든 행(row)을 순회
        for i in range(h):
            # 반전된 위치에 복사: 0번째 행 → (h-1)번째 행, 1번째 행 → (h-2)번째 행...
            # img[h - 1 - i]는 원본의 역순 행
            # flipped[i]는 새 배열의 순방향 행
            flipped[i] = img[h - 1 - i]
    
    # len(img.shape) == 2: 2차원 배열 = 흑백 이미지 (h, w)
    else:  
        flipped = np.zeros_like(img)
        for i in range(h):
            flipped[i] = img[h - 1 - i]
    
    # 반전된 이미지 반환
    return flipped
 
# 사용 예시
v_flipped = vertical_flip(img)  # 상하 반전 함수 호출
plt.imshow(v_flipped)  # 이미지 시각화
plt.axis('off')  # 축 눈금 제거
plt.show()  # 화면에 표시

2. 좌우 반전 (Horizontal Flip)

원리

  • 열의 순서를 역순으로 재배치
  • 첫 번째 열 ↔ 마지막 열, 두 번째 열 ↔ 마지막-1 열…

구현

def horizontal_flip(img):
    # img.shape[:2]로 높이(h)와 너비(w) 추출
    h, w = img.shape[:2]
    
    # 3차원 배열 확인 (컬러 이미지)
    if len(img.shape) == 3:
        # 원본과 같은 크기의 빈 배열 생성
        flipped = np.zeros_like(img)
        
        # 모든 행을 순회
        for i in range(h):
            # 모든 열을 순회
            for j in range(w):
                # (i, j) 위치에 (i, w-1-j) 위치의 픽셀 값 복사
                # j=0 → w-1, j=1 → w-2, ... (좌우 반전)
                # img[i, w - 1 - j]: 원본의 역순 열
                # flipped[i, j]: 새 배열의 순방향 열
                flipped[i, j] = img[i, w - 1 - j]
    
    # 2차원 배열 (흑백 이미지)
    else:
        flipped = np.zeros_like(img)
        for i in range(h):
            for j in range(w):
                # 흑백 이미지는 채널이 없으므로 2D 인덱싱
                flipped[i, j] = img[i, w - 1 - j]
    
    return flipped
 
# 사용 예시
h_flipped = horizontal_flip(img)  # 좌우 반전 함수 호출
plt.imshow(h_flipped)  # 이미지 표시
plt.axis('off')  # 축 제거
plt.show()  # 화면에 출력

3. 색상 반전 (Channel Flip - RGB ↔ BGR)

원리

  • RGB 순서를 BGR로 변경
  • R(0번) ↔ B(2번), G(1번)는 그대로

구현

def channel_flip(img):
    # img.shape = (h, w, c) 형태에서 높이, 너비, 채널 수 추출
    h, w, c = img.shape
    
    # 원본과 동일한 크기의 빈 배열 생성
    flipped = np.zeros_like(img)
    
    # 모든 행 순회
    for i in range(h):
        # 모든 열 순회
        for j in range(w):
            # 채널 순서 변경: RGB → BGR
            # flipped[i, j, 0] (새 배열의 R 채널) ← img[i, j, 2] (원본의 B 채널)
            flipped[i, j, 0] = img[i, j, 2]  
            
            # flipped[i, j, 1] (새 배열의 G 채널) ← img[i, j, 1] (원본의 G 채널)
            flipped[i, j, 1] = img[i, j, 1]  
            
            # flipped[i, j, 2] (새 배열의 B 채널) ← img[i, j, 0] (원본의 R 채널)
            flipped[i, j, 2] = img[i, j, 0]  
    
    return flipped
 
# 사용 예시
c_flipped = channel_flip(img)  # 색상 채널 반전
plt.imshow(c_flipped)  # 변환된 이미지 표시
plt.axis('off')  # 축 숨기기
plt.show()  # 화면 출력

4. 밝기 조절 (Brightness Adjustment)

원리

  • 모든 픽셀 값에 스칼라 곱셈
  • 범위 제한: 0 ≤ pixel ≤ 255

clamp/clip 직접 구현

def manual_clip(value, min_val, max_val):
    """
    값을 [min_val, max_val] 범위로 제한하는 함수
    
    Args:
        value: 제한할 값
        min_val: 최소값
        max_val: 최대값
    
    Returns:
        제한된 값
    """
    # value가 최소값보다 작으면 최소값 반환
    if value < min_val:
        return min_val
    # value가 최대값보다 크면 최대값 반환
    elif value > max_val:
        return max_val
    # 범위 내에 있으면 원래 값 반환
    else:
        return value
 
def adjust_brightness(img, factor):
    """
    이미지 밝기를 조절하는 함수
    
    Args:
        img: 입력 이미지 (NumPy 배열)
        factor: 밝기 배수 (factor > 1.0: 밝게, factor < 1.0: 어둡게)
    
    Returns:
        밝기 조절된 이미지
    """
    # 이미지의 높이, 너비, 채널 수 추출
    h, w, c = img.shape
    
    # 결과를 저장할 빈 배열 생성 (원본과 동일한 크기 및 타입)
    bright_img = np.zeros_like(img)
    
    # 모든 행을 순회
    for i in range(h):
        # 모든 열을 순회
        for j in range(w):
            # 모든 채널을 순회 (R, G, B)
            for k in range(c):
                # 원본 픽셀 값에 factor를 곱해서 밝기 조절
                new_value = img[i, j, k] * factor
                
                # 계산된 값을 [0, 255] 범위로 제한
                # 255 초과 방지, 0 미만 방지
                bright_img[i, j, k] = manual_clip(new_value, 0, 255)
    
    # float 타입으로 계산된 결과를 uint8 정수로 변환 (0~255 범위의 정수)
    return bright_img.astype(np.uint8)
 
# 사용 예시
bright_img = adjust_brightness(img, 1.5)  # 밝기 1.5배 증가 (밝게)
dark_img = adjust_brightness(img, 0.5)    # 밝기 0.5배 감소 (어둡게)
plt.imshow(bright_img)  # 밝아진 이미지 표시
plt.axis('off')  # 축 제거
plt.show()  # 화면 출력

5. 대비 조절 (Contrast Adjustment)

원리

  • 평균값을 중심으로 픽셀 차이를 확대/축소
  • 공식: new_pixel = mean + factor × (pixel - mean)

구현

def adjust_contrast(img, factor):
    """
    이미지 대비를 조절하는 함수
    
    Args:
        img: 입력 이미지
        factor: 대비 배수 (factor > 1.0: 고대비, factor < 1.0: 저대비)
    
    Returns:
        대비 조절된 이미지
    """
    # 이미지의 높이, 너비, 채널 수 추출
    h, w, c = img.shape
    
    # ===== 전체 픽셀의 평균값 계산 (직접 구현) =====
    total_sum = 0  # 모든 픽셀 값의 합
    total_count = h * w * c  # 전체 픽셀 개수 = 높이 × 너비 × 채널
    
    # 모든 픽셀을 순회하며 합계 계산
    for i in range(h):
        for j in range(w):
            for k in range(c):
                # 각 픽셀 값을 누적
                total_sum += img[i, j, k]
    
    # 평균 = 전체 합 / 전체 개수
    mean_val = total_sum / total_count
    
    # ===== 대비 조절 =====
    # 결과를 저장할 배열 생성
    contrast_img = np.zeros_like(img)
    
    # 모든 픽셀을 순회
    for i in range(h):
        for j in range(w):
            for k in range(c):
                # 현재 픽셀과 평균값의 차이 계산
                # 평균보다 밝으면 양수, 어두우면 음수
                diff = img[i, j, k] - mean_val
                
                # 새로운 픽셀 값 = 평균 + (차이 × factor)
                # factor > 1: 차이 확대 (고대비)
                # factor < 1: 차이 축소 (저대비)
                new_value = mean_val + factor * diff
                
                # [0, 255] 범위로 제한
                contrast_img[i, j, k] = manual_clip(new_value, 0, 255)
    
    # uint8 타입으로 변환하여 반환
    return contrast_img.astype(np.uint8)
 
# 사용 예시
high_contrast = adjust_contrast(img, 2.0)  # 대비 2배 증가 (고대비)
low_contrast = adjust_contrast(img, 0.5)   # 대비 0.5배 감소 (저대비)
plt.imshow(high_contrast)  # 고대비 이미지 표시
plt.axis('off')  # 축 숨김
plt.show()  # 화면 출력

6. 노이즈 추가 (Add Noise)

원리

  • 각 픽셀에 랜덤 값 추가
  • 가우시안 노이즈: 평균과 표준편차로 제어
  • 수학적 배경: 정규분포 N(mean, std²)에서 랜덤 값 샘플링
    • mean > 0: 전체적으로 밝아지는 노이즈
    • mean < 0: 전체적으로 어두워지는 노이즈
    • std ↑: 노이즈 강도 증가 (픽셀 간 편차 커짐)

구현

def add_noise(img, mean=0, std=25):
    """
    가우시안 노이즈를 추가하는 함수
    
    Args:
        img: 입력 이미지
        mean: 노이즈 평균 (양수면 밝아짐, 음수면 어두워짐)
        std: 노이즈 표준편차 (클수록 노이즈 강함)
    
    Returns:
        노이즈가 추가된 이미지
    """
    # 이미지 크기 추출
    h, w, c = img.shape
    
    # 결과 저장 배열
    noisy_img = np.zeros_like(img)
    
    # 모든 픽셀 순회
    for i in range(h):
        for j in range(w):
            for k in range(c):
                # np.random.normal(평균, 표준편차): 정규분포에서 랜덤 값 생성
                # 예: mean=0, std=25 → 대부분 -75~75 범위의 값 생성 (3σ 법칙)
                noise_value = np.random.normal(mean, std)
                
                # 원본 픽셀 + 노이즈
                new_value = img[i, j, k] + noise_value
                
                # [0, 255] 범위로 제한
                noisy_img[i, j, k] = manual_clip(new_value, 0, 255)
    
    return noisy_img.astype(np.uint8)
 
# 사용 예시
noisy_img = add_noise(img, mean=0, std=50)  # 평균 0, 표준편차 50인 노이즈
plt.imshow(noisy_img)
plt.axis('off')
plt.show()

7. 90도 회전 (Rotate 90°)

원리

  • 시계 반대 방향 90도 회전
  • 좌표 변환 공식: (i, j) → (j, h-1-i)
    • 원본의 (0, 0) → 회전 후 (0, h-1) (왼쪽 아래)
    • 원본의 (0, w-1) → 회전 후 (w-1, h-1) (오른쪽 아래)
    • 원본의 (h-1, 0) → 회전 후 (0, 0) (왼쪽 위)
  • 배열 크기 변화: (h, w) → (w, h) (가로↔세로 바뀜)

구현

def rotate_90_ccw(img):
    """
    시계 반대 방향 90도 회전
    
    Args:
        img: 입력 이미지
    
    Returns:
        90도 회전된 이미지
    """
    # 원본 이미지 크기
    h, w = img.shape[:2]
    
    # 3차원 배열 (컬러 이미지)인지 확인
    if len(img.shape) == 3:
        c = img.shape[2]  # 채널 수 추출
        
        # 주의: 회전 후 크기는 (w, h, c) - 가로와 세로가 바뀜
        rotated = np.zeros((w, h, c), dtype=img.dtype)
        
        # 원본의 모든 행 순회
        for i in range(h):
            # 원본의 모든 열 순회
            for j in range(w):
                # 좌표 변환: (i, j) → (j, h-1-i)
                # 원본의 i번째 행 → 회전 후 (h-1-i)번째 열
                # 원본의 j번째 열 → 회전 후 j번째 행
                rotated[j, h - 1 - i] = img[i, j]
    
    # 2차원 배열 (흑백 이미지)
    else:
        # 회전 후 크기: (w, h)
        rotated = np.zeros((w, h), dtype=img.dtype)
        for i in range(h):
            for j in range(w):
                rotated[j, h - 1 - i] = img[i, j]
    
    return rotated
 
# 사용 예시
rotated_90 = rotate_90_ccw(img)  # 반시계방향 90도 회전
plt.imshow(rotated_90)
plt.axis('off')
plt.show()

8. 중앙 크롭 (Center Crop)

원리

  • 이미지 중앙에서 지정된 크기만큼 잘라내기
  • 시작 좌표 계산: (전체 크기 - 크롭 크기) / 2
    • 예: 이미지 크기 200×200, 크롭 크기 100×100
    • 시작 좌표: (50, 50) - 중앙 정렬
  • 데이터 증강 목적: 중요한 객체가 중앙에 있다고 가정

구현

def center_crop(img, crop_h, crop_w):
    """
    중앙에서 crop_h × crop_w 크기로 자르기
    
    Args:
        img: 입력 이미지
        crop_h: 자를 높이
        crop_w: 자를 너비
    
    Returns:
        중앙에서 잘린 이미지
    """
    # 원본 이미지 크기
    h, w = img.shape[:2]
    
    # ===== 시작 좌표 계산 (직접 구현) =====
    # 중앙 정렬을 위해 남는 공간을 양쪽으로 균등 분배
    # //: 정수 나눗셈 (소수점 버림)
    start_y = (h - crop_h) // 2  # 세로 시작 위치
    start_x = (w - crop_w) // 2  # 가로 시작 위치
    
    # 예: h=200, crop_h=100 → start_y = (200-100)//2 = 50
    
    # 3차원 배열 확인
    if len(img.shape) == 3:
        c = img.shape[2]  # 채널 수
        
        # 크롭된 결과를 저장할 배열 생성
        cropped = np.zeros((crop_h, crop_w, c), dtype=img.dtype)
        
        # 크롭 영역만큼 순회
        for i in range(crop_h):
            for j in range(crop_w):
                # 원본에서 (start_y+i, start_x+j) 위치의 픽셀을
                # 크롭 이미지의 (i, j) 위치에 복사
                cropped[i, j] = img[start_y + i, start_x + j]
    
    # 2차원 배열 (흑백)
    else:
        cropped = np.zeros((crop_h, crop_w), dtype=img.dtype)
        for i in range(crop_h):
            for j in range(crop_w):
                cropped[i, j] = img[start_y + i, start_x + j]
    
    return cropped
 
# 사용 예시
cropped_img = center_crop(img, 200, 200)  # 중앙에서 200×200 크기로 자르기
plt.imshow(cropped_img)
plt.axis('off')
plt.show()

9. PyTorch 버전 - 직접 구현

PyTorch Tensor 변환 원리

# NumPy → PyTorch Tensor 변환 과정
tensor_img = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0
 
# 단계별 분해:
# 1. torch.from_numpy(img)
#    - NumPy 배열을 PyTorch Tensor로 변환
#    - shape: (H, W, C) - 높이, 너비, 채널 순서 유지
#    - dtype: uint8 (0~255 정수)
 
# 2. .permute(2, 0, 1)
#    - 차원 순서 변경: (H, W, C) → (C, H, W)
#    - PyTorch는 채널을 첫 번째 차원으로 사용 (CHW 포맷)
#    - 예: (480, 640, 3) → (3, 480, 640)
 
# 3. .float()
#    - dtype을 uint8 → float32로 변환
#    - 이유: 나눗셈 연산을 위해 부동소수점 필요
 
# 4. / 255.0
#    - 픽셀 값을 [0, 255] → [0.0, 1.0] 범위로 정규화
#    - 딥러닝 모델은 보통 [0, 1] 범위의 입력 선호
#    - 예: 픽셀 값 128 → 128/255 = 0.502

밝기 조절 원리

# 밝기 조절: tensor_img * 1.5
# 
# 동작 원리:
# - 모든 픽셀 값에 1.5를 곱함 (브로드캐스팅)
# - 예: 0.5 → 0.75 (50% 밝기 → 75% 밝기)
#
# 주의사항:
# - 1.0 초과 값 발생 가능 → clamp로 [0, 1] 제한 필요
# - 예: 0.8 * 1.5 = 1.2 → clamp → 1.0
#
# factor에 따른 효과:
# - factor > 1.0: 밝아짐 (픽셀 값 증가)
# - factor = 1.0: 변화 없음
# - factor < 1.0: 어두워짐 (픽셀 값 감소)

Clamp 직접 구현

def manual_clamp_tensor(tensor, min_val, max_val):
    """
    텐서 값을 [min_val, max_val] 범위로 제한
    
    Args:
        tensor: 입력 텐서
        min_val: 최소값
        max_val: 최대값
    
    Returns:
        제한된 텐서
    """
    # tensor.clone(): 원본을 복사하여 새 텐서 생성 (원본 보호)
    clamped = tensor.clone()
    
    # 불린 인덱싱: min_val보다 작은 모든 값을 min_val로 설정
    # clamped < min_val은 True/False 마스크 생성
    clamped[clamped < min_val] = min_val
    
    # max_val보다 큰 모든 값을 max_val로 설정
    clamped[clamped > max_val] = max_val
    
    return clamped
 
# 밝기 조절 예시
tensor_img = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0  # [0, 1] 범위
bright_tensor = manual_clamp_tensor(tensor_img * 1.5, 0, 1)  # 밝기 1.5배, [0, 1]로 제한

Flip 직접 구현

def manual_flip_tensor(tensor, dim):
    """
    특정 차원을 따라 flip
    
    Args:
        tensor: 입력 텐서 (C, H, W)
        dim: 뒤집을 차원
            - 0: 채널 뒤집기 (RGB ↔ BGR)
            - 1: 상하 뒤집기 (높이)
            - 2: 좌우 뒤집기 (너비)
    
    Returns:
        뒤집힌 텐서
    """
    # torch.arange(start, end, step): 순차적 인덱스 생성
    # tensor.size(dim): dim 차원의 크기
    # 역순 인덱스 생성: [size-1, size-2, ..., 1, 0]
    indices = torch.arange(tensor.size(dim) - 1, -1, -1)
    # 예: dim=2, size=640 → [639, 638, ..., 1, 0]
    
    # index_select(dim, indices): dim 차원에서 indices 순서대로 선택
    # 역순 인덱스로 선택 = flip 효과
    return tensor.index_select(dim, indices)
 
# 사용 예시
tensor_img = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0
 
# 좌우 반전 (dim=2, 너비 차원)
# (C, H, W)에서 W 차원을 역순으로
h_flipped_tensor = manual_flip_tensor(tensor_img, dim=2)
 
# 상하 반전 (dim=1, 높이 차원)
# (C, H, W)에서 H 차원을 역순으로
v_flipped_tensor = manual_flip_tensor(tensor_img, dim=1)
 
# 채널 반전 (dim=0, 채널 차원)
# (C, H, W)에서 C 차원을 역순으로 (RGB → BGR)
c_flipped_tensor = manual_flip_tensor(tensor_img, dim=0)

노이즈 추가 원리

# torch.randn_like(tensor): tensor와 같은 크기의 랜덤 텐서 생성
# - 표준 정규분포 N(0, 1)에서 샘플링
# - 평균=0, 표준편차=1
 
noise = torch.randn_like(tensor_img) * 0.5  # 표준편차를 0.5로 조정
 
# 동작 원리:
# 1. randn_like: 평균 0, 표준편차 1인 노이즈 생성
# 2. * 0.5: 표준편차를 0.5로 축소 (노이즈 강도 조절)
# 3. tensor_img + noise: 원본에 노이즈 추가
 
modified_tensor_img = torch.clamp(tensor_img + noise, 0, 1)  # [0, 1] 범위로 제한

핵심 개념 정리

1. Clipping/Clamping

def manual_clip(value, min_val, max_val):
    if value < min_val:
        return min_val
    elif value > max_val:
        return max_val
    return value
  • 조건문으로 범위 제한
  • 이미지는 보통 [0, 255] 또는 [0, 1] 범위

2. Flip 연산

  • 반전 인덱스 공식: new_index = size - 1 - old_index
  • 상하: 행(height) 반전
  • 좌우: 열(width) 반전
  • 색상: 채널(channel) 반전

3. 루프 최적화 팁

# 비효율적 (중첩 루프 3개)
for i in range(h):
    for j in range(w):
        for k in range(c):
            process(img[i, j, k])
 
# 효율적 (벡터화)
result = img * factor  # NumPy 브로드캐스팅
result = np.where(result > 255, 255, result)  # 조건부 처리