commit d28b2442586df85e91a748d1b724afc7b467e760 Author: Mashiro50070 <1251294066@qq.com> Date: Tue Jul 29 14:25:38 2025 +0800 已有认证识别基本功能,暂未支持高并发 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..d84036c Binary files /dev/null and b/app/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1bc185c Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/__pycache__/api.cpython-310.pyc b/app/__pycache__/api.cpython-310.pyc new file mode 100644 index 0000000..b3a6abf Binary files /dev/null and b/app/__pycache__/api.cpython-310.pyc differ diff --git a/app/__pycache__/database.cpython-310.pyc b/app/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000..c703a23 Binary files /dev/null and b/app/__pycache__/database.cpython-310.pyc differ diff --git a/app/__pycache__/main.cpython-310.pyc b/app/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000..4ad02d8 Binary files /dev/null and b/app/__pycache__/main.cpython-310.pyc differ diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..93d4562 Binary files /dev/null and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/__pycache__/schemas.cpython-310.pyc b/app/__pycache__/schemas.cpython-310.pyc new file mode 100644 index 0000000..ace73d5 Binary files /dev/null and b/app/__pycache__/schemas.cpython-310.pyc differ diff --git a/app/__pycache__/services.cpython-310.pyc b/app/__pycache__/services.cpython-310.pyc new file mode 100644 index 0000000..5708fc9 Binary files /dev/null and b/app/__pycache__/services.cpython-310.pyc differ diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..0f6f8d6 --- /dev/null +++ b/app/api.py @@ -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端点... diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..393102f --- /dev/null +++ b/app/database.py @@ -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() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..5f9365a --- /dev/null +++ b/app/main.py @@ -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"} diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..7d5f699 --- /dev/null +++ b/app/schemas.py @@ -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] diff --git a/app/services.py b/app/services.py new file mode 100644 index 0000000..07a15e9 --- /dev/null +++ b/app/services.py @@ -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() + + + diff --git a/face_recognition.db b/face_recognition.db new file mode 100644 index 0000000..6d4061e Binary files /dev/null and b/face_recognition.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..09eeeed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ + +fastapi +uvicorn[standard] +numpy +opencv-python +insightface +requests +pydantic