简体中文 | English
项目基于SeetaFace6Open
,分发不同使用场景:
- 提供基于
cuda
编译的tennis
前向推理引擎库(windows
,cuda11.2
); - 根据
pybind11
进行python
接口封装,使用面向对象的方式,让接口更加自然合理,使用更加便捷 ; - 封装的进行
extern C
的纯C
封装方式,并使用ctypes
调用提供python
接口,解除不同版本python
使用上的限制,使用简便,性能与原始c++模块基本一致,基于C,支持更多语言拓展; - 基于
ctypes python
接口的提供fastapi
的服务化部署方式,支持接口限流,异步orm
尽可能提高QPS
的工作负载,此外集成faiss
向量索引方式,理论上百万级别人脸库1:N
搜索耗时为ms
级别; - 项目采用
CMake
进行构建,支持windows/linux/macos
- 支持
python3.6+
版本。
本项目已经提供了预编译库,包含windows|linux|macos
(均为x86_64CPU
架构,其它架构请自行编译),其中windows
下提供GPU
编译库,由于系统差异巨大导致的二进制文件无法使用,请进行源码编译,编译详情参考SeetaFace6Open。
- CMake
- 各平台的编译工具
- 具体安装方式不在本文档的说明范畴内
git clone --recursive https://github.com/ChaoII/FacePythonAPI.git
仓库包含部分二进制文件,可能比较大,如果拉不下来,请自行百度
其中--recursive
可以同步拉取子模块
其中项目CPU
、GPU
主要依赖于tennis
引擎,但是选择不同的编译方式可以自动的进行库的选择,并添加cuda
路径配置
cmake .. -G "visual studio 16 2019" -DUSE_SPDLOG=ON
cmake --build . --config Release --target FaceAPI -j4
其中cmake ..
后面可跟-G
参数指定生成器,如果不会请自行参考百度,-DUSE_SPDLOG=ON,使用sdk使用spdlog日志库,但是库体积会变大,请自行衡量
注意 : windows
上如果是MSVC
请打开VS
开发命令提示符,执行上述命令,别傻乎乎的打开IDE
折腾.
cmake .. -DBUILD_WITH_GPU=ON -DCUDA_DIR="xxx"
cmake --build . --config Release --target FaceAPI -j4
最终编译得到FaceAPI.dll(windows) | libFaceAPI.so(linux) | libFaceAPI.dylib(MacOS)
- 确认
CPU
以及操作系统为64
位 - 下载
cpu-z
或者其它软件查看CPU
是否支持AVX | SSE | FMA
- 确认机器是够装有
NVIDIA
显卡,并且已经安装好CUDA
和对应CUDNN
(提供11.x
的GPU
库,如果不同请往上看自行编译)
如果:
- 条件1不满足,或者操作系统比较古老,请自行编译
- 条件2不满足,请将仓库中
third/seeta/lib/{你的平台}/tennis_pentium.xx
改成tennis.xx
(因为奔腾CPU
不支持AVX
指令集) - 条件3不满足,请老老实实用
CPU
去玩
既然人脸识别属于视觉范畴,所以不得不准备opencv-python
库,其安装方式为:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python
# 部分机器存在问题,可能是图形界面导致的,可以安装headless类型opencv
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python-headless
由于编译文件依赖于第三方的库,需要将第三方库放置于环境变量中(非windows
)
linux/macos
需要添加库路径
- 临时
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${seetaFace6Python目录路径}/seetaface/lib/linux
- 永久
sudo echo ${编译目录}/lib/linux > /etc/ld.so.conf.d/facepythonapi.conf
sudo ldconfig
windows
下将依赖库文件放在与编译生成的FaceAPI.dll
同目录即可
部分代码如下(想看更详细使用方式,,请查看serving 使用方式或者查看api.py
文档):
import os
import cv2
import pickle
import numpy as np
import os.path as oph
from base.utils import run_time
from base.log import logger
from base.config import settings
from api.faceapi import SeetaFace
from models.models import FaceInfo
from index import IndexManager
class FaceAPI:
def __init__(self):
# ------------------initial----------------
self.FACE_LIB_DIR = os.path.join(settings.BASE_DIR, "facelib")
self.FACE_IMAGE_DIR = os.path.join(self.FACE_LIB_DIR, "images")
self.INDEX_DIR = os.path.join(self.FACE_LIB_DIR, "index")
self.FACE_LIBS_FILE = os.path.join(self.FACE_LIB_DIR, "facelib.pkl")
# 初始化项目路径
self.initial_directory()
# 初始化人脸特征库对象
self.FACE_FEATURE_LIBS = dict()
# 初始化索引
self.index_manager = IndexManager(self.INDEX_DIR)
# 声明seetaFace对象
self.seetaFace = None
self.init_model()
def initial_directory(self):
if not os.path.exists(self.FACE_LIB_DIR):
os.mkdir(self.FACE_LIB_DIR)
if not os.path.exists(self.FACE_IMAGE_DIR):
os.mkdir(self.FACE_IMAGE_DIR)
if not os.path.exists(self.INDEX_DIR):
os.mkdir(self.INDEX_DIR)
@run_time
def init_model(self):
model_dir = os.path.join(settings.BASE_DIR, "model")
if not os.path.exists(model_dir):
logger.error("模型路径不存在,请先将模型所在文件夹【model】放置于,启动文件同级目录下")
exit(-1)
device = 2 if settings.USE_GPU else 1
self.seetaFace = SeetaFace(settings.FUNCTIONS, device=device, id=settings.GPU_ID)
self.seetaFace.SetTrackResolution(*settings.TRACKING_SIZE)
self.seetaFace.init_engine(model_dir)
self.initial_face_libraries_sub()
def get_seetaface(self):
return self.seetaFace
def initial_face_libraries_sub(self):
if not os.path.exists(self.FACE_LIBS_FILE):
return
with open(self.FACE_LIBS_FILE, "rb") as f:
self.FACE_FEATURE_LIBS = pickle.load(f)
# 初始化阶段构建索引库
self.index_manager.build_index(self.FACE_FEATURE_LIBS)
@run_time
def register_face_sub(self, face, uid: str, name: str = None):
if isinstance(face, np.ndarray):
img = face.copy()
elif isinstance(face, str):
if face.split(".")[-1].lower() in settings.ALLOW_IMAGES:
img = cv2.imread(face)
else:
logger.error(f"仅支持{str(settings.ALLOW_IMAGES)}图像格式文件")
return -1
else:
logger.error("图像格式必须是np.ndarray类型或str类型")
return -1
det_result = self.seetaFace.Detect(img)
if det_result.size == 0:
logger.warning("未检测到人脸,请确保画面中存在人脸...")
return -1
face = det_result.data[0].pos
points = self.seetaFace.mark5(img, face)
feature = self.seetaFace.Extract(img, points)
# update face libraries
self.FACE_FEATURE_LIBS.update({uid: {"name": name, "feature": self.seetaFace.get_feature_numpy(feature)}})
# 保存并覆盖
with open(self.FACE_LIBS_FILE, "wb") as f:
pickle.dump(self.FACE_FEATURE_LIBS, f)
# ------------------ 重新构建索引-------------------
self.index_manager.build_index(self.FACE_FEATURE_LIBS)
img_url = "/facelib/images/" + uid + ".jpg"
img_path = oph.join(self.FACE_IMAGE_DIR, uid + ".jpg")
# 判断人脸是否存在,存在
face = await FaceInfo.filter(uid=uid).first()
if face is None:
await FaceInfo.create(uid=uid, name=name, face_path=img_path, face_url=img_url)
else:
await FaceInfo.filter(uid=uid).update(name=name, face_path=img_path, face_url=img_url)
# 保存照片
cv2.imwrite(img_path, img)
return 0
@run_time
def delete_face_sub(self, uid: str):
face = await FaceInfo.filter(uid=uid).first()
if face is not None:
# 删除数据库记录
await FaceInfo.filter(uid=uid).delete()
# 删除缓存
face_info = self.FACE_FEATURE_LIBS.pop(uid)
# 删除图片
if oph.exists(face.face_url):
os.remove(face.face_url)
# 保存人脸库
with open(self.FACE_LIBS_FILE, "wb") as f:
pickle.dump(self.FACE_FEATURE_LIBS, f)
# ------------------ 重新构建索引-------------------
self.index_manager.build_index(self.FACE_FEATURE_LIBS)
logger.info("删除人脸成功, uid: " + uid + "name: " + face_info["name"])
return 0
else:
logger.warning("人脸不存在,uid :" + uid)
return -1
@run_time
def face_recognize_sub(self, img: np.ndarray):
det_result = self.seetaFace.Detect(img)
if det_result.size == 0:
logger.warning("未检测到人脸...")
return
# 仅识别画面中最大的人脸
face_areas = []
for i in range(det_result.size):
face_data = det_result.data[i]
face = face_data.pos
face_areas.append(face.width * face.height)
# 排序
face_areas.sort(reverse=True)
# 选取最大的人脸
face_data = det_result.data[0]
face = face_data.pos
points = self.seetaFace.mark5(img, face)
# 0: 真实人脸
# 1: 攻击人脸(假人脸)
# 2: 无法判断(人脸成像质量不好)
if settings.IS_ANTI_SPOOF:
livnees = self.seetaFace.Predict(img, face, points)
if livnees == 1:
logger.warning("攻击人脸...")
return -2
feature = self.seetaFace.Extract(img, points)
# 向量查找[HNSW32]以空间换时间,召回率高
ret = self.index_manager.search_result(self.seetaFace.get_feature_numpy(feature))
if ret is None:
logger.warning("人脸索引异常...")
return -1
distances, indexs = ret
# 阈值
if distances[0][0] > settings.REC_THRESHOLD:
uid = str(indexs[0][0])
# 比如uid为"023"经过索引后变成23这样会找不到id
if uid not in self.FACE_FEATURE_LIBS.keys():
logger.warning("当前人脸不在人脸库中, 人脸库可能出现混乱, 请重新梳理人脸库, 或者人脸id异常,注意:【人脸id必须是整形,并且,不能是以0开头】")
return -1
name = self.FACE_FEATURE_LIBS[uid]["name"]
return uid, name
else:
logger.warning("当前人脸不在人脸库中")
return -1
def age_predict(self, image, points):
age = self.seetaFace.PredictAgeWithCrop(image, points)
return age
def unload_engine(self):
self.seetaFace.unload_engine()