1. 文档说明

本文档针对在实际项目落地过程中,调用虹软(ArcSoft)人脸识别SDK出现的常见问题进行总结,涉及内容包括:

  • 性能优化(人脸识别速度提升、JPEG转BGR24性能优化等)
  • 人脸抠图
  • 多人脸图片中最优人脸选择
  • 服务端高并发图片请求优化
  • 内存泄漏排查

2. JPEG转BGR24耗时长优化

在实际项目开发过程中,会遇到JPEG转BGR24耗时长问题,导致人脸操作整体耗时增加。java中自带原生图像处理类ImageIO,但是该接口转码速度较慢,内部实际流程如下:

JPEG
 ↓
JPEG Decoder
 ↓
BufferedImage
 ↓
Raster
 ↓
byte[]

需要经历:

  • JPEG解码
  • BufferedImage构造
  • ColorModel转换
  • Raster数据拷贝

推荐使用javaCV接口实现图片转码,将jpeg转换成BRG24。

Mat mat = imdecode(...)

内部流程:

JPEG
 ↓
OpenCV
 ↓
libjpeg-turbo
 ↓
Mat(BGR)

直接输出:

BGR24

减少了中间对象和颜色格式转换。

import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.opencv.opencv_core.Mat;

import static org.bytedeco.opencv.global.opencv_imgcodecs.imdecode;
import static org.bytedeco.opencv.global.opencv_imgcodecs.IMREAD_COLOR;
public class JpegDecoder {

    public static BGRImage jpegToBGR(byte[] jpegData) {

    BytePointer jpegPtr = new BytePointer(jpegData);

    Mat jpegMat = new Mat(
            1,
            jpegData.length,
            org.bytedeco.opencv.global.opencv_core.CV_8UC1,
            jpegPtr
    );

    Mat mat = imdecode(jpegMat, IMREAD_COLOR);

    if (mat.empty()) {
        throw new RuntimeException("decode failed");
    }

    int width = mat.cols();
    int height = mat.rows();

    byte[] bgr = new byte[width * height * 3];

    mat.data().get(bgr);

    mat.release();
    jpegMat.release();
    jpegPtr.close();

    return new BGRImage(width, height, bgr);
}
}

3. 服务器场景下高并发人脸图片请求耗时优化

问题描述

在Linux Server场景中,经常会遇到大量人脸图片并发请求的情况,这可能会导致人脸相关操作被阻塞,例如:人脸特征提取,人脸特征提取在SDK提供的接口中,耗时相对较高,高并发的人脸识别请求,会导致人脸特征提取耗时更长。

推荐解决方案

使用引擎池和线程池,能够有效解决上述问题,这里推荐人脸SDK引擎的数量和线程池中线程数量相同,线程数量不超过设备的核心数。具体可以参考链接:虹软人脸服务器版SDK(Linux/ARM Pro)多线程调用及性能优化_虹软 服务器版本-CSDN博客


4. 如何实现高效精准的人脸抠图

问题描述

在服务器端人脸识别场景中,客户通常会上传一张图片,服务器调用人脸识别SDK完成人脸检测、人脸特征提取和身份识别。同时,很多项目还需要保存当前识别人员的人脸缩略图,用于:

  • 人员档案管理
  • 访客记录存档
  • 前端列表展示
  • 历史记录查询
  • 抓拍图片展示

由于SDK输出的人脸框主要服务于人脸识别和特征提取,其框选区域通常较为紧凑,直接按照检测框裁剪,可能出现额头、下巴或脸部边缘缺失的问题,影响展示效果。

推荐解决方案

建议基于SDK返回的人脸框进行适当扩展后再进行裁剪。下面给出C++版本的裁剪代码示例,可以通过调用calcFaceROI得到可靠的裁剪ROI,函数中的缩放因子可以根据实际调整。

struct FaceInput
{
    int x1, y1, x2, y2;
    float yaw;
    float pitch;
    int img_w;
    int img_h;
};

struct FaceROI
{
    int x1, y1, x2, y2;
};

#include 
#include 

static float clampf(float x, float a, float b)
{
    return std::min(std::max(x, a), b);
}

