import numpy as npimport torchimport matplotlib.pyplot as plt # plottingfrom PIL import Image # Python Image Libraryimg = 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 valuedef 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
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, 표준편차=1noise = 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) # 조건부 처리