玩转Android Camera开发(五):基于Google自带算法实时检测人脸并绘制人脸框(网络首发,附完整demo)

 http://blog.csdn.net/yanzi1225627/article/details/38098729/


本文主要介绍使用Google自带的FaceDetectionListener进行人脸检测,并将检测到的人脸用矩形框绘制出来。本文代码基于PlayCameraV1.0.0,在Camera的open和preview流程上进行了改动。原先是放在单独线程里,这次我又把它放到Surfaceview的生命周期里进行打开和开预览。

    首先要反省下,去年就推出了静态图片的人脸检测demo,当时许诺一周内推出Camera预览实时检测并绘制的demo,结果拖到现在才整。哎,屌丝一天又一天,蹉跎啊。在demo制作过程中还是遇到了一些麻烦的,第一个问题是检测到人脸rect默认是以预览界面为坐标系,这个坐标系是经过变换的,中心点为(0, 0),左上顶点坐标是(-1000, -1000),右下顶点是(1000, 1000).也就是说不管预览预览Surfaceview多大,检测出来的rect的坐标始终对应的是在这个变换坐标系。而Android里默认的view的坐标系是,左上顶点为(0, 0),横为x轴,竖为y轴。这就需要把rect坐标变换下。另一个难点是,这个人脸检测必须在camera开启后进行start,如果一旦拍照或停预览,则需要再次激活。激活时需要加个延迟,否则的话就不起作用了。

    另外,仍要交代下,在预览界面实时检测人脸并绘制(基于Google自带算法),还是有两个思路的。一是在PreviewCallback里的onPreviewFrame里得到yuv数据后,转成rgb后再转成Bitmap,然后利用静态图片的人脸检测流程,即利用FaceDetector类进行检测。另一个思路是,直接实现FaceDetectionListener接口,这样在onFaceDetection()里就得到检测到的人脸Face[] faces数据了。这里只需控制何时start,何时stop即可,这都是android标准接口。毫无疑问,这种方法是上选。从Android4.0后android源码里的camera app都是用的这个接口进行人脸检测。下面上源码:

一、GoogleFaceDetect.java

    考虑到下次准备介绍JNI里用opencv检测人脸,为此杂家新建了一个包org.yanzi.mode里面准备放所有的关于图像的东西。新建文件GoogleFaceDetect.java实现FaceDetectionListener,在构造函数里传进来一个Handler,将检测到的人脸数据发给Activity,经Activity中转再刷新UI.

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.mode;  
  2.   
  3. import org.yanzi.util.EventUtil;  
  4.   
  5. import android.content.Context;  
  6. import android.hardware.Camera;  
  7. import android.hardware.Camera.Face;  
  8. import android.hardware.Camera.FaceDetectionListener;  
  9. import android.os.Handler;  
  10. import android.os.Message;  
  11. import android.util.Log;  
  12.   
  13. public class GoogleFaceDetect implements FaceDetectionListener {  
  14.     private static final String TAG = "YanZi";  
  15.     private Context mContext;  
  16.     private Handler mHander;  
  17.     public GoogleFaceDetect(Context c, Handler handler){  
  18.         mContext = c;  
  19.         mHander = handler;  
  20.     }  
  21.     @Override  
  22.     public void onFaceDetection(Face[] faces, Camera camera) {  
  23.         // TODO Auto-generated method stub  
  24.           
  25.         Log.i(TAG, "onFaceDetection...");  
  26.         if(faces != null){  
  27.           
  28.             Message m = mHander.obtainMessage();  
  29.             m.what = EventUtil.UPDATE_FACE_RECT;  
  30.             m.obj = faces;  
  31.             m.sendToTarget();  
  32.         }  
  33.     }  
  34.       
  35. /*  private Rect getPropUIFaceRect(Rect r){ 
  36.         Log.i(TAG, "人脸检测  = " + r.flattenToString()); 
  37.         Matrix m = new Matrix(); 
  38.         boolean mirror = false; 
  39.         m.setScale(mirror ? -1 : 1, 1); 
  40.         Point p = DisplayUtil.getScreenMetrics(mContext); 
  41.         int uiWidth = p.x; 
  42.         int uiHeight = p.y; 
  43.         m.postScale(uiWidth/2000f, uiHeight/2000f); 
  44.         int leftNew = (r.left + 1000)*uiWidth/2000; 
  45.         int topNew = (r.top + 1000)*uiHeight/2000; 
  46.         int rightNew = (r.right + 1000)*uiWidth/2000; 
  47.         int bottomNew = (r.bottom + 1000)*uiHeight/2000; 
  48.          
  49.         return new Rect(leftNew, topNew, rightNew, bottomNew); 
  50.     }*/  
  51.   
  52. }  
  53. </span>  

