Android端侧部署人脸识别工程性能优化笔记——NV21数据变速变换

最近接手了一个工程,其中涉及人脸识别后打码的流程,然后功耗特别严重,看了一下主要是图像处理逻辑问题导致的,我会把发现的问题和解决过程记录一下。

主要问题:

  1. 识别模型是使用yolo实现的,最后端侧部署接口使用Bitmap,相机Preview回来的是NV21数据,但这里了用这样的方法去做数据转换:

这样转写起来方便,安卓自带就有,但性能问题很大,它会先把NV21图像通过DCT、量化、霍夫曼编码等一系列运算保存成JPEG格式数据;然后重现为Bitmap数据时又要通过IDCT、霍夫曼编码等解码流程重新解成RGB数据,多了很多本不必要的逻辑,本来YUV数据通过一个公式就可以一步到位转化为RGB数据了。

  1. 为了让接口输出的物体外接矩形数据和Preview窗口坐标可以直接对齐,这里使用了错误的转换做法:

因为Preview的显示方向和NV21的数据有90度的角度差,它可能不希望输出的外接矩形直接用起来和Preview没法对得上,然后就用了这样的方法旋转NV21的之前提到的步骤再生成位图:

在CPU做这种双重循环非常耗时,而且本来并没有必要,直接对最终产生的坐标做很简易的交换就可以了,根本不需要在识别前做这些事。

  1. 为了把Camera2接口回调过来的image数据转换为上述步骤需要的NV21数据,直接在Java层进行数据提取:

其实没有什么必要这么做,完全可以把Image传到JNI层,就能直接拿里面的NV21数据了

解决方法:

其实解决这个问题我个人认为有两种方法,一种是通过PBO直接把OpenGL ES中预览画面通过PBO直接截取就能拿到RGB数据。第二个就是把相机接口回调的Image对象通过JNI解读、转换并通过直接操作Bitmap对象的native buffer进行像素更新。

其实有一个很好用的库可以把刚刚的流程直接一部到位生成bitmap,而且主张bitmap还可以不停重复更新数据即可,这个要求可以libyuv在JNI层直接实现:

环境配置:

下载libYuv,并转换里面的cpp、CMakeLists、gradle配置到自己的工程中即可:

地址在 https://github.com/hzl123456/LibyuvDemo

另外因为要用到jnigraphics,CMakeLists要改一下,配置上要加上jnigraphics,如下:

复制代码
target_link_libraries(yuvutil ${log-lib} yuv jnigraphics)

编写JNI接口:

其中extractPlane用于解读Image中每个Y、U、V每个plane的数据,然后convertAndroidMediaImageToBitmap函数通过调用libyuv中的NV21ToARGB或其他YUV格式响应的库函数转换成RGB数据,再通过android自身的AndroidBitmap_lockPixels获取像素指针把转换出来的数据复制进去,此时NV21就顺利成功转换到Bitmap了。

复制代码
#include <jni.h>
#include <string>
#include "libyuv.h"
#include <android/log.h>
#include <android/bitmap.h>

#define TAG "YUV_JNI"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

struct PlaneInfo {
    uint8_t* data = nullptr;
    int rowStride = 0;
    int pixelStride = 0;
};

