本文基于虹软人脸服务器版SDK(Linux Pro和ARM Pro)(后续称人脸SDK),就视频和图片2种模式,对人脸多线程应用框架进行了讨论,并对SDK接入过程中的一些开发注意项以及性能优化点做了总结分析,旨在交流讨论服务器环境下,常见人脸开发业务的处理流程和设计思想。虹软人脸SDK服务器版本提供了人脸检测、人脸识别、人脸跟踪、人脸比对、人脸属性、人脸质量检测、RGB/IR活体等功能,提供了C/C++/Java版本接口,能够满足众多场景下的人脸业务需求。
本文主要参考的链接为:
官方服务器版本源码链接:开发者中心-虹软视觉开放平台
知乎博文参考链接:(31 封私信 / 6 条消息) 在Spring Boot项目中集成虹软Linux Pro SDK实现人脸1:N对比服务(极简版,快速上手) - 知乎
一、不同场景下人脸业务多线程架构
服务器版的人脸SDK,根据场景功能需求分为图片和视频流两种模式,具体选择哪种方式,需要综合前端硬件能力、网络情况、业务需求等来决定。
1.1 图片模式下人脸业务多线程架构
对于使用图片的场景,一般满足以下一些特征之一:前端设备具备图片上传能力、业务并发程度相对较高;环境网络相对不稳定、实时性要求不高。常见的场景有:考勤、门禁、VIP客户精准识别等。
以门禁考勤场景为例,前端设备是抓拍相机,整体框架结构如下:
图 1-1:图片模式人脸业务框架
在上图中,网络协议常见的有http/tcp等,报文内容根据实际业务内容自定义。数据流形式是:前端设备上传人脸照片;服务器接收照片,完成人脸检测业务,通常服务器要处理来自N个设备的并发请求。
1.2 视频流模式下人脸业务多线程架构
对于使用视频流的场景,一般前端设备是网络相机,并且这些设备不具备抓拍能力,需要依赖服务器来完成人脸检测、识别等流程。另外,在视频模式下,对网络要求相对较高,因为需要接入多路视频流。输入视频流常见的场景有:园区老旧摄像头改造接入人脸识别能力、服务器端需要人脸跟踪显示等。
以安防监控场景为例,前端是IPC相机,整体框架结构如下:
图 1-2:视频模式人脸业务框架
在上图中,不同于输入为图片的情况,框架中增加了RTSP取流和推流线程,推流线程中的视频流一般会自定义加入一些OSD信息显示,例如人脸框等。人员操作任务通常处理前端的人员操作请求:人脸增删改查,使用方法同上面的图片模式。在视频流模式下,可以根据业务功能需求,设计2个引擎池:FT(人脸跟踪)引擎池、通用引擎池(完成人脸识别、属性获取等)。这里设计2个引擎池的目的是:降低内存消耗,人脸SDK初始化掩码不同,内存会存在差异,后面会详细提到。
1.3 小结
从人脸SDK多线程使用角度,图片和视频模式两者差异不是很大,前者处理的是一张张来自不同前端设备的单独图片,每个任务是独立的;后者接收的是来自多台相机的码流数据,需要额外关注的是人脸跟踪,以及视频流图片抽帧(因为服务器处理的能力有限)。但是,对于SDK来说,最终期望处理的都是一张质量相对较高的人脸图片。
因此,这里结合2种模式下实际接入过程中经验,给出人脸SDK接入过程的一些注意点:
1)多线程调用SDK时,需要考虑资源竞争和同步问题,对于同一个引擎句柄,目前的人脸SDK暂不支持在不同线程中,同时调用同一个人脸处理接口(例如:人脸特征提取)
2)人脸SDK支持创建多个引擎句柄来加快业务处理速度,但是,不同引擎句柄之间,特征数据是不共享的,多引擎加速,需要注意引擎之间数据同步
3)为简化特征管理,加快人脸1VN比对速度,人脸SDK中提供了特征管理接口,但是,SDK特征是存储在内存中,特征操作时,需要注意同步到本地数据库中
图 1-3:不同句柄中人脸特征数据和本地数据库关系
上述的注意项,大致可以总结为多线程下人脸SDK引擎管理、以及特征数据管理问题。本文将在后续章节针对这些点,给出自己的处理思路,并针对现有的SDK接口,结合实际业务给出一些性能优化技巧(人脸注册和人脸识别业务性能优化)
二、SDK接口安全调用及资源管理
2.1 如何安全管理SDK引擎
在实际的人脸业务中,为加速业务处理速度,会使用多引擎的方式来并行处理业务。安全的管理多引擎句柄,最方便的方法就是使用引擎池。引擎池提供了线程安全的方法来保证引擎句柄的独占,解决竞争问题。
本文这里仅给出java里面现成的方法类:GenericObjectPool,后续将基于这个来描述引擎管理。
2.1.1 引擎创建
在人脸SDK中,创建引擎时,可以按需初始化对应功能的掩码,初始化不同的掩码,对应引擎所占用的内存也会存在差异。例如:视频模式下,在人脸检测线程中,对应的引擎只需要初始化ASF_FACE_DETECT掩码。下表给出了初始化全部掩码子功能和只初始化FD的内存占用情况对比,可以发现掩码初始化和内存占用有强相关。
表 2-1:不同初始化掩码内存占用对比
| 模块 | 内存 |
|---|---|
| ASF_FACE_DETECT/ASF_FACERECOGNITION/ASF_AGE/ASF_GENDER/ ASF_LIVENESS/ASF_IR_LIVENESS/ASF_MASKDETECT/ASF_IMAGEQUALITY | 192MB |
| ASF_FACE_DETECT | 66MB |
如下代码给出了输入为视频流模式下的引擎初始化过程。
private void init() {
GenericObjectPoolConfig<FaceEngine> detectPoolConfig = new GenericObjectPoolConfig<>();
detectPoolConfig.setMaxIdle(detectPoolSize);
detectPoolConfig.setMaxTotal(detectPoolSize);
detectPoolConfig.setMinIdle(detectPoolSize);
detectPoolConfig.setLifo(false);
EngineConfiguration detectCfg = new EngineConfiguration();
FunctionConfiguration detectFunctionCfg = new FunctionConfiguration();
detectFunctionCfg.setSupportFaceDetect(true);//开启人脸检测功能
detectFunctionCfg.setSupportAge(true);//开启年龄检测功能
detectFunctionCfg.setSupportGender(true);//开启性别检测功能
detectFunctionCfg.setSupportLiveness(true);//开启活体检测功能
detectCfg.setFunctionConfiguration(detectFunctionCfg);
detectCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);//图片检测模式,如果是连续帧的视频流图片,那么改成VIDEO模式
detectCfg.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);//人脸旋转角度
detectCfg.setFaceModel(faceModel);
generalPool = new GenericObjectPool<>(new FaceEngineFactory(appId, sdkKey, activeKey, activeFile, detectCfg), detectPoolConfig);//底层库算法对象池
GenericObjectPoolConfig<FaceEngine> detectFtPoolConfig = new GenericObjectPoolConfig<>();
detectFtPoolConfig.setMaxIdle(detectPoolSize);
detectFtPoolConfig.setMaxTotal(detectPoolSize);
detectFtPoolConfig.setMinIdle(detectPoolSize);
detectFtPoolConfig.setLifo(false);
EngineConfiguration detectFtCfg = new EngineConfiguration();
FunctionConfiguration detectFtFunctionCfg = new FunctionConfiguration();
detectFtFunctionCfg.setSupportFaceDetect(true);//开启人脸检测功能
detectFtCfg.setFunctionConfiguration(detectFtFunctionCfg);
detectFtCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_VIDEO);//视频检测模式
detectFtCfg.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);//人脸旋转角度
detectFtCfg.setFaceModel(faceModel);
//底层库算法对象池
ftPool = new GenericObjectPool<>(new FaceEngineFactory(appId, sdkKey, activeKey, activeFile, detectFtCfg), detectFtPoolConfig);
}
2.1.2 引擎使用
Java类GenericObjectPool中提供了borrowObject和returnObject接口,来实现引擎资源的独占和释放,具体调用方法示例如下:
public UserCompareInfo faceRecognition(byte[] faceFeature, float passRate) {
FaceEngine faceEngine = null;
try {
faceEngine = faceEngineComparePool.borrowObject();
FaceFeature searchfaceFeature = new FaceFeature();
SearchResult searchResult = new SearchResult();
searchfaceFeature.setFeatureData(faceFeature);
int errorCode = faceEngine.searchFaceFeature(searchfaceFeature, searchResult);
if (errorCode == 0) {
if (searchResult.getMaxSimilar() >= passRate) {
UserCompareInfo userCompareInfo = new UserCompareInfo();
userCompareInfo.setId(searchResult.getFaceFeatureInfo().getSearchId());
userCompareInfo.setSimilar(searchResult.getMaxSimilar());
userCompareInfo.setName(searchResult.getFaceFeatureInfo().getFaceTag());
return userCompareInfo;
}
}
} catch (Exception e) {
log.error("", e);
} finally {
if (faceEngine != null) {
faceEngineComparePool.returnObject(faceEngine);
}
}
return null;
}
为便于理解,下面给出了2种模式下,引擎的使用流程框图。
图片模式
在图片模式下,以人脸注册业务为例,引擎使用流程如下。
图 2-1:图片模式引擎调用流程
视频模式
在视频模式下,这里创建了2个引擎池来处理不同的业务,分别是:通用引擎池和FT引擎池。FT引擎池用于实现多路视频流的人脸跟踪,输出人脸框信息;通用引擎池用于人脸比对识别等通用业务。特别说明下,由于服务器的性能有限,对取流到的视频帧需要按需抽帧,降低服务器压力。
图 2-1:视频模式引擎调用流程
2.1.3 引擎销毁
本文使用引擎池对所用到的引擎进行统一管理,销毁时,使用getObject安全拿到引擎对象。此时,引擎池其它接口无法拿到该对象,保证销毁对象时是线程安全的。
关于销毁时机,可以是用户程序主动销毁引擎池中引擎;也可以是引擎池根据设置的最大/最小引擎数量,动态销毁空闲对象。但无论哪种方式,上层必须要通过引擎池提供的线程安全接口,才能够拿到引擎对象。
下面代码是引擎销毁的代码,destroyObject为BasePooledObjectFactory类的重载函数,会在引擎池对象销毁时,自动调用。
@Override
public void destroyObject(PooledObject<FaceEngine> p) throws Exception {
allObjects.remove(p);
FaceEngine faceEngine = p.getObject();
int result = faceEngine.unInit();
super.destroyObject(p);
}
2.2 人脸特征数据管理
实际业务中,人脸特征数据会被持久化存储在服务器的数据库中,而人脸SDK中的特征位于内存中,因此需要注意:数据库中特征值和人脸SDK中特征值之间的同步、多引擎句柄之间的数据同步。
人脸数据库和SDK之间数据同步
数据库和SDK之间的特征数据同步过程发生在以下几种情况:
1、应用程序起来之后,数据库中人脸特征数据需要同步到人脸SDK中
2、SDK发生人脸注册、删除、更新事件时,对应特征需要同步到数据库中
下面是应用程序起来之后,读取数据库人脸信息,调用SDK注册接口将本地数据库中的特征数据注册到内存中参考代码。
//初始化注册人脸,注册到本地内存
public void initSystemFace() {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
org.springframework.core.io.Resource[] resources = resolver.getResources("classpath*:static/images/*"); // 递归获取 static 目录下的所有文件
for (org.springframework.core.io.Resource resource : resources) {
InputStream inputStream = resource.getInputStream();
register(resource.getFilename(), null, inputStream);
}
} catch (IOException e) {
log.error("", e);
}
}
多引擎数据同步
多引擎句柄特征数据是独立的,因此,当使用某个引擎调用人脸注册、更新和删除接口时,特征数据需要同步到引擎池所有引擎中。另外,人脸SDK中人员操作接口(注册、删除、更新)是内部线程安全的。这里给出注册和删除函数的代码。
public void registerFaceFeature2Engine(int searchId, String tag, byte[] faceFeature) {
FaceEngineFactory factory = (FaceEngineFactory) faceEngineComparePool.getFactory();
List<FaceEngine> allObjects = factory.getAllObjects();
FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo();
faceFeatureInfo.setSearchId(searchId);
faceFeatureInfo.setFaceTag(tag);
faceFeatureInfo.setFeatureData(faceFeature);
log.info("Register:"+ JSON.toJSONString(faceFeatureInfo));
for (FaceEngine allObject : allObjects) {
int res = allObject.registerFaceFeature(faceFeatureInfo);
}
}
public void removeFaceFeature2Engine(int searchId) {
FaceEngineFactory factory = (FaceEngineFactory) faceEngineComparePool.getFactory();
List<FaceEngine> allObjects = factory.getAllObjects();
for (FaceEngine allObject : allObjects) {
allObject.removeFaceFeature(searchId);
}
}
三、性能优化
在使用人脸SDK开发时,考虑到服务器的性能,结合实际业务,个人认为在人脸注册、人脸识别两方面可以针对性做一些优化提升,优化的角度是:低质量人脸过滤、人脸分组提高识别效率。
3.1 低质量人脸过滤
在SDK中,提供了imageQualityDetect人脸质量接口,用于判断当前的人脸信息是否满足质量阈值。如果不满足,可以取消后续的操作。
在人脸SDK中,人脸特征提取接口相对耗时较长,对于人脸注册和识别业务,如果不做人脸质量筛查,会导致大量的图片停留在特征提取过程中。在个人设备(Intel i7-10700CPU@2.9GHZ)上,测试特征提取接口耗时数据如下:
表 3-1:人脸SDK特征提取耗时
| 证件照 | 大模型 | 均值(ms) |
|---|---|---|
| 注册照 | 大模型 | 548 |
| 识别照 | 大模型 | 269 |
| 注册照 | 中模型 | 130 |
| 识别照 | 中模型 | 77 |
因此,在人脸注册和识别业务中,应该加上人脸质量检测接口过滤掉一些人脸质量较差的照片,减少服务器性能消耗。对于人脸质量评分阈值,人脸SDK中建议:RGB识别照,人脸质量分阈值为0.49;注册照质量阈值为0.63。
3.2 人脸识别业务性能优化
目前官方提供的SDK有3种人脸比对接口:compareFaceFeature、searchFaceFeature,具体接口用法可以参考官方文档。
在大模型中,自测在1万人脸底库下,searchFaceFeature和compareFaceFeature性能数据如下:(Intel i7-10700CPU@2.9GHZ)
表 3-2:人脸识别接口1万底库耗时对比
| 接口 | 均值(ms) |
|---|---|
| searchFaceFeature | 2.473 |
| compareFaceFeature | 10 |
可见在1万底库下,人脸SDK提供的searchFaceFeature接口的性能已经是很好了,对于1万以下的人脸底库场景,接口性能绰绰有余;compareFaceFeature接口性能相对较差,因为该接口通常用于1:1场景的人脸比对,不太适合1:N场景的人脸比对。
而对于更大的人脸底库场景,比如几十万人脸底库场景,searchFaceFeature接口的耗时会相应增加。如果希望性能有更大的提升,建议对人脸底库进行分组,以公司场景为例,可以按照部门分组,将不同部门的人脸特征注册在不同的人脸句柄中,然后多线程调用多个引擎句柄并发调用searchFaceFeature接口,然后排序输出最大的人脸识别分数。
图 3-1:人脸分组比对调用流程
四、总结
本文基于虹软服务器版SDK(Linux/ARM Pro),就接入过程中的一些注意点和思考点进行了分享,包括:
1、探讨了图片和视频模式下,服务器程序的多线程框架;
2、提出使用GenericObjectPool构建引擎池,增强对人脸引擎的多线程管理,避免多线程调用过程中,由于竞争导致的数据不同步和异常crash问题;
3、就人脸SDK的特征管理方式,给出了人脸特征管理方法;
4、对人脸SDK的部分接口做了性能测试,推荐使用SDK提供的人脸质量检测接口过滤低质量图片,降低服务器性能消耗
5、提出使用人脸分组的策略,加速人脸比对速度
人脸服务器业务远不止文章中提到的这些,多线程程序设计时,应该充分考虑线程资源竞争和同步。