https://www.jianshu.com/p/5414ba2b5508
背景
最近需要做一个人脸检测并实时预览的功能。就是边检测人脸,边在预览界面上框出来。
当然本人并不是专门做Android的,另外也觉得网上的杂乱重复信息太多了,有的、没的、摘抄的、翻腾一遍又发一遍的都称得上是信息污染了,所以开始是不想写这个的,担心功力不够,给网络信息添乱,影响大家准确搜寻正确有用的信息。
主要是在网上搜罗了好久都没个具体方案,都是对于android-Camera2Basic这个Demo翻来倒去。人脸检测也只是输出一个坐标,缺失了很多实际应用中需要的东西,所以把做的东西分享出来供大家参考。
有不对或者偏颇的地方希望各位大牛指正。
前期探索
一直倾慕于OpenCV的强大,所以最开始是想用OpenCV(用的openCVLibrary340)做人脸检测,后面发现默认预览是横屏的,网上搜了一些方案,都不是太满意:
1.修改源码:改写CameraBridgeViewBase类的deliverAndDrawFrame方法,通过旋转画布等方法,自定义最终的绘制。尽管将逻辑可以下方到子类,但始终需要对源码做一定的修改,这种侵入源码的方式太不优雅,只能作最后的手段。
2.自定义图像:在CameraBridgeViewBase.CvCameraViewListener2监听中对图像进行处理。二次处理会在一定程度上影响速度。
当然还有其它的方式,但是基本都会有各自的缺陷,而之前有用过Android的人脸检测API FaceDetector,所以就想着用Android原生的方式。
首先肯定排除调用Android的人脸检测API做了,拍照后的后期检测裁剪之类的还行。预览实时检测的话资源耗费太大速度太慢。
然后Camera已经不推荐使用了,自然就选Camera2,好吧Camera2 API个人觉得挺不友好、不好用,但是既然Google让用,那就用吧。
界面
先看看效果图
做的过程中是预览画面与未处理过角度的拍照画面对比着进行分析矫正的,所以分析逻辑的时候可以先不对拍照画面进行处理。
界面结构与代码
最开始是想直接在预览画面上加上方框,后面发现费时费力还不讨好。好吧其实用的是TextureView,后面发现预览过程中获取不到Canvas不能绘制。当然不侵入预览画面,预览画面还可以做别用,不然就只能当预览画面了。
最终是在预览的TextureView上覆盖了一个同样大小的TextureView,在上面的TextureView上进行绘制。
界面结构:
界面代码:
`
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.face.detect.demo.MainActivity">
id="@+id/cView"
android:layout_width="180dp"
android:layout_height="240dp"
android:layout_marginStart="8dp" android:layout_marginTop="8dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> id="@+id/imageView" android:layout_width="150dp" android:layout_height="200dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:background="?android:attr/colorEdgeEffect" android:scaleType="centerInside" android:src="@android:color/transparent" app:layout_constraintBottom_toBottomOf="@+id/cView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/cView" app:layout_constraintTop_toTopOf="@+id/cView" />
`
界面比较简单就不多说了。
摄像头调用
开启摄像头并在TextureView进行画面预览大家应该是没什么问题的。稍微注意点儿的就是:
1.为预览的TextureView设置默认的缓冲区大小,不然画面会变形。代码中:
cView.getSurfaceTexture().setDefaultBufferSize(sSize.getWidth(),sSize.getHeight());
2.在创建摄像头Session前先初始化好画面呈现的目标Surface。
摄像头开启流程为:
1.获取CameraManager
2.通过CameraManager根据摄像头ID获取摄像头参数CameraCharacteristics,如果仅仅是预览,那么根据呈现目标获取所支持的输出尺寸就行,如果需要进行人脸检测坐标换算,那么需要获取摄像头成像尺寸。
//获取开启相机的相关参数
CameraCharacteristics characteristics = cManager.getCameraCharacteristics(cId);
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] previewSizes = map.getOutputSizes(SurfaceTexture.class);//获取预览尺寸
Size[] captureSizes = map.getOutputSizes(ImageFormat.JPEG);//获取拍照尺寸
cOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);//获取相机角度
Rect cRect = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);//获取成像区域
cPixelSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);//获取成像尺寸,同上
3.通过CameraManager开启摄像头,并在回调中获取到CameraDevice对象。
4.通过CameraDevice对象创建CameraCaptureSession。
5.通过CameraCaptureSession发起构建好(构建的同时配置摄像头成像的参数,如自动对焦,白平衡,曝光,人脸检测等)的预览或者拍照请求。
6.在预览请求的回调中处理人脸检测,在ImageReader的回调中处理拍照结果。
人脸检测结果换算
程序注释大概都标注了,另外需要单独说一下的地方是人脸检测结果的转换:
1.最重要的一点是获取相机成像尺寸,上述中的:
SENSOR_INFO_ACTIVE_ARRAY_SIZE
SENSOR_INFO_PIXEL_ARRAY_SIZE
都行,能获取到成像的尺寸,在此基础上才能进行换算。
2.根据成像尺寸,与标注人脸的TextureView的尺寸进行换算,得到缩放比例(由于角度关系,这里注意比例换算是需要长的边对长的边,短的边对短的边,不一定是Height对Height,Width对Width。如此例中为Height对Width,Width对Height。反正就是将画面转到人脸同角度时的长宽比例);
3.由于预览画面是角度变换过的,而获取到的人脸检测坐标是基于相机原始成像角度的原点以及left、top、bottom、right。所以缩放后,还需要进行原点以及left、top、bottom、right的换算:
人脸检测坐标原点为相机成像画面的左上角,left、top、bottom、right以成像画面左上下右为基准。预览画面如此例中:
前置摄像头成像画面相对于预览画面顺时针90°外加翻转。原画面的left、top、bottom、right变为预览画面的bottom、right、top、left,并且由于坐标原点由左上角变为右下角,X,Y方向都要进行坐标换算。
canvas.drawRect(canvas.getWidth()-b,
canvas.getHeight()-r,
canvas.getWidth()-t,
canvas.getHeight()-l,getPaint());
后置摄像头成像画面相对于预览画面顺时针270°,left、top、bottom、right变为bottom、left、top、right,并且由于坐标原点由左上角变为左下角,Y方向需要进行坐标换算。
canvas.drawRect(canvas.getWidth()-b,l,canvas.getWidth()-t,r,getPaint());
完整代码
下面上完整代码,有不对或者不妥不够优雅的地方请大家指正:
`package com.face.detect.demo;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.Face;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class MainActivity extends Activity {
private static void Log(String message) { Log.i(MainActivity.class.getName(), message); } //为了使照片竖直显示 private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); static { ORIENTATIONS.append(Surface.ROTATION_0, 90); ORIENTATIONS.append(Surface.ROTATION_90, 0); ORIENTATIONS.append(Surface.ROTATION_180, 270); ORIENTATIONS.append(Surface.ROTATION_270, 180); } private TextureView cView;//用于相机预览 private TextureView rView;//用于标注人脸 private ImageView imageView;//拍照照片显示 private TextView textView; private Button btnFront; private Button btnBack; private Button btnClose; private Button btnCapture; private Surface previewSurface;//预览Surface private ImageReader cImageReader; private Surface captureSurface;//拍照Surface HandlerThread cHandlerThread;//相机处理线程 Handler cHandler;//相机处理 CameraDevice cDevice; CameraCaptureSession cSession; CameraDevice.StateCallback cDeviceOpenCallback = null;//相机开启回调 CaptureRequest.Builder previewRequestBuilder;//预览请求构建 CaptureRequest previewRequest;//预览请求 CameraCaptureSession.CaptureCallback previewCallback;//预览回调 CaptureRequest.Builder captureRequestBuilder; CaptureRequest captureRequest; CameraCaptureSession.CaptureCallback captureCallback; int[] faceDetectModes;
// Rect rRect;//相机成像矩形
Size cPixelSize;//相机成像尺寸
int cOrientation;
Size captureSize;
boolean isFront;
Paint pb;
Bitmap bitmap;
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //GlobalExceptionHandler catchHandler = GlobalExceptionHandler.getInstance(); //catchHandler.init(this.getApplication()); initVIew(); } /** * 初始化界面 */ private void initVIew() { cView = findViewById(R.id.cView); rView = findViewById(R.id.rView); imageView = findViewById(R.id.imageView); textView = findViewById(R.id.textView); btnFront = findViewById(R.id.btnFront); btnBack = findViewById(R.id.btnBack); btnClose = findViewById(R.id.btnClose); btnCapture = findViewById(R.id.btnCapture); //隐藏背景色,以免标注人脸时挡住预览画面 rView.setAlpha(0.9f); btnFront.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { openCamera(true); } }); btnBack.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { openCamera(false); } }); btnClose.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { closeCamera(); } }); btnCapture.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { executeCapture(); } }); //TODO 摄像头静音尝试
// try {
// Class> cClass = Class.forName("android.hardware.Camera");
// Method mOpen = cClass.getDeclaredMethod("open");
// Object nCamera = mOpen.invoke(null);
// Method mDisableShutterSound = cClass.getDeclaredMethod("disableShutterSound");
// mDisableShutterSound.invoke(nCamera);
// Method mRelease = cClass.getDeclaredMethod("release");
// mRelease.invoke(nCamera);
// } catch (ClassNotFoundException e) {
// Log(Log.getStackTraceString(e));
// } catch (NoSuchMethodException e) {
// Log(Log.getStackTraceString(e));
// } catch (IllegalAccessException e) {
// Log(Log.getStackTraceString(e));
// } catch (InvocationTargetException e) {
// Log(Log.getStackTraceString(e));
// }
}
private void openCamera(boolean isFront) { closeCamera(); this.isFront = isFront; String cId = null; if (isFront) { cId = CameraCharacteristics.LENS_FACING_BACK + ""; } else { cId = CameraCharacteristics.LENS_FACING_FRONT + ""; } CameraManager cManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { //获取开启相机的相关参数 CameraCharacteristics characteristics = cManager.getCameraCharacteristics(cId); StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); Size[] previewSizes = map.getOutputSizes(SurfaceTexture.class);//获取预览尺寸 Size[] captureSizes = map.getOutputSizes(ImageFormat.JPEG);//获取拍照尺寸 cOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);//获取相机角度 Rect cRect = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);//获取成像区域 cPixelSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);//获取成像尺寸,同上 //可用于判断是否支持人脸检测,以及支持到哪种程度 faceDetectModes = characteristics.get(CameraCharacteristics.STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES);//支持的人脸检测模式 int maxFaceCount = characteristics.get(CameraCharacteristics.STATISTICS_INFO_MAX_FACE_COUNT);//支持的最大检测人脸数量 //此处写死640*480,实际从预览尺寸列表选择 Size sSize = new Size(640,480);//previewSizes[0]; //设置预览尺寸(避免控件尺寸与预览画面尺寸不一致时画面变形) cView.getSurfaceTexture().setDefaultBufferSize(sSize.getWidth(),sSize.getHeight()); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling // ActivityCompat#requestPermissions // here to request the missing permissions, and then overriding // public void onRequestPermissionsResult(int requestCode, String[] permissions, // int[] grantResults) // to handle the case where the user grants the permission. See the documentation // for ActivityCompat#requestPermissions for more details. Toast.makeText(this,"请授予摄像头权限",Toast.LENGTH_LONG).show(); ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CAMERA}, 0); return; } //根据摄像头ID,开启摄像头 try { cManager.openCamera(cId, getCDeviceOpenCallback(), getCHandler()); } catch (CameraAccessException e) { Log(Log.getStackTraceString(e)); } } catch (CameraAccessException e) { Log(Log.getStackTraceString(e)); } } private void closeCamera(){ if (cSession != null){ cSession.close(); cSession = null; } if (cDevice!=null){ cDevice.close(); cDevice = null; } if (cImageReader != null) { cImageReader.close(); cImageReader = null; captureRequestBuilder = null; } if(cHandlerThread!=null){ cHandlerThread.quitSafely(); try { cHandlerThread.join(); cHandlerThread = null; cHandler = null; } catch (InterruptedException e) { Log(Log.getStackTraceString(e)); } } } /** * 初始化并获取相机开启回调对象。当准备就绪后,发起预览请求 */ private CameraDevice.StateCallback getCDeviceOpenCallback(){ if(cDeviceOpenCallback == null){ cDeviceOpenCallback = new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { cDevice = camera; try { //创建Session,需先完成画面呈现目标(此处为预览和拍照Surface)的初始化 camera.createCaptureSession(Arrays.asList(getPreviewSurface(), getCaptureSurface()), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { cSession = session; //构建预览请求,并发起请求 Log("[发出预览请求]"); try { session.setRepeatingRequest(getPreviewRequest(), getPreviewCallback(), getCHandler()); } catch (CameraAccessException e) { Log(Log.getStackTraceString(e)); } } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { session.close(); } }, getCHandler()); } catch (CameraAccessException e) { Log(Log.getStackTraceString(e)); } } @Override public void onDisconnected(@NonNull CameraDevice camera) { camera.close(); } @Override public void onError(@NonNull CameraDevice camera, int error) { camera.close(); } }; } return cDeviceOpenCallback; } /** * 初始化并获取相机线程处理 * @return */ private Handler getCHandler(){ if(cHandler==null){ //单独开一个线程给相机使用 cHandlerThread = new HandlerThread("cHandlerThread"); cHandlerThread.start(); cHandler = new Handler(cHandlerThread.getLooper()); } return cHandler; } /** * 获取支持的最高人脸检测级别 * @return */ private int getFaceDetectMode(){ if(faceDetectModes == null){ return CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL; }else{ return faceDetectModes[faceDetectModes.length-1]; } } /*---------------------------------预览相关---------------------------------*/ /** * 初始化并获取预览回调对象 * @return */ private CameraCaptureSession.CaptureCallback getPreviewCallback (){ if(previewCallback == null){ previewCallback = new CameraCaptureSession.CaptureCallback(){ public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { MainActivity.this.onCameraImagePreviewed(result); } }; } return previewCallback; } /** * 生成并获取预览请求 * @return */ private CaptureRequest getPreviewRequest(){ previewRequest = getPreviewRequestBuilder().build(); return previewRequest; } /** * 初始化并获取预览请求构建对象,进行通用配置,并每次获取时进行人脸检测级别配置 * @return */ private CaptureRequest.Builder getPreviewRequestBuilder(){ if(previewRequestBuilder == null){ try { previewRequestBuilder = cSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); previewRequestBuilder.addTarget(getPreviewSurface()); previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);//自动曝光、白平衡、对焦 } catch (CameraAccessException e) { Log(Log.getStackTraceString(e)); } } previewRequestBuilder.set(CaptureRequest.STATISTICS_FACE_DETECT_MODE,getFaceDetectMode());//设置人脸检测级别 return previewRequestBuilder; } /** * 获取预览Surface * @return */ private Surface getPreviewSurface(){ if(previewSurface == null){ previewSurface = new Surface(cView.getSurfaceTexture()); } return previewSurface; } /** * 处理相机画面处理完成事件,获取检测到的人脸坐标,换算并绘制方框 * @param result */ private void onCameraImagePreviewed(CaptureResult result){ Face faces[]=result.get(CaptureResult.STATISTICS_FACES); showMessage(false,"人脸个数:["+faces.length+"]"); Canvas canvas = rView.lockCanvas(); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//旧画面清理覆盖 if(faces.length>0){ for(int i=0;i"[R"+i+"]:[left:"+fRect.left+",top:"+fRect.top+",right:"+fRect.right+",bottom:"+fRect.bottom+"]"); showMessage(true,"[R"+i+"]:[left:"+fRect.left+",top:"+fRect.top+",right:"+fRect.right+",bottom:"+fRect.bottom+"]"); //人脸检测坐标基于相机成像画面尺寸以及坐标原点。此处进行比例换算 //成像画面与方框绘制画布长宽比比例(同画面角度情况下的长宽比例(此处前后摄像头成像画面相对预览画面倒置(±90°),计算比例时长宽互换)) float scaleWidth = canvas.getHeight()*1.0f/cPixelSize.getWidth(); float scaleHeight = canvas.getWidth()*1.0f/cPixelSize.getHeight(); //坐标缩放 int l = (int) (fRect.left*scaleWidth); int t = (int) (fRect.top*scaleHeight); int r = (int) (fRect.right*scaleWidth); int b = (int) (fRect.bottom*scaleHeight); Log("[T"+i+"]:[left:"+l+",top:"+t+",right:"+r+",bottom:"+b+