본문 바로가기
  • You find inspiration to create your own path !
업무 자동화/AI

'개와 고양이 분류' 실습 #4

by ToolBOX01 2026. 5. 15.
반응형

■ 판별 프로그램 만들기

cats_vs_dogs_improved.py('개와 고양이 분류' 실습 #3 코드 실행)를 실행하면 CNN 알고리즘이 학습되어 가중치(패턴 지식)가 저장된 모델 파일이 만들어집니다

 

이미지 판별 프로그램 사용을 위한 두 파일 연결 방법 (순서)

① 먼저 실행 — cats_vs_dogs_improved.py → 학습 완료 후 cats_vs_dogs_model.h5 파일이 생성됨
② 그 다음 실행 — predict_cat_dog.py (이미지 판별 프로그램) → .h5 파일을 불러와 사진을 판별함

두 파일은 .h5 모델 파일 하나로 연결됩니다 (다이어그램의 주황색 부분).

 

배포 시 필요한 파일

파일 설명
predict_cat_dog.py 판별 프로그램 본체
cats_vs_dogs_model.h5 학습된 모델 (가중치)

 

단독 실행 파일(.exe)로 배포하고 싶다면

상대방이 Python 설치 없이 바로 실행할 수 있게 만들 수 있습니다.

pip install pyinstaller
pyinstaller --onefile --windowed --add-data "cats_vs_dogs_model.h5;." predict_cat_dog.py

실행하면 dist/ 폴더 안에 predict_cat_dog.exe 하나만 생성되고, 모델 파일이 exe 안에 포함됩니다. 단, .exe 파일 크기가 수백 MB 정도로 커집니다 (TensorFlow가 통째로 포함되기 때문).

TensorFlow가 필요한 이유

predict_cat_dog.py 안의 model.predict() 한 줄이 호출되는 순간, TensorFlow가 내부에서 다음을 처리합니다.

사진 한 장(150×150×3 = 67,500개 숫자)이 들어오면, Conv2D·MaxPooling·Dense 레이어를 통과하면서 수백만 번의 행렬 곱셈과 덧셈이 실행됩니다. 이 계산을 Python 코드로 직접 짜면 몇 분이 걸리지만, TensorFlow가 GPU/CPU에 최적화된 방식으로 처리하기 때문에 1초도 안 걸리는 겁니다.

즉, .h5 파일은 "계산에 쓸 숫자(가중치)"고, TensorFlow는 "그 숫자로 계산을 실제로 돌리는 엔진"입니다. 둘 중 하나라도 없으면 판별이 불가능합니다.

 

Predict cat dog.py

"""
개와 고양이 판별 프로그램
- 학습된 모델(cats_vs_dogs_model.h5)을 불러와서
- tkinter GUI로 사진을 드래그 앤 드롭하여 판별
- 사용법: python predict_cat_dog.py
"""

import tkinter as tk
from tkinter import filedialog, ttk
import threading
import os

# tkinter DnD는 별도 라이브러리 필요 → tkinterdnd2 사용
try:
    from tkinterdnd2 import TkinterDnD, DND_FILES
    DND_AVAILABLE = True
except ImportError:
    DND_AVAILABLE = False

import numpy as np
from PIL import Image, ImageTk
import tensorflow as tf

# ──────────────────────────────────────────
# 설정값
# ──────────────────────────────────────────
MODEL_PATH = "cats_vs_dogs_model.h5"   # 저장된 모델 경로
IMG_SIZE   = (150, 150)


# ──────────────────────────────────────────
# 모델 로드
# ──────────────────────────────────────────
def load_model():
    if not os.path.exists(MODEL_PATH):
        raise FileNotFoundError(
            f"모델 파일을 찾을 수 없습니다: {MODEL_PATH}\n"
            "먼저 cats_vs_dogs_improved.py 를 실행해 모델을 저장하세요."
        )
    return tf.keras.models.load_model(MODEL_PATH)


# ──────────────────────────────────────────
# 이미지 전처리 & 예측
# ──────────────────────────────────────────
def preprocess(image_path):
    img = Image.open(image_path).convert("RGB")
    img = img.resize(IMG_SIZE)
    arr = np.array(img, dtype=np.float32) / 255.0
    return np.expand_dims(arr, axis=0), img   # (1,150,150,3), PIL Image


def predict(model, image_path):
    arr, pil_img = preprocess(image_path)
    prob = float(model.predict(arr, verbose=0)[0][0])  # 0=고양이, 1=개
    label     = "🐶 개 (Dog)"   if prob >= 0.5 else "🐱 고양이 (Cat)"
    confidence = prob if prob >= 0.5 else 1 - prob
    return label, confidence, pil_img


# ──────────────────────────────────────────
# GUI
# ──────────────────────────────────────────
class App:
    def __init__(self, root, model):
        self.root  = root
        self.model = model

        root.title("🐾 개 vs 고양이 판별기")
        root.geometry("520x680")
        root.configure(bg="#0f1117")
        root.resizable(False, False)

        self._build_ui()

    # ── UI 구성 ──────────────────────────
    def _build_ui(self):
        BG   = "#0f1117"
        CARD = "#1a1d27"
        ACC  = "#7c6aff"
        TXT  = "#e8e6f0"
        SUB  = "#6b6880"

        # 제목
        tk.Label(self.root, text="🐾 Cat vs Dog Classifier",
                 font=("Helvetica", 18, "bold"),
                 bg=BG, fg=TXT).pack(pady=(28, 4))
        tk.Label(self.root, text="이미지를 드래그하거나 클릭하여 업로드하세요",
                 font=("Helvetica", 10),
                 bg=BG, fg=SUB).pack(pady=(0, 18))

        # 드롭존 프레임
        self.drop_frame = tk.Frame(self.root, bg=CARD,
                                   width=420, height=300,
                                   highlightthickness=2,
                                   highlightbackground=ACC)
        self.drop_frame.pack(padx=50)
        self.drop_frame.pack_propagate(False)

        self.preview_label = tk.Label(self.drop_frame,
                                      text="📂\n\n사진을 여기에 드래그하거나\n클릭하여 선택",
                                      font=("Helvetica", 12),
                                      bg=CARD, fg=SUB,
                                      cursor="hand2")
        self.preview_label.pack(expand=True, fill="both")
        self.preview_label.bind("<Button-1>", lambda e: self._open_file())

        # 드래그 앤 드롭 활성화
        if DND_AVAILABLE:
            self.drop_frame.drop_target_register(DND_FILES)
            self.drop_frame.dnd_bind("<<Drop>>", self._on_drop)
            self.preview_label.drop_target_register(DND_FILES)
            self.preview_label.dnd_bind("<<Drop>>", self._on_drop)

        # 버튼
        btn_frame = tk.Frame(self.root, bg=BG)
        btn_frame.pack(pady=20)

        self.btn = tk.Button(btn_frame, text="  📂  파일 선택  ",
                             font=("Helvetica", 11, "bold"),
                             bg=ACC, fg="white",
                             activebackground="#5c4de0",
                             activeforeground="white",
                             bd=0, padx=20, pady=10,
                             cursor="hand2",
                             command=self._open_file)
        self.btn.pack()

        # 결과 카드
        result_bg = tk.Frame(self.root, bg=CARD,
                             width=420, height=130,
                             highlightthickness=1,
                             highlightbackground="#2e2d3a")
        result_bg.pack(padx=50, pady=(0, 10))
        result_bg.pack_propagate(False)

        self.result_label = tk.Label(result_bg,
                                     text="판별 결과가 여기에 표시됩니다",
                                     font=("Helvetica", 14, "bold"),
                                     bg=CARD, fg=SUB)
        self.result_label.pack(expand=True)

        self.conf_label = tk.Label(result_bg, text="",
                                   font=("Helvetica", 10),
                                   bg=CARD, fg=SUB)
        self.conf_label.pack(pady=(0, 12))

        # 프로그레스바
        style = ttk.Style()
        style.theme_use("default")
        style.configure("custom.Horizontal.TProgressbar",
                        troughcolor=CARD,
                        background=ACC,
                        thickness=10)
        self.progress = ttk.Progressbar(self.root, length=420,
                                        style="custom.Horizontal.TProgressbar",
                                        mode="determinate")
        self.progress.pack(padx=50)

        # 상태 메시지
        self.status_var = tk.StringVar(value="모델 준비 완료 ✅")
        tk.Label(self.root, textvariable=self.status_var,
                 font=("Helvetica", 9),
                 bg=BG, fg=SUB).pack(pady=8)

    # ── 이벤트 핸들러 ────────────────────
    def _on_drop(self, event):
        path = event.data.strip().strip("{}")
        if path.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".webp")):
            self._run_predict(path)
        else:
            self.status_var.set("⚠️  지원 형식: PNG, JPG, JPEG, BMP, WEBP")

    def _open_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("이미지 파일", "*.png *.jpg *.jpeg *.bmp *.webp"),
                       ("모든 파일", "*.*")]
        )
        if path:
            self._run_predict(path)

    def _run_predict(self, path):
        self.status_var.set("🔍 분석 중...")
        self.result_label.config(text="분석 중...", fg="#7c6aff")
        self.conf_label.config(text="")
        self.progress["value"] = 0
        threading.Thread(target=self._predict_thread,
                         args=(path,), daemon=True).start()

    def _predict_thread(self, path):
        try:
            label, conf, pil_img = predict(self.model, path)
            self.root.after(0, self._update_ui, label, conf, pil_img)
        except Exception as e:
            self.root.after(0, self.status_var.set, f"❌ 오류: {e}")

    def _update_ui(self, label, conf, pil_img):
        # 미리보기 이미지
        pil_img.thumbnail((380, 270))
        tk_img = ImageTk.PhotoImage(pil_img)
        self.preview_label.config(image=tk_img, text="")
        self.preview_label.image = tk_img   # 참조 유지

        # 결과 표시
        color = "#7c6aff" if "개" in label else "#ff6a8e"
        self.result_label.config(text=label, fg=color,
                                 font=("Helvetica", 18, "bold"))
        self.conf_label.config(text=f"신뢰도: {conf*100:.1f}%", fg="#aaa8c0")
        self.progress["value"] = conf * 100
        self.status_var.set("✅ 분석 완료")


# ──────────────────────────────────────────
# 실행
# ──────────────────────────────────────────
if __name__ == "__main__":
    # 모델 로드
    print("모델 로딩 중...")
    model = load_model()
    print("모델 로드 완료!")

    # GUI 실행
    if DND_AVAILABLE:
        root = TkinterDnD.Tk()
    else:
        root = tk.Tk()
        print("⚠️  tkinterdnd2 미설치 → 드래그 비활성, 클릭으로 파일 선택 가능")
        print("   pip install tkinterdnd2  으로 설치하면 드래그 기능 활성화됩니다.")

    app = App(root, model)
    root.mainloop()

 

by korealionkk@gmail.com

반응형