【安卓随笔】使用OpenCV进行人脸跟踪和自动拍照

        为了满足人们不同的需求,市面上出现了各种各样的APP,随着这些年移动互联网的发展,我想再也没有人能有精力或者有必要去统计出所有应用的个数了吧。当无数种具有个性的产品百花齐放时,一些共性的需求也逐渐被人们发现,或者是说日夜折磨着开发者们。今天就在这里谈谈安卓开发中最具共性的相机开发之在相机开发中又最具共性的人脸跟踪与自动拍照,希望能帮到一些还在为此操劳的同行。

        众所周知,其实在安卓原生的方法里,已经具备谷歌的人脸检测算法,只需要传入一张图片,即可获取其中人脸的个数以及两眼之间的距离等信息,如果这些谷歌的算法真的非常强大的话,也就没有必须写这篇博文了,用过的同学都知道,那基本也只能称得上是个demo...而今天我要在这里介绍的是更为强大的OpenCV库。

        OpenCV的全称是:Open Source Computer Vision Library。OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,它是跨平台的,所以在安卓上也是可以使用的。OpenCV的具体功能这里不再详细介绍,总之只要相信它是一个很好很强大的开源库就行了,而我这里只是用它的九牛一毛-------人脸检测。

        首先我们需要去官网下载一份OpenCV的SDK,点击打开官网下载,截止到本文发布,最新版本为V3.2,那我们就以此版本为例。


一、在Android Studio中导入OpenCV

1.新建一个安卓工程。

2.点击File->New->Import Module,选择到刚才下载并解压过的OpenCV SDK的java目录,Module Name自己起一个见面知意的就行了,然后一路Next,最后Finish。此处也可以将java目录里的所有内容复制到自己的项目里,不过会让项目看起来很臃肿,所以建议使用Module的方式引入。


3.此时不出意外会出现一些gradle的错误,不要急着下载缺少的文件,直接根据自己项目的gradle文件来修改这个Module的gradle就行,然后重新Sync即可。

4.打开Project Structure,选中左侧的app,点击加号,选择第三个Module Dependency,然后选择我们刚才添加的OpenCV的Module即可完成依赖。


5.然后在自己工程的里创建jniLibs文件夹,注意不是Module的目录中,然后将\OpenCV-android-sdk\sdk\native\libs下的文件夹复制进去,这里我就只复制armeabi和armeabi-v7a,大家可以根据需求自己挑选。

6.在res下创建raw文件夹,将\OpenCV-android-sdk\sdk\etc\lbpcascades下的lbpcascade_frontalface.xml复制进去,这个是OpenCV的人脸模型文件,以后需要用到。

7.在清单文件中添加如下权限:

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-feature android:name="android.hardware.camera" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front" android:required="false"/>
    <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>
8.最终项目是这个样子就添加成功了。


二、人脸跟踪的调用

      直接将如下代码添加到项目Activity中,这里面初始化了OpenCV和人脸模型文件,通过SDK中的JavaCameraView调用相机,具体源码有兴趣可以自己去看,等会我们也会进行一些探究。

public class OpenCvCameraActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener {


    JavaCameraView openCvCameraView;
    private CascadeClassifier cascadeClassifier;
    private Mat grayscaleImage;
    private int absoluteFaceSize;

