从虹软开放了2.0版本SDK以来,因为具备免费、离线使用的特色,咱们公司在人脸识别门禁应用中使用了虹软SDK,识别效果还不错,所以比较关注虹软SDK的官方动态。近期上线了ArcFace 3.0 SDK版本,确实作了比较大的更新。首先本篇介绍一下关于Android平台算法的更新内容,下一篇将针对Windows平台的算法更新展开介绍。html
在实际开发过程当中使用新的图像数据结构具备必定的难度,本文将从如下几点对该图像数据结构及使用方式进行详细介绍java
SDK接口变更android
ArcSoftImageInfo类解析算法
SDK相关代码解析数组
步长的做用数据结构
将Camera2回传的Image转换为ArcSoftImageInfoide
在接入3.0版SDK时,发现FaceEngine
类中的detectFaces
、process
、extractFaceFeature
等传入图像数据的函数都有重载函数,重载函数的接口均使用ArcSoftImageInfo
对象做为入参的图像数据,以人脸检测为例,具体接口以下:函数
原始接口:工具
public int detectFaces(byte[] data, int width, int height, int format, List<FaceInfo> faceInfoList)
新增接口:ui
public int detectFaces(ArcSoftImageInfo arcSoftImageInfo, List<FaceInfo> faceInfoList)
能够看到,重载函数传入ArcSoftImageInfo
对象做为图像数据进行检测,arcSoftImageInfo
替代了原来的data, width, height, format
。
在我实际使用后发现,ArcSoftImageInfo
不仅是简单封装一下,它还将一维数组data
修改成二维数组planes
,还新增了一个与planes
对应的步长数组strides
。
步长概念介绍: 步长能够理解为一行像素的字节数。
类结构以下:
public class ArcSoftImageInfo { private int width; private int height; private int imageFormat; private byte[][] planes; private int[] strides; ... }
官方文档中对该类的介绍:
类型 | 变量名 | 描述 |
---|---|---|
int | width | 图像宽度 |
int | height | 图像高度 |
int | imageFormat | 图像格式 |
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
数据,而后是交叉存储的V
和U
数据。若是咱们使用的是Camera API
,那基本用不到ArcSoftImageInfo
类,由于Camera API
回传的NV21
数据是连续的,直接使用旧版接口便可;而当咱们使用的是其余API时,拿到的数据多是不连续的,例如使用Camera2 API
、MediaCodec
拿到的android.media.Image
类对象,其图像数据也是分通道的,咱们能够根据其通道内容,获取Y
通道数据和VU
通道数据,组成NV21
格式的ArcSoftImageInfo
对象用于处理。
咱们来看下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); }
解读: 各个图像数据的要求以下:
NV21
格式图像数据的高度是偶数,数据大小是:宽x高x3/2
BGR24
格式图像数据的大小是:宽x高x3
GRAY
格式图像数据的大小是:宽x高
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; } }
解读:
高度x每一个通道的步长
BGR24
、GRAY
、DEPTH_U16
格式图像数据都只有一个通道,但上述示例组成方式说明中提到它们的步长不一样,关系以下:
BGR24
格式图像数据步长通常为3 x width
GRAY
格式图像数据步长通常为width
DEPTH_U16
格式图像数据步长通常为2 x width
NV21
格式图像数据的高度是偶数,有两个通道,且第0个通道的数据大小是第1个通道数据大小的2倍。具体踩坑举例
以下图,这是在某台手机上使用Camera2 API
时,指定了以1520x760
分辨率进行预览时获取的数据。虽然指定的分辨率是1520x760
,可是预览数据的实际大小倒是1536x760
,解析存下的图像数据,发现右边填充的16像素内容均为0,此时若咱们以1520x760的分辨率去将这组YUV数据取出并转换为NV21
,并在进行人脸检测时传入的宽度是1520,SDK将没法检测到人脸;若咱们以1536x760的分辨率去解析,生成的NV21
传给SDK,而且传入的宽度是1536时,SDK可以检测到人脸。
步长的重要性 只是差了这几个像素,为何就致使人脸检测不到了呢?以前说到过,步长能够理解为一行像素的字节数。若是第一行像素的读取有误差,那后续像素的读取也会受到影响。<br>
如下是对一张大小为1000x554
的NV21
图像数据,以不一样步长进行解析的结果:
以正确的步长解析 | 以错误的步长解析 |
---|---|
![]() |
![]() |
能够看到,对于一张图像,若是使用了错误的步长去解析,咱们可能就没法看到正确的图像内容。
结论:经过引入图像步长可以有效的避免高字节对齐的问题。
Camera2 API回传数据处理
对于以上场景,咱们可提取android.media.Image
对象的Y
、U
、V
通道数据,组成NV21
格式的ArcSoftImageInfo
对象,传入SDK处理。示例代码以下:
Camera2 API
回传数据的Y
、U
、V
通道数据 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<FaceInfo> 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
对象并检测的具体实现。如下是将Y
、U
、V
数据组成ArcSoftImageInfo
对象的具体实现。
Y
、U
、V
数据组成ArcSoftImageInfo
对象对于
Y
通道,直接拷贝便可,对于U
通道和V
通道,须要考虑这组YUV数据的格式是YUV420
仍是YUV422
,再获取其中的U
、V
数据
/** * 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
对象传入分离的图像数据可避免数据拼接所需的额外内存消耗。Android Demo可在虹软人脸识别开放平台下载