commit a26cd2e22475bc3d40658c56228e08aefde297c8 Author: mwpn Date: Thu Mar 5 14:48:43 2026 +0700 init face embedding service diff --git a/README.md b/README.md new file mode 100644 index 0000000..563eeed --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# SMAN 1 Garut – Face Embedding Service + +Layanan Python kecil untuk menghasilkan **embedding wajah** + metrik kualitas yang dipakai backend CI4 (`FaceService`). + +## Fitur + +- Framework: **FastAPI** + **InsightFace**. +- Endpoint utama: `POST /embed` (single image → embedding + quality). +- Deteksi wajah, pilih wajah terbesar, hitung: + - `embedding` (vector float, default dimensi 512), + - `faces_count`, + - `face_size` (px), + - `blur` (variance of Laplacian), + - `brightness` (0..1), + - `quality_score` (kombinasi sederhana dari metrik di atas). + +## Setup Cepat + +```bash +cd face-service +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt + +uvicorn main:app --host 0.0.0.0 --port 5000 +``` + +Lalu di `.env` backend CI4 (`backend/.env`), set: + +```ini +FACE_SERVICE_URL = 'http://localhost:5000' +FACE_EMBEDDING_DIM = 512 +FACE_SIM_THRESHOLD = 0.85 +FACE_MIN_SIZE = 80 +FACE_MIN_BLUR = 30 +FACE_MIN_BRIGHTNESS = 0.2 +``` + +## Kontrak API: `POST /embed` + +**Request** (multipart/form-data): + +- Field `image`: file gambar (.jpg/.png). + +**Response** (`200 OK`, JSON): + +```json +{ + "embedding": [0.01, -0.23, ...], + "quality_score": 0.93, + "faces_count": 1, + "face_size": 120.5, + "blur": 45.2, + "brightness": 0.55 +} +``` + +Backend CI4 (`FaceService::extractEmbeddingWithQuality`) akan: + +- Menolak gambar dengan: + - `faces_count != 1`, + - `face_size < FACE_MIN_SIZE`, + - `blur < FACE_MIN_BLUR`, + - `brightness < FACE_MIN_BRIGHTNESS`. + +## Health Check + +`GET /health` → `{"status": "ok", "model": "buffalo_l"}` + +Dipakai untuk cek cepat apakah service sudah siap dipakai backend CI4. + diff --git a/main.py b/main.py new file mode 100644 index 0000000..8fd5e0a --- /dev/null +++ b/main.py @@ -0,0 +1,110 @@ +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} + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e8b6d0f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.0 +numpy==1.26.4 +opencv-python==4.10.0.84 +insightface==0.7.3 +onnxruntime==1.18.0