    private void initializeOpenCVDependencies() {
        try {
            // Copy the resource into a temp file so OpenCV can load it
            InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);
            File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
            File mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");
            FileOutputStream os = new FileOutputStream(mCascadeFile);
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = is.read(buffer)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            is.close();
            os.close();
            // Load the cascade classifier
            cascadeClassifier = new CascadeClassifier(mCascadeFile.getAbsolutePath());
        } catch (Exception e) {
            Log.e("OpenCVActivity", "Error loading cascade", e);
        }
        // And we are ready to go
        openCvCameraView.enableView();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_open_cv_camera);
        openCvCameraView = (JavaCameraView) findViewById(R.id.jcv);
        openCvCameraView.setCameraIndex(-1);
        openCvCameraView.setCvCameraViewListener(this);

    }

    @Override
    public void onResume() {
        super.onResume();
        if (!OpenCVLoader.initDebug()) {
            Log.e("log_wons", "OpenCV init error");
        }
        initializeOpenCVDependencies();
    }

    @Override
    public void onCameraViewStarted(int width, int height) {
        grayscaleImage = new Mat(height, width, CvType.CV_8UC4);


        // The faces will be a 20% of the height of the screen
        absoluteFaceSize = (int) (height * 0.2);
    }

    @Override
    public void onCameraViewStopped() {
    }

    @Override
    public Mat onCameraFrame(Mat aInputFrame) {

        // Create a grayscale image
        Imgproc.cvtColor(aInputFrame, grayscaleImage, Imgproc.COLOR_RGBA2RGB);
        MatOfRect faces = new MatOfRect();

        // Use the classifier to detect faces
        if (cascadeClassifier != null) {
            cascadeClassifier.detectMultiScale(grayscaleImage, faces, 1.1, 2, 2,
                    new Size(absoluteFaceSize, absoluteFaceSize), new Size());
        }

        // If there are any faces found, draw a rectangle around it
        Rect[] facesArray = faces.toArray();
        int faceCount = facesArray.length;

        for (int i = 0; i < facesArray.length; i++) {
            Imgproc.rectangle(aInputFrame, facesArray[i].tl(), facesArray[i].br(), new Scalar(0, 255, 0, 255), 3);
        }
        return aInputFrame;
    }

}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <org.opencv.android.JavaCameraView
        android:id="@+id/jcv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:paddingStart="0dp"
        app:paddingEnd="0dp"
        />

</RelativeLayout>
然后运行程序,就可以看到效果了。



三、拍照界面的调整
    这样虽然已经可以识别人脸,但是左右两侧会留下黑色的边框,在一些机器上只需要去掉上方的标题栏即可实现全屏,但在某些机器上这样还是会留下黑框,这个
