已有认证识别基本功能,暂未支持高并发

This commit is contained in:
2025-07-29 14:25:38 +08:00
commit d28b244258
16 changed files with 473 additions and 0 deletions

0
app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

107
app/api.py Normal file
View File

@ -0,0 +1,107 @@
from fastapi import APIRouter, HTTPException, Body
from . import schemas
from .services import face_service
router = APIRouter()
@router.post("/register", response_model=schemas.RegisterResponse, summary="注册新的人脸")
async def register(request: schemas.RegisterRequest):
"""
为用户注册一张新的人脸。
- **功能**: 将一张图片中的人脸与一个用户ID和姓名关联起来。
- **校验**:
- 检查用户ID是否已被他人注册。
- 确保图片中只包含一张清晰可识别的人脸。
- **操作**: 如果是新用户,会创建用户记录;然后将提取到的人脸特征向量存入数据库。
"""
try:
if not request.url and not request.face_data:
raise ValueError("必须提供图片的url或face_data。")
user_info = face_service.register_new_face(request.id, request.name, request)
return schemas.RegisterResponse(data=user_info)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器内部错误: {e}")
@router.post("/detect", response_model=schemas.DetectResponse, summary="1:N 人脸识别")
async def detect(request: schemas.ImageSource = Body(...)):
"""
在一张图片中检测并识别所有已知的人脸。
- **功能**: 对比图片中的人脸与数据库中的所有人脸,返回匹配结果。
- **阈值**: 内部使用相似度阈值来判断是否匹配成功。
- **返回**: 返回一个列表,包含所有被识别出的人员信息、位置和置信度。
"""
try:
if not request.url and not request.face_data:
raise ValueError("必须提供图片的url或face_data。")
detected_faces = face_service.detect_faces(request)
return schemas.DetectResponse(data=detected_faces)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器内部错误: {e}")
@router.post("/verify", response_model=schemas.VerifyResponse, summary="1:1 人脸认证")
async def verify(request: schemas.VerifyRequest):
"""
验证一张图片中的人脸是否属于指定的用户ID。
- **功能**: 精确比对,判断“这张脸是不是这个人”。
- **场景**: 用于人脸登录、刷脸支付等高安全要求的场景。
- **返回**: 返回布尔值 `match` 表示是否匹配,以及具体的相似度 `confidence`。
"""
try:
if not request.url and not request.face_data:
raise ValueError("必须提供图片的url或face_data。")
verification_result = face_service.verify_face(request.id, request)
return schemas.VerifyResponse(data=verification_result)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器内部错误: {e}")
@router.delete("/users/{user_id}", response_model=schemas.StandardResponse, summary="删除用户")
async def delete_user(user_id: int):
"""
根据用户ID从数据库中删除该用户及其所有注册的人脸信息。
"""
try:
deleted_user = face_service.delete_user(user_id)
return schemas.StandardResponse(message=f"成功删除用户 {deleted_user['name']} (ID: {user_id})。")
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"服务器内部错误: {e}")
@router.get("/users/{user_id}", response_model=schemas.RegisterResponse, summary="获取单个用户信息")
async def get_user(user_id: int):
"""
根据用户ID查询并返回用户的详细信息。
"""
user_info = face_service.get_user(user_id)
if not user_info:
raise HTTPException(status_code=404, detail=f"ID为 {user_id} 的用户不存在。")
return schemas.RegisterResponse(data=user_info)
@router.get("/users", response_model=schemas.UserListResponse, summary="获取所有用户列表")
async def list_users(skip: int = 0, limit: int = 100):
"""
获取数据库中所有已注册用户的列表,支持分页。
"""
users_list = face_service.list_all_users(skip=skip, limit=limit)
return schemas.UserListResponse(data=users_list)
# 可以在这里继续添加其他API端点...

47
app/database.py Normal file
View File