上面代码注释掉的一部分是我最初想自己写矩阵变换算法的过程,一番努力感觉变换后坐标还是有问题,后来参考Android4.0里的Camera APP源码才解决.这个变换转移到了FaceView里。

二、FaceView.java

    这个类继承ImageView,用来将Face[] 数据的rect取出来,变换后刷新到UI上。

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.ui;  
  2.   
  3. import org.yanzi.camera.CameraInterface;  
  4. import org.yanzi.playcamera.R;  
  5. import org.yanzi.util.Util;  
  6.   
  7. import android.content.Context;  
  8. import android.graphics.Canvas;  
  9. import android.graphics.Color;  
  10. import android.graphics.Matrix;  
  11. import android.graphics.Paint;  
  12. import android.graphics.Paint.Style;  
  13. import android.graphics.RectF;  
  14. import android.graphics.drawable.Drawable;  
  15. import android.hardware.Camera.CameraInfo;  
  16. import android.hardware.Camera.Face;  
  17. import android.util.AttributeSet;  
  18. import android.widget.ImageView;  
  19.   
  20. public class FaceView extends ImageView {  
  21.     private static final String TAG = "YanZi";  
  22.     private Context mContext;  
  23.     private Paint mLinePaint;  
  24.     private Face[] mFaces;  
  25.     private Matrix mMatrix = new Matrix();  
  26.     private RectF mRect = new RectF();  
  27.     private Drawable mFaceIndicator = null;  
  28.     public FaceView(Context context, AttributeSet attrs) {  
  29.         super(context, attrs);  
  30.         // TODO Auto-generated constructor stub  
  31.         initPaint();  
  32.         mContext = context;  
  33.         mFaceIndicator = getResources().getDrawable(R.drawable.ic_face_find_2);  
  34.     }  
  35.   
  36.   
  37.     public void setFaces(Face[] faces){  
  38.         this.mFaces = faces;  
  39.         invalidate();  
  40.     }  
  41.     public void clearFaces(){  
  42.         mFaces = null;  
  43.         invalidate();  
  44.     }  
  45.       
  46.   
  47.     @Override  
  48.     protected void onDraw(Canvas canvas) {  
  49.         // TODO Auto-generated method stub  
  50.         if(mFaces == null || mFaces.length < 1){  
  51.             return;  
  52.         }  
  53.         boolean isMirror = false;  
  54.         int Id = CameraInterface.getInstance().getCameraId();  
  55.         if(Id == CameraInfo.CAMERA_FACING_BACK){  
  56.             isMirror = false//后置Camera无需mirror  
  57.         }else if(Id == CameraInfo.CAMERA_FACING_FRONT){  
  58.             isMirror = true;  //前置Camera需要mirror  
  59.         }  
  60.         Util.prepareMatrix(mMatrix, isMirror, 90, getWidth(), getHeight());  
  61.         canvas.save();  
  62.         mMatrix.postRotate(0); //Matrix.postRotate默认是顺时针  
  63.         canvas.rotate(-0);   //Canvas.rotate()默认是逆时针   
  64.         for(int i = 0; i< mFaces.length; i++){  
  65.             mRect.set(mFaces[i].rect);  
  66.             mMatrix.mapRect(mRect);  
  67.             mFaceIndicator.setBounds(Math.round(mRect.left), Math.round(mRect.top),  
  68.                     Math.round(mRect.right), Math.round(mRect.bottom));  
  69.             mFaceIndicator.draw(canvas);  
  70. //          canvas.drawRect(mRect, mLinePaint);  
  71.         }  
  72.         canvas.restore();  
  73.         super.onDraw(canvas);  
  74.     }  
  75.   
  76.     private void initPaint(){  
  77.         mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  78. //      int color = Color.rgb(0, 150, 255);  
  79.         int color = Color.rgb(9821268);  
  80. //      mLinePaint.setColor(Color.RED);  
  81.         mLinePaint.setColor(color);  
  82.         mLinePaint.setStyle(Style.STROKE);  
  83.         mLinePaint.setStrokeWidth(5f);  
  84.         mLinePaint.setAlpha(180);  
  85.     }  
  86. }  
  87. </span>  
注意事项有两个

1.就是Rect变换问题,通过Util.prepareMatrix(mMatrix, isMirror, 90, getWidth(), getHeight());进行变换,为了解决人脸检测坐标系和实际绘制坐标系不一致问题。第三个参数90,是因为前手摄像头都设置了mCamera.setDisplayOrientation(90);

接下来的Matrix和canvas两个旋转我传的都是0,所以此demo只能在手机0、90、180、270四个标准角度下得到的人脸坐标是正确的。其他情况下,需要将OrientationEventListener得到的角度传过来。为了简单,我这块就么写,OrientationEventListener的用法参见我的前文,后续将再推出一个demo。