// 安全提取 Plane 的底层指针与步长
static PlaneInfo extractPlane(JNIEnv* env, jobject plane) {
    PlaneInfo info;
    if (!plane) return info;

    jclass planeCls = env->GetObjectClass(plane);
    if (!planeCls) return info;

    jmethodID getBuffer = env->GetMethodID(planeCls, "getBuffer", "()Ljava/nio/ByteBuffer;");
    jobject buf = env->CallObjectMethod(plane, getBuffer);
    if (buf) {
        info.data = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buf));
        env->DeleteLocalRef(buf);
    }

    jmethodID getRowStride = env->GetMethodID(planeCls, "getRowStride", "()I");
    info.rowStride = env->CallIntMethod(plane, getRowStride);

    jmethodID getPixelStride = env->GetMethodID(planeCls, "getPixelStride", "()I");
    info.pixelStride = env->CallIntMethod(plane, getPixelStride);

    env->DeleteLocalRef(planeCls);
    return info;
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_libyuv_util_YuvUtil_convertAndroidMediaImageToBitmap(
        JNIEnv* env, jclass type, jobject image, jobject bitmap) {

    if (!image || !bitmap) return JNI_FALSE;

    // 1. 获取 Y/U/V 三个平面
    jclass imgCls = env->GetObjectClass(image);
    jmethodID getPlanes = env->GetMethodID(imgCls, "getPlanes", "()[Landroid/media/Image$Plane;");
    jobjectArray planes = (jobjectArray) env->CallObjectMethod(image, getPlanes);
    env->DeleteLocalRef(imgCls);
    if (!planes) return JNI_FALSE;

    PlaneInfo y = extractPlane(env, env->GetObjectArrayElement(planes, 0));
    PlaneInfo u = extractPlane(env, env->GetObjectArrayElement(planes, 1));
    PlaneInfo v = extractPlane(env, env->GetObjectArrayElement(planes, 2));

    env->DeleteLocalRef(env->GetObjectArrayElement(planes, 0));
    env->DeleteLocalRef(env->GetObjectArrayElement(planes, 1));
    env->DeleteLocalRef(env->GetObjectArrayElement(planes, 2));
    env->DeleteLocalRef(planes);

    if (!y.data || !u.data || !v.data) {
        LOGE("Failed to get direct buffer address from Image planes");
        return JNI_FALSE;
    }

    // 2. 锁定 Bitmap 内存
    AndroidBitmapInfo bmpInfo;
    void* pixels = nullptr;
    if (AndroidBitmap_getInfo(env, bitmap, &bmpInfo) < 0 ||
        AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) {
        LOGE("Failed to lock ARGB_8888 Bitmap");
        return JNI_FALSE;
    }

    // 3. 根据 pixelStride 自动识别 YUV 布局并转换
    int res = -1;
    int w = bmpInfo.width;
    int h = bmpInfo.height;
    int dstStride = bmpInfo.stride;

    if (u.pixelStride == 1 && v.pixelStride == 1) {
        //I420 / YV12 (三平面独立)
        res = libyuv::I420ToARGB(
                y.data, y.rowStride,
                u.data, u.rowStride,
                v.data, v.rowStride,
                reinterpret_cast<uint8_t*>(pixels), dstStride,
                w, h);
    } else if (u.pixelStride == 2) {
        //NV21 (U/V 交错,V 在前)
        res = libyuv::NV21ToARGB(
                y.data, y.rowStride,
                u.data, u.rowStride,
                reinterpret_cast<uint8_t*>(pixels), dstStride,
                w, h);
    } else if (v.pixelStride == 2) {
        //NV12 (U/V 交错,U 在前)
        res = libyuv::NV12ToARGB(
                y.data, y.rowStride,
                v.data, v.rowStride,
                reinterpret_cast<uint8_t*>(pixels), dstStride,
                w, h);
    } else {
        LOGE("Unsupported YUV_420_888 layout: U_PS=%d, V_PS=%d", u.pixelStride, v.pixelStride);
    }

    // 4. 解锁 Bitmap
    AndroidBitmap_unlockPixels(env, bitmap);

    return (res == 0) ? JNI_TRUE : JNI_FALSE;
}

坐标转换算法更新:

由于现在送入检测的数据和preview的数据有和实际数据右转90度的角度差,因此yolo接口输出的坐标是不能直接用的,不过转换起来也很容易,用一些空间想象力和基本逻辑能力推理一下即可:

1、 因为传入的Bitmap和Preview的分辨率不同,还需要先把得到的坐标除以识别图像的宽高,得到归一化的方向向量,再乘以preview的宽高进行线性变换,量化得出这些x,y坐标应该实际在preview上的什么位置。

2、x,y坐标交换,但因为之前的y值转换到preview坐标系的x轴之后,就成了右边框距离识别框的距离,因此要保持位置准确,需要执行这个计算:preview中显示识别框的x轴起点 = preview的宽度 - 步骤1量化后的y值 - 物体的宽度。

复制代码
            float actualWidth = results[i].height / dectectImageSize.getHeight() * previewSize.getWidth();
            float actualHeight = results[i].width / dectectImageSize.getWidth() * previewSize.getHeight();
            float actualX = previewSize.getWidth() - actualWidth - results[i].y / dectectImageSize.getHeight() * previewSize.getWidth();
            float actualY = results[i].x / dectectImageSize.getWidth() * previewSize.getHeight();

结果:

在展锐的低端机器上,每一帧640*480的NV21数据,从需要接近100ms的转换时间,压缩到2ms内即可,随着CPU占用时间的降低,这一流程分支带来的功耗消耗也下降下来了。

相关推荐
张道宁4 小时前
从零开始训练YOLO手机检测模型:完整实战教程
python·yolo
星越华夏12 小时前
计算机视觉:YOLOv12安装环境
人工智能·yolo·计算机视觉
羊羊小栈16 小时前
AI赋能电力巡检:智能故障预警系统
人工智能·yolo·目标检测·毕业设计·大作业
动物园猫21 小时前
面向智慧牧场的牛行为识别数据集分享(适用于YOLO系列深度学习分类检测任务)
深度学习·yolo·分类
埃菲尔铁塔_CV算法21 小时前
YOLO11 与传统纹理特征融合目标检测 完整实现教程
人工智能·神经网络·yolo·计算机视觉
小白|1 天前
hccl:昇腾集合通信库架构深度实践
人工智能·yolo·目标检测
AI棒棒牛1 天前
YOLO26改进创新 | 全网首发!VECA弹性核心注意力重塑全局建模,线性复杂度增强检测骨干,嘎嘎创新!
python·yolo·目标检测·yolo26·主干改进
Deep-w1 天前
【目标检测系统网页版】基于YOLOv8的淡水鱼检测系统
人工智能·yolo·目标检测
星越华夏1 天前
智能驾驶工程师职业指南
人工智能·yolo·目标检测