图像处理开源了很多东西,保存下一些基础的东西,以用来follow最新的东西。
Google Face API 是什么?
Google 的 Face API 用于面部检测,从图片中找出人的面部,以及位置(它们在图片中的位置)以及朝向(它们面朝何方,相对于镜头而言)。它可以检测出特征点(面部五官),进行分析,判断眼睛是睁着的还是闭着的,以及是不是笑脸。Face API 还能在移动图片中检测并跟随面孔,即面部跟踪。
看一个demo:FaceSpotter
面部侦测和跟踪https://github.com/AccordionGuy/FaceSpotter
项目依赖
打开项目的 build.gradle (Module: app):
在 dependencies 节的最后,你会看到:
compile 'com.google.android.gms:play-services-vision:10.2.0' compile 'com.android.support:design:25.2.0'
第一句导入了 Android Vision API,它支持的不仅仅是面部侦测,也包括了二维码侦测和文字识别。
第二句导入了 Android Design 支持库,它提供了 Snackbar widget,用于通知用户这个 app 需要访问相机。
使用相机
FaceSpotter 在 AndroidManifest.xml 中声明需要使用相机并请求用户许可:
<uses-feature android:name="android.hardware.camera" /> <uses-permission android:name="android.permission.CAMERA" />
预定义的类
开始项目包含了几个预定义的类:
- FaceActivity: app 的主 activity,用于显示相机预览视图。
- FaceTracker: 跟随拍照界面中的面孔,采集它们的位置和特征点。
- FaceGraphic: 在拍照界面中的面孔上绘制计算机生成的图片。
- FaceData: 一个数据类,用于从 FaceTracker 传递数据给 FaceGraphic。当脸移动时, AR 眼珠会显示动画
- EyePhysics: 一个来自 github 上的 Google Mobile Vision 示例 app 中的类,它是一个简单的物理引擎,能够让 AR 随面孔一起移动。
- CameraSourcePreview: 来自于 Google 的另一个类。它将相机中的实时图片显示到一个 view。
- GraphicOverlay: 来自于 Google 的再一个类。 FaceGraphic 继承了它。
让我们看一下如何使用它们。
FaceActivity 定义了这个 app 唯一的 activity,用于处理触摸事件,在运行时请求相机权限(支持 Android 6.0 以上)。FaceActivity 还创建了两个 FaceSpotter 会用到的对象 CameraSource 和 FaceDetector。
打开 FaceActivity.java 找到 createCameraSource 方法:
private void createCameraSource() { Context context = getApplicationContext(); // 1 FaceDetector detector = createFaceDetector(context); // 2 int facing = CameraSource.CAMERA_FACING_FRONT; if (!mIsFrontFacing) { facing = CameraSource.CAMERA_FACING_BACK; } // 3 mCameraSource = new CameraSource.Builder(context, detector) .setFacing(facing) .setRequestedPreviewSize(320, 240) .setRequestedFps(60.0f) .setAutoFocusEnabled(true) .build(); }
代码解释如下:
- 创建一个 FaceDetector 对象,用于侦测来自于相机数据流图片中的面孔。
- 判断当前摄像头是哪一个。
-
用前两步的结果,以 Builder 模式创建一个 camera source。这些 builder 方法分别是:
- setFacing:指定要使用的镜头方向。
- setRequestdPreviewSize:设置相机预览图的分辨率。分辨率越低(比如 320x240)在低端机上工作得越好同时面部侦测的速度越快。分辨率越高(640x480 以上)适用于高端机,对小面孔和面部特征的侦测效果越好。请尝试不同的设置。
- setRequestFps:设置相机的帧率。帧率越高意味着更好的面部跟踪,但需要更多的处理器能力。请尝试不同的帧率。
- setAutoFocusEnabled:开启/关闭自动对焦。设为 true 能够提供更好的面部侦测和用户体验。如果设备部支持自动聚焦,这个设置无效。
然后看一下 createFaceDetector 方法:
@NonNull private FaceDetector createFaceDetector(final Context context) { // 1 FaceDetector detector = new FaceDetector.Builder(context) .setLandmarkType(FaceDetector.ALL_LANDMARKS) .setClassificationType(FaceDetector.ALL_CLASSIFICATIONS) .setTrackingEnabled(true) .setMode(FaceDetector.FAST_MODE) .setProminentFaceOnly(mIsFrontFacing) .setMinFaceSize(mIsFrontFacing ? 0.35f : 0.15f) .build(); // 2 MultiProcessor.Factory<Face> factory = new MultiProcessor.Factory<Face>() { @Override public Tracker<Face> create(Face face) { return new FaceTracker(mGraphicOverlay, context, mIsFrontFacing); } }; // 3 Detector.Processor<Face> processor = new MultiProcessor.Builder<>(factory).build(); detector.setProcessor(processor); // 4 if (!detector.isOperational()) { Log.w(TAG, "Face detector dependencies are not yet available."); // Check the device's storage. If there's little available storage, the native // face detection library will not be downloaded, and the app won't work, // so notify the user. IntentFilter lowStorageFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); boolean hasLowStorage = registerReceiver(null, lowStorageFilter) != null; if (hasLowStorage) { Log.w(TAG, getString(R.string.low_storage_error)); DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { finish(); } }; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.app_name) .setMessage(R.string.low_storage_error) .setPositiveButton(R.string.disappointed_ok, listener) .show(); } } return detector; }
代码解释如下:
-
以 Builder 模式创建一个 FaceDetector 对象,并设置如下属性:
- setLandMarkType:如果不需要侦测面部特征,设置为 NO_LANDMARKS(这会让面部侦测更快)。如果需要面部特征侦测,设置为 ALL_LANDMARKS。
- setClassificationType: 如果不想侦测眼睛是否睁开或闭着以及是否为笑脸,设置为 NO_CLASSIFICATIONS,否则设置为ALL_CLASSIFICATIONS。
- setTrackingEnabled: 开启/关闭面部跟踪,它会为每个面孔在每一帧维护一个一致的 ID。因为你需要在录像中跟踪多个面孔,请设置为 true。
- setMode: 设为 FAST_MODE ,侦测更少的面孔 (速度快), 设为 ACCURATE_MODE 侦测更多的面孔 (速度慢) 同时侦测面孔的欧拉 Y 角(后面介绍)。
- setProminentFaceOnly: 设为 true 只侦测每一帧中位置最前的面孔。
- setMinFaceSize: 指定允许被侦测的最小面孔尺寸,用面孔宽度相对于图片宽度的百分比表示。
-
创建一个工厂类,用于生成新 FaceTracker 实例。
- 一个 face detector 在侦测到一个面孔时,它会将结果返回给一个处理器,这个处理器定义了需要执行的动作。如果你只需要一次处理一个面孔,你可以使用单个处理器的示例。在这个 app 中,你将处理多个面孔,因此创建了一个 MultiProcessor 实例,用它为每个侦测到的面孔创建一个 FaceTracker 实例。然后,我们会将这个处理器绑定到 face detector。
- 面部检测库在 app 安装时下载。它很大,很可能在用户第一次运行 app 时它还没有下载完。这段代码用于处理设备空间不足以下载这个库的情况。
介绍完背景知识之后,我们来试着检测几个面孔!
查找面孔
首先添加一个 view 用于绘制面部侦测数据。
打开 FaceGraphic.java。你会看到 mFace 的变量用关键字 volatile 声明。mFace 用于保存 FaceTracker 发送来的面孔数据,可能被许多线程写入。将它标记为 volatile 保证你每次读它的值时,总是会得到最后被“写入”的结果。这很关键,因为面孔数据会修改得比较频繁。
从 FaceGraphic 中删除 draw() 方法,添加方法:
// 1 void update(Face face) { mFace = face; postInvalidate(); // Trigger a redraw of the graphic (i.e. cause draw() to be called). } @Override public void draw(Canvas canvas) { // 2 // Confirm that the face and its features are still visible // before drawing any graphics over it. Face face = mFace; if (face == null) { return; } // 3 float centerX = translateX(face.getPosition().x + face.getWidth() / 2.0f); float centerY = translateY(face.getPosition().y + face.getHeight() / 2.0f); float offsetX = scaleX(face.getWidth() / 2.0f); float offsetY = scaleY(face.getHeight() / 2.0f); // 4 // Draw a box around the face. float left = centerX - offsetX; float right = centerX + offsetX; float top = centerY - offsetY; float bottom = centerY + offsetY; // 5 canvas.drawRect(left, top, right, bottom, mHintOutlinePaint); // 6 // Draw the face's id. canvas.drawText(String.format("id: %d", face.getId()), centerX, centerY, mHintTextPaint); }
代码解释如下:
- 当 FaceTracker 对象获得所跟踪的面孔的更新,它调用对应的 FaceGraphic 实例的 update 方法,并传入面孔信息。这个方法将这个信息保存到 mFace 并调用 FaceGraphic 父类的 postInvalidate 方法,这个方法强制视图重绘。
- 在面孔周围绘制方框之前,draw 方法检查这个面孔是否仍然被跟踪,如果是,mFace 应当不为空。
- 计算面孔的中心坐标 x 和 y。FaceTracker 提供了相机坐标,但绘制 FaceGraphics 用的是视图坐标,因此调用 GrahpicOverlay 的
translateX 和 translateY 方法将 mFace 的相机坐标转换为画布中的视图坐标。 - 用 x-offset 和 y-offset 是算出方框的上、下、左、右。因为相机和视图坐标系统不同,需要将面孔的宽高用 GraphicOverlay 的 scaleX 和 scaleY 方法进行转换。
- 用计算出来的中心和偏移量,将面孔绘制一个方框框起来。
- 在面孔的中心用一个面孔的 id 进行标识。
在 FaceActivity 中,face detector 将它从相机数据流中侦测到的面孔数据发送给绑定的 multiprocessor。每当接收到一个面孔,multiprocessor 会生成一个新的 FaceTracker 实例。
在 FaceTracker.java 的构造函授后面添加下列方法:
// 1 @Override public void onNewItem(int id, Face face) { mFaceGraphic = new FaceGraphic(mOverlay, mContext, mIsFrontFacing); } // 2 @Override public void onUpdate(FaceDetector.Detections<Face> detectionResults, Face face) { mOverlay.add(mFaceGraphic); mFaceGraphic.update(face); } // 3 @Override public void onMissing(FaceDetector.Detections<Face> detectionResults) { mOverlay.remove(mFaceGraphic); } @Override public void onDone() { mOverlay.remove(mFaceGraphic); }
代码解释如下:
- onNewItem: 当侦测到新的面孔并且开始跟踪时调用。这个方法用于创建一个新的 FaceGraphic 实例,简单说:当侦测到一个新面孔,你都会创建一个新的 AR 图形显示出来。
- onUpdate: 当所跟踪的面孔的某些属性(比如位置、角度或状态)发生改变时调用这个方法。用这个方法将 FaceGraphic 实例添加到 GraphicOverlay 并调用 FaceGraphic 的 update 方法,将所跟踪的面孔数据传递给它。
- onMissing 和 onDone: 当所跟踪的面孔即将临时或永久消失时调用对应方法。这两个方法都会从 overlay 中删除 FaceGraphic 实例。
运行 app。它会在每个检测到的面孔上添加一个框,并加上一个 ID 号:
Landmarks 你好
Face API 可以识别面部特征。
接下来将修改 app 以便它能识别所跟踪的面孔的下列部位:
- 左眼
- 右眼
- 鼻底
- 左嘴角
- 下唇
- 右嘴角
这个信息保存在 FaceData 对象中,而不是 Face 对象。
对于面部特征来说,左和右引用的是目标的左和右。从前置摄像头看,目标的右眼位于屏幕的右边,但从后置摄像头看,则位于左边。
打开 FaceTracker.java 修改 onUpdate() 方法。调用 update() 的那句会有一个编译错误,因为我们还没有完成为了让 app 使用 FaceData 模型的修改,你会在后面再来解决它。
@Override public void onUpdate(FaceDetector.Detections detectionResults, Face face) { mOverlay.add(mFaceGraphic); // Get face dimensions. mFaceData.setPosition(face.getPosition()); mFaceData.setWidth(face.getWidth()); mFaceData.setHeight(face.getHeight()); // Get the positions of facial landmarks. updatePreviousLandmarkPositions(face); mFaceData.setLeftEyePosition(getLandmarkPosition(face, Landmark.LEFT_EYE)); mFaceData.setRightEyePosition(getLandmarkPosition(face, Landmark.RIGHT_EYE)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.LEFT_CHEEK)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.RIGHT_CHEEK)); mFaceData.setNoseBasePosition(getLandmarkPosition(face, Landmark.NOSE_BASE)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.LEFT_EAR)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.LEFT_EAR_TIP)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.RIGHT_EAR)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.RIGHT_EAR_TIP)); mFaceData.setMouthLeftPosition(getLandmarkPosition(face, Landmark.LEFT_MOUTH)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.BOTTOM_MOUTH)); mFaceData.setMouthRightPosition(getLandmarkPosition(face, Landmark.RIGHT_MOUTH)); mFaceGraphic.update(mFaceData); }
注意你现在在 FaceGraphic 的 update 方法中传入的是一个 FaceData 而不是从 onUpdate 参数得来的 Face 对象了。
这允许你定义传递给 FaceTracker 的面部信息,反过来当面孔移动得太快时你可以用一些计算技巧,根据面部特征的最后一次的位置推断它们当前的位置。你将用 mPreviousLandmarkPositions、getLandmarkPosition 方法和 updatePreviousLandmarkPositions 方法实现这个目的。
然后打开 FaceGraphic.java。
首先,因为从 FaceTracker 中接收到的是 FaceData 对象而不是 Face 对象,你需要将一个:
private volatile Face mFace;
修改为:
private volatile FaceData mFaceData;
修改 update() 方法为:
void update(FaceData faceData) { mFaceData = faceData; postInvalidate(); // Trigger a redraw of the graphic (i.e. cause draw() to be called). }
最后,需要修改 draw() 方法,在跟踪的面孔上面画一些点和文字标记出面部特征:
@Override public void draw(Canvas canvas) { final float DOT_RADIUS = 3.0f; final float TEXT_OFFSET_Y = -30.0f; // Confirm that the face and its features are still visible before drawing any graphics over it. if (mFaceData == null) { return; } // 1 PointF detectPosition = mFaceData.getPosition(); PointF detectLeftEyePosition = mFaceData.getLeftEyePosition(); PointF detectRightEyePosition = mFaceData.getRightEyePosition(); PointF detectNoseBasePosition = mFaceData.getNoseBasePosition(); PointF detectMouthLeftPosition = mFaceData.getMouthLeftPosition(); PointF detectMouthBottomPosition = mFaceData.getMouthBottomPosition(); PointF detectMouthRightPosition = mFaceData.getMouthRightPosition(); if ((detectPosition == null) || (detectLeftEyePosition == null) || (detectRightEyePosition == null) || (detectNoseBasePosition == null) || (detectMouthLeftPosition == null) || (detectMouthBottomPosition == null) || (detectMouthRightPosition == null)) { return; } // 2 float leftEyeX = translateX(detectLeftEyePosition.x); float leftEyeY = translateY(detectLeftEyePosition.y); canvas.drawCircle(leftEyeX, leftEyeY, DOT_RADIUS, mHintOutlinePaint); canvas.drawText("left eye", leftEyeX, leftEyeY + TEXT_OFFSET_Y, mHintTextPaint); float rightEyeX = translateX(detectRightEyePosition.x); float rightEyeY = translateY(detectRightEyePosition.y); canvas.drawCircle(rightEyeX, rightEyeY, DOT_RADIUS, mHintOutlinePaint); canvas.drawText("right eye", rightEyeX, rightEyeY + TEXT_OFFSET_Y, mHintTextPaint); float noseBaseX = translateX(detectNoseBasePosition.x); float noseBaseY = translateY(detectNoseBasePosition.y); canvas.drawCircle(noseBaseX, noseBaseY, DOT_RADIUS, mHintOutlinePaint); canvas.drawText("nose base", noseBaseX, noseBaseY + TEXT_OFFSET_Y, mHintTextPaint); float mouthLeftX = translateX(detectMouthLeftPosition.x); float mouthLeftY = translateY(detectMouthLeftPosition.y); canvas.drawCircle(mouthLeftX, mouthLeftY, DOT_RADIUS, mHintOutlinePaint); canvas.drawText("mouth left", mouthLeftX, mouthLeftY + TEXT_OFFSET_Y, mHintTextPaint); float mouthRightX = translateX(detectMouthRightPosition.x); float mouthRightY = translateY(detectMouthRightPosition.y); canvas.drawCircle(mouthRightX, mouthRightY, DOT_RADIUS, mHintOutlinePaint); canvas.drawText("mouth right", mouthRightX, mouthRightY + TEXT_OFFSET_Y, mHintTextPaint); float mouthBottomX = translateX(detectMouthBottomPosition.x); float mouthBottomY = translateY(detectMouthBottomPosition.y); canvas.drawCircle(mouthBottomX, mouthBottomY, DOT_RADIUS, mHintOutlinePaint); canvas.drawText("mouth bottom", mouthBottomX, mouthBottomY + TEXT_OFFSET_Y, mHintTextPaint); }
注意这些地方:
- 因为面部数据的改变非常频繁,必须进行检查以防止从 mFaceData 中读取的对象为空。否则 app 会崩溃。
- 这部分有点繁琐,但很简单:从所跟踪的面孔上抽取每个特征点的坐标,绘制圆点和文本。
运行 app。你会看到:
多张面孔是这个样子:
你已经识别出面孔上的特征点了,接下来开开始画卡通图片吧!但首先,我们要来学习表情类型。
表情类型
Face 类提供了这些和表情类型有关的方法:
- getIsLeftEyeOpenProbability 和 getIsRightEyeOpenProbability: 某只眼是睁还是闭的可能性,以及
- getIsSmilingProbability: 面孔是否在笑的可能性。
两者都会返回 0(非常不可能)到 1(肯定)之间的小数。你可以将这个结果用于判断眼睛是否睁着以及面孔是否在笑,并将这些信息传递给 FaceGraphic。
修改 FaceTracker 使它支持表情分类。首先,在 FaceTracker 中添加两个新实例变量用于保存眼睛的上一次状态。在使用面部特征时,当对象在快速移动时,face detector 有可能检测眼睛状态失败,这时提供一个之前的状态会方便许多:
private boolean mPreviousIsLeftEyeOpen = true; private boolean mPreviousIsRightEyeOpen = true;
onUpdate 也要修改:
@Override public void onUpdate(FaceDetector.Detections<Face> detectionResults, Face face) { mOverlay.add(mFaceGraphic); updatePreviousLandmarkPositions(face); // Get face dimensions. mFaceData.setPosition(face.getPosition()); mFaceData.setWidth(face.getWidth()); mFaceData.setHeight(face.getHeight()); // Get the positions of facial landmarks. mFaceData.setLeftEyePosition(getLandmarkPosition(face, Landmark.LEFT_EYE)); mFaceData.setRightEyePosition(getLandmarkPosition(face, Landmark.RIGHT_EYE)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.LEFT_CHEEK)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.RIGHT_CHEEK)); mFaceData.setNoseBasePosition(getLandmarkPosition(face, Landmark.NOSE_BASE)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.LEFT_EAR)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.LEFT_EAR_TIP)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.RIGHT_EAR)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.RIGHT_EAR_TIP)); mFaceData.setMouthLeftPosition(getLandmarkPosition(face, Landmark.LEFT_MOUTH)); mFaceData.setMouthBottomPosition(getLandmarkPosition(face, Landmark.BOTTOM_MOUTH)); mFaceData.setMouthRightPosition(getLandmarkPosition(face, Landmark.RIGHT_MOUTH)); // 1 final float EYE_CLOSED_THRESHOLD = 0.4f; float leftOpenScore = face.getIsLeftEyeOpenProbability(); if (leftOpenScore == Face.UNCOMPUTED_PROBABILITY) { mFaceData.setLeftEyeOpen(mPreviousIsLeftEyeOpen); } else { mFaceData.setLeftEyeOpen(leftOpenScore > EYE_CLOSED_THRESHOLD); mPreviousIsLeftEyeOpen = mFaceData.isLeftEyeOpen(); } float rightOpenScore = face.getIsRightEyeOpenProbability(); if (rightOpenScore == Face.UNCOMPUTED_PROBABILITY) { mFaceData.setRightEyeOpen(mPreviousIsRightEyeOpen); } else { mFaceData.setRightEyeOpen(rightOpenScore > EYE_CLOSED_THRESHOLD); mPreviousIsRightEyeOpen = mFaceData.isRightEyeOpen(); } // 2 // See if there's a smile! // Determine if person is smiling. final float SMILING_THRESHOLD = 0.8f; mFaceData.setSmiling(face.getIsSmilingProbability() > SMILING_THRESHOLD); mFaceGraphic.update(mFaceData); }
有几个地方需要修改:
- FaceGraphic 的职责是在脸上画图,而不是基于 face detector 提供的可能性来判断眼睛是闭还是睁。这意味着 FaceTracker 应该进行这些计算并为 FaceGraphic 在 FaceData 对象中准备好立马可以用的数据。这些计算包括从getIsLeftEyeOpenProbability 和 getIsRightEyeOpenProbability 方法获得结果并转换成简单的 true/false 值。如果 face detector 认为眼睛有超过 40% 的可能是睁着的,则认为它就是睁着的。
- 对 getIsSmilingProbability 来说也是同样的,但更严格一点。如果 face detector 认为有超过 80% 的可能是一张笑脸,则判定为这是笑脸。
对面部进行卡通化处理
现在,你已经获得了面部特征点和表情分类,可以用一些卡通图片贴在所跟踪的脸上了:
- 在眼睛上贴一张卡通眼睛,每个卡通眼都需要反映真眼的睁闭状态
- 在鼻子上贴一张猪鼻子
- 一个卡通胡须
- 如果脸部表情是笑着的,卡通眼中是一个微笑的星星
FaceGraphic 的 draw 方法需要修改为:
@Override public void draw(Canvas canvas) { final float DOT_RADIUS = 3.0f; final float TEXT_OFFSET_Y = -30.0f; // Confirm that the face and its features are still visible // before drawing any graphics over it. if (mFaceData == null) { return; } PointF detectPosition = mFaceData.getPosition(); PointF detectLeftEyePosition = mFaceData.getLeftEyePosition(); PointF detectRightEyePosition = mFaceData.getRightEyePosition(); PointF detectNoseBasePosition = mFaceData.getNoseBasePosition(); PointF detectMouthLeftPosition = mFaceData.getMouthLeftPosition(); PointF detectMouthBottomPosition = mFaceData.getMouthBottomPosition(); PointF detectMouthRightPosition = mFaceData.getMouthRightPosition(); if ((detectPosition == null) || (detectLeftEyePosition == null) || (detectRightEyePosition == null) || (detectNoseBasePosition == null) || (detectMouthLeftPosition == null) || (detectMouthBottomPosition == null) || (detectMouthRightPosition == null)) { return; } // Face position and dimensions PointF position = new PointF(translateX(detectPosition.x), translateY(detectPosition.y)); float width = scaleX(mFaceData.getWidth()); float height = scaleY(mFaceData.getHeight()); // Eye coordinates PointF leftEyePosition = new PointF(translateX(detectLeftEyePosition.x), translateY(detectLeftEyePosition.y)); PointF rightEyePosition = new PointF(translateX(detectRightEyePosition.x), translateY(detectRightEyePosition.y)); // Eye state boolean leftEyeOpen = mFaceData.isLeftEyeOpen(); boolean rightEyeOpen = mFaceData.isRightEyeOpen(); // Nose coordinates PointF noseBasePosition = new PointF(translateX(detectNoseBasePosition.x), translateY(detectNoseBasePosition.y)); // Mouth coordinates PointF mouthLeftPosition = new PointF(translateX(detectMouthLeftPosition.x), translateY(detectMouthLeftPosition.y)); PointF mouthRightPosition = new PointF(translateX(detectMouthRightPosition.x), translateY(detectMouthRightPosition.y)); PointF mouthBottomPosition = new PointF(translateX(detectMouthBottomPosition.x), translateY(detectMouthBottomPosition.y)); // Smile state boolean smiling = mFaceData.isSmiling(); // Calculate the distance between the eyes using Pythagoras' formula, // and we'll use that distance to set the size of the eyes and irises. final float EYE_RADIUS_PROPORTION = 0.45f; final float IRIS_RADIUS_PROPORTION = EYE_RADIUS_PROPORTION / 2.0f; float distance = (float) Math.sqrt( (rightEyePosition.x - leftEyePosition.x) * (rightEyePosition.x - leftEyePosition.x) + (rightEyePosition.y - leftEyePosition.y) * (rightEyePosition.y - leftEyePosition.y)); float eyeRadius = EYE_RADIUS_PROPORTION * distance; float irisRadius = IRIS_RADIUS_PROPORTION * distance; // Draw the eyes. drawEye(canvas, leftEyePosition, eyeRadius, leftEyePosition, irisRadius, leftEyeOpen, smiling); drawEye(canvas, rightEyePosition, eyeRadius, rightEyePosition, irisRadius, rightEyeOpen, smiling); // Draw the nose. drawNose(canvas, noseBasePosition, leftEyePosition, rightEyePosition, width); // Draw the mustache. drawMustache(canvas, noseBasePosition, mouthLeftPosition, mouthRightPosition); }
下面是画眼睛、鼻子和胡须的方法:
private void drawEye(Canvas canvas, PointF eyePosition, float eyeRadius, PointF irisPosition, float irisRadius, boolean eyeOpen, boolean smiling) { if (eyeOpen) { canvas.drawCircle(eyePosition.x, eyePosition.y, eyeRadius, mEyeWhitePaint); if (smiling) { mHappyStarGraphic.setBounds( (int)(irisPosition.x - irisRadius), (int)(irisPosition.y - irisRadius), (int)(irisPosition.x + irisRadius), (int)(irisPosition.y + irisRadius)); mHappyStarGraphic.draw(canvas); } else { canvas.drawCircle(irisPosition.x, irisPosition.y, irisRadius, mIrisPaint); } } else { canvas.drawCircle(eyePosition.x, eyePosition.y, eyeRadius, mEyelidPaint); float y = eyePosition.y; float start = eyePosition.x - eyeRadius; float end = eyePosition.x + eyeRadius; canvas.drawLine(start, y, end, y, mEyeOutlinePaint); } canvas.drawCircle(eyePosition.x, eyePosition.y, eyeRadius, mEyeOutlinePaint); } private void drawNose(Canvas canvas, PointF noseBasePosition, PointF leftEyePosition, PointF rightEyePosition, float faceWidth) { final float NOSE_FACE_WIDTH_RATIO = (float)(1 / 5.0); float noseWidth = faceWidth * NOSE_FACE_WIDTH_RATIO; int left = (int)(noseBasePosition.x - (noseWidth / 2)); int right = (int)(noseBasePosition.x + (noseWidth / 2)); int top = (int)(leftEyePosition.y + rightEyePosition.y) / 2; int bottom = (int)noseBasePosition.y; mPigNoseGraphic.setBounds(left, top, right, bottom); mPigNoseGraphic.draw(canvas); } private void drawMustache(Canvas canvas, PointF noseBasePosition, PointF mouthLeftPosition, PointF mouthRightPosition) { int left = (int)mouthLeftPosition.x; int top = (int)noseBasePosition.y; int right = (int)mouthRightPosition.x; int bottom = (int)Math.min(mouthLeftPosition.y, mouthRightPosition.y); if (mIsFrontFacing) { mMustacheGraphic.setBounds(left, top, right, bottom); } else { mMustacheGraphic.setBounds(right, top, left, bottom); } mMustacheGraphic.draw(canvas); }
运行 app,将镜头对准脸。对于两只眼睛都是睁着,且没有笑的脸来说,你会看到:
这是我在眨右眼(因此它显示为闭着的)同时微笑(因此我的眼中有一个微笑的小星星)的样子:
这个 app 同时在几张脸上画卡通图形…
甚至是在插图上,只要它足够真实:
它现在和 Snapchat 更像了!
角度
Face API 提供另一个数据:欧拉角。
“欧拉”一词及发音来自于数学家 Leonhard Euler,它用于描述侦测的脸的方向。这个 API 使用 x、y、z 坐标系:
并报告每张脸的下列欧拉角。
- 欧拉 y 角,沿 y 轴进行旋转的角度。当你摇头表示说 no 的时候,你让你的头沿 y 轴来回旋转。只有 face detector 被设置为 ACCURATE_MODE 的时候才能检测出这个角度。
- 欧拉 z 角,沿 z 轴进行旋转的角度。当你将头从一边歪到另一边的时候,你的头就在沿 z 轴来回旋转。
打开 FaceTracker.java ,在 onUpdate() 方法中添加这两行代码以支持欧拉角:
// Get head angles. mFaceData.setEulerY(face.getEulerY()); mFaceData.setEulerZ(face.getEulerZ());
你用欧拉 z 角去修改 FaceGraphic ,让它画一顶帽子在面孔头上,当欧拉 z 角倾斜到任何一边的角度大于 20 度时。
打开 FaceGraphic.java,在 draw 方法最后添加代码:
// Head tilt float eulerY = mFaceData.getEulerY(); float eulerZ = mFaceData.getEulerZ(); // Draw the hat only if the subject's head is titled at a sufficiently jaunty angle. final float HEAD_TILT_HAT_THRESHOLD = 20.0f; if (Math.abs(eulerZ) > HEAD_TILT_HAT_THRESHOLD) { drawHat(canvas, position, width, height, noseBasePosition); }
然后添加一个 drawHat 方法:
private void drawHat(Canvas canvas, PointF facePosition, float faceWidth, float faceHeight, PointF noseBasePosition) { final float HAT_FACE_WIDTH_RATIO = (float)(1.0 / 4.0); final float HAT_FACE_HEIGHT_RATIO = (float)(1.0 / 6.0); final float HAT_CENTER_Y_OFFSET_FACTOR = (float)(1.0 / 8.0); float hatCenterY = facePosition.y + (faceHeight * HAT_CENTER_Y_OFFSET_FACTOR); float hatWidth = faceWidth * HAT_FACE_WIDTH_RATIO; float hatHeight = faceHeight * HAT_FACE_HEIGHT_RATIO; int left = (int)(noseBasePosition.x - (hatWidth / 2)); int right = (int)(noseBasePosition.x + (hatWidth / 2)); int top = (int)(hatCenterY - (hatHeight / 2)); int bottom = (int)(hatCenterY + (hatHeight / 2)); mHatGraphic.setBounds(left, top, right, bottom); mHatGraphic.draw(canvas); }
运行 app。现在当头倾斜到一顶角度后,一顶帅气的帽子出现了:
眼珠弹动
最后用一个简单的物理引擎让眼珠滴溜溜地弹动。只需要对 FaceGraphic 做一点简单修改。首先,你需要声明两个实例变量,为每只眼睛各提供一个物理引擎。在 Drawable 变量下增加:
// We want each iris to move independently, so each one gets its own physics engine. private EyePhysics mLeftPhysics = new EyePhysics(); private EyePhysics mRightPhysics = new EyePhysics();
第二处需要改变的地方是调用 FaceGraphic 的 draw 方法。目前,你将眼珠的位置设置为眼睛的同一位置。
现在,修改 draw 方法中 “draw the eyes” 一段的代码,使用物理引擎去计算眼珠的位置:
// Draw the eyes. PointF leftIrisPosition = mLeftPhysics.nextIrisPosition(leftEyePosition, eyeRadius, irisRadius); drawEye(canvas, leftEyePosition, eyeRadius, leftIrisPosition, irisRadius, leftEyeOpen, smiling); PointF rightIrisPosition = mRightPhysics.nextIrisPosition(rightEyePosition, eyeRadius, irisRadius); drawEye(canvas, rightEyePosition, eyeRadius, rightIrisPosition, irisRadius, rightEyeOpen, smiling);
运行 app,现在每个人都有一双曲棍球式(googly,谷歌式,双关语)的眼睛!
结束
你可以从这里下载完成后的项目。
现在,你虽然不能说从一支增强现实和面部侦测的新手变成了老鸟,但总算知道如何在 Android app 中使用二者了吧!
现在,你已经完成了这个 app 的几个迭代,从最初的版本到完成版本,你应该很容易理解这张 FaceSpotter 对象关系图了吧:
接下来你应该浏览 Google 的移动视觉网站,尤其是 Face API 一节。
阅读他人代码是一种好的学习方式,Google 的 android-vision GitHub repository是一座引发无数想法和代码的宝藏。