keyfarel/svm-expression-api
0
1from fastapi import FastAPI, UploadFile, File, HTTPException2import joblib3import numpy as np4import cv25import mediapipe as mp6import math7import os8from skimage.feature import hog # <-- LIBRARY BARU UNTUK HOG9 10app = FastAPI(title="API SVM Expression Recognition")11 12# --- 1. SETUP MODEL & MEDIAPIPE ---13model_svm = None14scaler_data = None15pca_transform = None16label_encoder = None17 18mp_face_mesh = mp.solutions.face_mesh19 20@app.on_event("startup")21async def load_models():22 global model_svm, scaler_data, pca_transform, label_encoder23 current_dir = os.path.dirname(os.path.abspath(__file__))24 try:25 model_svm = joblib.load(os.path.join(current_dir, 'svm_gridsearch_best.pkl'))26 scaler_data = joblib.load(os.path.join(current_dir, 'scaler_fixed.pkl'))27 pca_transform = joblib.load(os.path.join(current_dir, 'pca_model.pkl'))28 label_encoder = joblib.load(os.path.join(current_dir, 'label_encoder_fixed.pkl'))29 print("--- SEMUA MODEL SUKSES DIMUAT ---")30 except Exception as e:31 print(f"--- ERROR LOAD MODEL: {e} ---")32 33# --- 2. FUNGSI PREPROCESSING & HOG ---34 35def get_face_landmarks_bgr(image_bgr):36 h, w = image_bgr.shape[:2]37 with mp_face_mesh.FaceMesh(static_image_mode=True, max_num_faces=1, refine_landmarks=True) as face_mesh:38 results = face_mesh.process(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB))39 40 if not results.multi_face_landmarks:41 return None42 lms = results.multi_face_landmarks[0]43 coords = []44 for lm in lms.landmark:45 coords.append((int(lm.x * w), int(lm.y * h)))46 return coords47 48def align_face_by_eyes(image_bgr, landmarks):49 left_idx, right_idx = 33, 26350 (lx, ly) = landmarks[left_idx]51 (rx, ry) = landmarks[right_idx]52 eyes_center = ((lx + rx) // 2, (ly + ry) // 2)53 dy = ry - ly54 dx = rx - lx55 angle = math.degrees(math.atan2(dy, dx))56 M = cv2.getRotationMatrix2D(eyes_center, angle, 1.0)57 h, w = image_bgr.shape[:2]58 rotated = cv2.warpAffine(image_bgr, M, (w, h), flags=cv2.INTER_LINEAR)59 return rotated, M60 61def crop_face_from_landmarks(rotated_bgr, landmarks, M, margin=0.25):62 pts = np.array([[x, y, 1] for (x, y) in landmarks]).T63 transformed = M.dot(pts).T64 xs = transformed[:,0]65 ys = transformed[:,1]66 x1, x2 = int(xs.min()), int(xs.max())67 y1, y2 = int(ys.min()), int(ys.max())68 w_box = x2 - x169 h_box = y2 - y170 x1m = max(0, int(x1 - margin * w_box))71 y1m = max(0, int(y1 - margin * h_box))72 x2m = min(rotated_bgr.shape[1], int(x2 + margin * w_box))73 y2m = min(rotated_bgr.shape[0], int(y2 + margin * h_box))74 x1m, y1m = max(0, x1m), max(0, y1m)75 x2m, y2m = min(rotated_bgr.shape[1], x2m), min(rotated_bgr.shape[0], y2m)76 face_crop = rotated_bgr[y1m:y2m, x1m:x2m]77 return face_crop78 79def preprocess_pipeline(image_bytes):80 nparr = np.frombuffer(image_bytes, np.uint8)81 img_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR)82 if img_bgr is None: raise ValueError("Gambar rusak")83 84 landmarks = get_face_landmarks_bgr(img_bgr)85 if landmarks is None: raise ValueError("Wajah tidak terdeteksi")86 87 rotated, M = align_face_by_eyes(img_bgr, landmarks)88 face_crop = crop_face_from_landmarks(rotated, landmarks, M, margin=0.25)89 90 if face_crop is None or face_crop.size == 0: raise ValueError("Crop gagal")91 92 gray = cv2.cvtColor(face_crop, cv2.COLOR_BGR2GRAY)93 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))94 img_clahe = clahe.apply(gray)95 img_resized = cv2.resize(img_clahe, (128, 128))96 97 return img_resized98 99# --- FUNGSI BARU: EKSTRAKSI FITUR HOG ---100def extract_features_hog(image_gray):101 # Parameter ini HARUS SAMA dengan training di Colab102 # Default umum: orientations=9, pixels_per_cell=(8, 8), cells_per_block=(2, 2)103 fd = hog(image_gray, 104 orientations=9, 105 pixels_per_cell=(8, 8), 106 cells_per_block=(2, 2), 107 block_norm='L2-Hys')108 return fd109 110# --- 3. ENDPOINT API ---111 112@app.get("/")113def home():114 return {"status": "ready"}115 116@app.post("/predict_image")117async def predict_image(file: UploadFile = File(...)):118 if model_svm is None:119 raise HTTPException(status_code=500, detail="Model belum siap")120 121 try:122 contents = await file.read()123 124 # 1. Preprocessing (Crop & Grayscale)125 try:126 processed_img = preprocess_pipeline(contents)127 except ValueError as e:128 return {"status": "error", "message": str(e)}129 130 # 2. HOG Feature Extraction (GANTI DARI FLATTEN KE HOG)131 # Input: Gambar (128, 128) -> Output: Vektor HOG (contoh: 8100 fitur)132 features_hog = extract_features_hog(processed_img)133 features = features_hog.reshape(1, -1)134 135 # 3. Scaling & PCA136 features_scaled = scaler_data.transform(features)137 features_pca = pca_transform.transform(features_scaled)138 139 # 4. Predict SVM140 prediction_index = model_svm.predict(features_pca)141 result_label = label_encoder.inverse_transform(prediction_index)142 143 return {144 "status": "success",145 "prediction": result_label[0]146 }147 148 except Exception as e:149 print(f"Error: {e}")150 error_msg = str(e)151 if "X has" in error_msg or "mismatch" in error_msg:152 detail_msg = f"Error Dimensi: Jumlah fitur HOG tidak cocok. Training vs Server berbeda. Error asli: {error_msg}"153 else:154 detail_msg = error_msg155 156 raise HTTPException(status_code=500, detail=detail_msg)