android 使用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.在清单文件中添加如下权限:

 

 
  1. <uses-permission android:name="android.permission.CAMERA"/>

  2. <uses-feature android:name="android.hardware.camera" android:required="false"/>

  3. <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

  4. <uses-feature android:name="android.hardware.camera.front" android:required="false"/>

  5. <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>

8.最终项目是这个样子就添加成功了。

 


二、人脸跟踪的调用

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

 

 
  1. public class OpenCvCameraActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener {

  2.  
  3.  
  4. JavaCameraView openCvCameraView;

  5. private CascadeClassifier cascadeClassifier;

  6. private Mat grayscaleImage;

  7. private int absoluteFaceSize;

  8.  
  9. private void initializeOpenCVDependencies() {

  10. try {

  11. // Copy the resource into a temp file so OpenCV can load it

  12. InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);

  13. File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);

  14. File mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");

  15. FileOutputStream os = new FileOutputStream(mCascadeFile);

  16. byte[] buffer = new byte[4096];

  17. int bytesRead;

  18. while ((bytesRead = is.read(buffer)) != -1) {

  19. os.write(buffer, 0, bytesRead);

  20. }

  21. is.close();

  22. os.close();

  23. // Load the cascade classifier

  24. cascadeClassifier = new CascadeClassifier(mCascadeFile.getAbsolutePath());

  25. } catch (Exception e) {

  26. Log.e("OpenCVActivity", "Error loading cascade", e);

  27. }

  28. // And we are ready to go

  29. openCvCameraView.enableView();

  30. }

  31.  
  32. @Override

  33. protected void onCreate(Bundle savedInstanceState) {

  34. super.onCreate(savedInstanceState);

  35. setContentView(R.layout.activity_open_cv_camera);

  36. openCvCameraView = (JavaCameraView) findViewById(R.id.jcv);

  37. openCvCameraView.setCameraIndex(-1);

  38. openCvCameraView.setCvCameraViewListener(this);

  39.  
  40. }

  41.  
  42. @Override

  43. public void onResume() {

  44. super.onResume();

  45. if (!OpenCVLoader.initDebug()) {

  46. Log.e("log_wons", "OpenCV init error");

  47. }

  48. initializeOpenCVDependencies();

  49. }

  50.  
  51. @Override

  52. public void onCameraViewStarted(int width, int height) {

  53. grayscaleImage = new Mat(height, width, CvType.CV_8UC4);

  54.  
  55.  
  56. // The faces will be a 20% of the height of the screen

  57. absoluteFaceSize = (int) (height * 0.2);

  58. }

  59.  
  60. @Override

  61. public void onCameraViewStopped() {

  62. }

  63.  
  64. @Override

  65. public Mat onCameraFrame(Mat aInputFrame) {

  66.  
  67. // Create a grayscale image

  68. Imgproc.cvtColor(aInputFrame, grayscaleImage, Imgproc.COLOR_RGBA2RGB);

  69. MatOfRect faces = new MatOfRect();

  70.  
  71. // Use the classifier to detect faces

  72. if (cascadeClassifier != null) {

  73. cascadeClassifier.detectMultiScale(grayscaleImage, faces, 1.1, 2, 2,

  74. new Size(absoluteFaceSize, absoluteFaceSize), new Size());

  75. }

  76.  
  77. // If there are any faces found, draw a rectangle around it

  78. Rect[] facesArray = faces.toArray();

  79. int faceCount = facesArray.length;

  80.  
  81. for (int i = 0; i < facesArray.length; i++) {

  82. Imgproc.rectangle(aInputFrame, facesArray[i].tl(), facesArray[i].br(), new Scalar(0, 255, 0, 255), 3);

  83. }

  84. return aInputFrame;

  85. }

  86.  
  87. }


布局文件:

 

 

 
  1. <?xml version="1.0" encoding="utf-8"?>

  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

  3. xmlns:tools="http://schemas.android.com/tools"

  4. xmlns:app="http://schemas.android.com/apk/res-auto"

  5. android:layout_width="match_parent"

  6. android:layout_height="match_parent"

  7. tools:context=".MainActivity">

  8.  
  9. <org.opencv.android.JavaCameraView

  10. android:id="@+id/jcv"

  11. android:layout_width="match_parent"

  12. android:layout_height="match_parent"

  13. app:paddingStart="0dp"

  14. app:paddingEnd="0dp"

  15. />

  16.  
  17. </RelativeLayout>

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

 


 

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

  2. canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),

  3. new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),

  4. (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),

  5. (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),

  6. (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);

  7. } else {

  8. canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),

  9. new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,

  10. (canvas.getHeight() - mCacheBitmap.getHeight()) / 2,

  11. (canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(),

  12. (canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null);

  13. }


修改为:

 
 
  1. if (mScale != 0) {

  2. canvas.drawBitmap(mCacheBitmap, new Rect(0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),

  3. new Rect(0, 0, canvas.getWidth(), canvas.getHeight()),null);

  4. } else {

  5. canvas.drawBitmap(mCacheBitmap, new Rect(0, 0, mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),

  6. new Rect(0, 0, canvas.getWidth(), canvas.getHeight()),null);

  7. }


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

 
四、捕获人脸后自动拍照
    捕获人脸后自动拍照,这个需求可能是最最常见的了,那在OpenCV里要如何实现呢?首先我们来观察一下JavaCameraView这个类,它继承自CameraBridgeViewBase