最终是通过mMatrix.mapRect(mRect);来将mRect变换成UI坐标系的人脸Rect.

    Util.prepareMatrix()代码如下:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.util;  
  2.   
  3. import android.graphics.Matrix;  
  4.   
  5. public class Util {  
  6.     public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation,  
  7.             int viewWidth, int viewHeight) {  
  8.         // Need mirror for front camera.  
  9.         matrix.setScale(mirror ? -1 : 11);  
  10.         // This is the value for android.hardware.Camera.setDisplayOrientation.  
  11.         matrix.postRotate(displayOrientation);  
  12.         // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).  
  13.         // UI coordinates range from (0, 0) to (width, height).  
  14.         matrix.postScale(viewWidth / 2000f, viewHeight / 2000f);  
  15.         matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);  
  16.     }  
  17. }  
  18. </span>  

2.得到实际UI里的人脸rect怎么画的问题。之前都是通过paint直接画,但实际上也可以通过Drawable.draw(canvas)来画。后者的好处是将一个图片画上去,而通过paint绘制基础图行如Rect、Circle比较方面。代码里把两种方法的代码都写了,供大家参考。

三.何时打开Camera,何时开预览?

本次将这两个流程放到了Surfaceview的两个生命周期里,因为之前放在单独Thread还是会有一些问题。如个别手机上,Surfaceview创建的很慢,这时的SurfaceHolder还没准备好,结果Camera已经走到开预览了,导致黑屏问题。

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">@Override  
  2.     public void surfaceCreated(SurfaceHolder holder) {  
  3.         // TODO Auto-generated method stub  
  4.         Log.i(TAG, "surfaceCreated...");  
  5.         CameraInterface.getInstance().doOpenCamera(null, CameraInfo.CAMERA_FACING_BACK);  
  6.     }  
  7.   
  8.     @Override  
  9.     public void surfaceChanged(SurfaceHolder holder, int format, int width,  
  10.             int height) {  
  11.         // TODO Auto-generated method stub  
  12.         Log.i(TAG, "surfaceChanged...");  
  13.         CameraInterface.getInstance().doStartPreview(mSurfaceHolder, 1.333f);  
  14.     }</span>  

四.何时注册并开始人脸检测?

若要开启人脸检测,必须要在Camera已经startPreview完毕之后。本文暂时采用在onCreate里延迟1.5s开启人脸检测,1.5s基本上camera已经开预览了。后续准备将Handler传到Surfaceview里,在开预览后通过Handler通知Activity已经开启预览了。

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1.   
自定义的MainHandler:
[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">    private  class MainHandler extends Handler{  
  2.   
  3.         @Override  
  4.         public void handleMessage(Message msg) {  
  5.             // TODO Auto-generated method stub  
  6.             switch (msg.what){  
  7.             case EventUtil.UPDATE_FACE_RECT:  
  8.                 Face[] faces = (Face[]) msg.obj;  
  9.                 faceView.setFaces(faces);  
  10.                 break;  
  11.             case EventUtil.CAMERA_HAS_STARTED_PREVIEW:  
  12.                 startGoogleFaceDetect();  
  13.                 break;  
  14.             }  
  15.             super.handleMessage(msg);  
  16.         }  
  17.   
  18.     }</span>  

在onCreate里:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">    protected void onCreate(Bundle savedInstanceState) {  
  2.         super.onCreate(savedInstanceState);  
  3.   
  4.         setContentView(R.layout.activity_camera);  
  5.         initUI();  
  6.         initViewParams();  
  7.         mMainHandler = new MainHandler();  
  8.         googleFaceDetect = new GoogleFaceDetect(getApplicationContext(), mMainHandler);  
  9.   
  10.   
  11.         shutterBtn.setOnClickListener(new BtnListeners());  
  12.         switchBtn.setOnClickListener(new BtnListeners());  
  13.         mMainHandler.sendEmptyMessageDelayed(EventUtil.CAMERA_HAS_STARTED_PREVIEW, 1500);  
  14.     }</span>  
这里写了两个重要的方法分别是开始检测和停止检测:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">private void startGoogleFaceDetect(){  
  2.         Camera.Parameters params = CameraInterface.getInstance().getCameraParams();  
  3.         if(params.getMaxNumDetectedFaces() > 0){  
  4.             if(faceView != null){  
  5.                 faceView.clearFaces();  
  6.                 faceView.setVisibility(View.VISIBLE);  
  7.             }  
  8.             CameraInterface.getInstance().getCameraDevice().setFaceDetectionListener(googleFaceDetect);  
  9.             CameraInterface.getInstance().getCameraDevice().startFaceDetection();  
  10.         }  
  11.     }  
  12.     private void stopGoogleFaceDetect(){  
  13.         Camera.Parameters params = CameraInterface.getInstance().getCameraParams();  
  14.         if(params.getMaxNumDetectedFaces() > 0){  
  15.             CameraInterface.getInstance().getCameraDevice().setFaceDetectionListener(null);  
  16.             CameraInterface.getInstance().getCameraDevice().stopFaceDetection();  
  17.             faceView.clearFaces();  
  18.         }  
  19.     }</span>  

