人脸识别过程:人脸拍照,照片传给平台,平台进行人脸比对,返回比对结果。下面将介绍人脸识别的每个过程
1 人脸拍照
人在摄像头前面走,摄像头会把每一帧的数据传给人脸识别SDK,SDK会根据每一帧的数据绘制人脸框并返回人脸信息(faceInfo),当返回的人脸信息的质量分数超过给定的分数,就可以调用相机进行拍照,拍摄好的照片上传到平台。
1.1 相机拍摄及预览
摄像头拍摄的数据一帧帧的显示到屏幕上,主要包括两个步骤,步骤一相机拍摄,步骤二预览帧的显示
(1)相机拍摄
下面从零开始介绍相机拍摄,最后再给出具体的实现代码。
开启相机: mCamera = Camera.open(mFrontCameraId); mFrontCameraId表示相机的id,例如前置摄像头id为1,后置摄像头id为2.
/**
* 开启指定摄像头
*/
private void openCamera() {
if (mCamera != null) {
throw new RuntimeException("相机已经被开启,无法同时开启多个相机实例!");
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
if (hasFrontCamera()) {
// 优先开启前置摄像头
mCamera = Camera.open(mFrontCameraId);
} else if (hasBackCamera()) {
// 没有前置,就尝试开启后置摄像头
mCamera = Camera.open(mBackCameraId);
} else {
throw new RuntimeException("没有任何相机可以开启!");
}
}
}
关闭相机: mCamera.release();
/**
* 关闭相机。
*/
private void closeCamera() {
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
根据 ID 获取 CameraInfo:
@Nullable
private Camera.CameraInfo mFrontCameraInfo = null;
private int mFrontCameraId = -1;
@Nullable
private Camera.CameraInfo mBackCameraInfo = null;
private int mBackCameraId = -1;
/**
* 初始化摄像头信息。
*/
private void initCameraInfo() {
int numberOfCameras = Camera.getNumberOfCameras();// 获取摄像头个数
for (int cameraId = 0; cameraId < numberOfCameras; cameraId++) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
// 后置摄像头信息
mBackCameraId = cameraId;
mBackCameraInfo = cameraInfo;
} else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT){
// 前置摄像头信息
mFrontCameraId = cameraId;
mFrontCameraInfo = cameraInfo;
}
}
}
CameraInfo包含如下信息:
-
facing
摄像头的方向,可选值有 Camera.CameraInfo.CAMERA_FACING_BACK 和Camera.CameraInfo.CAMERA_FACING_FRONT。
-
orientation
摄像头的画面经过顺时针旋转多少度之后是正常画面,这个属性比较难以理解,我们在后面会专门解释。
-
canDisableShutterSound
是否支持静音拍照,也就是说在用户拍照的时候是否会“咔嚓”一声,例如在日本由于隐私政策,所有手机都是不允许静音拍照的,那么该字段就会返回 false。该字段一般配合 Camera.enableShutterSound(boolean) 使用,当它返回 false 的时候,即使你调用 Camera.enableShutterSound(false),相机在拍照的时候也会发出声音。
(2)预览
介绍预览之前,需要先介绍几个概念:
分辨率
由于不同厂商对相机的实现都会有差异,所以很多参数在不同的手机上支持的情况也不一样,相机的预览尺寸也是,所以接下来我们就要通过 CameraCharacteristics 获取相机支持的预览尺寸列表。所谓的预览尺寸,指的就是相机把画面输出到手机屏幕上供用户预览的尺寸,通常来说我们希望预览尺寸在不超过手机屏幕分辨率的情况下,越大越好。另外,出于业务需求,我们的相机可能需要支持多种不同的预览比例供用户选择,例如 4:3 和 16:9 的比例。由于不同厂商对相机的实现都会有差异,所以很多参数在不同的手机上支持的情况也不一样,相机的预览尺寸也是。
图像数据的方向
如果你熟悉 Camera1 的话,也许已经发现了一个问题,就是 Camera2 不需要经过任何预览画面方向的矫正,就可以正确现实画面,而 Camera1 则需要根据摄像头传感器的方向进行预览画面的方向矫正。其实,Camera2 也需要进行预览画面的矫正,只不过系统帮我们做了而已,当我们使用 TextureView 或者 SurfaceView 进行画面预览的时候,系统会根据【设备自然方向】、【摄像传感器方向】和【显示方向】自动矫正预览画面的方向,并且该矫正规则只适用于显示方向和和设备自然方向一致的情况下
SurfaceView
Surface意为表层、表面,顾名思义SurfaceView就是指一个在表层的View对象。为什么说是在表层呢,这是因为它有点特殊跟其他View不一样,其他View是绘制在“表层”的上面,而它就是充当“表层”本身。SDK的文档 说到:SurfaceView就是在窗口上挖一个洞,它就是显示在这个洞里,其他的View是显示在窗口上,所以View可以显式在 SurfaceView之上,你也可以添加一些层在SurfaceView之上。 从API中可以看出SurfaceView属于View的子类 它是专门为制作游戏而产生的,它的功能非常强大,最重要的是它支持OpenGL ES库,2D和3D的效果都可以实现。创建SurfaceView的时候需要实现SurfaceHolder.Callback接口,它可以用来监听SurfaceView的状态,比如:SurfaceView的改变 、SurfaceView的创建 、SurfaceView 销毁等,我们可以在相应的方法中做一些比如初始化的操作或者清空的操作等等。
Android系统提供了View进行绘图处理,我们通过自定义的View可以满足大部分的绘图需求,但是这有个问题就是我们通常自定义的View是用于主动更新情况的,用户无法控制其绘制的速度,由于View是通过invalidate方法通知系统去调用view.onDraw方法进行重绘,而Android系统是通过发出VSYNC信号来进行屏幕的重绘,刷新的时间是16ms,如果在16ms内View完成不了执行的操作,用户就会看着卡顿,比如当draw方法里执行的逻辑过多,需要频繁刷新的界面上,例如游戏界面,那么就会不断的阻塞主线程,从而导致画面卡顿。而SurfaceView相当于是另一个绘图线程,它是不会阻碍主线程,并且它在底层实现机制中实现了双缓冲机制。
SurfaceView使用步骤
首先SurfaceView也是一个View,它也有自己的生命周期。因为它需要另外一个线程来执行绘制操作,所以我们可以在它生命周期的初始化阶 段开辟一个新线程,然后开始执行绘制,当生命周期的结束阶段我们插入结束绘制线程的操作。这些是由其内部一个SurfaceHolder对象完成的。
SurfaceView它的绘制原理是绘制前先锁定画布(获取画布),然后等都绘制结束以后在对画布进行解锁 ,最后在把画布内容显示到屏幕上。
通常情况下,使用以下步骤来创建一个SurfaceView的模板:
(1)创建SurfaceView
创建自定义的SurfaceView继承自SurfaceView,并实现两个接口:SurfaceHolder.Callback和Runnable.代码如下:
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback,Runnable
通过实现这两个接口,就需要在自定义的SurfaceView中实现接口的方法,对于SurfaceHolder.Callback方法,需要实现如下方法,其实就是SurfaceView的生命周期:
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
对于Runnable接口,需要实现run()方法,
@Override
public void run() {
}
(2)初始化SurfaceView
在自定义的MySurfaceView的构造方法中,需要对SurfaceView进行初始化,包括SurfaceHolder的初始化、画笔的初始化等。在自定义的SurfaceView中,通常需要定义以下三个成员变量:
private SurfaceHolder mHolder;
private Canvas mCanvas;//绘图的画布
private boolean mIsDrawing;//控制绘画线程的标志位
SurfaceHolder,顾名思义,它里面保存了一个对Surface对象的引用,而我们执行绘制方法本质上就是操控Surface。SurfaceHolder因为保存了对Surface的引用,所以使用它来处理Surface的生命周期。(说到底 SurfaceView的生命周期其实就是Surface的生命周期)例如使用 SurfaceHolder来处理生命周期的初始化。
初始化代码如下:
public MySurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
}
private void initView() {
mHolder = getHolder();//获取SurfaceHolder对象
mHolder.addCallback(this);//注册SurfaceHolder的回调方法
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true);
}
(3)使用SurfaceView
通过SurfaceHolder对象的lockCanvans()方法,我们可以获取当前的Canvas绘图对象。接下来的操作就和自定义View中的绘图操作一样了。需要注意的是这里获取到的Canvas对象还是继续上次的Canvas对象,而不是一个新的对象。因此,之前的绘图操作都会被保留,如果需要擦除,则可以在绘制前,通过drawColor()方法来进行清屏操作。
绘制的时候,在surfaceCreated()方法中开启子线程进行绘制,而子线程使用一个while(mIsDrawing)的循环来不停的进行绘制,在绘制的逻辑中通过lockCanvas()方法获取Canvas对象进行绘制,通过unlockCanvasAndPost(mCanvas)方法对画布内容进行提交。整体代码模板如下:
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class MySurfaceView extends SurfaceView
implements SurfaceHolder.Callback, Runnable {
// SurfaceHolder
private SurfaceHolder mHolder;
// 用于绘图的Canvas
private Canvas mCanvas;
// 子线程标志位
private boolean mIsDrawing;
public MySurfaceView(Context context) {
super(context);
initView();
}
public MySurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MySurfaceView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView() {
mHolder = getHolder();
mHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true);
//mHolder.setFormat(PixelFormat.OPAQUE);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
while (mIsDrawing) {
draw();
}
}
//绘图操作
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
// draw sth绘制过程
} catch (Exception e) {
} finally {
if (mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);//保证每次都将绘图的内容提交
}
}
}
总的摄像头预览代码
/**
* 实时预览帧 setPreviewCallback
*
* @author yusr
*/
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback {
private static final String TAG = "CameraPreview";
private Camera mCamera;
private PreDataCallBack preDataCallBack;
/**
* 预览高
*/
private int previewH;
/**
* 预览宽
*/
private int previewW;
/**
* 摄像头id
*/
private int cameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
/**
* Preview state
*/
private boolean isPreviewing = false;
private byte[] mPreviewBuffer = new byte[LocalFaceSDK.previewHeight * LocalFaceSDK.previewWidth * 3 / 2];
private CameraThread cameraThread;
public CameraPreview(Context context) {
super(context);
}
public CameraPreview(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public CameraPreview(Context context, AttributeSet attrs) {
super(context, attrs);
}
private Handler getExcuteHandler() {
if (cameraThread == null) {
cameraThread = new CameraThread(getClass().getSimpleName());
}
return cameraThread.getHandler();
}
/**
* functionName: setPreDataCallBack <p>
* description: 设置数据回调接口<p>
* params: preDataCallBack <p>
* return: void <p>
* author: Warren <p>
* date: 2018/10/9 <p>
*/
public void setPreDataCallBack(PreDataCallBack preDataCallBack) {
this.preDataCallBack = preDataCallBack;
}
/**
* functionName: setPreviewWH <p>
* description: 设置预览分辨率<p>
* params: previewW <p>
* params: previewH <p>
* return: void <p>
* author: Warren <p>
* date: 2018/10/9 <p>
*/
public void setPreviewWH(int previewW, int previewH) {
this.previewH = previewH;
this.previewW = previewW;
}
/**
* functionName: setCameraId <p>
* description: 设置摄像头id<p>
* params: cameraId <p>
* return: void <p>
* author: Warren <p>
* date: 2018/10/9 <p>
*/
public void setCameraId(int cameraId) {
this.cameraId = cameraId;
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
cwInitCamera();
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i2, int i3) {
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
cwReleaseCamera();
}
public void setExcuteThread(CameraThread cameraThread) {
if (this.cameraThread != null) {
this.cameraThread.stop();
}
this.cameraThread = cameraThread;
}
/**
* 打开摄像头开始预览,但是并未开始识别
*/
public void cwStartCamera() {
getHolder().addCallback(this);
cwInitCamera();
}
private Camera.AutoFocusMoveCallback mAutoFocusCallback = new Camera.AutoFocusMoveCallback() {
@Override public void onAutoFocusMoving(boolean start, Camera camera) {
if (start) {
camera.setOneShotPreviewCallback(null);
LoggerUtils.d(TAG,"自动聚焦");
}
}
};
/**
* Init camera open
*/
public void cwInitCamera() {
getExcuteHandler().post(new Runnable() {
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
@Override
public void run() {
try {
if (mCamera != null && isPreviewing) {
//表示已经打开就不再重复打开摄像头了
return;
}
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
LoggerUtils.e(TAG, "open camera cameraId is " + cameraId);
// Open camera
mCamera = Camera.open(cameraId);
mCamera.setAutoFocusMoveCallback(mAutoFocusCallback);
// set orientation angle,default 90
mCamera.setDisplayOrientation(0);
// display by surfaceView
mCamera.setPreviewDisplay(getHolder());
// set preview size
Camera.Parameters parameters = mCamera.getParameters();
printSupportedPreviewSizes(parameters.getSupportedPreviewSizes());
parameters.setPreviewSize(previewW, previewH);
mCamera.setParameters(parameters);
// set preview callback
mCamera.addCallbackBuffer(mPreviewBuffer);
mCamera.setPreviewCallbackWithBuffer(CameraPreview.this);
// start preview
mCamera.startPreview();
if (LocalFaceSDK.getInstance().getOnCameraStateCallback() != null) {
LocalFaceSDK.getInstance().getOnCameraStateCallback() .onCameraOpen(cameraId);
}
} catch (Exception e) {
e.printStackTrace();
LoggerUtils.e(TAG, "Camera open exception ex:" + e.toString());
if (LocalFaceSDK.getInstance().getOnCameraStateCallback() != null) {
LocalFaceSDK.getInstance().getOnCameraStateCallback() .onCameraError();
}
}
}
});
}
/**
* Release camera
*/
private void cwReleaseCamera() {
getExcuteHandler().post(new Runnable() {
@Override
public void run() {
LoggerUtils.e("mCamera", "cwReleaseCamera--------- mCamera is null ? = " + (mCamera == null));
if (mCamera != null) {
// stop preview
mCamera.stopPreview();
isPreviewing = false;
mCamera.addCallbackBuffer(null);
// release camera
mCamera.release();
mCamera = null;
}
}
});
}
/**
* 关闭摄像头预览,并且隐藏扫描框
*/
public void cwStopCamera() {
LoggerUtils.d(TAG, "可见光摄像头关闭");
cwReleaseCamera();
}
@TargetApi(Build.VERSION_CODES.FROYO)
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (preDataCallBack != null && data.length > 0) {
preDataCallBack.onPreviewFrame(data, 0, cameraId);
}
camera.addCallbackBuffer(mPreviewBuffer);
}
private void printSupportedPreviewSizes(List<Camera.Size> sizeList) {
for (Camera.Size temp : sizeList) {
Log.d(TAG, "previewWidth:" + temp.width + ";previewHeight:" + temp.height);
}
}
private Camera.Size getOptimizedPreviewSize(List<Camera.Size> sizeList) {
LoggerUtils.d(TAG, Arrays.toString(sizeList.toArray()));
Camera.Size optimizedPreviewSize = sizeList.get(0);
double minDiff = 100;
double tempDiff;
for (Camera.Size temp : sizeList) {
tempDiff = Math.abs((double) LocalFaceSDK.previewWidth / LocalFaceSDK.previewHeight
- (double) temp.width / temp.height);
if (tempDiff < minDiff) {
minDiff = tempDiff;
optimizedPreviewSize = temp;
}
}
return optimizedPreviewSize;
}
}