Android端侧部署人脸识别工程性能优化笔记——NV21数据快速变换
最近接手了一个工程其中涉及人脸识别后打码的流程然后功耗特别严重看了一下主要是图像处理逻辑问题导致的我会把发现的问题和解决过程记录一下。主要问题识别模型是使用yolo实现的最后端侧部署接口使用Bitmap相机Preview回来的是NV21数据但这里了用这样的方法去做数据转换这样转写起来方便安卓自带就有但性能问题很大它会先把NV21图像通过DCT、量化、霍夫曼编码等一系列运算保存成JPEG格式数据然后重现为Bitmap数据时又要通过IDCT、霍夫曼编码等解码流程重新解成RGB数据多了很多本不必要的逻辑本来YUV数据通过一个公式就可以一步到位转化为RGB数据了。为了让接口输出的物体外接矩形数据和Preview窗口坐标可以直接对齐这里使用了错误的转换做法因为Preview的显示方向和NV21的数据有90度的角度差它可能不希望输出的外接矩形直接用起来和Preview没法对得上然后就用了这样的方法旋转NV21的之前提到的步骤再生成位图在CPU做这种双重循环非常耗时而且本来并没有必要直接对最终产生的坐标做很简易的交换就可以了根本不需要在识别前做这些事。为了把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另外因为要用到jnigraphicsCMakeLists要改一下配置上要加上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_castuint8_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_castuint8_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_castuint8_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_castuint8_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的宽高进行线性变换量化得出这些xy坐标应该实际在preview上的什么位置。2、xy坐标交换但因为之前的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占用时间的降低这一流程分支带来的功耗消耗也下降下来了。