정의: 같은 클래스의 모든 객체를 동일한 번호로 분류
특징
예시
이미지에 사람이 3명 있는 경우:
- Semantic: 모두 1번 (person)
- 개별 구분 불가
정의: 같은 클래스라도 개별 객체를 다르게 구분
특징
예시
이미지에 사람이 3명 있는 경우:
- Instance: 사람1=ID1, 사람2=ID2, 사람3=ID3
- 각 사람을 개별적으로 추적 가능
Fully Connected Layer 대신 Convolution 사용
문제점: 픽셀마다 예상값을 만들어야 하는데 너무 큰 사이즈를 처리할 수 없었음
해결책
1. MaxPooling으로 이미지 크기를 계속 줄여감
2. 픽셀에 해당하는 값을 축소
3. 21개의 픽셀로 축소하여 예측
4. 다시 원본 크기로 복원
2015년 당시: MaxPooling만으로도 충분한 효과
1단계: 다운샘플링 (Encoder)
입력 이미지 (예: 128×128)
↓ Conv + MaxPool
64×64
↓ Conv + MaxPool
32×32
↓ Conv + MaxPool
16×16
2단계: 업샘플링 (Decoder)
16×16
↓ ConvTranspose2d (stride=2)
32×32
↓ ConvTranspose2d (stride=2)
64×64
↓ ConvTranspose2d (stride=2)
128×128
정보 손실
세밀한 경계 복원이 어려움
목표: 이미지의 특징 추출 및 크기 축소
과정
# Conv + ReLU + MaxPool 반복
x = torch.relu(self.conv1(x)) # 128×128
x = self.pool(x) # 64×64
x = torch.relu(self.conv2(x)) # 64×64
x = self.pool(x) # 32×32
특징
목표: 압축된 정보를 다시 복원
과정
# ConvTranspose2d로 크기 복원
x = self.up1(x) # 32×32 → 64×64
x = self.up2(x) # 64×64 → 128×128
특징
역할: 이미지 크기를 늘리는 업샘플링 연산
일반 Convolution과의 차이
| 구분 | Convolution | ConvTranspose2d |
|---|---|---|
| 방향 | 크기 감소 | 크기 증가 |
| stride=2 | 1/2로 축소 | 2배로 확대 |
| 용도 | 다운샘플링 | 업샘플링 |
예시: 4×4를 8×8로 확대
nn.ConvTranspose2d(
in_channels=64, # 입력 채널
out_channels=32, # 출력 채널
kernel_size=2, # 커널 크기
stride=2 # 2배로 확대
)
stride=2의 의미
시각적 이해
입력 (4×4):
[1 2 3 4]
[5 6 7 8]
[...]
↓ stride=2 (픽셀 사이에 0 삽입)
[1 0 2 0 3 0 4 0]
[0 0 0 0 0 0 0 0]
[5 0 6 0 7 0 8 0]
[...]
↓ Convolution 적용
출력 (8×8)
class FCN(nn.Module):
def __init__(self):
super().__init__()
# Encoder
self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
self.conv2 = nn.Conv2d(64, 128, 3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
# Decoder
self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2)
self.up2 = nn.ConvTranspose2d(64, 3, 2, stride=2)
크기 변화
입력: 128×128×3
↓ conv1 + pool
64×64×64
↓ conv2 + pool
32×32×128
↓ up1 (stride=2)
64×64×64
↓ up2 (stride=2)
128×128×3
마지막 출력 채널 = 클래스 수
출처: 옥스퍼드 대학 Visual Geometry Group (VGG)
특징
1. 품종 분류 (Classification)
2. 객체 위치 파악 (Object Localization)
3. 픽셀 수준 분할 (Pixel-level Segmentation) - Trimap
정의: 이미지를 세 가지 구분된 영역으로 정의하는 맵
어원
3가지 클래스
| 클래스 | 값 | 의미 | 설명 |
|---|---|---|---|
| Foreground | 1 | 전경 | 고양이 또는 강아지 자체에 속하는 픽셀 |
| Background | 2 | 배경 | 이미지에서 동물 외의 부분에 속하는 픽셀 |
| Boundary | 3 | 경계/불확실 영역 | 전경과 배경 사이의 경계 영역 |
Trimap의 중요성
경계 영역의 역할
데이터 값이 명확하지 않은 경우
from torchvision.datasets import OxfordIIITPet
import matplotlib.pyplot as plt
import numpy as np
# 다운로드
print("Oxford Pet 데이터셋 다운로드 중...")
pet_dataset = OxfordIIITPet(
root='./data',
split='trainval',
target_types='segmentation',
download=True
)
print(f"다운로드 완료! 총 {len(pet_dataset)}장")
마스크 값 확인
img, mask = pet_dataset[0]
# numpy 배열로 변환
mask_array = np.array(mask)
print(f"마스크 shape: {mask_array.shape}")
print(f"마스크 dtype: {mask_array.dtype}")
print(f"\n마스크 unique 값들: {np.unique(mask_array)}")
# 각 값의 개수 확인
for val in np.unique(mask_array):
count = np.sum(mask_array == val)
percent = count / mask_array.size * 100
print(f" 값 {val}: {count}개 ({percent:.1f}%)")
출력 예시
마스크 shape: (500, 394)
마스크 dtype: uint8
마스크 unique 값들: [1 2 3]
값 1: 45000개 (22.8%) # 전경
값 2: 150000개 (76.1%) # 배경
값 3: 2000개 (1.1%) # 경계
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
# 원본 이미지
axes[0].imshow(img)
axes[0].set_title('Original Image')
axes[0].axis('off')
# 클래스 1: 전경 (동물)
axes[1].imshow(mask_array == 1, cmap='Reds')
axes[1].set_title('Class 1 (Foreground/animal)')
axes[1].axis('off')
# 클래스 2: 배경
axes[2].imshow(mask_array == 2, cmap='Blues')
axes[2].set_title('Class 2 (Background)')
axes[2].axis('off')
# 클래스 3: 경계선
axes[3].imshow(mask_array == 3, cmap='Greens')
axes[3].set_title('Class 3 (Boundary)')
axes[3].axis('off')
plt.tight_layout()
plt.show()
정의: 이미지 크기를 변경할 때 새로운 픽셀 값을 추정하는 방법
필요성
일반 이미지 리사이즈
# 일반 이미지는 평균, 선형 보간 사용 가능
img_resized = T.Resize((128, 128))(img)
마스크 리사이즈의 문제점
원본 마스크:
[1 1 2 2]
[1 1 2 2]
↓ 평균 보간법 사용 시
[1.0 1.5 2.0]
[1.0 1.5 2.0]
↑
1.5는 존재하지 않는 클래스!
문제
NEAREST (최근접 이웃 보간)
원리: 가장 가까운 픽셀 값을 그대로 사용
원본:
[1 1 2 2]
[1 1 2 2]
↓ NEAREST 사용
[1 1 2 2]
[1 1 2 2]
특징
BILINEAR (선형 보간)
원리: 주변 4개 픽셀의 가중 평균
1과 5 사이를 보간:
가운데 값 = (1 + 5) / 2 = 3
특징
BICUBIC (3차 보간)
원리: 주변 16개 픽셀의 3차 함수로 추정
특징
올바른 방법
# 마스크는 반드시 NEAREST 사용
mask = T.Resize(
(128, 128),
interpolation=T.InterpolationMode.NEAREST
)(mask)
잘못된 방법
# 평균값이 나와서 이상한 값 발생
mask = T.Resize((128, 128))(mask) # 기본값: BILINEAR
| 보간법 | 속도 | 품질 | 마스크 사용 | 일반 이미지 사용 |
|---|---|---|---|---|
| NEAREST | 빠름 | 낮음 (계단 현상) | O 필수 | X |
| BILINEAR | 중간 | 중간 | X | O |
| BICUBIC | 느림 | 높음 | X | O |
Aliasing (계단 현상)
Anti-aliasing
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torchvision.datasets import OxfordIIITPet
import torchvision.transforms as T
import numpy as np
class PetDataset(Dataset):
def __init__(self):
self.pet = OxfordIIITPet(
root='./data',
split='trainval',
target_types='segmentation',
download=True
)
def __len__(self):
return len(self.pet)
def __getitem__(self, idx):
img, mask = self.pet[idx]
# 이미지 전처리
img = T.Resize((128, 128))(img)
img = T.ToTensor()(img)
# 마스크 전처리 (NEAREST 필수!)
mask = T.Resize(
(128, 128),
interpolation=T.InterpolationMode.NEAREST
)(mask)
mask = torch.tensor(np.array(mask), dtype=torch.long)
mask = mask - 1 # 1,2,3 -> 0,1,2
return img, mask
마스크 전처리 핵심
class FCN(nn.Module):
def __init__(self):
super().__init__()
# Encoder (다운샘플링)
self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
self.conv2 = nn.Conv2d(64, 128, 3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
# Decoder (업샘플링)
self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2)
self.up2 = nn.ConvTranspose2d(64, 3, 2, stride=2)
def forward(self, x):
# Encoder
x = torch.relu(self.conv1(x)) # 128×128×64
x = self.pool(x) # 64×64×64
x = torch.relu(self.conv2(x)) # 64×64×128
x = self.pool(x) # 32×32×128
# Decoder
x = self.up1(x) # 64×64×64
x = self.up2(x) # 128×128×3
return x
구조 요약
입력: [배치, 3, 128, 128]
↓ conv1 + ReLU
[배치, 64, 128, 128]
↓ MaxPool
[배치, 64, 64, 64]
↓ conv2 + ReLU
[배치, 128, 64, 64]
↓ MaxPool
[배치, 128, 32, 32]
↓ up1 (ConvTranspose2d)
[배치, 64, 64, 64]
↓ up2 (ConvTranspose2d)
출력: [배치, 3, 128, 128]
# 데이터 & 모델 준비
dataset = PetDataset()
loader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=2)
model = FCN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
print(f"데이터: {len(dataset)}장")
print(f"모델 파라미터: {sum(p.numel() for p in model.parameters()):,}개")
# 학습
epochs = 3
for epoch in range(epochs):
total_loss = 0
for i, (imgs, masks) in enumerate(loader):
optimizer.zero_grad()
# Forward
outputs = model(imgs)
loss = criterion(outputs, masks)
# Backward
loss.backward()
optimizer.step()
total_loss += loss.item()
if i % 50 == 0:
print(f"Epoch {epoch+1}, Batch {i}, Loss: {loss.item():.4f}")
print(f"Epoch {epoch+1} 완료, Avg Loss: {total_loss/len(loader):.4f}\n")
print("학습 완료!")
CrossEntropyLoss 사용
criterion = nn.CrossEntropyLoss()
loss = criterion(outputs, masks)
입력 형태
동작 과정
픽셀 (i, j)에서:
- 출력: [0.1, 0.7, 0.2] (3개 클래스 확률)
- 정답: 1 (두 번째 클래스)
- 손실: -log(0.7)
# 평가 모드
model.eval()
with torch.no_grad():
img, mask = dataset[0]
pred = model(img.unsqueeze(0))
pred = pred.argmax(1).squeeze()
# 시각화
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].imshow(img.permute(1, 2, 0))
axes[0].set_title('Input Image')
axes[0].axis('off')
axes[1].imshow(mask, cmap='tab20')
axes[1].set_title('Ground Truth')
axes[1].axis('off')
axes[2].imshow(pred, cmap='tab20')
axes[2].set_title('Prediction')
axes[2].axis('off')
plt.tight_layout()
plt.show()
참고 자료: https://www.tensorflow.org/tutorials/generative/pix2pix?hl=ko
개념: 픽셀을 픽셀로 바꾸는 생성 모델
세그멘테이션과의 관계
세그멘테이션 → 이미지 생성
Input: Segmentation Mask
↓ Pix2Pix
Output: 실제 이미지
이미지 → 세그멘테이션
Input: 원본 이미지
↓ Pix2Pix
Output: Segmentation Mask
특징
원본 크기
마스크 shape: (500, 394)
픽셀 개수: 197,000개
unique 값: [1, 2, 3]
Resize 후
마스크 shape: (128, 128)
픽셀 개수: 16,384개
unique 값: [1, 2, 3]
크기 변화
Resize 전 (10×10 영역)
[1 1 1 2 2 2 3 3 2 2]
[1 1 1 2 2 2 3 3 2 2]
[1 1 1 1 2 2 2 3 2 2]
...
Resize 후 (10×10 영역)
[1 1 2 2 2 3 2 2 2 2]
[1 1 1 2 2 2 2 2 2 2]
[1 1 1 1 2 2 2 2 2 2]
...
관찰
Resize 전
Class 1 (전경): 45,000 pixels
Class 2 (배경): 150,000 pixels
Class 3 (경계): 2,000 pixels
Resize 후
Class 1 (전경): 3,500 pixels
Class 2 (배경): 12,500 pixels
Class 3 (경계): 384 pixels
비율은 거의 유지됨
일반 CNN은 마지막에 Fully Connected Layer를 사용하여 하나의 클래스를 예측하지만, FCN은 마지막까지 Convolution Layer를 사용하여 픽셀마다 클래스를 예측합니다. 이를 통해 이미지의 각 위치에서 세그멘테이션을 수행할 수 있습니다.
ConvTranspose2d는 stride=2를 사용할 때 입력 픽셀 사이에 0을 삽입한 후 Convolution을 적용합니다. 이를 통해 입력보다 2배 큰 출력을 생성할 수 있습니다. 학습 가능한 파라미터를 통해 어떤 값으로 채울지 학습합니다.
마스크의 값은 클래스 번호(1, 2, 3)이므로 정확히 보존되어야 합니다. BILINEAR나 BICUBIC을 사용하면 1과 2 사이에 1.5 같은 값이 생성되는데, 이는 존재하지 않는 클래스입니다. NEAREST는 가장 가까운 픽셀 값을 그대로 사용하므로 클래스 값이 정확히 보존됩니다.
전경과 배경 사이의 경계는 모호한 영역입니다. 이를 별도의 클래스로 표시하면 모델이 불확실한 영역을 학습할 수 있고, 더 정확한 세그멘테이션 결과를 얻을 수 있습니다. 특히 물체 인식이나 지도 작업에서 경계 정보가 중요합니다.
FCN은 다운샘플링 과정에서 정보 손실이 발생하고, 업샘플링만으로는 세밀한 경계를 복원하기 어렵습니다. 반반 걸쳐져 있는 경계 부분의 처리가 곤란하여 성능 저하가 발생합니다. 이를 해결하기 위해 U-Net 등의 개선된 모델이 등장했습니다.
Semantic vs Instance: Semantic은 같은 클래스를 동일하게, Instance는 개별 객체를 다르게 구분
FCN 구조: Fully Connected Layer 대신 Convolution만 사용, 픽셀마다 예측 수행
Encoder-Decoder: Encoder는 특징 추출 및 축소, Decoder는 복원 및 예측
ConvTranspose2d: stride=2로 이미지 크기를 2배로 확대하는 업샘플링 연산
Oxford Pet 데이터셋: 37가지 품종, Trimap으로 전경/배경/경계 구분
Trimap: 1=전경, 2=배경, 3=경계로 구성된 3클래스 마스크
보간법 중요성: 마스크는 반드시 NEAREST 사용, 일반 이미지는 BILINEAR/BICUBIC 가능
마스크 전처리: NEAREST 보간 + 값 조정(1,2,3 → 0,1,2)
CrossEntropyLoss: 픽셀별로 다중 분류 손실 계산 후 평균
FCN의 한계: 정보 손실과 경계 복원 문제로 U-Net 등장
FCN 논문 정독
U-Net 학습
SegNet 학습
다양한 업샘플링 기법 비교
보간법 실험
데이터 증강
Loss 함수 실험
모델 개선
Pix2Pix와 GAN
업샘플링 기법
다운샘플링 기법
안티 에일리어싱
커스텀 데이터셋 구축
의료 영상 세그멘테이션
자율 주행 시뮬레이션
논문
온라인 자료
라이브러리
코드 저장소