init face embedding service

This commit is contained in:
mwpn
2026-03-05 14:48:43 +07:00
commit a26cd2e224
3 changed files with 187 additions and 0 deletions

71
README.md Normal file
View File

@@ -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.

110
main.py Normal file
View File

@@ -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}

6
requirements.txt Normal file
View File

@@ -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