这个系列的博客主要介绍如何在Android设备上移植你训练的cv神经网络模型。

主要过程如下:
1、使用Android Camera2 APIs获得摄像头实时预览的画面。
2、如果是对人脸图像进行处理,使用Android Camera2自带的Face类来对人脸检测,并完成在预览画面上画框将人脸框出、添加文字显示神经网络处理结果的功能。
3、使用Tensorflow Lite 将自己的训练得到的模型移植到Android上。

以上三个步骤会分为三个博客,同时也会提供示例代码。步骤二可以根据你的实际需求跳过或修改。这是这个系列的第二篇博客。

主要业务逻辑代码都在Camera2BasicFragment.java中修改。不特殊说明,改动都是在Camera2BasicFragment.java中进行。


文章目录

  • 使用Face类检测Camera2捕捉画面中的人脸
  • 准备工作
  • 一行代码获得人脸位置
  • 将人脸在预览图像上框出来
  • 获取成像尺寸数据
  • 添加surfaceview用于画框
  • drawRectangle方法
  • 添加文字显示功能
  • 准备工作
  • draw_Result
  • 下载Demo与使用


使用Face类检测Camera2捕捉画面中的人脸

准备工作

  • 首先,要获得一些相机人脸检测的参数,在setUpCameraOutputs方法中获得相机有关人脸检测的一些属性,代码加在mCameraId = cameraId;return;之间。主要是要获得mFaceDetectMode
mCameraId = cameraId;

//获取人脸检测参数
int[] FD =characteristics.get(CameraCharacteristics.STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES);  //支持的人脸检测模式
int maxFD=characteristics.get(CameraCharacteristics.STATISTICS_INFO_MAX_FACE_COUNT);  //支持的最大检测人脸数量

if (FD.length>0) {
    List<Integer> fdList = new ArrayList<>();
    for (int FaceD : FD) {
        fdList.add(FaceD);
        Log.e("draw", "setUpCameraOutputs: FD type:" + Integer.toString(FaceD));
    }
    if (maxFD > 0) {
        mFaceDetectSupported = true;
        mFaceDetectMode = Collections.max(fdList);
    }
}

return;
  • 然后要在createCameraPreviewSession方法中添加如下代码,createCameraPreviewSession方法用于创建一个新的相机预览,加上这句代码之后,我们指定下一个相机预览的人脸检测模式,这样才能直接从CaptureResult中直接得到人脸。
// We set up a CaptureRequest.Builder with the output Surface.
mPreviewRequestBuilder
        = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);
+ mPreviewRequestBuilder.set(CaptureRequest.STATISTICS_FACE_DETECT_MODE, mFaceDetectMode); // mFaceDetectMode is faceDetectMode 有0 1 2三种类型 加这一行

一行代码获得人脸位置

完成上述准备工作后,我们就可以在 CameraCaptureSession.CaptureCallback 方法中获得检测得到的人脸了。

使用Face类进行人脸检测依赖于CaptureResult,我们在CameraCaptureSession.CaptureCallback中写相关的代码,紧接着上一篇博客的内容。 Face、Rect 这些类型属于系统类型,如果没有引入造成报错,在AS里面,光标放到报错的Face等代码上,按住alt+Enter,选择第一个解决方法,会帮你引入这些类。

face[]中就是我们想要检测的人脸。face[0]就是最大人脸,调用face[0].getBounds()获取人脸矩形框左上角、右下角的坐标,传入drawRectangle方法,在预览图像上根据人脸画出矩形框。

switch (mState) {
    case STATE_PREVIEW: {
    // We have nothing to do when the camera preview is working normally.
    // ************************** 重点部分 *************************************
    FrameCount = FrameCount + 1;  // FrameCount定义在方法外面 初始值为0
    // We have nothing to do when the camera preview is working normally.
    //获得Face类 face[]中就是我们得到的人脸
    Face face[] = result.get(result.STATISTICS_FACES);
    Log.d("draw", "face: " + Arrays.toString(face));
    Log.d("draw", "face detected " + Integer.toString(face.length));
    // 打印出当前帧编号
    Log.d("draw", "frameCount is " + FrameCount);
    FrameCount = FrameCount + 1;
    
    if (face.length>0){
        Log.d(TAG, "face detected " + Integer.toString(face.length));
        //获取人脸矩形框
        Rect bounds = face[0].getBounds();
        // 成像画面与相机画面存在着比例缩放和角度变化需要在drawRectangle中再进行调整
        int[] face_rec = drawRectangle(bounds.left, bounds.top, bounds.right, bounds.bottom);

        // 每隔FrameInterval帧 将图像输入你的神经网络处理一次得到结果
        // FrameInterval为间隔帧需要你自己定义在方法之外
        if(FrameCount%FrameInterval == 0){
            // 预览模式下的情况 在这里获得预览图像用于输入你的神经网络中
            // bitmap_get就是我们通过getBitmap方法得到的实时画面
            Bitmap bitmap_get= mTextureView.getBitmap();
            do_someting(bitmap_get);
        }
    }
    // *************************************************************************
      break;
    }
    ..........
}

