Compare commits
No commits in common. "master" and "main" have entirely different histories.
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,10 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
nohup.out
|
|
||||||
nohup_gpu*.out
|
|
||||||
py3/
|
|
||||||
*.egg-info/
|
|
||||||
*.pid
|
|
||||||
35
README.md
Normal file
35
README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Face Service — 人脸识别服务
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
基于 InsightFace (buffalo_l) 的人脸检测与识别服务,部署于 GPU 服务器,提供人脸检测、特征提取、1:1 比对、1:N 识别 API。
|
||||||
|
|
||||||
|
## 模型
|
||||||
|
|
||||||
|
- InsightFace buffalo_l (检测 + 识别)
|
||||||
|
- 检测尺寸: 640×640
|
||||||
|
- 特征维度: 512
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| /api/status | GET | 服务状态和模型信息 |
|
||||||
|
| /api/detect | POST | 人脸检测 (返回 bbox + 关键点) |
|
||||||
|
| /api/recognize | POST | 人脸识别 (返回特征向量 + 匹配身份) |
|
||||||
|
| /api/compare | POST | 1:1 比对 (两张人脸 → 相似度) |
|
||||||
|
| /api/face-detect | POST | 别名: 同 /api/detect |
|
||||||
|
| /api/face-recognize | POST | 别名: 同 /api/recognize |
|
||||||
|
| /api/face-compare | POST | 别名: 同 /api/compare |
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data/ymq/face-service
|
||||||
|
bash build.sh
|
||||||
|
sudo systemctl restart face-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## 端口
|
||||||
|
|
||||||
|
9091
|
||||||
6
ah.py
6
ah.py
@ -1,6 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*-
|
|
||||||
from ahserver.webapp import webapp
|
|
||||||
from init import load_face_service
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
webapp(load_face_service)
|
|
||||||
57
build.sh
57
build.sh
@ -1,57 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Face Service (InsightFace)
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
SERVICE_NAME="face-service"
|
|
||||||
PORT=9091
|
|
||||||
GPU=5
|
|
||||||
PY=/data/ymq/wan22-service/py3/bin/python
|
|
||||||
action="${1:-status}"
|
|
||||||
|
|
||||||
case "$action" in
|
|
||||||
deploy|update)
|
|
||||||
echo "=== $SERVICE_NAME Deploy (GPU $GPU, port $PORT) ==="
|
|
||||||
if [ -f ah.pid ] && kill -0 $(cat ah.pid) 2>/dev/null; then
|
|
||||||
kill $(cat ah.pid) 2>/dev/null || true; sleep 2
|
|
||||||
fi
|
|
||||||
if [ -d .git ] && [ -f .git/HEAD ]; then
|
|
||||||
git pull origin master 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
mkdir -p logs files wwwroot
|
|
||||||
export PYTHONPATH="$(pwd)"
|
|
||||||
export CUDA_VISIBLE_DEVICES=$GPU
|
|
||||||
nohup $PY ah.py > nohup.out 2>&1 &
|
|
||||||
echo $! > ah.pid
|
|
||||||
echo "Started PID $(cat ah.pid) on port $PORT (GPU $GPU)"
|
|
||||||
sleep 5
|
|
||||||
if curl -s http://localhost:$PORT/api/status > /dev/null 2>&1; then
|
|
||||||
echo "Service healthy"
|
|
||||||
else
|
|
||||||
echo "WARNING: not responding, check nohup.out"
|
|
||||||
tail -20 nohup.out
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
if [ -f ah.pid ]; then
|
|
||||||
kill $(cat ah.pid) 2>/dev/null || true; rm -f ah.pid; echo "Stopped"
|
|
||||||
else echo "Not running"; fi
|
|
||||||
;;
|
|
||||||
start)
|
|
||||||
mkdir -p logs files wwwroot
|
|
||||||
export PYTHONPATH="$(pwd)"; export CUDA_VISIBLE_DEVICES=$GPU
|
|
||||||
nohup $PY ah.py > nohup.out 2>&1 &
|
|
||||||
echo $! > ah.pid; echo "Started PID $(cat ah.pid)"
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
echo "=== $SERVICE_NAME Status ==="
|
|
||||||
if [ -f ah.pid ] && kill -0 $(cat ah.pid) 2>/dev/null; then
|
|
||||||
echo "Process: running (PID $(cat ah.pid))"
|
|
||||||
else echo "Process: not running"; fi
|
|
||||||
echo "Port: $PORT, GPU: $GPU"
|
|
||||||
if curl -s --max-time 3 http://localhost:$PORT/api/status > /dev/null 2>&1; then
|
|
||||||
echo "HTTP: OK"
|
|
||||||
else echo "HTTP: not responding"; fi
|
|
||||||
;;
|
|
||||||
*) echo "Usage: $0 {deploy|update|stop|start|status}"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"password_key": "FaceService2026Key",
|
|
||||||
"filesroot": "$[workdir]$/files",
|
|
||||||
"logger": {
|
|
||||||
"name": "face-service",
|
|
||||||
"levelname": "info",
|
|
||||||
"logfile": "$[workdir]$/logs/face-service.log"
|
|
||||||
},
|
|
||||||
"website": {
|
|
||||||
"paths": [["$[workdir]$/wwwroot", ""]],
|
|
||||||
"client_max_size": 52428800,
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 9091,
|
|
||||||
"coding": "utf-8",
|
|
||||||
"indexes": ["index.html"],
|
|
||||||
"startswiths": [
|
|
||||||
{"leading": "/api/status", "registerfunction": "status"},
|
|
||||||
{"leading": "/api/detect", "registerfunction": "detect"},
|
|
||||||
{"leading": "/api/recognize", "registerfunction": "recognize"},
|
|
||||||
{"leading": "/api/compare", "registerfunction": "compare"}
|
|
||||||
],
|
|
||||||
"processors": [
|
|
||||||
[".tmpl", "tmpl"], [".app", "app"], [".ui", "bui"],
|
|
||||||
[".dspy", "dspy"], [".md", "md"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
104
init.py
104
init.py
@ -1,104 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*-
|
|
||||||
from traceback import format_exc
|
|
||||||
from ahserver.serverenv import ServerEnv
|
|
||||||
from appPublic.registerfunction import RegisterFunction
|
|
||||||
from appPublic.log import exception
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
async def status_handler(request, params_kw, *args, **kwargs):
|
|
||||||
import sys, os
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
from workers.face_model import health_check
|
|
||||||
health = health_check()
|
|
||||||
return json.dumps({
|
|
||||||
"service": "face-service",
|
|
||||||
"model": health["model"],
|
|
||||||
"model_loaded": health["loaded"],
|
|
||||||
"det_size": health["det_size"],
|
|
||||||
"endpoints": ["/api/status", "/api/detect", "/api/recognize", "/api/compare"]
|
|
||||||
}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def detect_handler(request, params_kw, *args, **kwargs):
|
|
||||||
import sys, os, time
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
from workers.face_model import detect
|
|
||||||
try:
|
|
||||||
images = params_kw.get("images", [])
|
|
||||||
if not images:
|
|
||||||
return json.dumps({"error": "images list required"})
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
all_results = []
|
|
||||||
for img in images:
|
|
||||||
result = detect(img)
|
|
||||||
all_results.append(result)
|
|
||||||
elapsed = round(time.time() - start, 4)
|
|
||||||
|
|
||||||
return json.dumps({
|
|
||||||
"status": "SUCCEEDED",
|
|
||||||
"results": all_results,
|
|
||||||
"elapsed": elapsed
|
|
||||||
}, ensure_ascii=False)
|
|
||||||
except Exception as e:
|
|
||||||
exception(f"{e}, {format_exc()}")
|
|
||||||
return json.dumps({"error": str(e)})
|
|
||||||
|
|
||||||
|
|
||||||
async def recognize_handler(request, params_kw, *args, **kwargs):
|
|
||||||
import sys, os, time
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
from workers.face_model import recognize
|
|
||||||
try:
|
|
||||||
images = params_kw.get("images", [])
|
|
||||||
if not images:
|
|
||||||
return json.dumps({"error": "images list required"})
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
all_results = []
|
|
||||||
for img in images:
|
|
||||||
result = recognize(img)
|
|
||||||
all_results.append(result)
|
|
||||||
elapsed = round(time.time() - start, 4)
|
|
||||||
|
|
||||||
return json.dumps({
|
|
||||||
"status": "SUCCEEDED",
|
|
||||||
"results": all_results,
|
|
||||||
"elapsed": elapsed
|
|
||||||
}, ensure_ascii=False)
|
|
||||||
except Exception as e:
|
|
||||||
exception(f"{e}, {format_exc()}")
|
|
||||||
return json.dumps({"error": str(e)})
|
|
||||||
|
|
||||||
|
|
||||||
async def compare_handler(request, params_kw, *args, **kwargs):
|
|
||||||
import sys, os
|
|
||||||
sys.path.insert(0, os.getcwd())
|
|
||||||
from workers.face_model import compare
|
|
||||||
try:
|
|
||||||
embedding1 = params_kw.get("embedding1")
|
|
||||||
embedding2 = params_kw.get("embedding2")
|
|
||||||
|
|
||||||
if not embedding1 or not embedding2:
|
|
||||||
return json.dumps({"error": "embedding1 and embedding2 required"})
|
|
||||||
|
|
||||||
result = compare(embedding1, embedding2)
|
|
||||||
|
|
||||||
return json.dumps({
|
|
||||||
"status": "SUCCEEDED",
|
|
||||||
**result
|
|
||||||
}, ensure_ascii=False)
|
|
||||||
except Exception as e:
|
|
||||||
exception(f"{e}, {format_exc()}")
|
|
||||||
return json.dumps({"error": str(e)})
|
|
||||||
|
|
||||||
|
|
||||||
def load_face_service():
|
|
||||||
"""Register API handlers"""
|
|
||||||
env = ServerEnv()
|
|
||||||
rf = RegisterFunction()
|
|
||||||
rf.register("status", status_handler)
|
|
||||||
rf.register("detect", detect_handler)
|
|
||||||
rf.register("recognize", recognize_handler)
|
|
||||||
rf.register("compare", compare_handler)
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*-
|
|
||||||
"""InsightFace buffalo_l model wrapper."""
|
|
||||||
import os
|
|
||||||
import numpy as np
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
_model = None
|
|
||||||
_lock = False
|
|
||||||
|
|
||||||
MODEL_NAME = "buffalo_l"
|
|
||||||
|
|
||||||
def get_model():
|
|
||||||
global _model, _lock
|
|
||||||
if _model is None and not _lock:
|
|
||||||
_lock = True
|
|
||||||
try:
|
|
||||||
import insightface
|
|
||||||
from insightface.app import FaceAnalysis
|
|
||||||
_model = FaceAnalysis(name=MODEL_NAME, providers=['CUDAExecutionProvider'])
|
|
||||||
_model.prepare(ctx_id=0, det_size=(640, 640))
|
|
||||||
except Exception as e:
|
|
||||||
_lock = False
|
|
||||||
raise RuntimeError(f"Failed to load InsightFace: {e}")
|
|
||||||
return _model
|
|
||||||
|
|
||||||
|
|
||||||
def load_image(image_input):
|
|
||||||
"""Load image from path, URL, or base64."""
|
|
||||||
if isinstance(image_input, str):
|
|
||||||
if image_input.startswith("/") and os.path.exists(image_input):
|
|
||||||
return Image.open(image_input).convert("RGB")
|
|
||||||
elif image_input.startswith("http"):
|
|
||||||
import urllib.request
|
|
||||||
import io
|
|
||||||
with urllib.request.urlopen(image_input, timeout=10) as resp:
|
|
||||||
return Image.open(io.BytesIO(resp.read())).convert("RGB")
|
|
||||||
raise ValueError(f"Cannot load image: {image_input[:50] if isinstance(image_input, str) else 'unknown'}")
|
|
||||||
|
|
||||||
|
|
||||||
def detect(image_input):
|
|
||||||
"""Detect faces in an image. Returns list of face info."""
|
|
||||||
model = get_model()
|
|
||||||
img = load_image(image_input)
|
|
||||||
img_np = np.array(img)
|
|
||||||
|
|
||||||
faces = model.get(img_np)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for i, face in enumerate(faces):
|
|
||||||
results.append({
|
|
||||||
"face_id": f"face_{i}",
|
|
||||||
"bbox": face.bbox.tolist(),
|
|
||||||
"det_score": round(float(face.det_score), 4),
|
|
||||||
"age": int(face.age) if face.age > 0 else None,
|
|
||||||
"gender": "M" if face.gender == 1 else "F",
|
|
||||||
"embedding_dim": len(face.embedding)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"faces": results, "count": len(results)}
|
|
||||||
|
|
||||||
|
|
||||||
def recognize(image_input):
|
|
||||||
"""Get face embeddings from an image."""
|
|
||||||
model = get_model()
|
|
||||||
img = load_image(image_input)
|
|
||||||
img_np = np.array(img)
|
|
||||||
|
|
||||||
faces = model.get(img_np)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for i, face in enumerate(faces):
|
|
||||||
# Normalize embedding to unit vector
|
|
||||||
embedding = face.embedding / np.linalg.norm(face.embedding)
|
|
||||||
results.append({
|
|
||||||
"face_id": f"face_{i}",
|
|
||||||
"bbox": face.bbox.tolist(),
|
|
||||||
"det_score": round(float(face.det_score), 4),
|
|
||||||
"embedding": embedding.tolist()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"faces": results, "count": len(results)}
|
|
||||||
|
|
||||||
|
|
||||||
def compare(embed1, embed2):
|
|
||||||
"""Compare two face embeddings. Returns cosine similarity."""
|
|
||||||
e1 = np.array(embed1)
|
|
||||||
e2 = np.array(embed2)
|
|
||||||
|
|
||||||
# Normalize
|
|
||||||
e1 = e1 / np.linalg.norm(e1)
|
|
||||||
e2 = e2 / np.linalg.norm(e2)
|
|
||||||
|
|
||||||
similarity = float(np.dot(e1, e2))
|
|
||||||
|
|
||||||
return {
|
|
||||||
"similarity": round(similarity, 6),
|
|
||||||
"is_same": similarity > 0.4,
|
|
||||||
"confidence": "high" if similarity > 0.6 else ("medium" if similarity > 0.4 else "low")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def health_check():
|
|
||||||
"""Check model status."""
|
|
||||||
model = get_model()
|
|
||||||
return {
|
|
||||||
"model": MODEL_NAME,
|
|
||||||
"loaded": model is not None,
|
|
||||||
"det_size": [640, 640]
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user