五.人脸检测如何和拍照及前后摄像头切换协调同步?

    先来看下官方对startFaceDetection()一段注释:

[plain]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">    /**  
  2.      * Starts the face detection. This should be called after preview is started.  
  3.      * The camera will notify {@link FaceDetectionListener} of the detected  
  4.      * faces in the preview frame. The detected faces may be the same as the  
  5.      * previous ones. Applications should call {@link #stopFaceDetection} to  
  6.      * stop the face detection. This method is supported if {@link  
  7.      * Parameters#getMaxNumDetectedFaces()} returns a number larger than 0.  
  8.      * If the face detection has started, apps should not call this again.  
  9.      *  
  10.      * <p>When the face detection is running, {@link Parameters#setWhiteBalance(String)},  
  11.      * {@link Parameters#setFocusAreas(List)}, and {@link Parameters#setMeteringAreas(List)}  
  12.      * have no effect. The camera uses the detected faces to do auto-white balance,  
  13.      * auto exposure, and autofocus.  
  14.      *  
  15.      * <p>If the apps call {@link #autoFocus(AutoFocusCallback)}, the camera  
  16.      * will stop sending face callbacks. The last face callback indicates the  
  17.      * areas used to do autofocus. After focus completes, face detection will  
  18.      * resume sending face callbacks. If the apps call {@link  
  19.      * #cancelAutoFocus()}, the face callbacks will also resume.</p>  
  20.      *  
  21.      * <p>After calling {@link #takePicture(Camera.ShutterCallback, Camera.PictureCallback,  
  22.      * Camera.PictureCallback)} or {@link #stopPreview()}, and then resuming  
  23.      * preview with {@link #startPreview()}, the apps should call this method  
  24.      * again to resume face detection.</p>  
  25.      *  
  26.      * @throws IllegalArgumentException if the face detection is unsupported.  
  27.      * @throws RuntimeException if the method fails or the face detection is  
  28.      *         already running.  
  29.      * @see FaceDetectionListener  
  30.      * @see #stopFaceDetection()  
  31.      * @see Parameters#getMaxNumDetectedFaces()  
  32.      */</span>  
相信大家都能看懂,杂家就不一句一句翻了。关键信息是,在调用takePicture和stopPreview时,必须重新start来恢复人脸检测。而在拍照前是不需要手动stop的。经杂家测试,手动stop反而会坏事。另外就是takePicture之后(实际上camera做了stopPreview和startPreview),不能立即startFaceDetection(),如果立即做是没有效果的,必须加个延时。

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">    private void takePicture(){  
  2.         CameraInterface.getInstance().doTakePicture();  
  3.         mMainHandler.sendEmptyMessageDelayed(EventUtil.CAMERA_HAS_STARTED_PREVIEW, 1500);  
  4.     }</span>  

    第二个问题是在Camera切换之后,Camera的实例发生了变化。必须调用stopFaceDetection(),在此之前调用setFaceDetectionListener(null)将其监听置为null。再切换过来重新预览后,再次start。

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. <span style="font-family:Comic Sans MS;font-size:18px;">    private void switchCamera(){  
  2.         stopGoogleFaceDetect();  
  3.         int newId = (CameraInterface.getInstance().getCameraId() + 1)%2;  
  4.         CameraInterface.getInstance().doStopCamera();  
  5.         CameraInterface.getInstance().doOpenCamera(null, newId);  
  6.         CameraInterface.getInstance().doStartPreview(surfaceView.getSurfaceHolder(), previewRate);  
  7.         startGoogleFaceDetect();  
  8.   
  9.     }</span>  

其他代码变化不大,杂家就不一一贴出来了,想看的请看源码。下面上效果图:

下图为预览界面,拍照图片和切换图片直接换成了Android4.4原生的,原来的实在太丑了。


下图为直接把Camera对着电视剧的检测效果:
再来一张,对着电脑里的图片:

很多人质疑google自带检测算法效果太弱,但就测试结果来看,人家做的已经很牛逼了。待下次推出opencv的demo后用同样的预览画面来对比分析。其实大多人抱怨的是google只提供了检测,没有提供识别和认证。不过,它要啥都提供,那杂家就没饭吃了。但可以预见,不久的将来该来的总会来。
补充说明:有的手机上尽管是4.0以上的版本,但可能仍然不支持这种人脸检测接口。个别手机后置camera支持良好,但前置Camera不支持,如中兴的Geek,切到前置Camera后直接报Camera Server died,ICamera died。这种问题只能骂手机厂家,不要喷我的代码哈。

--------------------本文系原创,转载请注明作者:yanzi1225627
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值