将人脸在预览图像上框出来

由于成像画面和预览画面存在缩放以及旋转变化,所以需要调整,具体思路是这样的:

  • 这个demo里面成像画面不是Canvas而是TextureView,TextureView加上FrameLayout部分才能构成Canvas,Canvas比TextureView大一些,网上很多博客使用了Canvas进行数据转换,会出现框不到人脸的错误,这一点要特别注意。
  • 简单总结一下:
    1、由于成像画面和预览画面存在的缩放问题,第一步是要把传入的基于成像画面的人脸框坐标缩放转换成预览画面中的人脸框坐标。
    2、完成缩放之后根据相机画面和预览画面的旋转角度写特定的代码把矩形框左上角和右下角的坐标转换过来。

使用如下代码可以知道当前摄像头相机画面和预览画面的旋转角度,华为Mate 9的后置摄像头是顺时针90°,前置摄像头是顺时针270°。不同设备不一样,根据相机的旋转角度实际情况自行调整,这部分代码可以参考我的写法自行修改。

// 这是查看相机画面和预览画面的旋转角度的方法
// CameraCharacteristics.SENSOR_ORIENTATION就是当前相机的旋转角度
Log.d("SENSOR_ORIENTATION", "cameraId is "+cameraId);
Log.d("SENSOR_ORIENTATION", "SENSOR_ORIENTATION is "+characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION));

获取成像尺寸数据

在写方法之前要先保存成像画面的尺寸,在private void setUpCameraOutputs(int width, int height) {}方法里面保存变量值,setUpCameraOutputs方法的作用就是设置一些相机初始值,曝光模式、聚焦模式等。

private static Size cPixelSize;  // 在外面一个位置 声明保存成像尺寸的变量

// 然后在setUpCameraOutputs方法里面保存cPixelSize变量的值
private void setUpCameraOutputs(int width, int height) {
   Activity activity = getActivity();
   CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
	try {    			
		for (String cameraId : manager.getCameraIdList()) {
			CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
			cPixelSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);   // 获取成像尺寸 同上用于后续的人脸检测框调整    
    		// .....      
    }
}

添加surfaceview用于画框

  • 编辑Application/res/layout的xml文件,添加一个如下所示的SurfaceView。在页面上新建一个SurfaceView并置顶用于显示矩形框
<SurfaceView
  android:id="@+id/surfaceview_show_rectangle"
  android:layout_width="match_parent"
  android:layout_height="483dp"
  android:layout_alignParentStart="true"
  android:layout_alignParentTop="true"
  android:layout_marginStart="0dp"
  android:layout_marginTop="0dp" 
/>
  • 在Camera2BasicFragment.java中修改onViewCreated方法,添加surfaceview_show_rectangle并将其初始化:
private SurfaceHolder surfaceHolder;
private SurfaceView surfaceview;
@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
    view.findViewById(R.id.picture).setOnClickListener(this);
    view.findViewById(R.id.info).setOnClickListener(this);
    mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);

    view.findViewById(R.id.surfaceview_show_rectangle).setOnClickListener(this);
    // 设置画人脸框的surface
    surfaceview = (SurfaceView)view.findViewById(R.id.surfaceview_show_rectangle);
    surfaceview.setZOrderOnTop(true);  //处于顶层
    surfaceview.getHolder().setFormat(PixelFormat.TRANSPARENT);  //设置surface为透明
    surfaceHolder = surfaceview.getHolder(); //设置一下SurfaceView并获取到surfaceHolder便于后面画框
}

drawRectangle方法

以下就是drawRectangle方法,这里只使用了前置摄像头,所以只进行了270°变换的矫正。

/**
 * 在SurfaceView上画人脸框图
 * */
