init face embedding service
This commit is contained in:
71
README.md
Normal file
71
README.md
Normal 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
110
main.py
Normal 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
6
requirements.txt
Normal 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
|
||||||
Reference in New Issue
Block a user