简单构建高并发
This commit is contained in:
28
Milvus基本配置.md
Normal file
28
Milvus基本配置.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Milvus安装方法
|
||||||
|
|
||||||
|
## 通过podman安装Milvus
|
||||||
|
|
||||||
|
### 安装podman的方法
|
||||||
|
1. 通过基本命令来安装podman
|
||||||
|
`sudo apt-get update` -- 更新Ubuntu现有环境
|
||||||
|
`sudo apt-get -y install podman` -- 安装podman
|
||||||
|
`podman info` -- 确认podman安装成功
|
||||||
|
|
||||||
|
### 安装Milvus的方法
|
||||||
|
1. 通过daocloud拉去官方的Milvus镜像
|
||||||
|
`sudo podman pull docker.m.daocloud.io/milvusdb/milvus:v2.5.15` -- 国内镜像拉取方法
|
||||||
|
|
||||||
|
2. 创建docker=podman的系统永久映射
|
||||||
|
`echo "alias docker=podman" >> ~/.bashrc` -- 创建映射
|
||||||
|
`source ~/.bashrc` -- 刷新bash
|
||||||
|
|
||||||
|
3. 启动Milvus服务
|
||||||
|
`sed 's/docker/podman/g' milvus_embed.sh > milvus_embed_podman.sh` -- 将脚本中的docker语句替换成docker/podman语句以实现兼容
|
||||||
|
`chmod +x milvus_embed_podman.sh` -- 设置权限
|
||||||
|
|
||||||
|
`mkdir -p "$(pwd)/volumes/milvus"` -- 创建宿主机侧路径
|
||||||
|
|
||||||
|
- 脚本里 -v $(pwd)/volumes/milvus:/var/lib/milvus 要求宿主机侧路径必须事先存在,Podman 不会像 Docker 那样自动创建。
|
||||||
|
|
||||||
|
`./milvus_embed_podman.sh start` -- 启动服务脚本
|
||||||
|
`./milvus_embed_podman.sh stop` -- 停止服务脚本
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
100
app/api.py
Normal file
100
app/api.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Body, Depends
|
||||||
|
from . import schemas
|
||||||
|
from .services import face_service, FaceRecognitionService
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Dependency to get the service instance
|
||||||
|
def get_face_service():
|
||||||
|
return face_service
|
||||||
|
|
||||||
|
@router.post("/register", response_model=schemas.RegisterResponse, summary="注册新的人脸")
|
||||||
|
async def register(request: schemas.RegisterRequest, service: FaceRecognitionService = Depends(get_face_service)):
|
||||||
|
"""
|
||||||
|
为用户注册一张新的人脸。元数据存入PostgreSQL,向量存入Milvus。
|
||||||
|
|
||||||
|
- **功能**: 将一张图片中的人脸与一个用户ID和姓名关联起来。
|
||||||
|
- **校验**:
|
||||||
|
- 检查用户ID是否已被他人注册。
|
||||||
|
- 确保图片中只包含一张清晰可识别的人脸。
|
||||||
|
- **操作**: 如果是新用户,会在PostgreSQL创建用户记录;然后将提取到的人脸特征向量存入Milvus。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not request.url and not request.face_data:
|
||||||
|
raise ValueError("必须提供图片的url或face_data。")
|
||||||
|
user_info = await 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 人脸识别 (Milvus)")
|
||||||
|
async def detect(request: schemas.ImageSource = Body(...), service: FaceRecognitionService = Depends(get_face_service)):
|
||||||
|
"""
|
||||||
|
在一张图片中检测并识别所有已知的人脸。使用Milvus进行高性能向量搜索。
|
||||||
|
|
||||||
|
- **功能**: 对比图片中的人脸与Milvus中的所有人脸,返回匹配结果。
|
||||||
|
- **阈值**: 内部使用相似度阈值来判断是否匹配成功。
|
||||||
|
- **返回**: 返回一个列表,包含所有被识别出的人员信息、位置和置信度。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not request.url and not request.face_data:
|
||||||
|
raise ValueError("必须提供图片的url或face_data。")
|
||||||
|
detected_faces = await 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 人脸认证 (Milvus)")
|
||||||
|
async def verify(request: schemas.VerifyRequest, service: FaceRecognitionService = Depends(get_face_service)):
|
||||||
|
"""
|
||||||
|
验证一张图片中的人脸是否属于指定的用户ID。在Milvus中进行限定范围的向量搜索。
|
||||||
|
|
||||||
|
- **功能**: 精确比对,判断“这张脸是不是这个人”。
|
||||||
|
- **场景**: 用于人脸登录、刷脸支付等高安全要求的场景。
|
||||||
|
- **返回**: 返回布尔值 `match` 表示是否匹配,以及具体的相似度 `confidence`。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not request.url and not request.face_data:
|
||||||
|
raise ValueError("必须提供图片的url或face_data。")
|
||||||
|
verification_result = await 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="删除用户 (同步删除PG和Milvus)")
|
||||||
|
async def delete_user(user_id: int, service: FaceRecognitionService = Depends(get_face_service)):
|
||||||
|
"""
|
||||||
|
根据用户ID,从PostgreSQL和Milvus中同步删除该用户的所有数据。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deleted_user = await 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, service: FaceRecognitionService = Depends(get_face_service)):
|
||||||
|
"""
|
||||||
|
根据用户ID,查询并返回用户的详细信息(元数据来自PG,人脸数量来自Milvus)。
|
||||||
|
"""
|
||||||
|
user_info = await 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, service: FaceRecognitionService = Depends(get_face_service)):
|
||||||
|
"""
|
||||||
|
获取数据库中所有已注册用户的列表,支持分页(元数据来自PG,人脸数量来自Milvus)。
|
||||||
|
"""
|
||||||
|
users_list = await service.list_all_users(skip=skip, limit=limit)
|
||||||
|
return schemas.UserListResponse(data=users_list)
|
14
app/config.py
Normal file
14
app/config.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Settings(BaseModel):
|
||||||
|
# PostgreSQL 数据库连接URL
|
||||||
|
# 格式: postgresql+asyncpg://user:password@host:port/database
|
||||||
|
DATABASE_URL: str = "postgresql+asyncpg://postgres:123456@localhost:5432/face_db"
|
||||||
|
|
||||||
|
# Milvus 连接信息 (将在第二阶段使用)
|
||||||
|
MILVUS_HOST: str = "localhost"
|
||||||
|
MILVUS_PORT: int = 19530
|
||||||
|
|
||||||
|
# 创建一个全局配置实例
|
||||||
|
settings = Settings()
|
32
app/database.py
Normal file
32
app/database.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import databases
|
||||||
|
import sqlalchemy
|
||||||
|
from .config import settings
|
||||||
|
|
||||||
|
# 使用 databases 库来提供异步连接池
|
||||||
|
database = databases.Database(settings.DATABASE_URL)
|
||||||
|
|
||||||
|
# 使用 SQLAlchemy Core 定义表结构 (元数据)
|
||||||
|
metadata = sqlalchemy.MetaData()
|
||||||
|
|
||||||
|
users = sqlalchemy.Table(
|
||||||
|
"users",
|
||||||
|
metadata,
|
||||||
|
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
|
||||||
|
sqlalchemy.Column("name", sqlalchemy.String, nullable=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
face_features = sqlalchemy.Table(
|
||||||
|
"face_features",
|
||||||
|
metadata,
|
||||||
|
sqlalchemy.Column("feature_id", sqlalchemy.Integer, primary_key=True, autoincrement=True),
|
||||||
|
sqlalchemy.Column("user_id", sqlalchemy.Integer, sqlalchemy.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
||||||
|
sqlalchemy.Column("embedding", sqlalchemy.LargeBinary, nullable=False),
|
||||||
|
sqlalchemy.Index("ix_face_features_user_id", "user_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建一个引擎,用于在启动时创建表
|
||||||
|
engine = sqlalchemy.create_engine(settings.DATABASE_URL.replace("+asyncpg", ""))
|
||||||
|
|
||||||
|
def create_tables():
|
||||||
|
"""创建所有定义的表"""
|
||||||
|
metadata.create_all(engine)
|
30
app/main.py
Normal file
30
app/main.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from .api import router as api_router
|
||||||
|
|
||||||
|
from .database import database, create_tables
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="人脸识别服务 V2 - PostgreSQL版",
|
||||||
|
description="一个使用PostgreSQL和Milvus的高性能人脸识别服务。",
|
||||||
|
version="2.1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def on_startup():
|
||||||
|
"""应用启动时,连接数据库并创建表"""
|
||||||
|
await database.connect()
|
||||||
|
create_tables() # SQLAlchemy的create_all是同步的,所以这样调用没问题
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def on_shutdown():
|
||||||
|
"""应用关闭时,断开数据库连接"""
|
||||||
|
await database.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
# 挂载API路由
|
||||||
|
app.include_router(api_router, prefix="/api", tags=["Face Recognition"])
|
||||||
|
|
||||||
|
@app.get("/", summary="服务健康检查")
|
||||||
|
def read_root():
|
||||||
|
return {"status": "ok", "message": "欢迎使用人脸识别服务 V2"}
|
68
app/milvus_helpers.py
Normal file
68
app/milvus_helpers.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
from pymilvus import connections, utility, Collection, CollectionSchema, FieldSchema, DataType
|
||||||
|
from .config import settings
|
||||||
|
|
||||||
|
# 定义集合名称和相关参数
|
||||||
|
COLLECTION_NAME = "face_features_collection"
|
||||||
|
FACE_VECTOR_DIM = 512 # InsightFace 'buffalo_l' 模型的特征维度
|
||||||
|
|
||||||
|
class MilvusHelper:
|
||||||
|
def __init__(self):
|
||||||
|
try:
|
||||||
|
# 连接 Milvus 服务
|
||||||
|
connections.connect("default", host=settings.MILVUS_HOST, port=settings.MILVUS_PORT)
|
||||||
|
print("成功连接到 Milvus 服务。")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"连接 Milvus 服务失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def has_collection(self):
|
||||||
|
"""检查集合是否存在"""
|
||||||
|
return utility.has_collection(COLLECTION_NAME)
|
||||||
|
|
||||||
|
def create_collection(self):
|
||||||
|
"""创建一个新的集合来存储人脸特征"""
|
||||||
|
if self.has_collection():
|
||||||
|
print(f"集合 '{COLLECTION_NAME}' 已存在。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 定义字段
|
||||||
|
# 主键字段,Milvus 会自动生成ID
|
||||||
|
pk_field = FieldSchema(name="feature_id", dtype=DataType.INT64, is_primary=True, auto_id=True)
|
||||||
|
# 对应的用户ID字段
|
||||||
|
user_id_field = FieldSchema(name="user_id", dtype=DataType.INT64)
|
||||||
|
# 人脸特征向量字段
|
||||||
|
embedding_field = FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=FACE_VECTOR_DIM)
|
||||||
|
|
||||||
|
# 创建集合 Schema
|
||||||
|
schema = CollectionSchema(
|
||||||
|
fields=[pk_field, user_id_field, embedding_field],
|
||||||
|
description="人脸识别特征集合",
|
||||||
|
enable_dynamic_field=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建集合
|
||||||
|
self.collection = Collection(name=COLLECTION_NAME, schema=schema)
|
||||||
|
print(f"集合 '{COLLECTION_NAME}' 创建成功。")
|
||||||
|
|
||||||
|
# 为向量字段创建索引以加速搜索
|
||||||
|
index_params = {
|
||||||
|
"metric_type": "IP", # IP (Inner Product) 等价于归一化向量的余弦相似度
|
||||||
|
"index_type": "IVF_FLAT",
|
||||||
|
"params": {"nlist": 1024} # nlist 的值需要根据数据量调整
|
||||||
|
}
|
||||||
|
self.collection.create_index(field_name="embedding", index_params=index_params)
|
||||||
|
print("向量索引创建成功。")
|
||||||
|
return self.collection
|
||||||
|
|
||||||
|
def get_collection(self):
|
||||||
|
"""获取集合对象并加载到内存中以便搜索"""
|
||||||
|
if not self.has_collection():
|
||||||
|
self.create_collection()
|
||||||
|
collection = Collection(COLLECTION_NAME)
|
||||||
|
collection.load()
|
||||||
|
return collection
|
||||||
|
|
||||||
|
# 在模块加载时创建一个全局实例
|
||||||
|
milvus_client = MilvusHelper()
|
||||||
|
collection = milvus_client.get_collection()
|
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]
|
191
app/services.py
Normal file
191
app/services.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from insightface.app import FaceAnalysis
|
||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from .database import database, users
|
||||||
|
from .milvus_helpers import collection as milvus_collection
|
||||||
|
|
||||||
|
class FaceRecognitionService:
|
||||||
|
"""封装所有核心人脸识别和数据库操作的业务逻辑"""
|
||||||
|
def __init__(self):
|
||||||
|
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
|
||||||
|
|
||||||
|
async def register_new_face(self, user_id: int, name: str, image_source):
|
||||||
|
"""注册新的人脸,元数据存入PostgreSQL,向量存入Milvus"""
|
||||||
|
query = users.select().where(users.c.id == user_id)
|
||||||
|
user = await database.fetch_one(query)
|
||||||
|
|
||||||
|
if user and user.name != name:
|
||||||
|
raise ValueError(f"ID {user_id} 已被 '{user.name}' 注册,无法更改为 '{name}'。")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
async with database.transaction():
|
||||||
|
if not user:
|
||||||
|
query = users.insert().values(id=user_id, name=name)
|
||||||
|
await database.execute(query)
|
||||||
|
|
||||||
|
data_to_insert = [[user_id], [embedding]]
|
||||||
|
milvus_collection.insert(data_to_insert)
|
||||||
|
milvus_collection.flush()
|
||||||
|
|
||||||
|
count_expr = f"user_id == {user_id}"
|
||||||
|
count = milvus_collection.query(expr=count_expr, output_fields=["user_id"])
|
||||||
|
|
||||||
|
return {"id": user_id, "name": name, "registered_faces_count": len(count)}
|
||||||
|
|
||||||
|
async def detect_faces(self, image_source):
|
||||||
|
"""在Milvus中进行1:N人脸识别"""
|
||||||
|
image = self._decode_image(image_source)
|
||||||
|
detected_faces = self.app.get(image)
|
||||||
|
|
||||||
|
if not detected_faces:
|
||||||
|
return []
|
||||||
|
|
||||||
|
search_vectors = [face.embedding for face in detected_faces]
|
||||||
|
search_params = {"metric_type": "IP", "params": {"nprobe": 10}}
|
||||||
|
|
||||||
|
results = milvus_collection.search(
|
||||||
|
data=search_vectors, anns_field="embedding", param=search_params,
|
||||||
|
limit=1, output_fields=["user_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
final_results = []
|
||||||
|
user_ids_to_fetch = {hits[0].entity.get('user_id') for hits in results if hits}
|
||||||
|
|
||||||
|
if not user_ids_to_fetch:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 批量查询用户信息,提高效率
|
||||||
|
user_query = users.select().where(users.c.id.in_(user_ids_to_fetch))
|
||||||
|
user_records = await database.fetch_all(user_query)
|
||||||
|
user_map = {user.id: user.name for user in user_records}
|
||||||
|
|
||||||
|
for i, hits in enumerate(results):
|
||||||
|
if not hits:
|
||||||
|
continue
|
||||||
|
best_hit = hits[0]
|
||||||
|
similarity = best_hit.distance
|
||||||
|
|
||||||
|
RECOGNITION_THRESHOLD = 0.5
|
||||||
|
if similarity > RECOGNITION_THRESHOLD:
|
||||||
|
matched_user_id = best_hit.entity.get('user_id')
|
||||||
|
user_name = user_map.get(matched_user_id)
|
||||||
|
|
||||||
|
if user_name:
|
||||||
|
count_expr = f"user_id == {matched_user_id}"
|
||||||
|
count_res = milvus_collection.query(expr=count_expr, output_fields=["user_id"])
|
||||||
|
x1, y1, x2, y2 = detected_faces[i].bbox.astype(int)
|
||||||
|
final_results.append({
|
||||||
|
"id": matched_user_id,
|
||||||
|
"name": user_name,
|
||||||
|
"registered_faces_count": len(count_res),
|
||||||
|
"confidence": float(similarity),
|
||||||
|
"location": {"x": x1, "y": y1, "width": x2 - x1, "height": y2 - y1}
|
||||||
|
})
|
||||||
|
return final_results
|
||||||
|
|
||||||
|
async def verify_face(self, user_id: int, image_source):
|
||||||
|
"""在Milvus中进行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
|
||||||
|
|
||||||
|
search_params = {"metric_type": "IP", "params": {"nprobe": 10}}
|
||||||
|
# 在指定user_id的范围内进行搜索
|
||||||
|
expr = f"user_id == {user_id}"
|
||||||
|
results = milvus_collection.search(
|
||||||
|
data=[embedding], anns_field="embedding", param=search_params,
|
||||||
|
limit=1, expr=expr, output_fields=["user_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
best_similarity = 0.0
|
||||||
|
if results and results[0]:
|
||||||
|
best_similarity = results[0][0].distance
|
||||||
|
|
||||||
|
VERIFICATION_THRESHOLD = 0.6
|
||||||
|
match = bool(best_similarity > VERIFICATION_THRESHOLD)
|
||||||
|
|
||||||
|
return {"match": match, "confidence": float(best_similarity)}
|
||||||
|
|
||||||
|
async def delete_user(self, user_id: int):
|
||||||
|
"""从PostgreSQL和Milvus中删除用户"""
|
||||||
|
query = users.select().where(users.c.id == user_id)
|
||||||
|
user = await database.fetch_one(query)
|
||||||
|
if not user:
|
||||||
|
raise ValueError(f"ID为 {user_id} 的用户不存在。")
|
||||||
|
|
||||||
|
async with database.transaction():
|
||||||
|
delete_query = users.delete().where(users.c.id == user_id)
|
||||||
|
await database.execute(delete_query)
|
||||||
|
|
||||||
|
# 从 Milvus 删除
|
||||||
|
expr = f"user_id == {user_id}"
|
||||||
|
milvus_collection.delete(expr)
|
||||||
|
|
||||||
|
return {"id": user.id, "name": user.name}
|
||||||
|
|
||||||
|
async def get_user(self, user_id: int):
|
||||||
|
"""获取单个用户的元数据和在Milvus中的人脸数量"""
|
||||||
|
query = users.select().where(users.c.id == user_id)
|
||||||
|
user = await database.fetch_one(query)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
count_expr = f"user_id == {user_id}"
|
||||||
|
count_res = milvus_collection.query(expr=count_expr, output_fields=["user_id"])
|
||||||
|
return {"id": user.id, "name": user.name, "registered_faces_count": len(count_res)}
|
||||||
|
|
||||||
|
async def list_all_users(self, skip: int = 0, limit: int = 100):
|
||||||
|
"""列出所有用户,并从Milvus统计人脸数量"""
|
||||||
|
query = users.select().offset(skip).limit(limit)
|
||||||
|
user_records = await database.fetch_all(query)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for user in user_records:
|
||||||
|
count_expr = f"user_id == {user.id}"
|
||||||
|
count_res = milvus_collection.query(expr=count_expr, output_fields=["user_id"])
|
||||||
|
results.append({"id": user.id, "name": user.name, "registered_faces_count": len(count_res)})
|
||||||
|
return results
|
||||||
|
|
||||||
|
face_service = FaceRecognitionService()
|
14
psql基本配置.md
Normal file
14
psql基本配置.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Post GRE SQL 数据库命令记录
|
||||||
|
|
||||||
|
## 安装好后默认创建一个超级用户名叫postgre,通过以下命令显示地用postgre登录psql,没有密码
|
||||||
|
`sudo -u postgres psql`
|
||||||
|
|
||||||
|
## 真正的角色用户进入psql的方式如下
|
||||||
|
`psql -U myuser -d mydb -h localhost`
|
||||||
|
|
||||||
|
# 进入数据库后修改超级账号的密码
|
||||||
|
`ALTER USER postgres WITH PASSWORD '123456';`
|
||||||
|
|
||||||
|
# 通过数据库命令创建第一个数据库
|
||||||
|
`CREATE DATABASE face_db;`
|
||||||
|
|
16
requirements.txt
Normal file
16
requirements.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
numpy
|
||||||
|
opencv-python
|
||||||
|
insightface
|
||||||
|
databases
|
||||||
|
psycopg2-binary
|
||||||
|
onnxruntime
|
||||||
|
sqlalchemy
|
||||||
|
|
||||||
|
requests
|
||||||
|
pydantic
|
||||||
|
asyncpg
|
||||||
|
milvus-lite
|
||||||
|
pymilvus
|
Reference in New Issue
Block a user