FaceROI calcFaceROI(const FaceInput& in)
{
    FaceROI out;

    float x1 = (float)in.x1;
    float y1 = (float)in.y1;
    float x2 = (float)in.x2;
    float y2 = (float)in.y2;

    float w = x2 - x1;
    float h = y2 - y1;

    //----------------------------
    // 根据人脸大小调整扩张比例
    //----------------------------
    float scale = 200.f / h;
    scale = clampf(scale, 0.8f, 1.3f);

    //----------------------------
    // 左右固定扩张
    //----------------------------
    float left_expand  = 0.35f * scale;
    float right_expand = 0.35f * scale;

    //----------------------------
    // 上下根据pitch轻微调整
    //----------------------------
    float pitch_n = clampf(in.pitch / 45.f, -1.f, 1.f);

    float top_expand    = 0.55f * (1.f - pitch_n * 0.3f);
    float bottom_expand = 0.45f * (1.f + pitch_n * 0.3f);

    top_expand *= scale;
    bottom_expand *= scale;

    //----------------------------
    // 计算ROI
    //----------------------------
    float nx1 = x1 - left_expand * w;
    float nx2 = x2 + right_expand * w;
    float ny1 = y1 - top_expand * h;
    float ny2 = y2 + bottom_expand * h;

    //----------------------------
    // 保持ROI尺寸,整体平移
    //----------------------------

    // X方向
    if (nx1 < 0)
    {
        nx2 -= nx1;
        nx1 = 0;
    }

    if (nx2 > in.img_w)
    {
        float diff = nx2 - in.img_w;
        nx1 -= diff;
        nx2 = (float)in.img_w;

        if (nx1 < 0)
            nx1 = 0;
    }

    // Y方向
    if (ny1 < 0)
    {
        ny2 -= ny1;
        ny1 = 0;
    }

    if (ny2 > in.img_h)
    {
        float diff = ny2 - in.img_h;
        ny1 -= diff;
        ny2 = (float)in.img_h;

        if (ny1 < 0)
            ny1 = 0;
    }

    out.x1 = (int)std::round(nx1);
    out.y1 = (int)std::round(ny1);
    out.x2 = (int)std::round(nx2);
    out.y2 = (int)std::round(ny2);

    return out;
}

5. 人脸注册耗时较长优化

问题描述

在服务器场景下,人脸相关应用初次工作起来时,会从数据库读取人脸特征信息注册到SDK内存,过程中可能会遇到注册耗时比较长的情况。

推荐解决方案

人脸SDK接口中有注册单一人脸和批量注册人脸接口。注册多个人脸时,可以使用批量注册人脸接口,减少SDK内部内存的搬运。另外,当程序刚起来时,从数据库读人脸特征信息注册到人脸SDK中时,可以一次性多注册一些人脸特征,例如一次性注册1000张人脸特征信息。这里的1000可以具体根据实际的设备内存情况调整。

public int registerFaceFeature(List faceFeatureInfoList)

另外,也可以调用人脸质量过滤接口,拦截一些低质量照片,降低服务器注册压力。

6. 人脸底库过大,比对太慢怎么办

问题描述

服务器场景下,当人脸底库较大时,高并发的人脸特征请求会导致服务器压力过大,增加比对的耗时,如何提高识别的效率?

推荐解决方案

除了根据硬件情况,增加SDK引擎数量之外,还可以对人脸底库进行针对性的分组,将大人脸底库分成多个小底库,通过compareFaceFeature接口来实现并发比对。例如,在学校的业务场景中,全校可能有几千到上万人,可以根据班级以及行政机构将所有人按组划分,当产生课堂签到点名需求时,课堂上的终端设备上传抓拍照,并带上当前班级信息,后端服务器只需要比对当前班级所在组的人员信息就行。

7. 照片中多人脸如何选择合适的人脸进行比对

问题描述

目前的人脸SDK中支持设置最大检测人脸的数量,例如设置最大人脸检测数量为5个。当图片中出现多张人脸时,应该如何选择合适的人脸进行比对?

推荐解决方案

在门禁场景中,可以结合人脸质量分、最大人脸策略,选择最优的人脸进行人脸识别。

