十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
从虹软开放了2.0版本SDK以来,由于具有免费、离线使用的特点,我们公司在人脸识别门禁应用中使用了虹软SDK,识别效果还不错,因此比较关注虹软SDK的官方动态。近期上线了ArcFace 3.0 SDK版本,确实做了比较大的更新。首先本篇介绍一下关于Android平台算法的更新内容。
创新互联公司坚持“要么做到,要么别承诺”的工作理念,服务领域包括:成都网站设计、网站制作、外贸营销网站建设、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的庆云网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!
在实际开发过程中使用新的图像数据结构具有一定的难度,本文将从以下几点对该图像数据结构及使用方式进行详细介绍
SDK接口变动
ArcSoftImageInfo类解析
SDK相关代码解析
步长的作用
将Camera2回传的Image转换为ArcSoftImageInfo
在接入3.0版SDK时,发现
FaceEngine
类中的
detectFaces
、
process
、
extractFaceFeature
等传入图像数据的函数都有重载函数,重载函数的接口均使用
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
不只是简单封装一下,它还将一维数组
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能够检测到人脸。
步长的重要性
只是差了这几个像素,为什么就导致人脸检测不到了呢?之前说到过,步长可以理解为一行像素的字节数。如果第一行像素的读取有偏差,那后续像素的读取也会受到影响。
以下是对一张大小为
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 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可在 虹软人脸识别开放平台下载