基于虹软Linux Pro V1.0 的多路摄像头下的大人脸库人脸检测及识别

开发环境配置

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为例):

字段类型描述
idINTEGER自增 ID,用于算法的 searchId, 用于查找人脸搜索到的注册人员完整信息
tokenTEXT注册图md5,用于注册图唯一标识,防止同一张图片多次注册(根据图片md5去重);根据应用需要,如果每个人只能注册一张图片,在注册人脸时,可以先做人脸搜索,当搜索到的top1相似度高于某个阈值时,可以让用户选择是否入库
imageTEXT注册图路径
descTEXT注册图相关描述信息
featureTEXT/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;
}

需要注意:

  1. 创建引擎时应按需创建,只保留需要检测的项目,确保检测耗时最小化。在人脸检测的过程中,除了使用3D人脸角度来过滤检测结果外。还需要使用质量来进行过滤,是否戴口罩质量分别有不同的阈值,因此在检测的过程中,需要做质量检测、口罩检测。因此mask参数需要用到3个,ASF_FACE_DETECT | ASF_MASKDETECT | ASF_IMAGEQUALITY。
  2. 视频读取结束,退出线程时,应该反初始化引擎。

根据检测结果,通过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;
}

需要注意:

  1. 根据所需人脸属性,创建引擎,去掉不必要的属性参数,确保检测耗时最小化。此处需要人脸识别、获取年龄、性别、活体评分、口罩等属性。因此mask参数为ASF_AGE | ASF_GENDER | ASF_LIVENESS | ASF_MASKDETECT | ASF_FACERECOGNITION
  2. 当检测线程退出,并且队列中没有需要做属性及人脸识别的数据时,退出识别线程,退出后反初始化引擎。

四、抠图(人脸抓拍图)

由于算法检测的人脸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"
}

六、总结

代码仓库
本文所述内容实现较为粗糙,如需细化,可以从以下层面出发:

  1. 图片、视频编解码增加硬件加速;使用其他图片解码库(libjpeg、libpng解码图片要比OPenCV要快)
  2. 增加人脸分组(权限组),不同分组的人脸可以使用不同的算法句柄;人脸信息与人脸分组绑定;摄像头与人脸分组绑定
  3. 增加HTTP Server或Websocket Server管理应用。增加后除了可以检测摄像头、视频信息外,可以自定义协议检测图片流信息。可以通过HTTP发送开启/关闭 人脸批量注册请求,开启websokcet server接收批量注册的人脸图片等信息,送去多线程人脸注册。
  4. 识别信息处理,检测信息及识别信息可以注册各种事件(例如信息上传到HTTP、FTP平台、触发门禁解锁、触发闸机开关)
  5. 作为考勤、门禁系统,需要注重活体检测
  6. 对于2k、4k这种高分辨率的相机,以1080p为例,可以将送去检测的视频帧resize到1080p,然后根据检测到的坐标,在原视频帧中抠图去做属性及搜索。
  7. 可以根据业务需求,可以设计不同的抓拍识别模式。比如:
  • 最快的时间内抓拍一张人脸并且送去做属性,做人脸识别
  • 每隔m(ms)抓拍一张人脸并且送去做属性,做人脸识别。最大抓拍n张
  • 从人脸出现到人脸消失,抓拍效果最优的一张人脸做属性,做人脸识别
  • 抓拍一张刚进入画面的、抓拍一张离开画面时的、抓拍一张效果最优的人脸
  1. 不同的模块使用不同的引擎。一方面可以去掉不需要的参数,确保检测时间最小化。另一方面使用多引擎可以并发检测,达到硬件资源最大化利用。
    很多人喜欢使用引擎池,我个人认为不太适用。何为池,就是初始化创建了很多引擎,有需要检测的任务时,获取一个空闲的引擎用于处理。
    人脸批量批量注册时可以使用多引擎加快人脸注册。可以在批量注册事件开始后,创建多线程去做人脸检测,批量注册事件结束后销毁线程。此处倒是可以使用引擎池,但是没必要。在线程开始时创建引擎,线程结束时销毁引擎,可以做到按需创建,及时回收。
    除了批量注册,最好保留一个线程用于单人脸注册。线程生命周期从程序启动到程序退出为止。
    人脸检测,引擎池也不适用。因为要进行face_id追踪,同一个摄像头的数据需要送到同一个引擎中,随机获取空闲引擎显然是不适用的;另外除非是摄像头断电、断网,人为退出程序,不然整个解码、检测、识别流程就持续存在,不需要用到引擎池。
    人脸识别,若在应用层面做人脸分组,倒是可以使用引擎池,因为除了加载到引擎的人脸,没有其他依赖信息。不过在每个摄像头的识别线程中已经创建过引擎,并且需要识别的数据经过一系列的策略过滤,单线程有足够的时间去做属性检测及人脸识别。
    因此,汇总如下:
    人脸批量注册可以以动态创建/销毁多个线程的方式去实现。
    每个摄像头,分别创建人脸检测、人脸识别两个线程
    识别结果处理事件,可以每个摄像头创建一个线程去处理。也可以创建多个结果处理线程,将保存的结果送去线程处理
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值