*    检测到 N 张人脸
     *       │
     *       ▼
     *  ┌──────────────────────────┐
     *  │ Step 1: 质量分过滤        │
     *  │ 对每张人脸做 imageQualityDetect │
     *  │ 保留 qualityScore >= 60 的 │
     *  └──────────┬───────────────┘
     *             │
     *       有合格的?
     *       ┌─────┴─────┐
     *       │           │
     *      有          没有
     *       │           │
     *       ▼           ▼
     *  ┌─────────┐  ┌──────────────┐
     *  │ Step 2: │  │ Step 2:      │
     *  │ 从合格  │  │ 退化为最大人脸 │
     *  │ 的中选  │  │ (直接选面积   │
     *  │ 最大人脸│  │  最大的)      │
     *  └────┬────┘  └──────┬───────┘
     *       └───────┬──────┘
     *               ▼
     *         返回选中的人脸

private FaceInfo findLargestFace(List faceInfoList) {
        if (faceInfoList == null || faceInfoList.isEmpty()) {
            return null;
        }

        FaceInfo largestFace = faceInfoList.get(0);
        int largestArea = 0;

        for (FaceInfo faceInfo : faceInfoList) {
            Rect rect = faceInfo.getRect();
            if (rect != null) {
                int area = rect.getWidth() * rect.getHeight();
                if (area > largestArea) {
                    largestArea = area;
                    largestFace = faceInfo;
                }
            }
        }

        Rect rect = largestFace.getRect();
        logger.info("最终选中: 面积 = {}", largestArea);
        return largestFace;
    }

public FaceInfo selectBestFace(ImageInfo imageInfo, List faceInfoList) {
    // Step 1: 质量分过滤
    List qualifiedFaces = new ArrayList<>();
    for (int i = 0; i < faceCount; i++) {
        ImageQuality imageQuality = new ImageQuality();
        faceEngineInstance.imageQualityDetect(imageInfo, faceInfo,
            FaceQualityMask.ASF_FACE_QUALITY_WITH_LIVENESS.getValue(), imageQuality);
        
        float score = imageQuality.getQualityScore();
        if (score >= QUALITY_THRESHOLD) {  // 60.0f
            qualifiedFaces.add(faceInfo);  // 合格
        }
    }

    // Step 2: 从合格的里面选面积最大的
    if (!qualifiedFaces.isEmpty()) {
        return findLargestFace(qualifiedFaces);  // 正常路径
    }

    // 全部不合格 → 降级为最大人脸
    logger.warn("全部不合格,退化为最大人脸策略");
    return findLargestFace(faceInfoList);
}

8. 调用人脸识别SDK 出现内存泄漏排查

问题描述

在调用人脸SDK Java版本接口开发相关应用时,会遇到内存泄露的情况,应该如何排查?

推荐解决方案

对于人脸SDK运行时内存可以分为java堆内存和native内存,java堆内存由JVM自己管理,会自动触发GC。而native内存是在人脸SDK中的so库中分配的,JVM无法管理。调用虹软人脸SDK时,最大的风险点就是创建了引擎,后续不使用时却没有及时给释放掉,导致native层内存泄漏。

例如,有的开发者可能会对于一个前端请求,就创建一个人脸SDK引擎,但是创建了之后却没有及时的释放,导致内存泄漏。建议创建引擎池来管理引擎,最后统一释放所有引擎,避免内存泄漏。

public void initPool() {
        // 初始化
        logger.info("开始初始化人脸识别引擎实例池和线程池...");

        // 初始化FaceEngine实例池
        enginePool = new LinkedBlockingQueue<>(poolSize);
        for (int i = 0; i < poolSize; i++) {
            ArcFaceEngine arcFaceEngine = new ArcFaceEngine();
            // 激活并初始化SDK引擎
            if (!arcFaceEngine.activateAndInitSdk(appId, sdkKey, activeKey, libPath)) {
                logger.error("人脸识别SDK初始化和激活失败,应用程序将无法正常运行。");
                return;
            }
            // 将初始化好的引擎放入队列
            try{
                enginePool.put(arcFaceEngine); 
                logger.info("人脸识别引擎实例 {} 初始化成功并加入池中.", i);
            }catch(InterruptedException e){
                logger.error("人脸识别引擎实例 {} 加入池失败.", i);
                return;
            }
        }

        // 初始化线程池,大小与引擎池一致,以便每个线程可以独占一个引擎
        executorService = Executors.newFixedThreadPool(poolSize);
        logger.info("人脸识别线程池初始化成功,大小为: {}", poolSize);
    }

原文地址: https://www.cveoy.top/t/topic/qHhz 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录