개발일기

[차량속성모델 만들기 1] 차량의 색상 모델 만들기, RESNET50, Yolo Segmentation 적용 본문

Project Portfolio

[차량속성모델 만들기 1] 차량의 색상 모델 만들기, RESNET50, Yolo Segmentation 적용

츄98 2025. 5. 19. 10:59

 

만들고자 하는 최종 목표 모델 : 차량의 종류, 색상을 뽑는 모델 만들기
목표 정확도 : 99% 이상

 

 

지금까지 차량의 색상 뽑기 테스트들을 했었다.

모델을 만들지 않고, Kmeans 알고리즘이나 색상 히스토그램을 활용하여 색상을 뽑아보았다.

색상을 뽑아본 결과, 도로와 창문 등 외부요소들이 색상값에 영향을 주었다.

더보기

 

그래서 Yolo segmentation으로 외부요소들을 제거하고, 색상을 뽑아보고자 한다.

백본 모델은 RESNET50이다.

 

1. Dataset

먼저 차량과 창문을 딴 라벨링 데이터가 필요하다. 라벨링은 labelme라는 툴에서 하면 된다.

라벨링을 하게 되면, JSON 파일에 라벨링 결과가 저장되어있다.

(Yolo Segmentation 모델을 만드는 방식에 대해서는 다른 글에서 설명하겠다.)

 

이미지 당 하나의 JSON 파일이 있어야 한다.

그럼 모든 준비가 끝났다.

 

2. 데이터 정규화 및 훈련, 검증 데이터 나누기

이미지 데이터 정규화를 할 때, 일반적으로 ImageNet 데이터셋의 RGB 채널 평균과 표준편차를 사용한다.

transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                     std=[0.229, 0.224, 0.225]) # ImageNet 데이터셋의 RGB 채널 평균과 표준편차

 

그러나, 데이터셋의 특성을 반영하거나 더 좋은 일반화 성능을 위해서는 직접 평균과 표준편차를 구해 적용하는 것이 좋다.

그래서 나 또한 이미지 데이터의 평균과 표준편차를 직접 구해 정규화했다.

 

2.1 직접 평균과 표준편차를 계산했을 때의 장점

1. 데이터셋 특성 반영 => 더 정확한 학습 가능, 더 좋은 일반화 성능

ImageNet과 다른 도메인(예: CCTV 영상, 의료 영상, 특정 차량 이미지 등)을 가진 경우, 데이터셋 특성 반영

2. 빠른 수렴 및 안정적 학습

훈련 초반의 불안정한 학습과정을 줄이고, 학습 속도를 높인다.

3. 데이터 품질 평가 용이

데이터의 특정 편향(bias)이나 이상치(outlier)를 발견하기 쉬워지고, 전처리 단계에서의 문제를 개선할 수 있다.

 

temp_transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor()
    ])
    temp_dataset = VehicleDataset(train_folder, temp_transform)
    temp_loader = DataLoader(temp_dataset, batch_size=16, shuffle=False)

    mean = torch.zeros(3)
    std = torch.zeros(3)
    for imgs, _, _ in temp_loader:
        batch_size = imgs.size(0)
        for i in range(3):
            mean[i] += imgs[:, i, :, :].mean() * batch_size
            std[i] += imgs[:, i, :, :].std() * batch_size
    mean.div_(len(temp_dataset))
    std.div_(len(temp_dataset))
    print("Total_length", len(temp_dataset))
    print(f"Dataset Mean: [{mean[0]:.4f}, {mean[1]:.4f}, {mean[2]:.4f}], "
          f"STD: [{std[0]:.4f}, {std[1]:.4f}, {std[2]:.4f}]")

정규화 결과

 

2.2 훈련, 검증 데이터 나누기

8대 2로 나누었다.

dataset = VehicleDataset(train_folder, transform)
    # 훈련과 검증 8:2 비율로 나누기
    train_size = int(0.8 * len(dataset))
    val_size = len(dataset) - train_size
    train_subset, val_subset = torch.utils.data.random_split(dataset, [train_size, val_size], generator=torch.Generator().manual_seed(42))

    train_loader = DataLoader(train_subset, batch_size=16, shuffle=True)
    val_loader = DataLoader(val_subset, batch_size=16, shuffle=False)
    print(f"Train 이미지 수: {len(train_subset)}")
    print(f"Val 이미지 수: {len(val_subset)}")
    print(f"Train 배치 수: {len(train_loader)}, Val 배치 수: {len(val_loader)}")

 

3. 이미지 전처리

도로와 창문과 같이 정확한 차량의 색상을 뽑기에 장애가 되는 요소들을 제거하기 위해서 이미지 마스킹을 적용했다.

JSON 파일을 읽어서 창문과 도로를 마스킹 하였다.

그 외에는, 너무 어두운 영상, 너무 밝은 영상의 경우 밝기를 조절하는 기법을 적용했다.

 

