import io import math import os from typing import Any, Dict import cv2 import numpy as np from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.responses import JSONResponse from insightface.app import FaceAnalysis app = FastAPI(title="SMAN1 Face Embedding Service", version="1.0.0") MODEL_NAME = os.getenv("FACE_MODEL_NAME", "buffalo_l") DETECT_SIZE = int(os.getenv("FACE_DET_SIZE", "640")) app_insight: FaceAnalysis | None = None def get_app() -> FaceAnalysis: global app_insight if app_insight is None: app_insight = FaceAnalysis(name=MODEL_NAME) # ctx_id = 0 -> GPU, -1 -> CPU ctx_id = int(os.getenv("FACE_CTX_ID", "-1")) app_insight.prepare(ctx_id=ctx_id, det_size=(DETECT_SIZE, DETECT_SIZE)) return app_insight def compute_blur_score(gray: np.ndarray) -> float: # Variance of Laplacian return float(cv2.Laplacian(gray, cv2.CV_64F).var()) def compute_brightness(img: np.ndarray) -> float: # Normalize brightness to 0..1 (approx) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) v = hsv[:, :, 2] return float(v.mean() / 255.0) @app.post("/embed") async def embed(image: UploadFile = File(...)) -> JSONResponse: """ Terima satu file gambar, kembalikan embedding + metrik kualitas. Response JSON: { "embedding": [...], "quality_score": float, "faces_count": int, "face_size": float, "blur": float, "brightness": float } """ if not image.filename: raise HTTPException(status_code=400, detail="File gambar wajib diisi") content = await image.read() img_array = np.frombuffer(content, dtype=np.uint8) img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) if img is None: raise HTTPException(status_code=400, detail="Gagal membaca gambar") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) app_face = get_app() faces = app_face.get(img) faces_count = len(faces) if faces_count == 0: raise HTTPException(status_code=422, detail="Wajah tidak ditemukan") # Ambil face terbesar main_face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])) x1, y1, x2, y2 = map(int, main_face.bbox) w = max(0, x2 - x1) h = max(0, y2 - y1) face_size = float(max(w, h)) blur_score = compute_blur_score(gray[y1:y2, x1:x2]) if w > 0 and h > 0 else 0.0 brightness = compute_brightness(img) # InsightFace sudah memberi embedding ter-normalisasi embedding_vec = main_face.embedding.astype(float).tolist() # Quality score sederhana: kombinasi face_size, blur, brightness (bisa disesuaikan) quality_score = float( math.tanh((face_size / 100.0)) * math.tanh(blur_score / 50.0) * math.tanh(brightness * 2.0) ) return JSONResponse( { "embedding": embedding_vec, "quality_score": quality_score, "faces_count": faces_count, "face_size": face_size, "blur": blur_score, "brightness": brightness, } ) @app.get("/health") async def health() -> Dict[str, Any]: return {"status": "ok", "model": MODEL_NAME}