问题在国外的论坛里也是被问的很多的,但一直没有一个很明确的答复,这里我就抛砖引玉提出一种解决方法。首先在OpenCV的包里找到CameraBridgeViewBase,
找到416行附近,做如下修改:
原始代码:
if (mScale != 0) {
                    canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
                         new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),
                         (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),
                         (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),
                         (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);
                } else {
                     canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
                         new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,
                         (canvas.getHeight() - mCacheBitmap.getHeight()) / 2,
                         (canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(),
                         (canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null);
                }

修改为:
 if (mScale != 0) {
                    canvas.drawBitmap(mCacheBitmap, new Rect(0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
                            new Rect(0, 0, canvas.getWidth(), canvas.getHeight()),null);
                } else {
                    canvas.drawBitmap(mCacheBitmap, new Rect(0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
                            new Rect(0, 0, canvas.getWidth(), canvas.getHeight()),null);
                }

这样即可实现全屏,原理是直接对整个Canvas进行绘制,强行拉伸了画面,会使比例有一些不对,如果谁有更好的办法欢迎发出来。

四、捕获人脸后自动拍照
    捕获人脸后自动拍照,这个需求可能是最最常见的了,那在OpenCV里要如何实现呢?首先我们来观察一下JavaCameraView这个类,它继承自CameraBridgeViewBase
这个类,再往下翻会发现一个非常熟悉的Camera对象,没错这个类里其实是使用了Android原生的API构造了一个相机对象(还好是原生的,至今还没忘却被JNI相机
支配的恐惧...),然后这个类实现了PreviewCallback接口,经常做相机开发的同学一点不陌生,那么我们就从这里入手吧。
一旦实现了PreviewCallback接口,肯定会有onPreviewFrame(byte[] frame,Camera camera)这个回调函数,这里面的字节数组frame对象,就是当前的视频帧,注意这里是视频
帧,是YUV编码的,并不能直接转换为Bitmap。这个回调函数在预览过程中会一直被调用,那么只要确定了哪一帧有人脸,只需要在这里获取就行,代码如下。
private boolean takePhotoFlag = false;
    @Override
    public void onPreviewFrame(byte[] frame, Camera arg1) {
        if (takePhotoFlag){
            Camera.Size previewSize = mCamera.getParameters().getPreviewSize();
            BitmapFactory.Options newOpts = new BitmapFactory.Options();
            newOpts.inJustDecodeBounds = true;
            YuvImage yuvimage = new YuvImage(
                    frame,
                    ImageFormat.NV21,
                    previewSize.width,
                    previewSize.height,
                    null);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            yuvimage.compressToJpeg(new Rect(0, 0, previewSize.width, previewSize.height), 100, baos);
            byte[] rawImage = baos.toByteArray();
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            Bitmap bmp = BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length, options);
            try {
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
                bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos);
                bos.flush();
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            bmp.recycle();
            takePhotoFlag = false;
        }
        synchronized (this) {
            mFrameChain[mChainIdx].put(0, 0, frame);
            mCameraFrameReady = true;
            this.notify();
        }
        if (mCamera != null)
            mCamera.addCallbackBuffer(mBuffer);
    }
这里我们先在外层声明一个布尔类型的变量,在无限的回调过程中,一旦次布尔值为真,就对该视频帧保存为文件,上面的代码就将YUV视频帧转换为Bitmap对象的方法,然后
将Bitmap存成文件,当然这也的结构不太合理,我只是为了展示方便这样书写。
    现在已经可以抓取照片了,那么如何才能判断是不是有人脸来修改这个布尔值呢,我们再定义这样一个方法:
 public void takePhoto(String name){
        fileName = name;
        takePhotoFlag = true;
    }
一旦调用了takePhoto这个方法,传入一个保存路径,就能修改此布尔值,完成拍照,我们离完成越来越接近了。那么回到我们一开始的Activity,这里面包含刚刚修改的
JavaCameraView对象,可以对他进行操作。然后找到Activity的onCameraFrame回调函数,修改为:
 int faceSerialCount = 0;
    @Override
    public Mat onCameraFrame(Mat aInputFrame) {
        Imgproc.cvtColor(aInputFrame, grayscaleImage, Imgproc.COLOR_RGBA2RGB);
        MatOfRect faces = new MatOfRect();
        if (cascadeClassifier != null) {
            cascadeClassifier.detectMultiScale(grayscaleImage, faces, 1.1, 2, 2,
                    new Size(absoluteFaceSize, absoluteFaceSize), new Size());
        }
        Rect[] facesArray = faces.toArray();
        int faceCount = facesArray.length;
        if (faceCount > 0) {
            faceSerialCount++;
        } else {
            faceSerialCount = 0;
        }
        if (faceSerialCount > 6) {
            openCvCameraView.takePhoto("sdcard/aaa.jpg");
            faceSerialCount = -5000;         
        }
        for (int i = 0; i < facesArray.length; i++) {
            Imgproc.rectangle(aInputFrame, facesArray[i].tl(), facesArray[i].br(), new Scalar(0, 255, 0, 255), 3);
        }
        return aInputFrame;
    }
首先在外层定义一个faceSerialCount的整数,代表人脸连续出现的次数。当使用OpenCV的CascadeClassifier后,可以回去当前人脸的个数,然后我们用faceCount来记录下来,
一旦该变量大于0,就让faceSerialCount自增,else的话就清零,如果faceSerialCount>6就调用刚才我们定义的takePhoto方法进行拍照,这样一切就大功告成了。这里再解释
下为何让连续出现的次数大于6是再拍照,因为有可能只出现一次时拍照会有很模糊的情况,或者识别到了一个非人脸的东西,这属于误差,所以当6帧都有人脸时,基本可以判
断当前可以拍照,具体这个阈值大家可再自己探索。
    现在一切都完成了,但这样还是让项目加入了好多so文件和Module,那么有没有办法更加简洁呢,甚至一个文件就搞定?下次我将分享AAR组件开发的相关经验,谢谢大家的
捧场,如有哪里不对,欢迎指正。大笑
 
(此博文中的Demo代码明天将提供下载,还需要整理下)





 


  • 11
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 25
    评论
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值