千家信息网

虹软人脸识别3.0 - 图像数据结构介绍(Android)

发表于:2025-01-25 作者:千家信息网编辑
千家信息网最后更新 2025年01月25日,从虹软开放了2.0版本SDK以来,由于具有免费、离线使用的特点,我们公司在人脸识别门禁应用中使用了虹软SDK,识别效果还不错,因此比较关注虹软SDK的官方动态。近期上线了ArcFace 3.0 SDK
千家信息网最后更新 2025年01月25日虹软人脸识别3.0 - 图像数据结构介绍(Android)

从虹软开放了2.0版本SDK以来,由于具有免费、离线使用的特点,我们公司在人脸识别门禁应用中使用了虹软SDK,识别效果还不错,因此比较关注虹软SDK的官方动态。近期上线了ArcFace 3.0 SDK版本,确实做了比较大的更新。首先本篇介绍一下关于Android平台算法的更新内容。

  • 特征比对支持比对模型选择,有 生活照比对模型人证比对模型
  • 识别率、防攻击效果显著提升
  • 特征值更新,升级后人脸库需重新注册
  • Android平台新增64位的SDK
  • 图像处理工具类
  • 人脸检测同时支持全角度及单一角度
  • 新增了一种图像数据传入方式

在实际开发过程中使用新的图像数据结构具有一定的难度,本文将从以下几点对该图像数据结构及使用方式进行详细介绍

  1. SDK接口变动

  2. ArcSoftImageInfo类解析

  3. SDK相关代码解析

  4. 步长的作用

  5. 将Camera2回传的Image转换为ArcSoftImageInfo

一、SDK接口变动

在接入3.0版SDK时,发现 FaceEngine类中的 detectFacesprocessextractFaceFeature等传入图像数据的函数都有重载函数,重载函数的接口均使用 ArcSoftImageInfo对象作为入参的图像数据,以人脸检测为例,具体接口如下:

原始接口:

public int detectFaces(byte[] data, int width, int height, int format, List faceInfoList)

新增接口:

public int detectFaces(ArcSoftImageInfo arcSoftImageInfo, List faceInfoList)

可以看到,重载函数传入 ArcSoftImageInfo对象作为图像数据进行检测, arcSoftImageInfo替代了原来的 data, width, height, format

二、ArcSoftImageInfo类解析

在我实际使用后发现, ArcSoftImageInfo不只是简单封装一下,它还将一维数组 data修改为二维数组 planes,还新增了一个与 planes对应的步长数组 strides

步长概念介绍:
步长可以理解为一行像素的字节数。

类结构如下:

public class ArcSoftImageInfo {    private int width;    private int height;    private int imageFormat;    private byte[][] planes;    private int[] strides;    ...}

官方文档中对该类的介绍:

  • 成员描述
类型变量名描述
intwidth图像宽度
intheight图像高度
intimageFormat图像格式
byte[][]planes图像通道
int[]strides每个图像通道的步长
  • 组成方式介绍
// arcSoftImageInfo组成方式举例:// NV21格式数据,有两个通道,// Y通道步长一般为图像宽度,若图像经过8字节对齐、16字节对齐等操作,需填入对齐后的图像步长// VU通道步长一般为图像宽度,若图像经过8字节对齐、16字节对齐等操作,需填入对齐后的图像步长ArcSoftImageInfo arcSoftImageInfo = new ArcSoftImageInfo(width, height, FaceEngine.CP_PAF_NV21, new byte[][]{planeY, planeVU}, new int[]{yStride, vuStride});// GRAY,只有一个通道,// 步长一般为图像宽度,若图像经过8字节对齐、16字节对齐等操作,需填入对齐后的图像步长arcSoftImageInfo = new ArcSoftImageInfo(width, height, FaceEngine.CP_PAF_GRAY, new byte[][]{gray}, new int[]{grayStride});// BGR24,只有一个通道,// 步长一般为图像宽度的三倍,若图像经过8字节对齐、16字节对齐等操作,需填入对齐后的图像步长arcSoftImageInfo = new ArcSoftImageInfo(width, height, FaceEngine.CP_PAF_BGR24, new byte[][]{bgr24}, new int[]{bgr24Stride});// DEPTH_U16,只有一个通道,// 步长一般为图像宽度的两倍,若图像经过8字节对齐、16字节对齐等操作,需填入对齐后的图像步长arcSoftImageInfo = new ArcSoftImageInfo(width, height, FaceEngine.CP_PAF_DEPTH_U16, new byte[][]{depthU16}, new int[]{depthU16Stride});

