在使用过程中,发现还是应该写一个demo,这样才更好入门,今天要做的就是这个demo:使用FastAPI来部署一个人脸识别引擎。
01 关于人脸识别引擎
人脸识别大体上分成三个步骤:
- 人脸检测(GPU算法)
- 人脸对齐(CPU算法)
- 人脸识别(GPU算法)
刚开始的时候,引擎只提供一个接口,同时包含上面三个功能,但是这其实是不合理的,因为在我们的场景中人脸检测的使用比人脸识别的概率多很多,因为摄像头中很多帧图像可能都是没有人脸的。所以,在做架构的时候,我们拆成了两个接口:
- 人脸检测与人脸对齐
- 人脸识别
整体架构:
人脸检测和人脸识别都会被部署成多个服务,然后使用Nginx来做负载均衡。
02 FastAPI接口层的主要文件介绍
接口层相关的文件其实主要是两个:
- main.py: FastAPI的主入口文件,检测和识别接口就定义在这个文件里;
- main_settings.py: 接口参数配置文件,如输入输出参数等。
当然比较相关的文件还有一个项目的配置文件settings.py。
03 主入口:main.py
这是FastAPI的主入口:
代码语言:javascript复制# 详见:https://github.com/IBBD/docs/blob/main/demo/FastAPI/main.py
DESCRIPTION = """
主要实现功能:
- 人脸检测
- 人脸识别
### 接口列表:
"""
DET_URL = '/images/detect'
REG_URL = '/images/recognize'
if det_args['open']:
DESCRIPTION = '- 人脸检测 %sn' % DET_URL
if reg_args['open']:
DESCRIPTION = '- 人脸识别 %sn' % REG_URL
DESCRIPTION = """n
### 使用说明
1. 将图像转为base64格式
可以看这里:https://github.com/ibbd-dev/python-image-utils/blob/master/image_utils/convert.py ,方法`pil_base64`或者`cv2_base64`
2. 测试代码: 看[这里](https://github.com/IBBD/docs/blob/main/tests/人脸识别引擎测试.ipynb)。
"""
# 版本号
VERSION = '0.8'
app = FastAPI(
title="人脸检测与识别引擎",
description=DESCRIPTION,
version=VERSION,
)
if det_args['open']:
@app.post(DET_URL, response_model=List[DetItem],
summary='人脸检测')
async def api_detect(params: DetParams):
"""输入多个图像,返回人脸box及其关键点
说明:一个图像可能会识别到多个人脸
"""
if len(params.images) == 0:
return []
params.images = conc_map(base64_cv2, params.images,
max_workers=cpu_workers)
res = images_detect(**dict(params))
for img_data in res: # 格式化返回值
img_data['faces'] = conc_map(cv2_base64, img_data['faces'],
max_workers=cpu_workers)
img_data['scores'] = list(img_data['scores'])
img_data['boxes'] = [list(b) for b in img_data['boxes']]
img_data['landmarks'] = [[list(l) for l in lmk]
for lmk in img_data['landmarks']]
return res
if reg_args['open']:
@app.post(REG_URL, response_model=List[RegItem],
summary='人脸识别')
async def api_recognize(params: RegParams):
"""人脸识别,返回人脸特征向量,每个人脸图像会对应一个特征向量
每个特征向量是一个多维的列表
"""
if len(params.faces) == 0:
return []
params.faces = conc_map(base64_cv2, params.faces,
max_workers=cpu_workers)
res = images_recognize(**dict(params))
for data in res: # 格式化返回值
data['embedding'] = data['embedding'].tolist()
return res
我们部署的时候,部署成多个容器服务,但是我们代码实现还是在同一个项目里面的,所以我们允许在配置文件里配置该启动什么接口。检测与识别主要功能在函数images_detect和images_recognize中实现,而在接口层中,主要实现的是对输入参数的格式化,和对输出数据的规范化。
如果实现的接口比较多,则可以将接口分拆到不同的文件里。展示的效果大概如下:
在接口文档的这部分,应该将接口的基本情况说清楚。
04 接口参数配置
我们的接口参数都是分拆出来,实现在一个独立的文件里:
代码语言:javascript复制# 详见:https://github.com/IBBD/docs/blob/main/demo/FastAPI/main_settings.py
# 人脸关键点的样例
landmarks_example = [
[30.2946, 51.6963],
[65.5318, 51.5014],
[48.0252, 71.7366],
[33.5493, 92.3655],
[62.7299, 92.2041]
]
class DetParams(BaseModel):
"""检测参数"""
images: List[str] = Field(..., title='待检测的图像列表',
description='base64格式。每个图像可能会识别到多个人脸')
threshold: float = Field(det_args['threshold'], ge=0.,
title='人脸检测的得分阈值',
description='可以使用该值过滤掉一些质量比较差的人脸')
align: bool = Field(det_args['align'], title='人脸是否进行对齐',
description='若为true,则需要对返回的人脸进行对齐。')
class DetItem(BaseModel):
"""单个图像的检测结果"""
faces: List[str] = Field(..., title='识别到的人脸图像列表',
description='base64格式')
boxes: List[List[float]] = Field(..., example=[[10, 11, 15, 16]],
title='人脸图像在原图像中的坐标',
description='注意这里的坐标是相对于输入图像的。坐标格式:[x1, y1, x2, y2]')
scores: List[float] = Field(..., example=[0.995],
title='人脸的得分值',
description='该值越大,人脸质量通常越好。前端可以根据该值做进一步的过滤')
landmarks: List[List[List[float]]] = Field(..., example=[landmarks_example],
title='人脸图像的关键点坐标',
description='每个人脸有关键点共5个,注意这里的坐标是相对于输入图像的。坐标格式:[[x1, y1], [x2, y2], [x3, y3], [x4, y4], [x5, y5]]')
class RegParams(BaseModel):
"""识别参数"""
faces: List[str] = Field(..., title='待识别的人脸图像列表',
description='base64格式')
class RegItem(BaseModel):
"""单个人脸图像的识别结果"""
embedding: List[float] = Field(..., title='人脸特征向量',
description='每个人脸对应一个特征向量')
前面的文件也介绍过,每一个参数都应该有一些必要的属性,如:
- 默认值:如果在请求的时候,不传该值,则会使用默认值。如果是三个点,则表示会该参数是必须的。
- 样例值example:这个值只是展示在界面上,注意和默认值区分。
- 标题title
- 描述description
- 还有一些参数校验的,如限制取值范围的:ge, le, gt, lt等
我们看一下人脸检测接口输入参数的文档效果:
其他的部分就不展示了。
05 小结
FastAPI最大的好处是能保持代码与文档的一致性,根据历史经验,以前文档和代码分离的时候,经常出现文档和代码不一致,文档明明这样说,但是请求接口时却掉进了坑里。
所以,我们在使用FastAPI来部署接口的时候,应该要养成习惯,在代码里写好文档。