GLPFish_old / utils /inference.py
Kemas M. Aryadary Rasyad
fix inference
3812f7f
Raw History Blame Contribute Delete
5.16 kB
"""
Inference utilities - Fish Freshness Classifier
"""
import os
import json
import torch
import torch.nn as nn
import numpy as np
from PIL import Image
from torchvision import transforms, models
# ── Constants ────────────────────────────────────────────────────────────────
IMG_SIZE = 224
NUM_CLASSES = 4
MODEL_PATH = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "models", "best_model_fx.pth"))
CLASS_JSON = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "models", "class_names.json"))
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
GRADE_RULES = {
# (eye_result, gill_result) -> grade
("fresh", "fresh"): ("A", "Sangat Segar", "#22c55e"),
("fresh", "non-fresh"): ("B", "Cukup Segar", "#f59e0b"),
("non-fresh", "fresh"): ("B", "Cukup Segar", "#f59e0b"),
("non-fresh", "non-fresh"): ("C", "Tidak Segar", "#ef4444"),
}
GRADE_DESC = {
"A": "Ikan sangat segar, layak ekspor premium. Mata jernih dan insang merah cerah.",
"B": "Ikan cukup segar, layak ekspor standar. Salah satu indikator menunjukkan penurunan kesegaran.",
"C": "Ikan tidak segar, tidak memenuhi syarat ekspor. Mata dan insang menunjukkan tanda tidak segar.",
}
# ── Transform ─────────────────────────────────────────────────────────────────
infer_transform = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
# ── Model loader (singleton cache) ───────────────────────────────────────────
_model_cache = None
_classes_cache = None
def load_model():
global _model_cache, _classes_cache
if _model_cache is not None:
return _model_cache, _classes_cache
if not os.path.exists(MODEL_PATH):
raise FileNotFoundError(
f"Model tidak ditemukan di: {MODEL_PATH}\n"
"Jalankan train.py terlebih dahulu."
)
# Load class names
with open(CLASS_JSON) as f:
classes = json.load(f)
# Build model
model = models.mobilenet_v3_large(weights=None) # Ganti dengan DEFAULT jika ada akses internet
in_features = model.classifier[3].in_features
model.classifier[3] = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(in_features, NUM_CLASSES),
)
model.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))
model.to(DEVICE)
model.eval()
_model_cache = model
_classes_cache = classes
return model, classes
def predict_single(image_pil):
"""
Predict freshness from a single cropped image (eye OR gill).
Returns:
class_name : str e.g. 'eye-fresh'
confidence : float
all_probs : dict {class_name: prob}
"""
model, classes = load_model()
tensor = infer_transform(image_pil.convert("RGB")).unsqueeze(0).to(DEVICE)
with torch.no_grad():
logits = model(tensor)
probs = torch.softmax(logits, dim=1)[0].cpu().numpy()
idx = int(probs.argmax())
class_name = classes[idx]
all_probs = {classes[i]: float(probs[i]) for i in range(len(classes))}
return class_name, float(probs[idx]), all_probs
def determine_grade(eye_class, gill_class):
"""
Map eye + gill prediction to export grade.
eye_class / gill_class: 'eye-fresh', 'eye-non-fresh', 'gill-fresh', 'gill-non-fresh'
Returns: grade, label, color, description
"""
# Extract just 'fresh' or 'non-fresh'
eye_status = "fresh" if "non" not in eye_class else "non-fresh"
gill_status = "fresh" if "non" not in gill_class else "non-fresh"
grade, label, color = GRADE_RULES[(eye_status, gill_status)]
desc = GRADE_DESC[grade]
return grade, label, color, desc, eye_status, gill_status
def classify_fish(eye_image_pil, gill_image_pil):
"""
Full pipeline: predict eye + gill β†’ determine grade.
Returns a result dict.
"""
eye_class, eye_conf, eye_probs = predict_single(eye_image_pil)
gill_class, gill_conf, gill_probs = predict_single(gill_image_pil)
grade, label, color, desc, eye_status, gill_status = determine_grade(eye_class, gill_class)
return {
"grade": grade,
"label": label,
"color": color,
"description": desc,
"eye": {
"class": eye_class,
"status": eye_status,
"confidence": eye_conf,
"probs": eye_probs,
},
"gill": {
"class": gill_class,
"status": gill_status,
"confidence": gill_conf,
"probs": gill_probs,
},
}
def is_model_ready():
return os.path.exists(MODEL_PATH) and os.path.exists(CLASS_JSON)