public int[] drawRectangle(int left, int top, int right, int bottom){

    //定义画笔
    Paint mpaint = new Paint();
    mpaint.setColor(Color.BLUE);
    // mpaint.setAntiAlias(true);//去锯齿
    mpaint.setStyle(Paint.Style.STROKE);//空心
    // 设置paint的外框宽度
    mpaint.setStrokeWidth(5f);

    Canvas canvas = new Canvas();
    canvas = surfaceHolder.lockCanvas();
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); //清楚掉上一次的画框。

    // 由于成像画面和预览画面存在缩放旋转关系 所以需要调整
    //成像画面与方框绘制画布长宽比比例(同画面角度情况下的长宽比例(此处前后摄像头成像画面相对预览画面倒置(±90°),计算比例时长宽互换))
    float scaleWidth = canvas.getHeight()*1.0f/cPixelSize.getWidth();
    float scaleHeight = canvas.getWidth()*1.0f/cPixelSize.getHeight();
    //坐标缩放
    int l = (int) (left*scaleWidth);
    int t = (int) (top*scaleHeight);
    int r = (int) (right*scaleWidth);
    int b = (int) (bottom*scaleHeight);

    // left、top、bottom、right变为bottom、right、top、left,并且由于坐标原点由左上角变为右下角,X,Y方向都要进行坐标换算
    // 逆时针旋转了270°
    left = canvas.getWidth()-b-180; // -180
    top = mTextureView.getHeight()-r-180;  // -180
    right = canvas.getWidth()-t-90;  // -90
    bottom = mTextureView.getHeight()-l+20;  // +20

    // 开始画框
    Rect draw_r = new Rect(left, top, right, bottom);
    canvas.drawRect(draw_r, mpaint);
    surfaceHolder.unlockCanvasAndPost(canvas);
    int[] rec_coordinate = {left, top, right, bottom};
    return rec_coordinate;
}

添加文字显示功能

一般我们框出人脸处理了图片之后,可能需要我们将结果以文字形式显示在旁边,这时候我们就要写一个显示文字的方法来完成这个工作了。

准备工作

  • 编辑Application/res/layout的xml文件,添加一个如下所示的SurfaceView。在页面上新建一个SurfaceView并置顶用于文字显示
<SurfaceView
    android:id="@+id/surfaceview_show_result"
    android:layout_width="match_parent"
    android:layout_height="483dp"
    android:layout_alignParentStart="true"
    android:layout_alignParentTop="true"
    android:layout_marginStart="0dp"
    android:layout_marginTop="0dp" />
  • 在Camera2BasicFragment.java中修改onViewCreated方法,添加surfaceview_show_result并将其初始化:
@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
    // 初始化所有的控件 为控件添加点击事件
    view.findViewById(R.id.picture).setOnClickListener(this);
    view.findViewById(R.id.info).setOnClickListener(this);
    view.findViewById(R.id.change_btn).setOnClickListener(this);
    view.findViewById(R.id.single_btn).setOnClickListener(this);
    mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);

    // 设置画人脸框的surface
    surfaceview = (SurfaceView)view.findViewById(R.id.surfaceview_show_rectangle);
    surfaceview.setZOrderOnTop(true);  //处于顶层
    surfaceview.getHolder().setFormat(PixelFormat.TRANSPARENT);  //设置surface为透明
    surfaceHolder = surfaceview.getHolder(); //设置一下SurfaceView并获取到surfaceHolder便于后面画框

    // 设置展示识别结果的surface
    surfaceview_result = (SurfaceView)view.findViewById(R.id.surfaceview_show_result);
    surfaceview_result.setZOrderOnTop(true);  //处于顶层
    surfaceview_result.getHolder().setFormat(PixelFormat.TRANSPARENT);  //设置surface为透明
    surfaceResultHolder = surfaceview_result.getHolder(); //设置一下SurfaceView并获取到surfaceHolder便于后面画框
}

draw_Result

完成上述工作之后,就可以添加draw_Result方法了。在do_something方法调用draw_Result将结果以文字形式展示出来。

/**
 *  drawResult 显示结果的方法
 * */
public void drawResult(String recognition_result, int[] located){
    //定义画笔
    Paint mpaint = new Paint();
    mpaint.setColor(Color.BLUE);
    mpaint.setAntiAlias(true);//去锯齿
    mpaint.setTextSize(90f);
    Canvas canvas = new Canvas();
    canvas = surfaceResultHolder.lockCanvas();
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); //清楚掉上一次的识别结果。
    canvas.drawText(recognition_result, located[2] ,located[1], mpaint);
    surfaceResultHolder.unlockCanvasAndPost(canvas);
}

下载Demo与使用

DemoforCamera2

git clone或下载压缩包解压之后,在AS Build成功之后,将自己的代码放到Camera2BasicFragment.java的do_something方法中,do_something方法输入的是一个Bitmap和人脸框图坐标,处理Bitmap得到结果,会将结果显示在人脸框图坐标旁边。