可以看到, ArcSoftImageInfo用于存储分离的图像数据,以 NV21数据为例, NV21数据有两个通道,那二维数组 planes存储的就是两个数组: y数组和 vu数组。以下是 NV21数据的排列方式:

NV21图像格式属于 YUV颜色空间中的 YUV420SP格式,每四个Y分量共用一组U分量和V分量,Y连续存储,U与V交叉存储。

排列方式如下(以8x4的图像为例):

Y Y   Y Y   Y Y   Y Y

Y Y   Y Y   Y Y   Y Y

Y Y   Y Y   Y Y   Y Y

Y Y   Y Y   Y Y   Y Y

V U  V U   V U  V U

V U  V U   V U  V U

以上数据分为两个通道,首先是连续的 Y数据,然后是交叉存储的 VU数据。如果我们使用的是 Camera API,那基本用不到 ArcSoftImageInfo类,因为 Camera API回传的 NV21数据是连续的,直接使用旧版接口即可;而当我们使用的是其他API时,拿到的数据可能是不连续的,例如使用 Camera2 APIMediaCodec拿到的 android.media.Image类对象,其图像数据也是分通道的,我们可以根据其通道内容,获取 Y通道数据和 VU通道数据,组成 NV21格式的 ArcSoftImageInfo对象用于处理。

三、SDK相关代码解析

我们来看下SDK中判断图像数据是否合法的校验代码:

注:原始代码由于被编译器修改过,阅读体验不佳,以下代码是我修改过的,将常量值替换回常量名,更便于阅读。

  • 校验分离的图像信息数据

         private static boolean isImageDataValid(byte[] data, int width, int height, int format) {         return          (format == CP_PAF_NV21 && (height & 1) == 0 && data.length == width * height * 3 / 2)||          (format == CP_PAF_BGR24 && data.length == width * height * 3)||          (format == CP_PAF_GRAY && data.length == width * height) ||         (format == CP_PAF_DEPTH_U16 && data.length == width * height * 2);     }

    解读:
    各个图像数据的要求如下:

    1. NV21格式图像数据的高度是偶数,数据大小是: 宽x高x3/2
    2. BGR24格式图像数据的大小是: 宽x高x3
    3. GRAY格式图像数据的大小是: 宽x高
    4. DEPTH_U16格式图像数据的大小是: 宽x高x2


  • 校验 ArcSoftImageInfo对象

         private static boolean isImageDataValid(ArcSoftImageInfo arcSoftImageInfo) {         byte[][] planes = arcSoftImageInfo.getPlanes();         int[] strides = arcSoftImageInfo.getStrides();         if (planes != null && strides != null) {             if (planes.length != strides.length) {                 return false;             } else {                 byte[][] var3 = planes;                 int var4 = planes.length;                 for(int var5 = 0; var5 < var4; ++var5) {                     byte[] plane = var3[var5];                     if (plane == null || plane.length == 0) {                         return false;                     }                 }                 switch(arcSoftImageInfo.getImageFormat()) {                 case CP_PAF_BGR24:                 case CP_PAF_GRAY:                 case CP_PAF_DEPTH_U16:                     return planes.length == 1 && planes[0].length == arcSoftImageInfo.getStrides()[0] * arcSoftImageInfo.getHeight();                 case CP_PAF_NV21:                     return (arcSoftImageInfo.getHeight() & 1) == 0 && planes.length == 2 && planes[0].length == planes[1].length * 2 && planes[0].length == arcSoftImageInfo.getStrides()[0] * arcSoftImageInfo.getHeight() && planes[1].length == arcSoftImageInfo.getStrides()[1] * arcSoftImageInfo.getHeight() / 2;                 default:                     return false;                 }             }         } else {             return false;         }     }

    解读:

    1. 每个通道数据的大小是: 高度x每个通道的步长
    2. BGR24GRAYDEPTH_U16格式图像数据都只有一个通道,但上述示例组成方式说明中提到它们的步长不同,关系如下:
      • BGR24格式图像数据步长一般为 3 x width
      • GRAY格式图像数据步长一般为 width
      • DEPTH_U16格式图像数据步长一般为 2 x width
    3. NV21格式图像数据的高度是偶数,有两个通道,且第0个通道的数据大小是第1个通道数据大小的2倍。

    四、步长的作用

    • 具体踩坑举例

      如下图,这是在某台手机上使用 Camera2 API时,指定了以 1520x760分辨率进行预览时获取的数据。虽然指定的分辨率是 1520x760,但是预览数据的实际大小却是 1536x760,解析存下的图像数据,发现右边填充的16像素内容均为0,此时若我们以1520x760的分辨率去将这组YUV数据取出并转换为 NV21,并在进行人脸检测时传入的宽度是1520,SDK将无法检测到人脸;若我们以1536x760的分辨率去解析,生成的 NV21传给SDK,并且传入的宽度是1536时,SDK能够检测到人脸。

  • 步长的重要性

