Compare commits
No commits in common. "main" and "master" have entirely different histories.
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
nohup.out
|
||||||
|
nohup_gpu*.out
|
||||||
|
py3/
|
||||||
|
*.egg-info/
|
||||||
|
*.pid
|
||||||
35
README.md
35
README.md
@ -1,35 +0,0 @@
|
|||||||
# 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
Normal file
6
ah.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
from ahserver.webapp import webapp
|
||||||
|
from init import load_face_service
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
webapp(load_face_service)
|
||||||
57
build.sh
Executable file
57
build.sh
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
#!/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
|
||||||
27
conf/config.json
Normal file
27
conf/config.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
104
init.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# -*- 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)
|
||||||
0
workers/__init__.py
Normal file
0
workers/__init__.py
Normal file
109
workers/face_model.py
Normal file
109
workers/face_model.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# -*- 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