본문 바로가기

[OpenCV + Machine Learning] Kaggle 주조 제품 불량 이미지를 이용한 Random Forest 분류기 만들기

@eunyoung-study2026. 3. 9. 17:08

[ 문제 상황 ]

시험 문제로 주조(Casting) 제품의 불량 이미지를 자동으로 판별하는 프로그램을 구현해야 했다.

조건은 대략 이런 느낌이었다.

  • 데이터셋: Kaggle – Real-life Industrial Dataset of Casting Product
  • 입력: 제품의 전면 이미지
  • 출력:
    • OK (정상 제품)
    • Defective (불량 제품)
  • 제약:
    • Colab 환경에서 돌아가야 함
    • Kaggle에서 직접 데이터를 받아와야 함
    • 폴더 구조가 사람마다 조금씩 달라도 코드가 깨지지 않고 알아서 루트를 찾을 것
    • OpenCV 기반 전처리 + 머신러닝으로 분류기 구현

단순히 “이미지 몇 장 불러와서 분류기 한 번 돌려본다” 수준이 아니라,
실제 산업용 데이터셋을 robust 하게 다루는 흐름을 설계하는 것이 포인트였다.


[ 데이터 준비: Kaggle에서 Colab까지 자동 파이프라인 ]

1. Kaggle API 인증

Colab에서 바로 Kaggle 데이터셋을 받기 위해 kaggle.json을 업로드하고, 홈 디렉터리에 복사했다.

from google.colab import files
files.upload()  # kaggle.json 업로드

!mkdir -p ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

이렇게 한 번만 설정해두면, 이후부터는 Kaggle CLI로 자유롭게 다운로드할 수 있다.

2. 데이터셋 다운로드 & 압축 해제

!pip install -q kaggle opencv-python-headless scikit-learn scikit-image

!kaggle datasets download ravirajsinh45/real-life-industrial-dataset-of-casting-product
!unzip -o -q real-life-industrial-dataset-of-casting-product.zip

문제는… 압축을 풀고 나면 폴더 구조가 제각각이라는 점이다.

  • 어떤 사람은 /content/casting_512x512/casting_512x512/train/...
  • 다른 환경에서는 /content/casting_data/...

그래서 데이터 루트를 자동으로 찾아주는 헬퍼 함수를 먼저 만들었다.

from pathlib import Path

def get_data_root(base):
    base = Path(base)
    # 1) cast_ok / cast_def 폴더를 직접 찾기
    for d in base.rglob("*"):
        if d.is_dir() and ((d / "cast_ok").exists() or (d / "cast_def").exists()):
            return d
    # 2) 이름에 ok / def 가 들어가는 하위 폴더를 가진 상위 폴더를 찾기
    for d in base.rglob("*"):
        if d.is_dir():
            for sub in d.iterdir():
                if sub.is_dir() and ("ok" in sub.name.lower() or "def" in sub.name.lower()):
                    return d
    # 3) 못 찾으면 그냥 base 리턴
    return base

BASE_DIR = Path("/content")
DATA_ROOT = get_data_root(BASE_DIR)
print("데이터 루트 (이미지 폴더 기준):", DATA_ROOT)

이렇게 해두면, 폴더 구조가 조금씩 달라도 “이미지가 있는 루트”를 알아서 잡아준다.


[ 레이블 수집: 폴더 이름만으로 OK / Defective 구분하기 ]

주어진 데이터셋은 cast_ok, cast_def, ok_front, def_front
이름만 봐도 어떤 폴더가 정상 / 불량인지 알 수 있게 되어 있다.

그래서 폴더 이름에 들어 있는 문자열을 기준으로 레이블을 자동 부여했다.

