已有认证识别基本功能,暂未支持高并发
This commit is contained in:
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
BIN
app/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/api.cpython-310.pyc
Normal file
BIN
app/__pycache__/api.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/database.cpython-310.pyc
Normal file
BIN
app/__pycache__/database.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-310.pyc
Normal file
BIN
app/__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/schemas.cpython-310.pyc
Normal file
BIN
app/__pycache__/schemas.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/services.cpython-310.pyc
Normal file
BIN
app/__pycache__/services.cpython-310.pyc
Normal file
Binary file not shown.
107
app/api.py
Normal file
107
app/api.py
Normal 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
47
app/database.py
Normal 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
22
app/main.py
Normal 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
74
app/schemas.py
Normal 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
215
app/services.py
Normal 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
BIN
face_recognition.db
Normal file
Binary file not shown.
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
numpy
|
||||
opencv-python
|
||||
insightface
|
||||
requests
|
||||
pydantic
|
Reference in New Issue
Block a user