def preprocess_image_with_mask(img_path: str, json_path: str):
    image = Image.open(img_path).convert("RGB")
    img_np = np.array(image)

    with open(json_path, 'r') as f:
        metadata = json.load(f)

    mask = np.zeros((img_np.shape[0], img_np.shape[1]), dtype=np.uint8)

    for shape in metadata['shapes']:
        points = np.array(shape['points'], dtype=np.int32)
        if shape['label'] in type_classes:
            cv2.fillPoly(mask, [points], color=1)
        elif shape['label'] == 'window':
            cv2.fillPoly(mask, [points], color=0)

    # 마스크 적용: 창문을 제외한 차량 외곽만
    masked_img = cv2.bitwise_and(img_np, img_np, mask=mask)

    # HSV 변환 및 밝기 조절
    img_hsv = cv2.cvtColor(masked_img, cv2.COLOR_RGB2HSV)
    h, s, v = cv2.split(img_hsv)
    mean_brightness = np.mean(v[mask == 1])

    if mean_brightness < 90:
        v = np.clip(v.astype(np.float32) * 1.3, 0, 255).astype(np.uint8)
    elif mean_brightness > 160:
        v = np.clip(v.astype(np.float32) * 0.8, 0, 255).astype(np.uint8)

    v = cv2.equalizeHist(v)
    img_hsv = cv2.merge((h, s, v))
    img_rgb = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2RGB)

    img = Image.fromarray(img_rgb)
    return img

 

마스킹 처리가 끝난 결과

 

4. 손실함수, 클래스별 가중치 적용

클래스별 가중치를 계산하여 적용했을 때의 장점 : 

1. 클래스 불균형 문제 해결

2. 정확성 및 일반성 성능 향상

3. 모델의 편향 감소

4. 데이터 수가 없는 클래스에 대한 안정적 처리

 

클래스 불균형 문제를 해결하고, 안정적인 학습을 위해서 클래스별 가중치를 계산하여 적용했다.

"역빈도 가중치" 방식을 사용했다.

 

  • 데이터가 적은 클래스에 높은 가중치를 부여
  • 데이터가 많은 클래스에 상대적으로 낮은 가중치를 부여
  • 데이터가 없는 클래스는 평균 가중치로 대체
# === 6. 클래스별 weight 계산 ===
from collections import Counter

def compute_class_weights(label_list, num_classes):
    counter = Counter(label_list)
    total = sum(counter.values())
    raw_weights = []

    for i in range(num_classes):
        count = counter.get(i, 0)
        if count > 0:
            weight = total / (count * num_classes)
        else:
            weight = None
        raw_weights.append(weight)

    # 평균 가중치로 대체
    mean_weight = np.mean([w for w in raw_weights if w is not None])
    weights = [w if w is not None else mean_weight for w in raw_weights]

    return torch.tensor(weights, dtype=torch.float)

 

 

5. 얼리스타핑 적용

과적합을 방지하기 의해서 얼리스타핑을 적용했다.

# Early stopping
if val_losses[-1] < best_valid_loss:
    best_valid_loss = val_losses[-1]
    early_stop_counter = 0
    torch.save(model.state_dict(), os.path.join(save_dir,"color_resnet50_best.pt"))
    print("✅ Best model saved!")
else:
    early_stop_counter += 1

 

6. 모델 설계

RESNET50을 백본으로 사용하고, 차량 이미지의 색상을 분류하는 모델을 설계했다.

RESNET50의 마지막 완전연결층을 제거하여 클래스분류기능없이 오로지 특징 추출만 수행하도록 했다.

백본에서 추출된 특징을 받아 색상 클래스를 예측했다.

 

차량 이미지 (RGB)
    ↓
ResNet-50 백본 (마지막 FC층 제거)
    ↓
특징 추출 (2048차원 feature vector)
    ↓
완전연결층 (색상 클래스 분류)
    ↓
색상 클래스 예측 (출력)

 

# === 5. 모델 정의 ===
class VehicleClassifier(nn.Module):
    def __init__(self, num_colors, num_types):
        super(VehicleClassifier, self).__init__()
        base = resnet50(weights=ResNet50_Weights.DEFAULT)
        self.backbone = nn.Sequential(*list(base.children())[:-1])  # remove FC
        self.fc_color = nn.Linear(2048, num_colors)

    def forward(self, x):
        x = self.backbone(x)          # (B, 2048, 1, 1)
        x = x.view(x.size(0), -1)     # (B, 2048)
        return self.fc_color(x)       # (B, num_colors)

 

7. 훈련 결과

훈련 과정에서의 Loss, Acc 결과
best model로 test inference 결과

색상을 잘 뽑아낸 것을 확인할 수 있다.

 

다음으로는 차량의 종류와 색상 2가지를 한번에 추론하는 멀티태스크 모델을 설계해도록 하겠다.