这篇文章主要介绍基于虹软人脸识别SDK,适配Camera1、Camera2、CameraX。文章分下面几点展开。
一、应用设计流程图
二、应用界面展示
三、虹软SDK介绍
四、代码实现
ArcSoft官方的demo目前是采用的Camera1接口,我前面也写过一篇单独Camera2 接口集成Arcsoft接口的文章。(https://www.jianshu.com/p/cb2f65ead747)
一、应用设计流程图
如下图所示,应用流程比较简单,分别从不同的API接口获取到Camera数据流数据,然后送到ArcSoft人脸识别算法库中进行识别,最终将识别结果绘制到界面上。
二、应用界面
CameraX需要和界面生命周期进行绑定,所以主界面设计成了2个Button入口,一个入口时Camera1和Camera2共用,一个是CameraX独立的入口。
如下图所示:Camera1和Camera2之间可以互相切换。
CameraX是单独的界面。
三、虹软SDK介绍
本应用基于虹软人脸识别SDK 3.0版本,目前有更新的4.1版本,支持更多的功能。
虹软视觉开放平台: https://ai.arcsoft.com.cn/
我们点击选择进入“开发者中心”。
如上图所示,登录进入开发者中心后,我们可以创建我们的应用,进而获取到对应的APP_ID 和SDK_KEY,并且可以下载对应的SDK。
我们根据自己需要下载对应的sdk,sdk里面包含了需要用的jar包、so库,还有详细的集成说明文档,已经samplecode供我们参考集成,可谓是集成起来是非常的方便了。
四、代码实现
1) 虹软SDK关键代码
i、我们在开发者平台上申请的APP_ID和SDK_KEY,在激活人脸识别引擎的时候需要用到。
public static final String APP_ID = "JDk8rV3BBGnToh6HNYzCFpxYXMowitBjFuCjPGQr4CcC";
public static final String SDK_KEY = "EBH3RG2D55ayfVrGK7SeTivMWxjzsskwNt1RyfVPVbg9";
ii、初始化SDK引擎
private void initEngine() {
faceEngine = new FaceEngine();
afCode = faceEngine.init(this, DetectMode.ASF_DETECT_MODE_VIDEO, ConfigUtil.getFtOrient(this),
16, 20,
FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_AGE | FaceEngine.ASF_FACE3DANGLE | FaceEngine.ASF_GENDER | FaceEngine.ASF_LIVENESS);
if (afCode != ErrorInfo.MOK) {
showToast( getString(R.string.init_failed, afCode));
}
}
iii、将实时的nv21数据送到sdk中进行识别。detectFaces方法显示检测是否有人脸。检测到人脸后,process方法是获取识别的人脸详细信息(年龄、性别...)。
int code = faceEngine.detectFaces(nv21, previewSize.width,
previewSize.height,
FaceEngine.CP_PAF_NV21, faceInfoList);
if (code == ErrorInfo.MOK && faceInfoList.size() > 0) {
code = faceEngine.process(nv21, previewSize.width,
previewSize.height,
FaceEngine.CP_PAF_NV21,
faceInfoList, processMask);
2) Camera1 API的使用:
private void startCameraByApi1() {
CameraListener cameraListener = new CameraListener() {
@Override
public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) {
......
}
@Override
public void onPreview(byte[] nv21, Camera camera) {
//接收到实时预览数据回调,送入虹软sdk进行人脸识别
drawFaceInfo(nv21);
}
......
};
3) Camera2 API的使用:
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if(image == null){
return;
}
synchronized (mImageReaderLock) {
if(!mImageReaderLock.equals(1)){
Log.v(TAG, "--- image not available,just return!!!");
image.close();
return;
}
if (ImageFormat.YUV_420_888 == image.getFormat()) {
Image.Plane[] planes = image.getPlanes();
lock.lock();
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);
if (nv21 == null) {
nv21 = new byte[planes[0].getRowStride() * mPreviewSize.getHeight() * 3 / 2];
}
if(nv21 != null && (nv21.length != planes[0].getRowStride() * mPreviewSize.getHeight() *3/2)){
return;
}
// 回传数据是YUV422
if (y.length / u.length == 2) {
ImageUtil.yuv422ToYuv420sp(y, u, v, nv21, planes[0].getRowStride(), mPreviewSize.getHeight());
}
// 回传数据是YUV420
else if (y.length / u.length == 4) {
ImageUtil.yuv420ToYuv420sp(y, u, v, nv21, planes[0].getRowStride(), mPreviewSize.getHeight());
}
//调用Arcsoft算法,绘制人脸信息
drawFaceInfo(nv21);
}
lock.unlock();
}
}
image.close();
}
};
3) CameraX API的使用:
private void startCameraX() {
Log.v(TAG,"--- startCameraX();");
mPreviewSize = new Size(640,480);
setPreviewViewAspectRatio();
initArcsoftDrawHelper();
Rational rational = new Rational(mPreviewSize.getHeight(), mPreviewSize.getWidth());
// 1. preview
PreviewConfig previewConfig = new PreviewConfig.Builder()
.setTargetAspectRatio(rational)
.setTargetResolution(mPreviewSize)
.build();
Preview preview = new Preview(previewConfig);
preview.setOnPreviewOutputUpdateListener(new Preview.OnPreviewOutputUpdateListener() {
@Override
public void onUpdated(Preview.PreviewOutput output) {
previewView.setSurfaceTexture(output.getSurfaceTexture());
configureTransform(previewView.getWidth(),previewView.getHeight());
}
});
// 2. capture
ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig.Builder()
.setTargetAspectRatio(rational)
.setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
.build();
final ImageCapture imageCapture = new ImageCapture(imageCaptureConfig);
// 3. analyze
HandlerThread handlerThread = new HandlerThread("Analyze-thread");
handlerThread.start();
ImageAnalysisConfig imageAnalysisConfig = new ImageAnalysisConfig.Builder()
.setCallbackHandler(new Handler(handlerThread.getLooper()))
.setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
.setTargetAspectRatio(rational)
.setTargetResolution(mPreviewSize)
.build();
ImageAnalysis imageAnalysis = new ImageAnalysis(imageAnalysisConfig);
imageAnalysis.setAnalyzer(new MyAnalyzer());
CameraX.bindToLifecycle(this, preview, imageCapture, imageAnalysis);
}
private class MyAnalyzer implements ImageAnalysis.Analyzer {
private byte[] y;
private byte[] u;
private byte[] v;
private byte[] nv21;
private ReentrantLock lock = new ReentrantLock();
private Object mImageReaderLock = 1;//1 available,0 unAvailable
@Override
public void analyze(ImageProxy imageProxy, int rotationDegrees) {
Image image = imageProxy.getImage();
if(image == null){
return;
}
synchronized (mImageReaderLock) {
if(!mImageReaderLock.equals(1)){
image.close();
return;
}
if (ImageFormat.YUV_420_888 == image.getFormat()) {
Image.Plane[] planes = image.getPlanes();
if(mImageReaderSize == null){
mImageReaderSize = new Size(planes[0].getRowStride(),image.getHeight());
}
lock.lock();
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);
if (nv21 == null) {
nv21 = new byte[planes[0].getRowStride() * image.getHeight() * 3 / 2];
}
if(nv21 != null && (nv21.length != planes[0].getRowStride() * image.getHeight() *3/2)){
return;
}
// 回传数据是YUV422
if (y.length / u.length == 2) {
ImageUtil.yuv422ToYuv420sp(y, u, v, nv21, planes[0].getRowStride(), image.getHeight());
}
// 回传数据是YUV420
else if (y.length / u.length == 4) {
nv21 = ImageUtil.yuv420ToNv21(image);
}
//调用Arcsoft算法,绘制人脸信息
drawFaceInfo(nv21,mPreviewSize);
}
lock.unlock();
}
}
}
}
四、遇到的问题
1)预览变形
这个是由于设置Camera预览的size和TextureView的size比例不一致导致。
我们一般会根据当前设备屏幕的size,遍历camera支持的preview size,找到适合当前设备的预览size,再根据当前预览size,动态调整textureView的显示。
2)Arcsoft sdk code异常
中间遇到的关于Arcsoft sdk code异常的,可以在Arcsoft开发中心,帮助界面,输入对应的error code,根据提示信息,可以帮忙我们快速定位排查问题。
五、附录:
1)Demo地址:
链接:https://pan.baidu.com/s/1rKyncLbxPfH5ajDTINBhCg
提取码:xcbj
2)ArcSoft官网sdk下载地址:
https://ai.arcsoft.com.cn/
*本人从事Android Camera相关开发已有5年,
*目前在深圳上班,
*小伙伴记得点我头像,看【个人介绍】进行关注哦,希望和更多的小伙伴一起交流 ~
** 为了方便大家沟通交流,我建了个wx交流群,想加入的同学,欢迎私信我加入~*
-------- 2021.09.11 深圳