def find_image_paths_and_labels(base_path):
    base_path = Path(base_path)
    image_extensions = {'.jpg', '.jpeg', '.png', '.bmp'}
    results = []  # (path, label) where label is 'OK' or 'Defective'

    for folder in base_path.rglob("*"):
        if not folder.is_dir():
            continue
        name_lower = folder.name.lower()
        if 'def' in name_lower or 'defective' in name_lower or 'bad' in name_lower:
            label = 'Defective'
        elif 'ok' in name_lower or 'good' in name_lower or 'normal' in name_lower:
            label = 'OK'
        else:
            continue

        for f in folder.iterdir():
            if f.suffix.lower() in image_extensions:
                results.append((str(f), label))

    return results

그리고, 데이터셋에 따라 train/test 폴더가 있을 수도 있고, 없을 수도 있기 때문에:

  • train / test 폴더가 있으면 →
    • train보정(학습)
    • test평가
  • 그렇지 않으면 → 전체 데이터를 80/20 비율로 나누어 사용했다.
train_pairs = find_image_paths_and_labels(DATA_ROOT / "train")
test_pairs = find_image_paths_and_labels(DATA_ROOT / "test")
all_pairs = find_image_paths_and_labels(DATA_ROOT)

중복 경로가 생길 수 있어서 한 번 더 dedupe:

def dedupe(pairs):
    seen = set()
    out = []
    for path, label in pairs:
        if path not in seen:
            seen.add(path)
            out.append((path, label))
    return out

train_pairs = dedupe(train_pairs)
test_pairs = dedupe(test_pairs)
all_pairs = dedupe(all_pairs)

최종적으로, 시험 당시에는 다음과 같이 분할되었다.

보정용 1040장, 평가용 260장으로 분할
평가용 이미지 레이블별: {'OK': 105, 'Defective': 155}
평가용 총 이미지 수: 260

[ 특징 추출: OpenCV + HOG로 “이미지 지문” 만들기 ]

모델은 딥러닝 대신, 클래식 머신러닝(Random Forest) 를 사용했다.
그래서 이미지를 바로 넣지 않고, 고정 길이의 특징 벡터로 변환하는 과정이 필요했다.

1. 공통 전처리: 그레이스케일 + 리사이즈

import cv2

FEATURE_IMG_SIZE = (64, 64)

def load_grayscale(image_path, target_size=FEATURE_IMG_SIZE):
    img = cv2.imread(image_path)
    if img is None:
        return None
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.resize(gray, target_size)
    return gray
  • 컬러 정보보다는 형태(Shape)와 표면 질감(Texture) 이 더 중요하다고 보고,
    • 그레이스케일로 변환
    • 64x64 고정 크기로 리사이즈

2. HOG(Histogram of Oriented Gradients) 특징 추출

from skimage.feature import hog

def extract_hog_features(image_path):
    gray = load_grayscale(image_path)
    if gray is None:
        return None
    features = hog(
        gray,
        orientations=9,
        pixels_per_cell=(8, 8),
        cells_per_block=(2, 2),
        block_norm='L2-Hys',
        visualize=False,
        feature_vector=True
    )
    return features

def extract_features(image_path):
    return extract_hog_features(image_path)

HOG는 이미지의 엣지 방향 분포를 보면서,
“이 이미지 안에 어떤 형태가 얼마나 많이 존재하는지”를 숫자로 표현해 준다.

  • 주조 제품의 테두리 모양, 홀의 형상, 표면의 결함 패턴 같은 것들이
    이 HOG 벡터 안에 요약되어 들어가게 된다.

[ 모델 학습: Random Forest로 OK / Defective 분류하기 ]

이제 준비된 이미지-레이블 쌍을 가지고 Random Forest 분류기를 학습시켰다.

1. 학습 데이터 구성

from sklearn.ensemble import RandomForestClassifier
import numpy as np

training_pairs = train_pairs if train_pairs else calibration_pairs
if not training_pairs:
    raise RuntimeError("학습할 이미지가 없습니다. train 또는 calibration 데이터를 확인하세요.")

X_train_list = []
y_train_list = []
for path, label in training_pairs:
    feat = extract_features(path)
    if feat is not None:
        X_train_list.append(feat)
        y_train_list.append(label)

