自从Play Services 8.1中引入了Vision开发库,开发者可以方便地对视频或图像进行人脸定位。只要有一张包含了人脸信息的图片,你就可以收集每一张图片上的人脸信息,例如人脸的位置、是否微笑、睁眼或者闭眼和他们具体的面部特征。
这些信息对于许多应用来说是非常有用的,例如一个相机应用可以利用这些信息做到当所有人都睁眼微笑的时候拍照,或者利用它增加一些搞笑效果,例如给照片中的人头上添加一个独角兽的角。不过大家要注意的是,这只能用来做人脸检测,而不是人脸识别。我们只能利用它检测到人脸信息,但是不能通过它判断两张照片上的是否是同一个人。
这篇教程通过人脸检测API对静态图片分析,识别图片中的人物,同时对覆盖图形(overlaid graphics)进行绘制。所有教程使用的代码可以在GitHub上找到。
1、项目配置
首先,为了将Vision库添加到你的工程,你需要导入Play Services 8.1或者更高的版本进入你的工程。本教程只导入Play Services Vision库。打开你工程中的build.gradle文件然后添加以下的编译依赖节点代码。
- compile 'com.google.android.gms:play-services-vision:8.1.0'
当你已经在工程中包含了Play Services,就可以关闭工程中的build.gradle文件,然后打开 AndroidManifest.xml文件。在你的manifest文件中加入下列数据定义人脸检测的依赖项。让Vision库知道你将会在应用中使用它。
- <meta-data android:name="com.google.android.gms.vision.DEPENDENCIES"android:value="face"/>
一旦完成了AndroidManifest.xml的配置,你就可以关闭这个文件。下一步,你需要创建一个新的类文件FaceOverlayView.java。这个类继承自View类,用来进行人脸检测逻辑、显示经过分析的图像和在图像上绘制信息来说明观点等功能。
现在,我们开始增加成员变量并实现构造函数。这个Bitmap(位图)对象用来存储将要被分析的位图数据,SparseArray数组用来存储在图像中发现的人脸信息。
- public class FaceOverlayView extends View {
- private Bitmap mBitmap;
- private SparseArray<Face> mFaces;
- public FaceOverlayView(Context context) {
- this(context, null);
- }
- public FaceOverlayView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
- public FaceOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
- }
然后,我们在FaceOverlayView类中增加一个setBitmap(Bitmap bitmap)函数,现在我们只通过这个函数存储位图对象,一会将用这个方法来分析位图数据。
- public void setBitmap( Bitmap bitmap ) {
- mBitmap = bitmap;
- }
接下来,我们需要一张位图图片。我已经在GitHub上的示例工程中添加了一张,当然你可以使用任何一张你喜欢的图片,然后看看它到底可不可行。当你选好图片后,把它放到res/raw目录下。本教程假定图片的名字叫face.jpg。
当你把图片放到res/raw目录后,打开res/layout/activity_main.xml文件。在这个布局文件中引用一个FaceOverlayView对象,使它在MainActivity中显示出来。
- <?xml version="1.0" encoding="utf-8"?>
- <com.tutsplus.facedetection.FaceOverlayView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/face_overlay"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
定义完布局文件后,打开MainActivity然后在onCreate()函数中引用一个FaceOverlayView的实例。通过输入流从raw文件夹中读入face.jpg并转成位图数据。在拥有了位图数据之后,你就可以通过调用FaceOverlayView的setBitmap方法在自定义视图中设置位图了。
- public class MainActivity extends AppCompatActivity {
- private FaceOverlayView mFaceOverlayView;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- mFaceOverlayView = (FaceOverlayView) findViewById( R.id.face_overlay );
- InputStream stream = getResources().openRawResource( R.raw.face );
- Bitmap bitmap = BitmapFactory.decodeStream(stream);
- mFaceOverlayView.setBitmap(bitmap);
- }
- }
2、检测人脸
现在你的工程已经设置好了,是时候来开始检测人脸了。在setBitmap( Bitmap bitmap )方法中定义一个FaceDetector对象。我们可以通过用FaceDetector中的构造器来实现,通过FaceDetector.Builder你可以定义多个参数来控制人脸检测的速度和FaceDetector生成的其他数据。
具体的设置取决于你的应用程序的用途。如果开启了面部特征搜索,那么人脸检测的速度回变得很慢。在大多数程序设计中,每一件事都有它的优缺点。如果想要了解关于FaceDetector.Builder的更多信息,你可以通过查找安卓开发者网站的官网文档获得。
- FaceDetector detector = new FaceDetector.Builder( getContext() )
- .setTrackingEnabled(false)
- .setLandmarkType(FaceDetector.ALL_LANDMARKS)
- .setMode(FaceDetector.FAST_MODE)
- .build();
你需要检查FaceDetector是否是可操作的。每当用户第一次在设备上使用人脸检测,Play Services服务需要加载一组小型本地库去处理应用程序的请求。虽然这些工作一般在应用程序启动之前就完成了,但是做好失败处理同样是必要的。
如果FaceDetector是可操作的,那么你需要将位图数据转化成Frame对象,并通过detect函数传入用来做人脸数据分析。当完成数据分析后,你需要释放探测器,防止内存泄露。最后调用invalidate()函数来触发视图刷新。
- if (!detector.isOperational()) {
- //Handle contingency
- } else {
- Frame frame = new Frame.Builder().setBitmap(bitmap).build();
- mFaces = detector.detect(frame);
- detector.release();
- }
- invalidate();
现在你已经在图片中发现了人脸信息,并可以使用了。例如,你可以沿着检测出的每一张脸画一个框。在invalidate()函数调用之后,我们可以在OnDraw(Canvas canvas)函数中添加所有必要的逻辑。我们需要确保位图和人脸数据是有效的,在那之后画布上画出位图数据,然后再沿着每张脸的方位画一个框。
因为不同的设备的分辨率不同,你需要通过控制位图的缩放尺寸来保证图片总是能被正确显示出来。
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- if ((mBitmap != null) && (mFaces != null)) {
- double scale = drawBitmap(canvas);
- drawFaceBox(canvas, scale);
- }
- }
drawBitmap(Canvas canvas)方法会将图像自适应大小的画在画布上,同时返回一个正确的缩放值供你使用。
- private double drawBitmap( Canvas canvas ) {
- double viewWidth = canvas.getWidth();
- double viewHeight = canvas.getHeight();
- double imageWidth = mBitmap.getWidth();
- double imageHeight = mBitmap.getHeight();
- double scale = Math.min( viewWidth / imageWidth, viewHeight / imageHeight );
- Rect destBounds = new Rect( 0, 0, (int) ( imageWidth * scale ), (int) ( imageHeight * scale ) );
- canvas.drawBitmap( mBitmap, null, destBounds, null );
- return scale;
- }
drawFaceBox(Canvas canvas, double scale)方法会更有趣,被检测到人脸数据以位置信息的方式存储到mFaces中,这个方法将基于这些位置数据中的宽、高在检测到的人脸位置画一个绿色的矩形框。
你需要定义自己的绘画对象,然后从你的SparseArray数组中循环的找出位置、高度和宽度信息,再利用这些信息在画布上画出矩形。
- private void drawFaceBox(Canvas canvas, double scale) {
- //paint should be defined as a member variable rather than
- //being created on each onDraw request, but left here for
- //emphasis.
- Paint paint = new Paint();
- paint.setColor(Color.GREEN);
- paint.setStyle(Paint.Style.STROKE);
- paint.setStrokeWidth(5);
- float left = 0;
- float top = 0;
- float right = 0;
- float bottom = 0;
- for( int i = 0; i < mFaces.size(); i++ ) {
- Face face = mFaces.valueAt(i);
- left = (float) ( face.getPosition().x * scale );
- top = (float) ( face.getPosition().y * scale );
- right = (float) scale * ( face.getPosition().x + face.getWidth() );
- bottom = (float) scale * ( face.getPosition().y + face.getHeight() );
- canvas.drawRect( left, top, right, bottom, paint );
- }
- }
这时运行你的应用程序,你会发现每张被检测到的人脸都被矩形包围着。值得注意的是,现在我们所使用的人脸检测API版本非常新,所以它不一定能检测到所有的人脸。你可以通过修改FaceDetector.Builder中的配置,使它获得到更多的信息,但是我不能保证这一定会起作用。
3、理解面部特征
面部特征指的是脸上的一些特殊点。人脸检测API不是依靠面部特征来检测一张人脸,而是在检测到人脸之后才能检测面部特征。这就是为什么检测面部特征是一个可选的设置,我们可以通过FaceDetector.Builder开启。
你可以把这些面部特征信息做为一个附加的信息来源,例如需找模特的眼睛在哪里,这样就可以在应用中做相应的处理了。有十二种面部特征是可能被检测出来的: 左右眼 左右耳朵 左右耳垂 鼻子 左右脸颊 左右嘴角 嘴
面部特征的检测取决于检测的角度。例如,有人侧对着的话,那么只能检测到他的一个眼睛,这意味着另一只眼睛不会被检测到。下表概述了哪些面部特征应该检测到(Y是基于脸部的欧拉角(左或右))。
欧拉角 Y | 可见的标志 |
---|---|
< -36° | 左眼、左嘴角、左耳朵、鼻子、左脸颊 |
-36° to -12° | 左嘴角、鼻子、下嘴角、右眼、左眼、左脸颊、左耳垂 |
-12° to 12° | 右眼、左眼、鼻子、左脸颊、右脸颊、左嘴角、右嘴角、下嘴角 |
12° to 36° | 右嘴角、鼻子、下嘴角、左眼、右眼、右脸颊、右耳垂 |
> 36° | 右眼、右嘴角、右耳朵、鼻子、右脸颊 |
如果在人脸检测中,你已经开启了面部特征检测,那么你可以很容易地使用面部特征信息。你只需要调用getLandmarks()函数获得一个面部特征列表就可以了,你可以直接使用它。
在本教程中,你可以利用一个新的函数drawFaceLandmarks(Canvas canvas, double scale)在人脸检测中检测出的每一个面部特征上画一个小圆圈,在onDraw(canvas canvas)函数中,用drawFaceLandmarks替换drawFaceBox。该方法以每个面部特征点的位置为中心,自适应位图大小,用一个圆圈把面部特征点圈起来。
- private void drawFaceLandmarks( Canvas canvas, double scale ) {
- Paint paint = new Paint();
- paint.setColor( Color.GREEN );
- paint.setStyle( Paint.Style.STROKE );
- paint.setStrokeWidth( 5 );
- for( int i = 0; i < mFaces.size(); i++ ) {
- Face face = mFaces.valueAt(i);
- for ( Landmark landmark : face.getLandmarks() ) {
- int cx = (int) ( landmark.getPosition().x * scale );
- int cy = (int) ( landmark.getPosition().y * scale );
- canvas.drawCircle( cx, cy, 10, paint );
- }
- }
- }
调用该方法之后,您应该看到如下图所示的画面,面部特征点被绿色的小圆圈圈起来。
4、额外的面部数据
人脸的位置和面部特征信息是非常有用的,除此之外,我们在应用中还可以通过Face的内置方法获得人脸检测的更多信息。通过getIsSmilingProbability()、getIsLeftEyeOpenProbability()和getIsRightEyeOpenProbability()方法的返回值(范围从0.0到1.0)我们可以判断人的左右眼是否睁开,是否微笑。当数值越接近于1.0那么可能性也就越大。
你也可以通过人脸检测获得Y和Z轴的欧拉值,Z轴的欧拉值是一定会返回的,如果你想接收到X轴的值,那么你必须在检测时使用一个准确的模式,下面是一个如何或者这些值的例子。
- private void logFaceData() {
- float smilingProbability;
- float leftEyeOpenProbability;
- float rightEyeOpenProbability;
- float eulerY;
- float eulerZ;
- for( int i = 0; i < mFaces.size(); i++ ) {
- Face face = mFaces.valueAt(i);
- smilingProbability = face.getIsSmilingProbability();
- leftEyeOpenProbability = face.getIsLeftEyeOpenProbability();
- rightEyeOpenProbability = face.getIsRightEyeOpenProbability();
- eulerY = face.getEulerY();
- eulerZ = face.getEulerZ();
- Log.e( "Tuts+ Face Detection", "Smiling: " + smilingProbability );
- Log.e( "Tuts+ Face Detection", "Left eye open: " + leftEyeOpenProbability );
- Log.e( "Tuts+ Face Detection", "Right eye open: " + rightEyeOpenProbability );
- Log.e( "Tuts+ Face Detection", "Euler Y: " + eulerY );
- Log.e( "Tuts+ Face Detection", "Euler Z: " + eulerZ );
- }
- }
结论
在本教程中,你已经学会了Play Services Vision库中的一个主要组件:人脸检测。你现在知道了如何在一张静态图片中检测到人脸、如何收集人脸的信息并找到每个人脸的重要面部特征。
用你学到的这些东西,可以给自己的图像应用增加一个有意思的特性,在视频中跟踪人脸,或者做任何你能想到的事情。