@ -0,0 +1,47 @@
import sqlite3
from contextlib import contextmanager
DATABASE_URL = "E:/geminicli/face_recognition_service_v2/face_recognition.db"
@contextmanager
def get_db_connection():
"""获取数据库连接,并使用上下文管理器确保连接关闭"""
conn = sqlite3.connect(DATABASE_URL)
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.close()
def initialize_database():
"""初始化数据库,创建所需的表"""
with get_db_connection() as conn:
cursor = conn.cursor()
# 创建用户表
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
""")
# 创建人脸特征表
cursor.execute("""
CREATE TABLE IF NOT EXISTS face_features (
feature_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
embedding BLOB NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
""")
# 创建索引以加速查询
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_id ON face_features (user_id);")
conn.commit()
print("数据库初始化完成。")
if __name__ == '__main__':
initialize_database()

22
app/main.py Normal file
View File

@ -0,0 +1,22 @@
from fastapi import FastAPI
from .api import router as api_router
from .database import initialize_database
app = FastAPI(
title="人脸识别服务 V2",
description="一个健壮、可靠的人脸识别与认证API服务。",
version="2.0.0"
)
@app.on_event("startup")
def on_startup():
"""应用启动时,初始化数据库"""
initialize_database()
# 挂载API路由
app.include_router(api_router, prefix="/api", tags=["Face Recognition"])
@app.get("/", summary="服务健康检查")
def read_root():
return {"status": "ok", "message": "欢迎使用人脸识别服务 V2"}

74
app/schemas.py Normal file
View File

@ -0,0 +1,74 @@
from pydantic import BaseModel, Field
from typing import Optional, List
# ===================================================================
# 基础模型 (Base Models)
# ===================================================================
class UserInfo(BaseModel):
"""用户的基本信息"""
id: int = Field(..., description="用户的唯一ID", example=1001)
name: str = Field(..., description="用户的姓名", example="张三")
registered_faces_count: int = Field(..., description="该用户已注册的人脸数量", example=2)
class FaceLocation(BaseModel):
"""人脸在图片中的位置和尺寸"""
x: int = Field(..., description="人脸框左上角的X坐标")
y: int = Field(..., description="人脸框左上角的Y坐标")
width: int = Field(..., description="人脸框的宽度")
height: int = Field(..., description="人脸框的高度")
# ===================================================================
# API 请求模型 (Request Models)
# ===================================================================
class ImageSource(BaseModel):
"""图片来源可以是URL或Base64编码的数据"""
url: Optional[str] = Field(None, description="图片的URL地址", example="http://example.com/image.jpg")
face_data: Optional[str] = Field(None, description="图片的Base64编码字符串")
class RegisterRequest(ImageSource):
"""注册新用户的请求体"""
id: int = Field(..., description="要注册用户的唯一ID", example=1001)
name: str = Field(..., description="要注册用户的姓名", example="张三")
class VerifyRequest(ImageSource):
"""1:1人脸认证的请求体"""
id: int = Field(..., description="要验证的用户ID", example=1001)
# ===================================================================
# API 响应模型 (Response Models)
# ===================================================================
class StandardResponse(BaseModel):
"""标准API响应模型"""
code: int = Field(0, description="响应码0为成功非0为失败", example=0)
message: str = Field("success", description="响应消息", example="操作成功")
data: Optional[dict] = None
class UserListResponse(StandardResponse):
"""获取用户列表的响应"""
data: List[UserInfo]
class RegisterResponse(StandardResponse):
"""注册成功后的响应"""
data: UserInfo
class VerificationResult(BaseModel):
"""1:1认证结果"""
match: bool = Field(..., description="是否匹配")
confidence: float = Field(..., description="置信度 (0.0 to 1.0)")
class VerifyResponse(StandardResponse):
"""1:1人脸认证的响应"""
data: VerificationResult
class DetectedFace(UserInfo):
"""1:N识别结果中的单个人脸信息"""
location: FaceLocation
confidence: float = Field(..., description="识别的置信度")
class DetectResponse(StandardResponse):
"""1:N人脸识别的响应"""
data: List[DetectedFace]

215
app/services.py Normal file
View File

@ -0,0 +1,215 @@
import cv2
import numpy as np
from insightface.app import FaceAnalysis
from . import database
import base64
import requests
class FaceRecognitionService:
def __init__(self):
# 初始化InsightFace分析器
self.app = FaceAnalysis(name="buffalo_l", providers=['CPUExecutionProvider'])
self.app.prepare(ctx_id=0, det_size=(640, 640))
def _decode_image(self, image_source):
"""从URL或Base64解码图片"""
img = None
if image_source.face_data:
try:
img_data = base64.b64decode(image_source.face_data)
nparr = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
except Exception as e:
raise ValueError(f"Base64解码失败: {e}")
elif image_source.url:
try:
response = requests.get(image_source.url, timeout=10)
response.raise_for_status()
img_array = np.asarray(bytearray(response.content), dtype=np.uint8)
img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
except Exception as e:
raise ValueError(f"从URL获取图片失败: {e}")
if img is None:
raise ValueError("无法加载图片")
return img
def register_new_face(self, user_id: int, name: str, image_source):
"""注册新的人脸"""
with database.get_db_connection() as conn:
cursor = conn.cursor()
# 1. 检查ID是否已存在
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if user and user['name'] != name:
raise ValueError(f"ID {user_id} 已被 '{user['name']}' 注册,无法更改为 '{name}'")
# 2. 解码和处理图片
image = self._decode_image(image_source)
faces = self.app.get(image)
if not faces:
raise ValueError("图片中未检测到人脸。")
if len(faces) > 1:
raise ValueError("注册图片中只能包含一张人脸。")
embedding = faces[0].embedding
# 3. 存储到数据库
if not user:
# 如果是新用户先在users表创建记录
cursor.execute("INSERT INTO users (id, name) VALUES (?, ?)", (user_id, name))
# 插入新的人脸特征
cursor.execute("INSERT INTO face_features (user_id, embedding) VALUES (?, ?)", (user_id, embedding.tobytes()))
conn.commit()
# 4. 返回用户信息
cursor.execute("SELECT COUNT(*) FROM face_features WHERE user_id = ?", (user_id,))
count = cursor.fetchone()[0]
return {"id": user_id, "name": name, "registered_faces_count": count}
def detect_faces(self, image_source):
"""1:N 人脸识别"""
image = self._decode_image(image_source)
detected_faces = self.app.get(image)
if not detected_faces:
return []
with database.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT user_id, embedding FROM face_features")
db_features = cursor.fetchall()
if not db_features:
# 如果数据库为空,所有检测到的人脸都是未知的
# (此处简化处理,实际可返回带位置的未知人脸列表)
return []
# 将数据库特征加载到Numpy数组中以便快速计算
db_user_ids = np.array([f['user_id'] for f in db_features])
db_embeddings = np.array([np.frombuffer(f['embedding'], dtype=np.float32) for f in db_features])
results = []
for face in detected_faces:
embedding = face.embedding
# 计算与数据库中所有特征的余弦相似度
# (insightface的特征是归一化的点积等价于余弦相似度)
similarities = np.dot(db_embeddings, embedding)
best_match_index = np.argmax(similarities)
best_similarity = similarities[best_match_index]
# 设置一个阈值来判断是否为已知人脸
# ArcFace 官方建议的阈值通常在 0.4 到 0.6 之间这里我们用0.5
RECOGNITION_THRESHOLD = 0.5
if best_similarity > RECOGNITION_THRESHOLD:
matched_user_id = db_user_ids[best_match_index]
# 查询用户信息
cursor.execute("SELECT name, (SELECT COUNT(*) FROM face_features WHERE user_id=?) FROM users WHERE id=?", (matched_user_id, matched_user_id))
user_info = cursor.fetchone()
x1, y1, x2, y2 = face.bbox.astype(int)
results.append({
"id": matched_user_id,
"name": user_info['name'],
"registered_faces_count": user_info[2],
"confidence": float(best_similarity),
"location": {"x": x1, "y": y1, "width": x2 - x1, "height": y2 - y1}
})
return results
def verify_face(self, user_id: int, image_source):
"""1:1 人脸认证"""
# 1. 解码图片并检测人脸
image = self._decode_image(image_source)
detected_faces = self.app.get(image)
if not detected_faces:
raise ValueError("图片中未检测到人脸。")
if len(detected_faces) > 1:
raise ValueError("用于认证的图片中只能包含一张人脸。")
embedding = detected_faces[0].embedding
# 2. 从数据库获取该ID对应的所有人脸特征
with database.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT embedding FROM face_features WHERE user_id = ?", (user_id,))
db_features = cursor.fetchall()
if not db_features:
raise ValueError(f"数据库中不存在ID为 {user_id} 的用户,或该用户未注册任何人脸。")
db_embeddings = np.array([np.frombuffer(f['embedding'], dtype=np.float32) for f in db_features])
# 3. 计算与该ID所有特征的相似度取最高值
similarities = np.dot(db_embeddings, embedding)
best_similarity = np.max(similarities)
# 1:1 认证通常使用比 1:N 更严格的阈值
VERIFICATION_THRESHOLD = 0.6
match = bool(best_similarity > VERIFICATION_THRESHOLD)
return {"match": match, "confidence": float(best_similarity)}
def delete_user(self, user_id: int):
"""根据ID删除用户及其所有的人脸数据"""
with database.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT name FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user:
raise ValueError(f"ID为 {user_id} 的用户不存在。")
# 使用了外键的 ON DELETE CASCADE删除users表中的记录会自动删除face_features中的相关记录
cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
conn.commit()
# cursor.rowcount 在 SQLite 中可能不总是可靠,我们确认用户存在即认为删除成功
return {"id": user_id, "name": user['name']}
def get_user(self, user_id: int):
"""根据ID获取用户信息"""
with database.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT id, name, (SELECT COUNT(*) FROM face_features WHERE user_id=?) FROM users WHERE id=?", (user_id, user_id))
user = cursor.fetchone()
if not user:
return None
return {"id": user['id'], "name": user['name'], "registered_faces_count": user[2]}
def list_all_users(self, skip: int = 0, limit: int = 100):
"""列出所有已注册的用户,支持分页"""
with database.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT u.id, u.name, COUNT(f.feature_id) as face_count
FROM users u
LEFT JOIN face_features f ON u.id = f.user_id
GROUP BY u.id, u.name
ORDER BY u.id
LIMIT ? OFFSET ?
""", (limit, skip))
users = cursor.fetchall()
return [{"id": u['id'], "name": u['name'], "registered_faces_count": u['face_count']} for u in users]
# 在文件末尾实例化服务方便API层调用
face_service = FaceRecognitionService()

BIN
face_recognition.db Normal file

Binary file not shown.

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
fastapi
uvicorn[standard]
numpy
opencv-python
insightface
requests
pydantic