开发环境配置
SDK版本:ArcSoft_ArcFacePro_linuxPro_V1.0
开发环境:ubuntu2004
编辑器:vscode
编译工具:g++、cmake
依赖第三方库:OpenCV、SQLite、jsoncpp
$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.2' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-9QDOt0/gcc-9-9.4.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2)
$ cmake --version
cmake version 3.16.3
CMake suite maintained and supported by Kitware (kitware.com/cmake).
安装ffmpeg
sudo apt install libavutil-dev libavformat-dev libavcodec-dev
整体流程
一、人脸注册
人脸注册作为人脸识别重要的一环,需要严格控制注册的人脸图片,例如图像的清晰度、人脸大小、人脸角度、人脸部分是否遮挡(口罩)等。
另外,大量的静态图片解码,人脸检测是比较耗时的,因此注册的人脸需要将特征值存储到数据库中,程序启动后率先将数据库中的人脸加载到算法句柄中,再去处理未注册到数据库中的人脸。
数据库设计如下(以SQLite为例):
字段 | 类型 | 描述 |
---|---|---|
id | INTEGER | 自增 ID,用于算法的 searchId, 用于查找人脸搜索到的注册人员完整信息 |
token | TEXT | 注册图md5,用于注册图唯一标识,防止同一张图片多次注册(根据图片md5去重);根据应用需要,如果每个人只能注册一张图片,在注册人脸时,可以先做人脸搜索,当搜索到的top1相似度高于某个阈值时,可以让用户选择是否入库 |
image | TEXT | 注册图路径 |
desc | TEXT | 注册图相关描述信息 |
feature | TEXT/BLOB | 人脸特征信息 |
人脸表如下:
CREATE TABLE IF NOT EXISTS face_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT(50) NOT NULL,
image TEXT(256) NOT NULL,
desc TEXT(512),
feature BLOB
);
此处仅用于静态图片人脸注册,示例如下:
#define MIN_FACE_SIZE (80)
#define MAX_REGISTER_YAW (10)
#define MAX_REGISTER_ROLL (10)
#define MAX_REGISTER_PITCH (10)
bool register_face(MHandle handle, const std::string &path)
{
bool ret = false;
do {
if (path.empty()) {
break;
}
// 计算md5
std::string md5 = calc_md5(path);
if (md5.empty()) {
std::cerr << "calc md5 failed" << std::endl;
break;
}
auto db_handle = FaceDB::getInstance();
if (!db_handle) {
std::cerr << "FaceDB::getInstance failed" << std::endl;
break;
}
// 判断人脸是否已经注册
if (db_handle->is_exist(md5)) {
std::cerr << "Face has been registered" << std::endl;
break;
}
cv::Mat image = cv::imread(path);
if (image.empty()) {
std::cerr << "Error: Could not read image." << std::endl;
break;
}
ASVLOFFSCREEN offscreen = { 0 };
ColorSpaceConversion(image.cols, image.rows, ASVL_PAF_RGB24_B8G8R8, image.data, offscreen);
// 人脸检测
ASF_MultiFaceInfo detectedFaces = { 0 };
MRESULT res = ASFDetectFacesEx(handle, &offscreen, &detectedFaces);
if (res != MOK) {
printf("ASFDetectFacesEx fail: %ld\n", res);
break;
}
// 人脸检测结果
if (detectedFaces.faceNum < 0) {
printf("No face detected: %s.\n", path.c_str());
break;
} else if (detectedFaces.faceNum > 1) { // 此处设计的比较严谨,不允许多人脸的图片注册(可以分为两种模式,图片录入,视频录入, 视频录入时以最大人脸为准)
printf("More than one face detected: %s.\n", path.c_str());
break;
}
// 判断人脸大小
int width = detectedFaces.faceRect[0].right - detectedFaces.faceRect[0].left;
int height = detectedFaces.faceRect[0].bottom - detectedFaces.faceRect[0].top;
if (width < MIN_FACE_SIZE || height < MIN_FACE_SIZE) {
printf("Face size is too small: %s.\n", path.c_str());
break;
}
// 判断人脸角度
if (std::abs(detectedFaces.face3DAngleInfo.yaw[0]) > MAX_REGISTER_YAW ||
std::abs(detectedFaces.face3DAngleInfo.pitch[0]) > MAX_REGISTER_PITCH ||
std::abs(detectedFaces.face3DAngleInfo.roll[0]) > MAX_REGISTER_ROLL) {
printf("Face angle is too large: %s.\n", path.c_str());
break;
}
// 判断是否戴口罩
res = ASFProcessEx(handle, &offscreen, &detectedFaces, ASF_MASKDETECT);
if (res != MOK) {
printf("ASFProcessEx failed: %ld\n", res);
break;
}
// 获取是否戴口罩
ASF_MaskInfo maskInfo = { 0 };
res = ASFGetMask(handle, &maskInfo);
if (res != MOK || maskInfo.num < 0){
printf("ASFGetMask failed: %ld\n", res);
break;
}
if (maskInfo.maskArray[0] != 0) {
printf("Face is wearing a mask.\n");
break;
}
ASF_SingleFaceInfo faceInfo = { 0 };
faceInfo.faceRect.left = detectedFaces.faceRect[0].left;
faceInfo.faceRect.top = detectedFaces.faceRect[0].top;
faceInfo.faceRect.right = detectedFaces.faceRect[0].right;
faceInfo.faceRect.bottom = detectedFaces.faceRect[0].bottom;
faceInfo.faceOrient = detectedFaces.faceOrient[0];
faceInfo.faceDataInfo = detectedFaces.faceDataInfoList[0];
MFloat confidenceLevel = 0.0f;
res = ASFImageQualityDetectEx(handle, &offscreen, &faceInfo, maskInfo.maskArray[0], &confidenceLevel);
if(MOK != res){
printf("ASFImageQualityDetectEx failed: %ld\n", res);
break;
}
printf("ASFImageQualityDetectEx sucessed: %f\n", confidenceLevel);
ASF_FaceFeature feature = { 0 };
// 第一张认为是注册照,注册照要求不戴口罩
res = ASFFaceFeatureExtractEx(handle, &offscreen, &faceInfo, ASF_REGISTER, 0, &feature);
if (res != MOK){
printf("%s ASFFaceFeatureExtractEx fail: %ld\n", path.c_str(), res);
break;
}
size_t pos = path.find_last_of("/");
std::string newpath = "../data/face_library/" + path.substr(pos + 1);
auto search_id = FaceDB::getInstance()->add_face(md5, newpath, newpath, (char *)feature.feature, feature.featureSize);
if (search_id < 0) {
printf("add face to db failed: %s\n", path.c_str());
break;
}
ASF_FaceFeatureInfo faceFeatureInfo = {0};
faceFeatureInfo.searchId = search_id;
faceFeatureInfo.feature = &feature;
res = ASFRegisterFaceFeature(handle, &faceFeatureInfo, 1);
if (res != MOK) {
printf("ASFRegisterFaceFeature fail: %ld\n", res);
}
rename(path.c_str(), newpath.c_str());
ret = true;
} while (0);
if (!ret) {
// 注册失败,移动到invalid_face_library
std::string newpath = "../data/invalid_face_library/" + path.substr(path.find_last_of("/") + 1);
rename(path.c_str(), newpath.c_str());
}
return ret;
}
应用运行的过程中,允许人脸注册,因此起一个线程用于人脸注册。该线程监听指定的目录,当发现目录中有图片时,便进行人脸注册。
由于搜索和注册要用同一个引擎,因此在每个搜索线程中只加载已经注册到数据库中的人脸特征。在人脸注册线程中只将人脸信息插入到数据库中,上述函数的ASFRegisterFaceFeature操作可以去掉。实时注册的人脸特征需要重启应用才能生效(后续通过函数实时同步到每个识别线程)
void register_face_thread(const std::string &path)
{
//初始化引擎
MHandle handle = NULL;
MInt32 mask = ASF_FACE_DETECT | ASF_FACERECOGNITION |
ASF_LIVENESS | ASF_IR_LIVENESS | ASF_MASKDETECT | ASF_IMAGEQUALITY;
MRESULT res = ASFInitEngine(ASF_DETECT_MODE_IMAGE, ASF_OP_0_ONLY, ASF_MAX_DETECTFACENUM, mask, ASF_REC_LARGE, &handle);
if (res != MOK) {
printf("ASFInitEngine fail: %ld\n", res);
return;
}
while (g_run) {
DIR *dir = nullptr;
if ((dir = opendir(path.c_str())) == nullptr) {
std::this_thread::sleep_for(std::chrono::seconds(10));
continue;
}
struct dirent *ptr = nullptr;
while ((ptr = readdir(dir)) != NULL && g_run) {
if (strcmp(ptr->d_name, ".") == 0 || strcmp(ptr->d_name, "..") == 0) {
continue;
}
std::string pic_path = path + "/" + std::string(ptr->d_name);
if (!register_face(handle, pic_path)) {
std::string newpath = "../data/invalid_face_library/" + std::string(ptr->d_name);
rename(pic_path.c_str(), newpath.c_str());
}
// 由于搜索和注册要用同一个引擎,因此在每个搜索线程加载已经注册到数据库中的人脸特征
// 此处监听人脸目录,实时从图片检测人脸,将符合条件的人脸信息注册到数据库。
// 注册的人脸特征需要重启应用才能生效(后续通过函数广播给每个识别线程)
}
closedir(dir);
}
// 反初始化
res = ASFUninitEngine(handle);
if (res != MOK) {
printf("ASFUninitEngine fail: %ld\n", res);
} else {
printf("ASFUninitEngine sucess: %ld\n", res);
}
}
二、人脸检测追踪
只要人脸不被遮挡,发生快速移动,视频模式检测到的人脸id是不会变化的。而角度大、质量差、人脸溢出等情况,无论是抓拍还是识别,都没有必要。因此需要过滤到角度过大、人脸溢出、比较模糊的人脸。将符合要求的人脸送去做人脸属性,人脸识别。
考虑到检测耗时,有必要时需要抽帧去人脸检测,全帧检可能来不及处理(抽帧帧率以应用支持的摄像头个数考量,可以作为参数,放在配置文件)
由于做人脸属性、人脸搜索比较耗时,因此将人脸检测和人脸属性及搜索放在不同的线程中处理。如下所示:
void StreamProcess::decode_thread()
{
// 读取视频文件
cv::VideoCapture cap(url_);
// 检查是否成功打开视频文件
if (!cap.isOpened()) {
std::cerr << "Error: Could not open video." << std::endl;
return;
}
// 创建一个窗口用于显示视频
// cv::namedWindow("Video", cv::WINDOW_AUTOSIZE);
MRESULT res = MOK;
//初始化引擎
MHandle handle = NULL;
MInt32 mask = ASF_FACE_DETECT | ASF_MASKDETECT | ASF_IMAGEQUALITY;
res = ASFInitEngine(ASF_DETECT_MODE_VIDEO, ASF_OP_0_ONLY, ASF_MAX_DETECTFACENUM, mask, ASF_REC_LARGE, &handle);
if (res != MOK) {
printf("ASFInitEngine fail: %ld\n", res);
return;
}
else
printf("ASFInitEngine sucess: %ld\n", res);
//设置活体置信度 SDK内部默认值为 IR:0.7 RGB:0.5(无特殊需要,可以不设置)
ASF_LivenessThreshold threshold = { 0 };
threshold.thresholdmodel_BGR = 0.5;
threshold.thresholdmodel_IR = 0.7;
res = ASFSetLivenessParam(handle, &threshold);
if (res != MOK)
printf("ASFSetLivenessParam fail: %ld\n", res);
else
printf("RGB Threshold: %f\nIR Threshold: %f\n", threshold.thresholdmodel_BGR, threshold.thresholdmodel_IR);
unsigned long frame_cnt = 0;
std::vector<face_info_t> track_res;
while (!decode_stop_) {
// 读取一帧视频
cv::Mat frame;
cap >> frame;
// 如果读取失败(到达视频末尾或出错)
if (frame.empty()) {
std::cout << "Video has ended or failed to open." << std::endl;
break;
}
if (frame_cnt++ % 2 == 0) { // 此处为2抽1,每2帧送1帧去做人脸检测。
ASVLOFFSCREEN offscreen = { 0 };
ColorSpaceConversion(frame.cols, frame.rows, ASVL_PAF_RGB24_B8G8R8, frame.data, offscreen);
track_res.clear();
track(handle, &offscreen, track_res);
}
for (size_t i = 0; i < track_res.size(); i++) {
// 定义矩形的顶点
cv::Point top_left(track_res[i].rect.left, track_res[i].rect.top); // 矩形的左上角
cv::Point bottom_right(track_res[i].rect.right, track_res[i].rect.bottom); // 矩形的右下角
// 定义矩形的颜色和线条粗细
cv::Scalar color(0, 255, 0); // 绿色
int thickness = 2; // 线条粗细
// 绘制矩形
cv::rectangle(frame, top_left, bottom_right, color, thickness);
}
// 显示当前帧
// cv::imshow("Video", frame);
// cv::waitKey(1); // 等待1毫秒,以控制帧率
frame.release();
}
// 释放资源
cap.release();
cv::destroyAllWindows();
// 反初始化
res = ASFUninitEngine(handle);
if (res != MOK) {
printf("ASFUninitEngine fail: %ld\n", res);
} else {
printf("ASFUninitEngine sucess: %ld\n", res);
}
std::cout << "exit decode thread" << std::endl;
}
需要注意:
- 创建引擎时应按需创建,只保留需要检测的项目,确保检测耗时最小化。在人脸检测的过程中,除了使用3D人脸角度来过滤检测结果外。还需要使用质量来进行过滤,是否戴口罩质量分别有不同的阈值,因此在检测的过程中,需要做质量检测、口罩检测。因此mask参数需要用到3个,ASF_FACE_DETECT | ASF_MASKDETECT | ASF_IMAGEQUALITY。
- 视频读取结束,退出线程时,应该反初始化引擎。
根据检测结果,通过3D角度、质量进行过滤,如下所示:
void StreamProcess::track(MHandle handle, LPASVLOFFSCREEN image_data, std::vector<face_info_t> &track_res)
{
track_res.clear();
ASF_MultiFaceInfo detectedFaces = { 0 };
ASF_MaskInfo maskInfo = { 0 }; //是否带口罩
MRESULT res = ASFDetectFacesEx(handle, image_data, &detectedFaces);;
if (res != MOK || detectedFaces.faceNum < 1)
{
return;
}
// 检测是否戴口罩
bool mask_detect_success = true;
res = ASFProcessEx(handle, image_data, &detectedFaces, ASF_MASKDETECT);
if (res != MOK){
printf("ASFProcessEx failed: %ld\n", res);
mask_detect_success = false;
}
// 获取是否戴口罩
res = ASFGetMask(handle, &maskInfo);
if (res != MOK){
printf("ASFGetMask failed: %ld\n", res);
mask_detect_success = false;
}
do {
LPASF_MultiFaceInfo face_info = (LPASF_MultiFaceInfo)malloc(sizeof(ASF_MultiFaceInfo));
multi_face_info_alloc(detectedFaces.faceNum, face_info);
int idx = 0;
for (int i = 0; i < detectedFaces.faceNum; i++) {
if (detectedFaces.faceRect[i].right - detectedFaces.faceRect[i].left < MIN_FACE_SIZE ||
detectedFaces.faceRect[i].bottom - detectedFaces.faceRect[i].top < MIN_FACE_SIZE) {
continue; // 人脸太小
}
if (std::abs(detectedFaces.face3DAngleInfo.yaw[i]) > MAX_TRACK_YAW ||
std::abs(detectedFaces.face3DAngleInfo.pitch[i]) > MAX_TRACK_PITCH ||
std::abs(detectedFaces.face3DAngleInfo.roll[i]) > MAX_TRACK_ROLL) {
continue; // 角度太大
}
if (detectedFaces.faceIsWithinBoundary[i] == 0) {
continue; // 人脸超出边界
}
ASF_SingleFaceInfo SingleDetectedFaces = { 0 };
SingleDetectedFaces.faceRect.left = detectedFaces.faceRect[i].left;
SingleDetectedFaces.faceRect.top = detectedFaces.faceRect[i].top;
SingleDetectedFaces.faceRect.right = detectedFaces.faceRect[i].right;
SingleDetectedFaces.faceRect.bottom = detectedFaces.faceRect[i].bottom;
SingleDetectedFaces.faceOrient = detectedFaces.faceOrient[i];
SingleDetectedFaces.faceDataInfo = detectedFaces.faceDataInfoList[i];
MFloat confidenceLevel = 0.0f;
MInt32 isMask = mask_detect_success && (1 == maskInfo.maskArray[i]);
res = ASFImageQualityDetectEx(handle, image_data, &SingleDetectedFaces, isMask, &confidenceLevel);
if(MOK != res){
printf("ASFImageQualityDetectEx failed: %ld\n", res);
continue;
}
if ((isMask && (confidenceLevel < 0.29)) || (!isMask && (confidenceLevel < 0.49))) {
continue; // 质量太差
}
if (detectedFaces.faceRect) {
// 将人脸坐标作为输出参数,外部可能需要将人脸框叠加在图像上
if (i >= track_res.size()) {
face_info_t face_info;
track_res.push_back(face_info);
}
track_res[i].rect.left = detectedFaces.faceRect[i].left;
track_res[i].rect.top = detectedFaces.faceRect[i].top;
track_res[i].rect.right = detectedFaces.faceRect[i].right;
track_res[i].rect.bottom = detectedFaces.faceRect[i].bottom;
}
// 将符合条件的人脸信息深拷贝并送到识别队列中
face_info->faceRect[idx] = detectedFaces.faceRect[i];
face_info->faceOrient[idx] = detectedFaces.faceOrient[i];
face_info->faceID[idx] = detectedFaces.faceID[i];
face_info->faceDataInfoList[idx].dataSize = detectedFaces.faceDataInfoList[idx].dataSize;
face_info->faceDataInfoList[idx].data = (MUInt8*)malloc(detectedFaces.faceDataInfoList[idx].dataSize);
memcpy(face_info->faceDataInfoList[idx].data, detectedFaces.faceDataInfoList[idx].data, detectedFaces.faceDataInfoList[idx].dataSize);
face_info->faceIsWithinBoundary[idx] = detectedFaces.faceIsWithinBoundary[i];
face_info->foreheadRect[idx] = detectedFaces.foreheadRect[i];
face_info->face3DAngleInfo.roll[idx] = detectedFaces.face3DAngleInfo.roll[i];
face_info->face3DAngleInfo.yaw[idx] = detectedFaces.face3DAngleInfo.yaw[i];
face_info->face3DAngleInfo.pitch[idx] = detectedFaces.face3DAngleInfo.pitch[i];
idx++;
}
if (idx > 0) {
// 送去识别线程,做人脸属性及人脸搜索
select_info_t sel_info = { 0 };
LPASVLOFFSCREEN image_data2 = (LPASVLOFFSCREEN)malloc(sizeof(ASVLOFFSCREEN));
if (image_data2) {
offscreen_copy((LPASVLOFFSCREEN)image_data, image_data2);
sel_info.image_data = image_data2;
sel_info.face_info = face_info;
std::unique_lock<std::mutex> lock(selected_queue_mutex_);
selected_queue_.push(sel_info);
lock.unlock();
}
}
} while (0);
}
获取人脸属性、人脸匹对是比较耗时的。人脸识别也不用每一帧都送去做识别,因此需要一定的策略去处理需要送去做属性的帧。检测到的face id都是唯一且递增的,因此可以使用map来维护face id对应的状态。此处使用map来维护face id做属性的次数,以及上次做属性的时间。
以最多识别MAX_RECOG_CNT次,每隔RECOG_INTERVAL ms识别一次为例,示例如下:
typedef struct track_state_s {
MInt32 cnt; // 识别次数
MInt64 capture_time; // 上次识别时间,单位:毫秒
} track_state_t;
void StreamProcess::track(MHandle handle, LPASVLOFFSCREEN image_data, std::vector<face_info_t> &track_res)
{
// ...
do {
// ...
for (int i = 0; i < detectedFaces.faceNum; i++) {
MInt64 curr_time_ms = 0;
auto iter = tracked_id_map_.find(detectedFaces.faceID[i]);
if (iter != tracked_id_map_.end()) {
if (iter->second.cnt >= MAX_RECOG_CNT) {
continue; // 连续识别超过10帧
}
struct timeval now;
gettimeofday(&now, NULL);
curr_time_ms = now.tv_sec * 1000 + now.tv_usec / 1000;
if (curr_time_ms - iter->second.capture_time < RECOG_INTERVAL) {
continue;
}
iter->second.cnt++;
iter->second.capture_time = curr_time_ms;
} else {
track_state_t state;
state.cnt = 1;
state.capture_time = curr_time_ms;
tracked_id_map_[detectedFaces.faceID[i]] = state;
}
// ...
}
} while (0);
// ...
}
三、人脸属性提取及人脸识别
将符合条件的人脸,提取属性,提取特征,人脸匹对,抠图保存检测及识别信息保存
void StreamProcess::recog_thread()
{
MRESULT res = MOK;
//初始化引擎
MHandle handle = NULL;
MInt32 mask = ASF_AGE | ASF_GENDER | ASF_LIVENESS | ASF_MASKDETECT | ASF_FACERECOGNITION;
res = ASFInitEngine(ASF_DETECT_MODE_VIDEO, ASF_OP_0_ONLY, ASF_MAX_DETECTFACENUM, mask, ASF_REC_LARGE, &handle);
if (res != MOK) {
printf("recog_thread ASFInitEngine fail: %ld\n", res);
return;
}
printf("recog_thread ASFInitEngine sucess: %ld\n", res);
// 加载已经注册过的人脸信息
std::string token;
do {
std::vector<face_item_t> faces;
FaceDB::getInstance()->query_face(token, 10, faces);
// 打印人脸信息
for (auto &face : faces) {
std::cout << "Face ID: " << face.id << std::endl;
std::cout << "Token: " << face.token << std::endl;
std::cout << "Image: " << face.image << std::endl;
std::cout << "Desc: " << face.desc << std::endl;
std::cout << "Feature: " << face.feature << std::endl;
std::cout << "Feature Size: " << face.feature_size << std::endl;
std::cout << "----------------------------------------" << std::endl;
ASF_FaceFeature feature = { 0 };
feature.feature = (MByte*)face.feature;
feature.featureSize = face.feature_size;
ASF_FaceFeatureInfo faceFeatureInfo = {0};
faceFeatureInfo.searchId = face.id;
faceFeatureInfo.feature = &feature;
res = ASFRegisterFaceFeature(handle, &faceFeatureInfo, 1);
if (res != MOK) {
printf("ASFRegisterFaceFeature fail: %ld\n", res);
}
// 释放人脸特征
if (face.feature) {
delete[] face.feature;
face.feature = nullptr;
}
}
if (faces.empty() || faces.size() < 10) {
std::cout << "no face" << std::endl;
break;
}
token = faces[faces.size() - 1].token;
} while (1);
while (!recog_stop_) {
std::unique_lock<std::mutex> lock(selected_queue_mutex_);
if (selected_queue_.empty()) {
lock.unlock();
if (decode_stop_) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
select_info_t sel_info = selected_queue_.front();
selected_queue_.pop();
lock.unlock();
// 人脸信息检测
MInt32 processMask = ASF_AGE | ASF_GENDER | ASF_LIVENESS | ASF_MASKDETECT;
res = ASFProcessEx(handle, sel_info.image_data, sel_info.face_info, processMask);
if (res != MOK) {
printf("ASFProcessEx fail: %ld\n", res);
break;
}
// 获取是否戴口罩
ASF_MaskInfo maskInfo = { 0 }; //是否带口罩
res = ASFGetMask(handle, &maskInfo);
if (res != MOK){
printf("ASFGetMask failed: %ld\n", res);
} else{
printf("ASFGetMask sucessed: %d\n", maskInfo.maskArray[0]);
}
ASF_AgeInfo ageInfo = { 0 };
if (has_flag(processMask, ASF_AGE)) {
// 获取年龄
res = ASFGetAge(handle, &ageInfo);
if (res != MOK) {
printf("ASFGetAge fail: %ld\n", res);
break;
}
for (int i = 0; i < ageInfo.num; i++) {
if (ageInfo.ageArray[i]) {
printf("age: %d\n", ageInfo.ageArray[i]);
}
}
}
ASF_GenderInfo genderInfo = { 0 };
if (has_flag(processMask, ASF_GENDER)) {
// 获取性别
res = ASFGetGender(handle, &genderInfo);
if (res != MOK) {
printf("ASFGetGender fail: %ld\n", res);
break;
}
for (int i = 0; i < genderInfo.num; i++) {
if (genderInfo.genderArray[i]) {
printf("First face gender: %d\n", genderInfo.genderArray[i]);
}
}
}
ASF_LivenessInfo rgbLivenessInfo = { 0 };
if (has_flag(processMask, ASF_LIVENESS)) {
//获取活体信息
res = ASFGetLivenessScore(handle, &rgbLivenessInfo);
if (res != MOK) {
printf("ASFGetLivenessScore fail: %ld\n", res);
break;
}
for (int i = 0; i < rgbLivenessInfo.num; i++) {
printf("live: %d\n", rgbLivenessInfo.isLive[i]);
}
}
for (int i = 0; i < sel_info.face_info->faceNum; i++) {
// 人脸信息
ASF_SingleFaceInfo SingleDetectedFaces = { 0 };
SingleDetectedFaces.faceRect.left = sel_info.face_info->faceRect[0].left;
SingleDetectedFaces.faceRect.top = sel_info.face_info->faceRect[0].top;
SingleDetectedFaces.faceRect.right = sel_info.face_info->faceRect[0].right;
SingleDetectedFaces.faceRect.bottom = sel_info.face_info->faceRect[0].bottom;
SingleDetectedFaces.faceOrient = sel_info.face_info->faceOrient[0];
SingleDetectedFaces.faceDataInfo = sel_info.face_info->faceDataInfoList[0];
// 作为预览模式下的识别照
ASF_FaceFeature feature = { 0 };
MFloat confidenceLevel;
ASF_FaceFeatureInfo faceFeatureInfo = {0};
res = ASFFaceFeatureExtractEx(handle, sel_info.image_data, &SingleDetectedFaces,
ASF_RECOGNITION, maskInfo.maskArray[i], &feature);
if (res == MOK){
// 单人脸特征比对
res = ASFFaceFeatureCompare_Search(handle, &feature, &confidenceLevel, &faceFeatureInfo);
if (res == MOK) {
printf("ASFFaceFeatureCompare_Search success: %d %lf\n", faceFeatureInfo.searchId, confidenceLevel);
} else {
printf("ASFFaceFeatureCompare_Search fail: %ld\n", res);
}
} else {
printf("ASFFaceFeatureExtractEx fail: %ld\n", res);
}
// 人脸信息裁剪
MRECT rect2 = sel_info.face_info->faceRect[i];
extend_rect(rect2, 0.5, sel_info.image_data->i32Width, sel_info.image_data->i32Height);
LPASVLOFFSCREEN crop_image = (LPASVLOFFSCREEN)malloc(sizeof(ASVLOFFSCREEN));
res = offscreen_crop(sel_info.image_data, crop_image, rect2);
if (res == MOK) {
// 人脸信息裁剪成功
printf("crop success\n");
} else {
// 人脸信息裁剪失败
printf("crop fail\n");
}
// 创建 cv::Mat 对象
cv::Mat mat(crop_image->i32Height, crop_image->i32Width, CV_8UC3, crop_image->ppu8Plane[0]);
struct timeval tv;
gettimeofday(&tv, NULL);
long capture_time = tv.tv_sec * 1000 + tv.tv_usec / 1000;
std::string filename = std::string(FACE_CAPTURE_PATH) + "/face_id" + std::to_string(sel_info.face_info->faceID[i])
+ "_" + std::to_string(capture_time) + ".jpg";
// 设置JPEG编码质量为95
std::vector<int> params;
params.push_back(cv::IMWRITE_JPEG_QUALITY);
params.push_back(95);
cv::imwrite(filename, mat, params);
// 释放 Mat 对象
mat.release();
// save result
face_result_t face_result;
face_result.capture_time = capture_time;
face_result.id = sel_info.face_info->faceID[i];
face_result.rect = sel_info.face_info->faceRect[i];
face_result.attr.age = ageInfo.ageArray[i];
face_result.attr.gender = genderInfo.genderArray[i];
face_result.attr.mask = maskInfo.maskArray[i];
face_result.attr.liveness = rgbLivenessInfo.isLive[i];
face_result.capture_image = filename;
// query face token
FaceDB::getInstance()->query_face(faceFeatureInfo.searchId, face_result.token);
face_result.score = confidenceLevel;
save_face_result(&face_result);
// 释放人脸信息裁剪数据
offscreen_free(&crop_image);
}
// 释放图像及人脸信息
offscreen_free(&sel_info.image_data);
multi_face_info_free(&sel_info.face_info);
}
while (1) {
std::unique_lock<std::mutex> lock(selected_queue_mutex_);
if (selected_queue_.empty()) {
lock.unlock();
break;
}
select_info_t sel_info = selected_queue_.front();
selected_queue_.pop();
lock.unlock();
offscreen_free(&sel_info.image_data);
multi_face_info_free(&sel_info.face_info);
}
// 反初始化
res = ASFUninitEngine(handle);
if (res != MOK) {
printf("ASFUninitEngine fail: %ld\n", res);
} else {
printf("ASFUninitEngine sucess: %ld\n", res);
}
std::cout << "recog thread exit" << std::endl;
}
需要注意:
- 根据所需人脸属性,创建引擎,去掉不必要的属性参数,确保检测耗时最小化。此处需要人脸识别、获取年龄、性别、活体评分、口罩等属性。因此mask参数为ASF_AGE | ASF_GENDER | ASF_LIVENESS | ASF_MASKDETECT | ASF_FACERECOGNITION
- 当检测线程退出,并且队列中没有需要做属性及人脸识别的数据时,退出识别线程,退出后反初始化引擎。
四、抠图(人脸抓拍图)
由于算法检测的人脸rect仅是人脸部分,无法完整显示整个头部,因此抠图前需扩展人脸坐标,扩展后的坐标范围应该保持在rect[0, 0, width, height] 范围内,实现如下:
/**
* 扩展矩形区域
* @param rect 要扩展的矩形区域,类型为 MRECT
* @param ratio 扩展比例,类型为 float
* @param width 图像宽度,类型为 MInt32
* @param height 图像高度,类型为 MInt32
*/
void extend_rect(MRECT &rect, float ratio, MInt32 width, MInt32 height)
{
MInt32 src_width = rect.right - rect.left;
MInt32 src_height = rect.bottom - rect.top;
MInt32 dst_width = src_width * (1 + ratio);
MInt32 dst_height = src_height * (1 + ratio);
MRECT dst_rect = {
rect.left - (dst_width - src_width) / 2,
rect.top - (dst_height - src_height) / 2,
rect.left - (dst_width - src_width) / 2 + dst_width,
rect.top - (dst_height - src_height) / 2 + dst_height
};
if (dst_rect.left < 0) {
dst_rect.left = 0;
}
if (dst_rect.top < 0) {
dst_rect.top = 0;
}
if (dst_rect.right > width) {
dst_rect.right = width;
}
if (dst_rect.bottom > height) {
dst_rect.bottom = height;
}
rect = dst_rect;
}
从原图抠图
int offscreen_crop(const LPASVLOFFSCREEN src, LPASVLOFFSCREEN dst, MRECT rect)
{
if (!src || !dst) {
printf("param error\n");
return -1;
}
if (src->u32PixelArrayFormat != ASVL_PAF_RGB24_B8G8R8) {
printf("not support format\n");
return -1;
}
if (rect.left < 0 || rect.left >= src->i32Width || rect.right < 0 || rect.right > src->i32Width ||
rect.top < 0 || rect.top >= src->i32Height || rect.bottom < 0 || rect.bottom > src->i32Height) {
printf("rect error\n");
return -1;
}
int dst_width = rect.right - rect.left;
int dst_height = rect.bottom - rect.top;
dst->i32Width = dst_width;
dst->i32Height = dst_height;
dst->u32PixelArrayFormat = ASVL_PAF_RGB24_B8G8R8;
dst->ppu8Plane[0] = (MUInt8*)malloc(dst_width * dst_height * 3);
dst->pi32Pitch[0] = dst_width * 3;
for (int row = rect.top; row < rect.bottom; ++row) {
for (int col = rect.left; col < rect.right; ++col) {
int src_index = row * src->pi32Pitch[0] + col * 3;
int dst_index = (row - rect.top) * dst->pi32Pitch[0] + (col - rect.left) * 3;
dst->ppu8Plane[0][dst_index] = src->ppu8Plane[0][src_index];
dst->ppu8Plane[0][dst_index + 1] = src->ppu8Plane[0][src_index + 1];
dst->ppu8Plane[0][dst_index + 2] = src->ppu8Plane[0][src_index + 2];
}
}
return 0;
}
存储抠图,使用LPASVLOFFSCREEN构造cv::Mat,然后使用cv::imwrite将抠图编码写到文件
// 创建 cv::Mat 对象
cv::Mat mat(crop_image->i32Height, crop_image->i32Width, CV_8UC3, crop_image->ppu8Plane[0]);
std::string filename = std::to_string(sel_info.face_info->faceID[i]) + "_" + std::to_string(time(NULL)) + ".jpg";
cv::imwrite(filename, mat);
// 释放 Mat 对象
mat.release();
五、检测及识别结果保存
将当前检测到的人脸信息,包括基于原始图像的人脸坐标、人脸属性(性别、年龄、口罩等)、人脸抓拍图、人脸搜索结果等信息以json形式报错到文件中。
此处以jsoncpp为例:
void save_face_result(const face_result_t *face_result)
{
if (face_result == nullptr) {
return;
}
Json::Value root;
root["id"] = face_result->id;
Json::Int64 tm = (int64_t)face_result->capture_time;
root["capture_time"] = tm;
root["rect"] = Json::Value(Json::arrayValue);
root["rect"].append(face_result->rect.left);
root["rect"].append(face_result->rect.top);
root["rect"].append(face_result->rect.right);
root["rect"].append(face_result->rect.bottom);
root["attr"]["age"] = face_result->attr.age;
root["attr"]["gender"] = face_result->attr.gender;
root["attr"]["mask"] = face_result->attr.mask;
root["attr"]["liveness"] = face_result->attr.liveness;
root["capture_image"] = face_result->capture_image;
root["token"] = face_result->token;
root["score"] = face_result->score;
Json::StyledWriter writer;
std::string json = writer.write(root);
std::ofstream ofs;
std::string filename = std::string(FACE_CAPTURE_PATH) + "/face_id" + std::to_string(face_result->id) + "_" + std::to_string(face_result->capture_time) + ".json";
ofs.open(filename, std::ios::app);
ofs << json;
ofs.close();
}
json文件内容如下:
{
"attr" : {
"age" : 30,
"gender" : 0,
"liveness" : 1,
"mask" : 0
},
"capture_image" : "../data/capture/face_id1_1733049750901.jpg",
"capture_time" : 1733049750901,
"id" : 1,
"rect" : [ 262, 114, 382, 234 ],
"score" : 0.96131998300552368,
"token" : "afe307215a70e89790a8c97ec67a51fc"
}
六、总结
代码仓库
本文所述内容实现较为粗糙,如需细化,可以从以下层面出发:
- 图片、视频编解码增加硬件加速;使用其他图片解码库(libjpeg、libpng解码图片要比OPenCV要快)
- 增加人脸分组(权限组),不同分组的人脸可以使用不同的算法句柄;人脸信息与人脸分组绑定;摄像头与人脸分组绑定
- 增加HTTP Server或Websocket Server管理应用。增加后除了可以检测摄像头、视频信息外,可以自定义协议检测图片流信息。可以通过HTTP发送开启/关闭 人脸批量注册请求,开启websokcet server接收批量注册的人脸图片等信息,送去多线程人脸注册。
- 识别信息处理,检测信息及识别信息可以注册各种事件(例如信息上传到HTTP、FTP平台、触发门禁解锁、触发闸机开关)
- 作为考勤、门禁系统,需要注重活体检测
- 对于2k、4k这种高分辨率的相机,以1080p为例,可以将送去检测的视频帧resize到1080p,然后根据检测到的坐标,在原视频帧中抠图去做属性及搜索。
- 可以根据业务需求,可以设计不同的抓拍识别模式。比如:
- 最快的时间内抓拍一张人脸并且送去做属性,做人脸识别
- 每隔m(ms)抓拍一张人脸并且送去做属性,做人脸识别。最大抓拍n张
- 从人脸出现到人脸消失,抓拍效果最优的一张人脸做属性,做人脸识别
- 抓拍一张刚进入画面的、抓拍一张离开画面时的、抓拍一张效果最优的人脸
- 不同的模块使用不同的引擎。一方面可以去掉不需要的参数,确保检测时间最小化。另一方面使用多引擎可以并发检测,达到硬件资源最大化利用。
很多人喜欢使用引擎池,我个人认为不太适用。何为池,就是初始化创建了很多引擎,有需要检测的任务时,获取一个空闲的引擎用于处理。
人脸批量批量注册时可以使用多引擎加快人脸注册。可以在批量注册事件开始后,创建多线程去做人脸检测,批量注册事件结束后销毁线程。此处倒是可以使用引擎池,但是没必要。在线程开始时创建引擎,线程结束时销毁引擎,可以做到按需创建,及时回收。
除了批量注册,最好保留一个线程用于单人脸注册。线程生命周期从程序启动到程序退出为止。
人脸检测,引擎池也不适用。因为要进行face_id追踪,同一个摄像头的数据需要送到同一个引擎中,随机获取空闲引擎显然是不适用的;另外除非是摄像头断电、断网,人为退出程序,不然整个解码、检测、识别流程就持续存在,不需要用到引擎池。
人脸识别,若在应用层面做人脸分组,倒是可以使用引擎池,因为除了加载到引擎的人脸,没有其他依赖信息。不过在每个摄像头的识别线程中已经创建过引擎,并且需要识别的数据经过一系列的策略过滤,单线程有足够的时间去做属性检测及人脸识别。
因此,汇总如下:
人脸批量注册可以以动态创建/销毁多个线程的方式去实现。
每个摄像头,分别创建人脸检测、人脸识别两个线程
识别结果处理事件,可以每个摄像头创建一个线程去处理。也可以创建多个结果处理线程,将保存的结果送去线程处理