X_train = np.array(X_train_list)
y_train = np.array(y_train_list)
print(f"학습 샘플 수: {len(X_train)} (OK: {np.sum(y_train == 'OK')}, Defective: {np.sum(y_train == 'Defective')})")

2. Random Forest 학습

clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
print("Random Forest 학습 완료.")
  • 딥러닝 대신 Random Forest를 선택한 이유:
    • 특징 벡터(HOG)가 이미 꽤 좋은 표현력을 가지고 있고,
    • 데이터셋 크기에 비해 구현과 튜닝이 간단하며,
    • Colab 시험 환경에서 빠르게 학습 + 예측이 가능하기 때문.

[ 평가 및 결과 출력: “image.jpg -> OK” 형식 맞추기 ]

시험 요구사항 중 하나는 결과 출력 형식이었다.

image1.jpg -> OK
image2.jpg -> Defective
...
Total images : N
Accuracy : 0.xx

이를 맞추기 위해, 평가용 이미지에 대해서도 동일하게 특징을 추출한 뒤 예측을 수행했다.

X_test_list = []
valid_eval_indices = []
for i, (path, label) in enumerate(evaluation_pairs):
    feat = extract_features(path)
    if feat is not None:
        X_test_list.append(feat)
        valid_eval_indices.append(i)

X_test = np.array(X_test_list) if X_test_list else np.empty((0, X_train.shape[1]))
if len(valid_eval_indices) != len(evaluation_pairs):
    print("일부 이미지 로드 실패로 제외됨.")

y_pred = clf.predict(X_test) if len(X_test_list) else np.array([])

predictions = []
for i, idx in enumerate(valid_eval_indices):
    path, true_label = evaluation_pairs[idx]
    pred = y_pred[i]
    predictions.append((path, true_label, pred))

마지막으로, 파일 이름과 예측 결과, 정확도를 정해진 포맷으로 출력했다.

# 결과 출력
for path, true_label, pred in predictions:
    name = os.path.basename(path)
    print(f"{name} -> {pred}")

total = len(predictions)
correct = sum(1 for _, true, pred in predictions if true == pred)
accuracy = correct / total if total else 0.0

print()
print(f"Total images : {total}")
print(f"Accuracy : {accuracy:.2f}")

시험 환경에서는 대략 다음과 같은 결과를 얻었다.

...
Total images : 260
Accuracy : 0.88

약 88% 정확도로 OK / Defective를 구분하는 분류기를 만들 수 있었다.


[ 핵심 정리 ]

항목 내용
데이터셋 Kaggle – Real-life Industrial Dataset of Casting Product
환경 Google Colab, Kaggle API, OpenCV, scikit-learn, scikit-image
전처리 그레이스케일 변환 + 64x64 리사이즈
특징 추출 HOG (Histogram of Oriented Gradients)
모델 RandomForestClassifier (n_estimators=100)
데이터 분할 전략 train/test 폴더 존재 여부에 따라 자동으로 보정/평가용 분리
폴더/경로 처리 get_data_root, find_image_paths_and_labels, dedupe로 robust
출력 포맷 image.jpg -> OK/Defective, Total images, Accuracy
성능(시험 환경 기준) 약 88% 정확도
더보기

[ 오늘의 포인트 ]

  • 폴더 구조가 조금만 바뀌어도 깨지지 않는 유연한 데이터 탐색
  • OpenCV 기반의 명확한 전처리 파이프라인
  • HOG + Random Forest 같은 클래식 ML 조합으로도 꽤 쓸 만한 성능

이 세 가지를 한 번에 경험해볼 수 있는, 꽤 실전적인 과제였다.

eunyoung-study
@eunyoung-study :: 은영의 이해 노트

개념을 이해하고, 논문을 풀어보고, 코드로 확인하는 기록 ! 오늘도 파이팅 😉

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차