只是差了这几个像素,为什么就导致人脸检测不到了呢?之前说到过,步长可以理解为一行像素的字节数。如果第一行像素的读取有偏差,那后续像素的读取也会受到影响。

以下是对一张大小为 1000x554NV21图像数据,以不同步长进行解析的结果:

以正确的步长解析以错误的步长解析

可以看到,对于一张图像,如果使用了错误的步长去解析,我们可能就无法看到正确的图像内容。

结论:通过引入图像步长能够有效的避免高字节对齐的问题。

五、将Camera2回传的Image转换为ArcSoftImageInfo

  • Camera2 API回传数据处理

      对于以上场景,我们可提取`android.media.Image`对象的`Y`、`U`、`V`通道数据,组成`NV21`格式的`ArcSoftImageInfo`对象,传入SDK处理。示例代码如下:
    • 取出 Camera2 API回传数据的 YUV通道数据

                  private class OnImageAvailableListenerImpl implements ImageReader.OnImageAvailableListener{                private byte[] y;                private byte[] u;                private byte[] v;                @Override                public void onImageAvailable(ImageReader reader) {                    Image image = reader.acquireNextImage();                    // 实际结果一般是 Y:U:V == 4:2:2                    if (camera2Listener != null && image.getFormat() == ImageFormat.YUV_420_888) {                        Image.Plane[] planes = image.getPlanes();                        // 重复使用同一批byte数组,减少gc频率                        if (y == null) {                            y = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()];                            u = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()];                            v = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()];                        }                        if (image.getPlanes()[0].getBuffer().remaining() == y.length) {                            planes[0].getBuffer().get(y);                            planes[1].getBuffer().get(u);                            planes[2].getBuffer().get(v);                            camera2Listener.onPreview(y, u, v, mPreviewSize, planes[0].getRowStride());                        }                    }                    image.close();                }            }
    • 转换为 ArcSoftImageInfo对象

    注意: 拿到的YUV数据可能是 YUV422,也可能是 YUV420,需要分别实现两者转换为 NV21格式的 ArcSoftImageInfo对象的函数。

              @Override          public void onPreview(final byte[] y, final byte[] u, final byte[] v, final Size previewSize, final int stride) {              if (arcSoftImageInfo == null) {                  arcSoftImageInfo = new ArcSoftImageInfo(previewSize.getWidth(), previewSize.getHeight(), FaceEngine.CP_PAF_NV21);              }              // 回传数据是YUV422              if (y.length / u.length == 2) {                  ImageUtil.yuv422ToNv21ImageInfo(y, u, v, arcSoftImageInfo, stride, previewSize.getHeight());              }              // 回传数据是YUV420              else if (y.length / u.length == 4) {                  ImageUtil.yuv420ToNv21ImageInfo(y, u, v, arcSoftImageInfo, stride, previewSize.getHeight());              }              // 此时的arcSoftImageInfo数据即可传给SDK使用              if (faceEngine != null) {                  List faceInfoList = new ArrayList<>();                  int code = faceEngine.detectFaces(arcSoftImageInfo, faceInfoList);                  if (code == ErrorInfo.MOK) {                      Log.i(TAG, "onPreview: " + code + "  " + faceInfoList.size());                  } else {                      Log.i(TAG, "onPreview: no face detected , code is : " + code);                  }              } else {                  Log.e(TAG, "onPreview: faceEngine is null");                  return;              }              ...          }

以上代码中便是 Camera2 API回传的数据转换为 ArcSoftImageInfo对象并检测的具体实现。以下是将 YUV数据组成 ArcSoftImageInfo对象的具体实现。

  • YUV数据组成 ArcSoftImageInfo对象

    对于 Y通道,直接拷贝即可,对于 U通道和 V通道,需要考虑这组YUV数据的格式是 YUV420还是 YUV422,再获取其中的 UV数据

             /**          * YUV420数据转换为NV21格式的ArcSoftImageInfo          *          * @param y                YUV420数据的y分量          * @param u                YUV420数据的u分量          * @param v                YUV420数据的v分量          * @param arcSoftImageInfo NV21格式的ArcSoftImageInfo          * @param stride           y分量的步长,一般情况下,由于YUV数据的对应关系,Y分量步长确定了,U和V也随之确定          * @param height           图像高度          */         public static void yuv420ToNv21ImageInfo(byte[] y, byte[] u, byte[] v, ArcSoftImageInfo arcSoftImageInfo, int stride, int height) {             if (arcSoftImageInfo.getPlanes() == null) {                 arcSoftImageInfo.setPlanes(new byte[][]{new byte[stride * height], new byte[stride * height / 2]});                 arcSoftImageInfo.setStrides(new int[]{stride, stride});             }             System.arraycopy(y, 0, arcSoftImageInfo.getPlanes()[0], 0, y.length);             // 注意,vuLength 不能直接通过步长和高度计算,实测发现Camera2 API回传的数据有数据丢失,需要使用真实数据长度             byte[] vu = arcSoftImageInfo.getPlanes()[1];             int vuLength = u.length / 2 + v.length / 2;             int uIndex = 0, vIndex = 0;             for (int i = 0; i < vuLength; i++) {                 vu[i] = v[vIndex++];                 vu[i + 1] = u[uIndex++];             }         }         /**          * YUV422数据转换为NV21格式的ArcSoftImageInfo          *          * @param y                YUV422数据的y分量          * @param u                YUV422数据的u分量          * @param v                YUV422数据的v分量          * @param arcSoftImageInfo NV21格式的ArcSoftImageInfo          * @param stride           y分量的步长,一般情况下,由于YUV数据的对应关系,Y分量步长确定了,U和V也随之确定          * @param height           图像高度          */         public static void yuv422ToNv21ImageInfo(byte[] y, byte[] u, byte[] v, ArcSoftImageInfo arcSoftImageInfo, int stride, int height) {             if (arcSoftImageInfo.getPlanes() == null) {                 arcSoftImageInfo.setPlanes(new byte[][]{new byte[stride * height], new byte[stride * height / 2]});                 arcSoftImageInfo.setStrides(new int[]{stride, stride});             }             System.arraycopy(y, 0, arcSoftImageInfo.getPlanes()[0], 0, y.length);             byte[] vu = arcSoftImageInfo.getPlanes()[1];             // 注意,vuLength 不能直接通过步长和高度计算,实测发现Camera2 API回传的数据有数据丢失,需要使用真实数据长度             int vuLength = u.length / 2 + v.length / 2;             int uIndex = 0, vIndex = 0;             for (int i = 0; i < vuLength; i += 2) {                 vu[i] = v[vIndex];                 vu[i + 1] = u[uIndex];                 vIndex += 2;                 uIndex += 2;             }         }

六、ArcSoftImageInfo优点总结

  1. 在获取的图像数据源是分通道的数据时,使用 ArcSoftImageInfo对象传入分离的图像数据可避免数据拼接所需的额外内存消耗。
  2. 引入了步长的概念,在使用时传入了各个通道的步长,使开发者在使用SDK时对图像数据的了解更清晰。

Android Demo可在 虹软人脸识别开放平台下载

0