这个类,再往下翻会发现一个非常熟悉的Camera对象,没错这个类里其实是使用了Android原生的API构造了一个相机对象(还好是原生的,至今还没忘却被JNI相机
支配的恐惧...),然后这个类实现了PreviewCallback接口,经常做相机开发的同学一点不陌生,那么我们就从这里入手吧。
一旦实现了PreviewCallback接口,肯定会有onPreviewFrame(byte[] frame,Camera camera)这个回调函数,这里面的字节数组frame对象,就是当前的视频帧,注意这里是视频
帧,是YUV编码的,并不能直接转换为Bitmap。这个回调函数在预览过程中会一直被调用,那么只要确定了哪一帧有人脸,只需要在这里获取就行,代码如下。
 
 
  1. private boolean takePhotoFlag = false;

  2. @Override

  3. public void onPreviewFrame(byte[] frame, Camera arg1) {

  4. if (takePhotoFlag){

  5. Camera.Size previewSize = mCamera.getParameters().getPreviewSize();

  6. BitmapFactory.Options newOpts = new BitmapFactory.Options();

  7. newOpts.inJustDecodeBounds = true;

  8. YuvImage yuvimage = new YuvImage(

  9. frame,

  10. ImageFormat.NV21,

  11. previewSize.width,

  12. previewSize.height,

  13. null);

  14. ByteArrayOutputStream baos = new ByteArrayOutputStream();

  15. yuvimage.compressToJpeg(new Rect(0, 0, previewSize.width, previewSize.height), 100, baos);

  16. byte[] rawImage = baos.toByteArray();

  17. BitmapFactory.Options options = new BitmapFactory.Options();

  18. options.inPreferredConfig = Bitmap.Config.RGB_565;

  19. Bitmap bmp = BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length, options);

  20. try {

  21. BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));

  22. bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos);

  23. bos.flush();

  24. bos.close();

  25. } catch (IOException e) {

  26. e.printStackTrace();

  27. }

  28. bmp.recycle();

  29. takePhotoFlag = false;

  30. }

  31. synchronized (this) {

  32. mFrameChain[mChainIdx].put(0, 0, frame);

  33. mCameraFrameReady = true;

  34. this.notify();

  35. }

  36. if (mCamera != null)

  37. mCamera.addCallbackBuffer(mBuffer);

  38. }

这里我们先在外层声明一个布尔类型的变量,在无限的回调过程中,一旦次布尔值为真,就对该视频帧保存为文件,上面的代码就将YUV视频帧转换为Bitmap对象的方法,然后

将Bitmap存成文件,当然这也的结构不太合理,我只是为了展示方便这样书写。
    现在已经可以抓取照片了,那么如何才能判断是不是有人脸来修改这个布尔值呢,我们再定义这样一个方法:
 
 
  1. public void takePhoto(String name){

  2. fileName = name;

  3. takePhotoFlag = true;

  4. }

一旦调用了takePhoto这个方法,传入一个保存路径,就能修改此布尔值,完成拍照,我们离完成越来越接近了。那么回到我们一开始的Activity,这里面包含刚刚修改的

JavaCameraView对象,可以对他进行操作。然后找到Activity的onCameraFrame回调函数,修改为:
 
 
  1. int faceSerialCount = 0;

  2. @Override

  3. public Mat onCameraFrame(Mat aInputFrame) {

  4. Imgproc.cvtColor(aInputFrame, grayscaleImage, Imgproc.COLOR_RGBA2RGB);

  5. MatOfRect faces = new MatOfRect();

  6. if (cascadeClassifier != null) {

  7. cascadeClassifier.detectMultiScale(grayscaleImage, faces, 1.1, 2, 2,

  8. new Size(absoluteFaceSize, absoluteFaceSize), new Size());

  9. }

  10. Rect[] facesArray = faces.toArray();

  11. int faceCount = facesArray.length;

  12. if (faceCount > 0) {

  13. faceSerialCount++;

  14. } else {

  15. faceSerialCount = 0;

  16. }

  17. if (faceSerialCount > 6) {

  18. openCvCameraView.takePhoto("sdcard/aaa.jpg");

  19. faceSerialCount = -5000;

  20. }

  21. for (int i = 0; i < facesArray.length; i++) {

  22. Imgproc.rectangle(aInputFrame, facesArray[i].tl(), facesArray[i].br(), new Scalar(0, 255, 0, 255), 3);

  23. }

  24. return aInputFrame;

  25. }

首先在外层定义一个faceSerialCount的整数,代表人脸连续出现的次数。当使用OpenCV的CascadeClassifier后,可以回去当前人脸的个数,然后我们用faceCount来记录下来,

一旦该变量大于0,就让faceSerialCount自增,else的话就清零,如果faceSerialCount>6就调用刚才我们定义的takePhoto方法进行拍照,这样一切就大功告成了。这里再解释
下为何让连续出现的次数大于6是再拍照,因为有可能只出现一次时拍照会有很模糊的情况,或者识别到了一个非人脸的东西,这属于误差,所以当6帧都有人脸时,基本可以判
断当前可以拍照,具体这个阈值大家可再自己探索。
    现在一切都完成了,但这样还是让项目加入了好多so文件和Module,那么有没有办法更加简洁呢,甚至一个文件就搞定?下次我将分享AAR组件开发的相关经验,谢谢大家的
捧场,如有哪里不对,欢迎指正。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值